Blazor:在上一个操作完成之前,第二个操作在此上下文中开始

Blazor: A second operation started on this context before a previous operation completed

我正在创建服务器端 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();
    }

并且在 razor 文件中。

@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: 'A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.'

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

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

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

我没有在服务的生命周期内拥有一个上下文实例,而是为每次调用创建一个实例。它似乎减轻了这个问题,但它是否被视为 "best practice" 我还不确定。

因此,例如:

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

已编辑 2020 年 8 月

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

The recommended solution to create a new DbContext with dependencies is to use a factory.

//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

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

构造函数:

private AppDbContext _db;
protected override void OnInitialized()
{
    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();
}

后来我处理了它:

public void Dispose()
{
    _db?.Dispose();
}

在我的例子中,我解决了转AddDbContext ServiceLifetime.Transient

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