使用 Entity Framework 核心的 Blazor 并发问题

Blazor concurrency problem using Entity Framework Core

我的目标

我想创建一个新的 IdentityUser 并显示已通过同一 Blazor 页面创建的所有用户。此页面有:

  1. 通过您的表单将创建一个 IdentityUser
  2. 一个第三方的网格组件(DevExpress Blazor DxDataGrid),显示所有使用 UserManager.Users 属性 的用户。该组件接受 IQueryable 作为数据源。

问题

当我通过表单 (1) 创建新用户时,我会得到以下并发错误:

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

我认为问题与 CreateAsync(IdentityUser user)UserManager.Users 指的是同一个事实有关DbContext

问题与第三方组件无关,因为我用一个简单的列表替换了它,重现了同样的问题。

重现问题的步骤

  1. 使用身份验证创建一个新的 Blazor 服务器端项目
  2. 用以下代码更改Index.razor:

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

我注意到了什么

系统信息

我已经看到了

为什么我要使用 IQueryable

我想将 IQueryable 作为我的第三方组件的数据源传递,因为它可以直接对查询应用分页和过滤。此外,IQueryable 对 CUD 敏感 操作。

也许不是最好的方法,但将异步方法重写为非异步可以解决问题:

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

确保 UI 仅在创建新用户后更新。


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

更新 (08/19/2020)

在这里您可以找到关于如何一起使用 Blazor 和 EFCore 的文档

更新 (07/22/2020)

EFCore 团队在 Entity Framework Core .NET 5 Preview 7

中引入了 DbContextFactory

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

如果您有兴趣,可以在 Announcing Entity Framework Core EF Core 5.0 Preview 7

阅读更多内容

更新 (07/06/2020)

Microsoft 发布了一个有趣的新 video 关于 Blazor(两种型号)和 Entity Framework Core。请看一下 19:20,他们正在讨论如何使用 EFCore

管理并发问题

一般解决方案

我就此问题询问了 Daniel Roth BlazorDeskShow - 2:24:20,它似乎是 Blazor 服务器端 设计的问题。 DbContext 默认生命周期设置为 Scoped。因此,如果同一页面中至少有两个组件试图执行 async 查询,那么我们将遇到异常:

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

关于这个问题有两个解决方法

  • (A) 将 DbContext 的生命周期设置为 Transient
services.AddDbContext<ApplicationDbContext>(opt =>
    opt.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")), ServiceLifetime.Transient);
  • (B) 正如 Carl Franklin 建议的那样(在我的问题之后):使用静态方法创建单例服务,returns DbContext.
  • 的新实例

无论如何,每个解决方案都有效,因为它们创建了 DbContext.

的新实例

关于我的问题

我的问题并非与 DbContext 严格相关,而是与 UserManager<TUser> 相关,后者的生命周期为 Scoped。将 DbContext 的生命周期设置为 Transient 并没有解决我的问题,因为 ASP.NET Core 在我第一次打开会话时创建了一个新的 UserManager<TUser> 实例并且它一直存在直到我不关闭它。此 UserManager<TUser> 在同一页面上的两个组件内。然后我们遇到了之前描述的相同问题:

  • 两个组件拥有相同的 UserManager<TUser> 实例,其中包含瞬态 DbContext

目前,我通过另一种解决方法解决了这个问题:

  • 我没有直接使用 UserManager<TUser>,而是通过 IServiceProvider 创建了一个新的实例,然后它就可以工作了。 I am still looking for a method to change the UserManager's lifetime 而不是使用 IServiceProvider.

tips: pay attention to services' lifetime

这是我学到的。不知道说的对不对

嗯,我有一个非常相似的场景,我 'solve' 我的是将所有内容从 OnInitializedAsync() 移动到

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

好像已经解决了,但是我一直没有找到证明。我想只是跳过初始化让组件成功构建,然后我们可以更进一步。

/******************************更新************ **********************/

我仍然面临这个问题,似乎我给出了错误的解决方案。当我检查这个 时,我清楚了我的问题。因为我实际上是在处理大量使用 dbContext 操作的组件初始化。根据@dani_herrera 的说法,如果您有超过 1 个组件同时执行 Init,则可能会出现问题。 当我听取他的建议将我的 dbContext 服务更改为 时,我就摆脱了这个问题。

我发现你的问题是在寻找与你收到的相同错误消息有关的答案。

我的并发问题似乎是由于更改触发了可视化树的重新渲染,同时(或由于这一事实)我正在尝试调用 DbContext.SaveChangesAsync().

我通过覆盖组件的 ShouldRender() 方法解决了这个问题,如下所示:

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

然后我将我的 SaveChangesAsync() 调用包装在适当设置私有 bool 字段 _updatingDb 的代码中:

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

对 StateHasChanged() 的调用可能是必要的,也可能不是必要的,但为了以防万一,我将其包括在内。

这解决了我的问题,该问题与根据是否正在编辑数据字段有选择地呈现绑定的输入标签或仅呈现文本有关。其他读者可能会发现他们的并发问题也与触发重新渲染的事情有关。如果是这样,此技术可能会有所帮助。

我下载了您的示例并且能够重现您的问题。问题是因为 Blazor 将在您从 EventCallback(即您的 Add 方法)调用的代码中 await 后立即重新呈现组件。

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

如果您将 System.Diagnostics.WriteLine 添加到 Add 的开头和 Add 的末尾,然后还在 Razor 页面的顶部添加一个,在底部,当你点击你的按钮时,你会看到下面的输出。

//First render
Start: BuildRenderTree
End: BuildRenderTree

//Button clicked
Start: Add
(This is where the `await` occurs`)
Start: BuildRenderTree
Exception thrown

您可以像这样防止这个中间方法重新渲染....

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

这将防止在您的方法为 运行 时重新渲染。请注意,如果您将 Users 定义为 IdentityUser[] Users,您将不会看到此问题,因为数组直到 await 完成并且未被惰性评估后才设置,因此您不会得到这个重入问题。

我相信你想使用 IQueryable<T> 因为你需要将它传递给第 3 方组件。问题是,不同的组件可以在不同的线程上渲染,所以如果你将 IQueryable<T> 传递给其他组件,那么

  1. 它们可能在不同的线程上呈现并导致相同的问题。
  2. 他们很可能在使用 IQueryable<T> 的代码中有一个 await,您将再次遇到同样的问题。

理想情况下,您需要的是让第 3 方组件有一个请求您提供数据的事件,为您提供某种查询定义(页码等)。我知道 Telerik Grid 和其他人一样这样做。

这样您就可以执行以下操作

  1. 获取锁
  2. 运行 应用过滤器的查询
  3. 解除锁定
  4. 将结果传递给组件

您不能在异步代码中使用 lock(),因此您需要使用 SpinLock 之类的东西来锁定资源。

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

@Leonardo Lurci 已经从概念上讲过了。如果你们还不想迁移到 .NET 5.0 预览版,我建议您查看 Nuget 包 'EFCore.DbContextFactory',文档非常简洁。本质上它模拟 AddDbContextFactory。当然,它会为每个组件创建一个上下文。

@Leonardo Lurci 对这个问题有多种解决方案。我会对每个解决方案给出我的意见,我认为这是最好的。

  1. 使 DBContext 成为瞬态 - 这是一个解决方案,但并未针对这种情况进行优化..

  2. Carl Franklin 建议 - 单例服务将无法控制上下文的生命周期,将取决于服务请求者在使用后处置上下文。

  3. Microsoft 文档他们谈到将 DBContext Factory 注入到具有 IDisposable 接口的组件中,以便在组件被销毁时处置上下文。这不是一个很好的解决方案,因为它会发生很多问题,例如:执行上下文操作并在完成该操作之前离开组件,将处理上下文并抛出异常..

终于。到目前为止最好的解决方案是在组件中注入 DBContext Factory 是的,但是只要你需要它,你就可以使用下面的 using 语句创建一个新实例:

public async Task GetSomething()
{
   using var context = DBFactory.CreateDBContext();

   return await context.Something.ToListAsync();
}

由于 DbFactory 在创建新的上下文实例时进行了优化,因此没有显着的开销,使其成为比 Transient 上下文更好的选择和更好的性能,它还在方法的末尾处理上下文,因为“using”语句.

希望有用。

到目前为止,这对我来说工作正常,没有任何问题...

我通过新的 DbContext.InvokeAsync 方法仅通过与我的 DbContext 交互来确保 single-threaded 访问,该方法使用 SemaphoreSlim 确保只执行一个操作一次。

我选择了SemaphoreSlim因为你可以await它。

而不是这个

return Db.Users.FirstOrDefaultAsync(x => x.EmailAddress == emailAddress);

这样做

return Db.InvokeAsync(() => ...the query above...);
// Add the following methods to your DbContext
private SemaphoreSlim Semaphore { get; } = new SemaphoreSlim(1);

public TResult Invoke<TResult>(Func<TResult> action)
{
    Semaphore.Wait();
    try
    {
        return action();
    }
    finally
    {
        Semaphore.Release();
    }
}

public async Task<TResult> InvokeAsync<TResult>(Func<Task<TResult>> action)
{
    await Semaphore.WaitAsync();
    try
    {
        return await action();
    }
    finally
    {
        Semaphore.Release();
    }
}

public Task InvokeAsync(Func<Task> action) =>
    InvokeAsync<object>(async () =>
    {
        await action();
        return null;
    });

public void InvokeAsync(Action action) =>
    InvokeAsync(() =>
    {
        action();
        return Task.CompletedTask;
    });