Blazor:在前一个操作完成之前在此上下文上启动了第二个操作

Posted

技术标签:

【中文标题】Blazor:在前一个操作完成之前在此上下文上启动了第二个操作【英文标题】:Blazor: A second operation started on this context before a previous operation completed 【发布时间】:2019-10-11 18:17:45 【问题描述】:

我正在创建一个服务器端 Blazor 应用程序。以下代码在Startup.cs中。

services.AddDbContext<MyContext>(o => o.UseSqlServer(Configuration.GetConnectionString("MyContext")), ServiceLifetime.Transient);
services.AddTransient<MyViewModel, MyViewModel>();

在 ViewModel 中:

public class MyViewModel : INotifyPropertyChanged

    public MyViewModel(MyContext myContext)
    
        _myContext = myContext;
    

    public async Task<IEnumerable<Dto>> GetList(string s)
    
        return await _myContext.Table1.where(....)....ToListAsync();
    

在剃刀文件中。

@inject ViewModels.MyViewModel VM
<input id="search" type="text" @bind="search" />
<input id="search" type="button" value="Go" @onclick="SearchChanged" />   
@code 
    string search = "";
    int currentCount = 0;
    async void SearchChanged() 
        currentCount++;
        dtos = GetList(search);
    

但是,有时点击搜索按钮时会出现以下错误?

System.InvalidOperationException: '在前一个操作完成之前,在此上下文上启动了第二个操作。这通常是由使用相同 DbContext 实例的不同线程引起的。有关如何避免 DbContext 线程问题的更多信息,请参阅https://go.microsoft.com/fwlink/?linkid=2097913。'

【问题讨论】:

也许,在第一个请求完成之前禁用搜索按钮。还是取消第一个异步搜索请求,然后发送新请求? 【参考方案1】:

已编辑 2020 年 8 月

官方指导:https://docs.microsoft.com/ca-es/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-3.1 提供多种解决方案。在我看来,post 的最佳方法是“创建新的 DbContext 实例”:

创建具有依赖关系的新 DbContext 的推荐解决方案是使用工厂。

//The factory
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data

    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        
            this.provider = provider;
        

        public TContext CreateDbContext()
        
            if (provider == null)
            
                throw new InvalidOperationException(
                    $"You must configure an instance of IServiceProvider");
            

            return ActivatorUtilities.CreateInstance<TContext>(provider);
        
    

注入工厂:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source=nameof(ContactContext.ContactsDb).db")
    .EnableSensitiveDataLogging());

使用工厂:

private async Task DeleteContactAsync()

    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    

    Filters.Loading = false;

    await ReloadAsync();


我不推荐使用的答案:

您可以尝试为每个请求创建一个新范围:

public class MyViewModel : INotifyPropertyChanged

    
    protected readonly IServiceScopeFactory _ServiceScopeFactory;

    public MyViewModel(IServiceScopeFactory serviceScopeFactory)
    
        _ServiceScopeFactory = serviceScopeFactory;
    

    public async Task<IEnumerable<Dto>> GetList(string s)
    
        using (var scope = _ServiceScopeFactory.CreateScope())
        
            var referenceContext = scope.ServiceProvider.GetService<MyContext>();    
            return await _myContext.Table1.where(....)....ToListAsync();
        
    

在以下屏幕截图中,您可以看到此问题的示例。用户在几个分页元素中快速点击。新请求在前一个请求结束之前开始。

Daniel Roth(Blazor 产品经理)在这里谈论 Using Entity Framework Core with Blazor

【讨论】:

每个方法不会有一个新的 EF 上下文意味着 a) 我们有必须实例化一个新上下文的轻微开销和 b) 我们不能利用缓存(参见 @987654324 @)。使用 semaphoreslim(1,1) 锁定长期存在的 EF 上下文有什么缺点? @DrGriff,在 MVC 上,每个请求都会创建一个新范围。我不相信长期存在的 EF 上下文,只相信批处理命令和 AutoDetectChangesEnabled = false 所以最终的解决方案是:Razor Page @inherits OwningComponentBase&lt;Data.MyService&gt; Startup services.AddDbContext&lt;MyContext&gt;(); services.AddScoped&lt;MyService&gt;(); @DrGriff,我刚刚发布了comment on github "Using Entity Framework Core with Blazor" thread。也许,你读起来很亲切。 @daniherrera 你还在使用这个解决方案来解决你上面链接的 github 问题吗?我遇到了同样的问题,有人可以向按钮发送垃圾邮件并使应用程序崩溃......【参考方案2】:

就我而言,我解决了转向 AddDbContext ServiceLifetime.Transient

        services.AddDbContext<MY_Context>(options =>
             options.UseSqlServer(
    Configuration.GetConnectionString("DefaultConnection")),
 ServiceLifetime.Transient);

【讨论】:

在重负载下它是如何承受的?【参考方案3】:

错误消息与 EF 上下文一次不能执行多个操作有关。

我的理解是,如果您在一个页面上,那么您可以通过 SingalR 连接持续连接到“服务”文件。

如果您的页面通过服务多次调用,则可能是上下文在完成前一个操作之前被调用以执行操作。

我没有在服务的生命周期内创建一个上下文实例,而是在每次调用时创建一个实例。它似乎可以缓解这个问题,但我还不确定它是否被视为“最佳实践”。

所以,例如:

public class MyService

    private MyContext Context => new MyContext(new DbContextOptions<MyContext>()));

    private async Task DoSomething()
    
        await using var context = this.Context;  //New context for the lifetime of this method
        var r = await context.Something
            .Where(d => d....)
            .AsNoTracking()
            .FirstOrDefaultAsync()
            .ConfigureAwait(false);

         // context gets disposed of
         // Other code
    
    private async Task DoSomethingElse()
    
        await using var context = this.Context;   //New context for the lifetime of this method
        var r = await context.Something
            .Where(d => d....)
            .AsNoTracking()
            .FirstOrDefaultAsync()
            .ConfigureAwait(false);

         // context gets disposed of
         // Other code
    

【讨论】:

收到System.ObjectDisposedException: 'Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.的错误【参考方案4】:

我解决了这个问题,但是我认为我失去了工作单元,因为现在我有多个 dbContex :

构造函数:

private AppDbContext _db;
protected override void OnInitialized()

    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();

后来我把它处理掉了:

public void Dispose()

    _db?.Dispose();

【讨论】:

以上是关于Blazor:在前一个操作完成之前在此上下文上启动了第二个操作的主要内容,如果未能解决你的问题,请参考以下文章

UserManager 错误“在前一个操作完成之前在此上下文上启动了第二个操作”

UserManager 错误 - 在前一个异步操作完成之前在此上下文上启动了第二个操作

Entity Framework Core:在前一个操作完成之前在此上下文上启动了第二个操作

在前一个异步操作完成之前,在此上下文上启动了第二个操作。使用“等待”来确保

asp.net core 在前一个操作完成之前在此上下文上启动了第二个操作

对 ITVF 的引用会引发“在前一个操作完成之前在此上下文上启动的第二个操作”异常