问题
My goal
I want to create a new IdentityUser and show all the users already created through the same Blazor page. This page has:
- a form through you will create an IdentityUser
- 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
- create a new Blazor server-side project with authentication
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 transientDbContext
.
Currently, I solved this problem with another workaround:
- I don't use
UserManager<TUser>
directly instead, I create a new instance of it throughIServiceProvider
and then it works. I am still looking for a method to change the UserManager's lifetime instead of usingIServiceProvider
.
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