Blazor concurrency problem using Entity Framework Core

后端 未结 6 1540
半阙折子戏
半阙折子戏 2021-01-05 03:46

My goal

I want to create a new IdentityUser and show all the users already created through the same Blazor page. This page has:

  1. a form
6条回答
  •  北荒
    北荒 (楼主)
    2021-01-05 04:47

    I downloaded your sample and was able to reproduce your problem. The problem is caused because Blazor will re-render the component as soon as you await in code called from EventCallback (i.e. your Add method).

    public async Task Add()
    {
        await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
    }
    

    If you add a System.Diagnostics.WriteLine to the start of Add and to the end of Add, and then also add one at the top of your Razor page and one at the bottom, you will see the following output when you click your button.

    //First render
    Start: BuildRenderTree
    End: BuildRenderTree
    
    //Button clicked
    Start: Add
    (This is where the `await` occurs`)
    Start: BuildRenderTree
    Exception thrown
    

    You can prevent this mid-method rerender like so....

    protected override bool ShouldRender() => MayRender;
    
    public async Task Add()
    {
        MayRender = false;
        try
        {
            await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
        }
        finally
        {
            MayRender = true;
        }
    }
    

    This will prevent re-rendering whilst your method is running. Note that if you define Users as IdentityUser[] Users you will not see this problem because the array is not set until after the await has completed and is not lazy evaluated, so you don't get this reentrancy problem.

    I believe you want to use IQueryable because you need to pass it to 3rd party components. The problem is, different components can be rendered on different threads, so if you pass IQueryable to other components then

    1. They might render on different threads and cause the same problem.
    2. They most likely will have an await in the code that consumes the IQueryable and you'll have the same problem again.

    Ideally, what you need is for the 3rd party component to have an event that asks you for data, giving you some kind of query definition (page number etc). I know Telerik Grid does this, as do others.

    That way you can do the following

    1. Acquire a lock
    2. Run the query with the filter applied
    3. Release the lock
    4. Pass the results to the component

    You cannot use lock() in async code, so you'd need to use something like SpinLock to lock a resource.

    private SpinLock Lock = new SpinLock();
    
    private async Task ReadData(SomeFilterFromTelerik filter)
    {
      bool gotLock = false;
      while (!gotLock) Lock.Enter(ref gotLock);
      try
      {
        IUserIdentity result = await ApplyFilter(MyDbContext.Users, filter).ToArrayAsync().ConfigureAwait(false);
        return new WhatTelerikNeeds(result);
      }
      finally
      {
        Lock.Exit();
      }
    }
    

提交回复
热议问题