Blazor concurrency problem using Entity Framework Core

家住魔仙堡 提交于 2020-05-11 11:06:28

问题


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 through you will create an IdentityUser
  2. a third-party's grid component (DevExpress Blazor DxDataGrid) that shows all users using UserManager.Users property. This component accepts an IQueryable as a data source.

Problem

When I create a new user through the form (1) I will get the following concurrency error:

InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread-safe.

I think the problem is related to the fact that CreateAsync(IdentityUser user) and UserManager.Users are referring the same DbContext

The problem isn't related to the third-party's component because I reproduce the same problem replacing it with a simple list.

Step to reproduce the problem

  1. create a new Blazor server-side project with authentication
  2. change Index.razor with the following code:

    @page "/"
    
    <h1>Hello, world!</h1>
    
    number of users: @Users.Count()
    <button @onclick="@(async () => await Add())">click me</button>
    <ul>
    @foreach(var user in Users) 
    {
        <li>@user.UserName</li>
    }
    </ul>
    
    @code {
        [Inject] UserManager<IdentityUser> UserManager { get; set; }
    
        IQueryable<IdentityUser> Users;
    
        protected override void OnInitialized()
        {
            Users = UserManager.Users;
        }
    
        public async Task Add()
        {
            await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
        }
    }
    

What I noticed

  • If I change Entity Framework provider from SqlServer to Sqlite then the error will never show.

System info

  • ASP.NET Core 3.1.0 Blazor Server-side
  • Entity Framework Core 3.1.0 based on SqlServer provider

What I have already seen

  • Blazor A second operation started on this context before a previous operation completed: the solution proposed doesn't work for me because even if I change my DbContext scope from Scoped to Transient I still using the same instance of UserManager and its contains the same instance of DbContext
  • other guys on StackOverflow suggests creating a new instance of DbContext per request. I don't like this solution because it is against Dependency Injection principles. Anyway, I can't apply this solution because DbContext is wrapped inside UserManager
  • Create a generator of DbContext: this solution is pretty like the previous one.
  • Using Entity Framework Core with Blazor

Why I want to use IQueryable

I want to pass an IQueryable as a data source for my third-party's component because its can apply pagination and filtering directly to the Query. Furthermore IQueryable is sensitive to CUD operations.


回答1:


General solution

I asked Daniel Roth BlazorDeskShow - 2:24:20 about this problem and it seems to be a Blazor Server-Side problem by design. DbContext default lifetime is set to Scoped. So if you have at least two components in the same page which are trying to execute an async query then we will encounter the exception:

InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread-safe.

There are two workaround about this problem:

  • (A) set DbContext's lifetime to Transient
services.AddDbContext<ApplicationDbContext>(opt =>
    opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
  • (B) as Carl Franklin suggested (after my question): create a singleton service with a static method which returns a new instance of DbContext.

anyway, each solution works because they create a new instance of DbContext.

About my problem

My problem wasn't strictly related to DbContext but with UserManager<TUser> which has a Scoped lifetime. Set DbContext's lifetime to Transient didn't solve my problem because ASP.NET Core creates a new instance of UserManager<TUser> when I open the session for the first time and it lives until I don't close it. This UserManager<TUser> is inside two components on the same page. Then we have the same problem described before:

  • two components that own the same UserManager<TUser> instance which contains a transient DbContext.

Currently, I solved this problem with another workaround:

  • I don't use UserManager<TUser> directly instead, I create a new instance of it through IServiceProvider and then it works. I am still looking for a method to change the UserManager's lifetime instead of using IServiceProvider.

tips: pay attention to services' lifetime

This is what I learned. I don't know if it is all correct or not.




回答2:


I found your question looking for answers about the same error message you had.

My concurrency issue appears to have been due to a change that triggered a re-rendering of the visual tree to occur at the same time as (or due to the fact that) I was trying to call DbContext.SaveChangesAsync().

I solved this by overriding my component's ShouldRender() method like this:

    protected override bool ShouldRender()
    {
        if (_updatingDb)
        { 
            return false; 
        }
        else
        {
            return base.ShouldRender();
        }
    }

I then wrapped my SaveChangesAsync() call in code that set a private bool field _updatingDb appropriately:

        try
        {
            _updatingDb = true;
            await DbContext.SaveChangesAsync();
        }
        finally
        {
            _updatingDb = false;
            StateHasChanged();
        }

The call to StateHasChanged() may or may not be necessary, but I've included it just in case.

This fixed my issue, which was related to selectively rendering a bound input tag or just text depending on if the data field was being edited. Other readers may find that their concurrency issue is also related to something triggering a re-render. If so, this technique may be helpful.




回答3:


Perhaps not the best approach but rewriting async method as non-async fixes the problem:

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

It ensures that UI is updated only after the new user is created.


The whole code for Index.razor

@page "/"
@inherits OwningComponentBase<UserManager<IdentityUser>>
<h1>Hello, world!</h1>

number of users: @Users.Count()
<button @onclick="@Add">click me. I work if you use Sqlite</button>

<ul>
@foreach(var user in Users.ToList()) 
{
    <li>@user.UserName</li>
}
</ul>

@code {
    IQueryable<IdentityUser> Users;

    protected override void OnInitialized()
    {
        Users = Service.Users;
    }

    public void Add()
    {
        Task.Run(async () => await Service.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" })).Wait();            
    }
}



回答4:


Well, I have a quite similar scenario with this, and I 'solve' mine is to move everything from OnInitializedAsync() to

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if(firstRender)
    {
        //Your code in OnInitializedAsync()
        StateHasChanged();
    }
{

It seems solved, but I had no idea to find out the proves. I guess just skip from the initialization to let the component success build, then we can go further.



来源:https://stackoverflow.com/questions/59747983/blazor-concurrency-problem-using-entity-framework-core

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!