My goal
I want to create a new IdentityUser and show all the users already created through the same Blazor page. This page has:
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
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
System info
What I have already seen
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.
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
as the lifetime of each component. 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:
services.AddDbContext<ApplicationDbContext>(opt =>
opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
DbContext
. anyway, each solution works because they create a new instance of DbContext
.
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 or change the Blazor page. This UserManager<TUser>
is inside two components on the same page. Then we have the same problem described before:
UserManager<TUser>
object which contains a transient DbContext
.Currently, I solved this problem with another workaround:
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.