Aspnetcore: Blazor concurrency problem using Entity Framework Core

Created on 14 Jan 2020  Â·  20Comments  Â·  Source: dotnet/aspnetcore

My goal:

I want to create a Blazor page with a list of users that it is populated through an IQueryable using UserManager's property and a button that create a new user when is clicked.

What did I expect

  1. When the page is loaded I want to see users previously created.
  2. When I click on "click me" button I want to see a new row inside the users' list

Problem:

When I set my dbContext to SQLServer provider
and I click on "click me" button I willl get the following error:

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

What I tried:

  • someone on Stackoverflow suggests changing dbContext scope to transient. I did it but the problem still persists. I think because Create method and IQueryable Users are called from the same UserManager's instance so, same dbContext instance.

Problem sample:

https://github.com/Blackleones/BlazorConcurrencyProblem or create a new ASP.NET Blazor Server Side project with Authentication and change Index.razor page in this way
`

@page "/"

<h1>Hello, world!</h1>

number of users: @Users.Count()
<button @onclick="@(async () => await Add())">click me. I work if you use Sqlite</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()}" });
    }
}

`

Extra info about the problem:

  • if I use Sqlite provider then the error will never show.
  • this is a reproduction of a problem that I have inside my project. I've seen that there are a few workarounds to avoid concurrency problems, like:

    • set dbContext scope to Transient: I don't know if it is correct doing it.

    • create a dbContext instance per CRUD request. I would like to avoid this because it is against dependency injection concepts.

  • I've already seen this post: Using Entity Framework Core with Blazor #10448 but I didn't find a solution.
  • someone suggests to not expose IQueryable but return an IList to the user's interface. I am exposing IQueryable because I am using DevExpress components that they can manage IQueryable. These components can apply filters directly on SQL query if the data source is an IQueryable.

System info:

  • ASP.NET Core 3.1.0 Blazor server side
  • Entity Framework Core 3.1.0
area-blazor question

All 20 comments

@Blackleones thanks for contacting us.

See this comment for hte right pattern to follow here and an associated sample.
https://github.com/dotnet/aspnetcore/issues/16745#issuecomment-549505605

I've filed this doc issue to cover this in docs https://github.com/aspnet/AspNetCore.Docs/issues/16558

I've filed this doc issue to cover this in docs aspnet/AspNetCore.Docs#16558

Thank you Javiercn. I've tried your solution using OwningComponentBase but this not solve my problem. Even if I use

  • OwningComponentBase<UserManager<IdentityUser>>
  • OwningComponentBase<ApplicationDbContext>

I still get

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

Please try it using the following code inside a blank ASP.NET Core Blazor server-side Project with authentication

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

number of users: @Users.Count()
<button @onclick="@(async () => await Add())">click me. I work if you use Sqlite</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()}" });
    }

}

Do not inject the user manager, use the Service property on the base class instead, that's the one that it's isolated.

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

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

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

@code {
    IQueryable<IdentityUser> Users;

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

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

I've overwritten my index.razor file with your code but I still get the same error as before.
After that, I 've even tried to set DbContext scope from Scoped to Transient but the error persists.

@Blackleones could you provide a minimal repro project that illustrates your issue and detailed repro steps on how to trigger the problem? That would help us a lot investigating.

Here you can find my sample project.

You have to change the connectionString at Startup.cs line 35. Let me know if you will have some problems. Anyway, if you want you can create your sample project following these steps:

  1. create a new Blazor server-side project with authentication
  2. overwrite the index.razor page with the code that you suggest me before
  3. run the project and click on the button that you will find at index.razor

Keep me updated. Thank you.

I've tried to run the project through the terminal and I got the following stack trace

fail: Microsoft.EntityFrameworkCore.Query[10100]
      An exception occurred while iterating over the results of a query for context type 'BlazorApp4.Data.ApplicationDbContext'.
      System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
         at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
         at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
      Unhandled exception rendering component: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
   at BlazorApp4.Pages.Index.BuildRenderTree(RenderTreeBuilder __builder)
   at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
      Unhandled exception in circuit 'APYHJaMAmW2F5oxResAWOXh8GDnuflXDzjH88IqXenI'.
System.InvalidOperationException: A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.Enumerator.MoveNext()
   at System.Linq.Enumerable.Single[TSource](IEnumerable`1 source)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at System.Linq.Queryable.Count[TSource](IQueryable`1 source)
   at BlazorApp4.Pages.Index.BuildRenderTree(RenderTreeBuilder __builder)
   at Microsoft.AspNetCore.Components.ComponentBase.<.ctor>b__6_0(RenderTreeBuilder builder)
   at Microsoft.AspNetCore.Components.Rendering.ComponentState.RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment renderFragment)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.RenderInExistingBatch(RenderQueueEntry renderQueueEntry)
   at Microsoft.AspNetCore.Components.RenderTree.Renderer.ProcessRenderQueue()
fail: Microsoft.EntityFrameworkCore.Query[10100]
      An exception occurred while iterating over the results of a query for context type 'BlazorApp4.Data.ApplicationDbContext'.
      System.InvalidOperationException: Invalid attempt to call ReadAsync when reader is closed.
         at Microsoft.Data.Common.ADP.ExceptionWithStackTrace(Exception e)
      --- End of stack trace from previous location where exception was thrown ---
         at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: Invalid attempt to call ReadAsync when reader is closed.
   at Microsoft.Data.Common.ADP.ExceptionWithStackTrace(Exception e)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: Invalid attempt to call ReadAsync when reader is closed.
   at Microsoft.Data.Common.ADP.ExceptionWithStackTrace(Exception e)
--- End of stack trace from previous location where exception was thrown ---
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.ShapedQueryCompilingExpressionVisitor.SingleOrDefaultAsync[TSource](IAsyncEnumerable`1 asyncEnumerable, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.FindByNameAsync(String userName)
   at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateUserName(UserManager`1 manager, TUser user, ICollection`1 errors)
   at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateAsync(UserManager`1 manager, TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.ValidateUserAsync(TUser user)
   at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
   at BlazorApp4.Pages.Index.Add() in C:\Users\leonardo.lurci\source\repos\BlazorProblemReleatedTo18340\BlazorApp4\Pages\Index.razor:line 26

I've created a post on StackOverflow and what is shown up is that if you change the code as below then the code works because we lock user interface's rendering until Task.Run() ends.

This is a workaround that works on this sample project. But we can't use that on the real project because we will lock the user interface's rendering for each CUD operations and I think this isn't the scope of Blazor.

@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()
    {
        /*
        ******************
         LOOK HERE
         *****************
        */
        Task.Run(async () => await Service.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" })).Wait();            
    }
}

I think this problem (or something very similar) was previously mentioned here

Hi, guys, I decide to use this open issue, because I have a similar problem and spent many hours to find a way to solve it but for now is still a problem. For now, I use a workaround solution, creating a new scope for every action to get a fresh database. A server-side scenario with "OwningComponentBase" according to me is not enough. Without "OwningComponentBase" the SignalR connection controls scoped services and there are scoped to this connection. With "OwningComponentBase" scoped services are scoped to the lifetime of the component. If I call a method like GetAllUsersAsync() two times, I will receive the same instance of dbContext. The application is still running and some other user is registering to the system. I call again my get method and because my dbContext is the same as before and I will not see the new user. I must refresh the page or navigate to another page and return after that because somehow I must terminate the component to get a new scope with fresh dbContext.
Sorry if the comment is not for here and thank you for your time :).

Startup.cs
`services.AddDbContext(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity()
.AddEntityFrameworkStores();

services.AddTransient();

EntityService.cs
public EntityService(ApplicationDbContext applicationDbContext)
{
this.applicationDbContext = applicationDbContext;
}`

I am still facing this problem. Finally, I've "solved" my problem with a workaround and changing the project's architecture.
So now everything works fine if I change dbContext's lifetime from Scoped to Transient. Anyway, I don't think that this is a good solution because if I want to work asynchronously I have to pay losing all the benefit of the change tracker.
Other people proposed to maintain DbContext lifetime to Scoped and create a scope each time you have to query anything from the database. like the following example

using (var scope = scopeFactory.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<MyDbContext>();
    …
}

I disagree with this solution because you will finish using dbContext as Transient because the change tracker will track everything is going on inside the using statement.

It would be nice if there will be a fix during the next releases.

I resolved the issue like this :
Constructor :

private AppDbContext _db;
protected override void OnInitialized()
{
    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();
}

and later I dispose it:

public void Dispose()
{
    _db?.Dispose();
}

I resolved the issue like this :
Constructor :

private AppDbContext _db;
protected override void OnInitialized()
{
    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();
}

and later I dispose it:

public void Dispose()
{
    _db?.Dispose();
}

create a new AppDbContext is like changing DbContext lifetime from Scoped to Transient as I replied before :) this is a workaround that works but then you don't have a scoped change tracker.

I resolved the issue like this :
Constructor :

private AppDbContext _db;
protected override void OnInitialized()
{
    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();
}

and later I dispose it:

public void Dispose()
{
    _db?.Dispose();
}

create a new AppDbContext is like changing DbContext lifetime from Scoped to Transient as I replied before :) this is a workaround that works but then you don't have a scoped change tracker.

Exactly. I tried to change DbContext to Transient but it doesn't work by no reason. Then I tried to use new Context in each call the solution works But then we lose the Unit Of Work! It's not a perfect solution but it works.

Curiously, this error does not occur if we run the application under linux. Tested on CentOS 7 with .NET Core 3.1.3.

Curiously, this error does not occur if we run the application under linux. Tested on CentOS 7 with .NET Core 3.1.3.

please take a look at https://stackoverflow.com/questions/59747983/blazor-concurrency-problem-using-entity-framework-core

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();
  }
}

I've also posted this answer on StackOverflow

Thank you for contacting us. Due to a lack of activity on this discussion issue we're closing it in an effort to keep our backlog clean. If you believe there is a concern related to the ASP.NET Core framework, which hasn't been addressed yet, please file a new issue.

This issue will be locked after 30 more days of inactivity. If you still wish to discuss this subject after then, please create a new issue!

Was this page helpful?
0 / 5 - 0 ratings