Asp.net core 使用实体框架核心的Blazor并发问题

Asp.net core 使用实体框架核心的Blazor并发问题,asp.net-core,entity-framework-core,blazor,blazor-server-side,Asp.net Core,Entity Framework Core,Blazor,Blazor Server Side,我的目标 我想创建一个新的IdentityUser,并显示通过同一Blazor页面已经创建的所有用户。本页有: 通过您创建的表单将创建一个IdentityUser 显示使用UserManager.users属性的所有用户的第三方网格组件(DevExpress Blazor DxDataGrid)。此组件接受IQueryable作为数据源 问题 当我通过表单(1)创建新用户时,将出现以下并发错误: InvalidOperationException:在上一个操作完成之前,在此上下文上启动了第二个操

我的目标

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

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

    当我通过表单(1)创建新用户时,将出现以下并发错误:

    InvalidOperationException:在上一个操作完成之前,在此上下文上启动了第二个操作。任何实例成员都不能保证线程安全

    我认为问题与以下事实有关:CreateAsync(IdentityUser用户)UserManager

    这个问题与第三方的组件无关,因为我用一个简单的列表替换了相同的问题

    重现问题的步骤

  • 创建具有身份验证的新Blazor服务器端项目
  • 使用以下代码更改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()}" });
        }
    }
    
    @page/“
    你好,世界!
    用户数:@users.Count()
    点击我
    
      @foreach(用户中的var用户) {
    • @user.UserName
    • }
    @代码{ [注入]用户管理器用户管理器{get;set;} 易读用户; 受保护的覆盖无效OnInitialized() { Users=UserManager.Users; } 公共异步任务添加() { 等待UserManager.CreateAsync(新标识用户{UserName=$“test{Guid.NewGuid().ToString()}”); } }
  • 我注意到的

    • 如果我将实体框架提供程序从SqlServer更改为Sqlite,那么错误将永远不会显示
    系统信息

    • ASP.NET Core 3.1.0 Blazor服务器端
    • 基于SqlServer provider的实体框架Core 3.1.0
    我已经看到的

    • :建议的解决方案对我不起作用,因为即使我将DbContext范围从Scoped更改为Transient,我仍然使用相同的UserManager实例,并且它包含相同的DbContext实例
    • StackOverflow上的其他人建议为每个请求创建一个新的DbContext实例。我不喜欢这个解决方案,因为它违背了依赖注入原则。无论如何,我无法应用此解决方案,因为DbContext被包装在UserManager中
    • :此解决方案与上一个非常相似
    我为什么要使用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();            
        }
    }
    
    @page/“
    @继承OwningComponentBase
    你好,世界!
    用户数:@users.Count()
    点击我。如果使用Sqlite,我就可以工作
    
      @foreach(Users.ToList()中的var user) {
    • @user.UserName
    • }
    @代码{ 易读用户; 受保护的覆盖无效OnInitialized() { 用户=服务。用户; } 公共无效添加() { Task.Run(async()=>await Service.CreateAsync(新的IdentityUser{UserName=$“test{Guid.NewGuid().ToString()}”).Wait(); } }
    更新(2020年8月19日)

    在这里你可以找到

    更新(2020年7月22日)

    EFCore团队在实体框架Core.NET5预览版7中引入了DbContextFactory

    […]这种解耦对于Blazor应用程序非常有用,建议使用IDbContextFactory,但在其他场景中也可能有用

    如果你感兴趣,你可以在

    更新(2020年6月7日)

    微软发布了一个关于Blazor(两种模型)和实体框架核心的有趣的新版本。请看19:20,他们正在讨论如何使用EFCore管理并发问题


    通解 我问Daniel Roth这个问题,从设计上看,这似乎是Blazor服务器端的问题。 DbContext默认生存期设置为
    范围
    。因此,如果在同一页面中至少有两个组件正在尝试执行异步查询,那么我们将遇到异常:

    InvalidOperationException:在上一个操作完成之前,在此上下文上启动了第二个操作。任何实例成员都不能保证线程安全

    此问题有两种解决方法:

    • (A) 将DbContext的生存期设置为Transient
    services.AddDbContext而不是使用
    IServiceProvider
    提示:关注服务的生命周期


    这是我学到的。我不知道这一切是否正确。

    好吧,我有一个非常类似的场景,我的“解决方案”是将所有内容从OnInitializedAsync()移动到

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if(firstRender)
        {
            //Your code in OnInitializedAsync()
            StateHasChanged();
        }
    {
    
    这似乎解决了,但我不知道该怎么办。我想只需跳过初始化,让组件成功构建,然后我们就可以更进一步了

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

    我仍然面临着这个问题,似乎我给出了一个错误的解决方案。当我检查这个时,我把问题弄清楚了。因为我实际上要处理很多组件初始化和dbContext操作。根据@dani_herrera的说法,如果一次有多个组件执行Init,那么问题可能会出现。 当我接受了他的建议,将dbContext服务更改为时,我就摆脱了这个问题

    I fo
        protected override bool ShouldRender()
        {
            if (_updatingDb)
            { 
                return false; 
            }
            else
            {
                return base.ShouldRender();
            }
        }
    
            try
            {
                _updatingDb = true;
                await DbContext.SaveChangesAsync();
            }
            finally
            {
                _updatingDb = false;
                StateHasChanged();
            }
    
    public async Task Add()
    {
        await UserManager.CreateAsync(new IdentityUser { UserName = $"test_{Guid.NewGuid().ToString()}" });
    }
    
    //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;
        }
    }
    
    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();
      }
    }