Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Blazor concurrency problem using Entity Framework Core

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.

like image 903
Leonardo Lurci Avatar asked Jan 15 '20 09:01

Leonardo Lurci


People also ask

Which concurrency approach is not supported in EF core?

Pessimistic Concurrency It is simply not practical at all in disconnected scenarios such as web applications. Entity Framework Core provides no support for pessimistic concurrency control.

How does Entity Framework handle concurrency?

Entity Framework supports optimistic concurrency by default. EF saves an entity data to the database, assuming that the same data has not been changed since the entity was loaded. If it finds that the data has changed, then an exception is thrown and you must resolve the conflict before attempting to save it again.

How do you configure entity framework for optimistic concurrency?

If you do want to implement this approach to concurrency, you have to mark all non-primary-key properties in the entity you want to track concurrency for by adding the ConcurrencyCheck attribute to them. That change enables the Entity Framework to include all columns in the SQL WHERE clause of UPDATE statements.


2 Answers

UPDATE (08/19/2020)

Here you can find the documentation about how to use Blazor and EFCore together

UPDATE (07/22/2020)

EFCore team introduces DbContextFactory inside Entity Framework Core .NET 5 Preview 7

[...] This decoupling is very useful for Blazor applications, where using IDbContextFactory is recommended, but may also be useful in other scenarios.

If you are interested you can read more at Announcing Entity Framework Core EF Core 5.0 Preview 7

UPDATE (07/06/2020)

Microsoft released a new interesting video about Blazor (both models) and Entity Framework Core. Please take a look at 19:20, they are talking about how to manage concurrency problem with EFCore


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.

like image 180
Leonardo Lurci Avatar answered Sep 27 '22 23:09

Leonardo Lurci


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<T> 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<T> 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<T> 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<WhatTelerikNeeds> 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();
  }
}
like image 20
Peter Morris Avatar answered Sep 28 '22 01:09

Peter Morris