在ASP.NET Core中使用DbContext注入进行并行EF Core查询

8
我正在编写一个ASP.NET Core Web应用程序,需要从数据库中获取一些表的所有数据,以便稍后将其组织成可读格式进行分析。
我的问题是,这些数据可能非常庞大,为了提高性能,我决定同时获取这些数据而不是逐个表获取。
我的问题是,我不太理解如何通过继承依赖注入来实现这一点,因为为了能够进行并行工作,我需要为每个并行工作实例化DbContext
以下代码会产生此异常:
---> (Inner Exception #6) 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.
Object name: 'MyDbContext'.
   at Microsoft.EntityFrameworkCore.DbContext.CheckDisposed()
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_ChangeTracker()

ASP.NET Core项目:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddDistributedMemoryCache();

    services.AddDbContext<AmsdbaContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("ConnectionString"))
            .UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));

    services.AddSession(options =>
    {
        options.Cookie.HttpOnly = true;
    });
}

public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory)
{
    if (HostingEnvironment.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    loggerFactory.AddLog4Net();
    app.UseStaticFiles();
    app.UseCookiePolicy();
    app.UseSession();
    app.UseMvc();
}

控制器的动作方法:

[HttpPost("[controller]/[action]")]
public ActionResult GenerateAllData()
{
    List<CardData> cardsData;

    using (var scope = _serviceScopeFactory.CreateScope())
    using (var dataFetcher = new DataFetcher(scope))
    {
        cardsData = dataFetcher.GetAllData(); // Calling the method that invokes the method 'InitializeData' from below code
    }

    return something...;
}

.NET Core库项目:

DataFetcher的InitializeData - 根据一些无关参数获取所有表记录:

private void InitializeData()
{
    var tbl1task = GetTbl1FromDatabaseTask();
    var tbl2task = GetTbl2FromDatabaseTask();
    var tbl3task = GetTbl3FromDatabaseTask();

    var tasks = new List<Task>
    {
        tbl1task,
        tbl2task,
        tbl3task,
    };

    Task.WaitAll(tasks.ToArray());

    Tbl1 = tbl1task.Result;
    Tbl2 = tbl2task.Result;
    Tbl3 = tbl3task.Result;
}

DataFetcher的示例任务:

private async Task<List<SomeData>> GetTbl1FromDatabaseTask()
{
    using (var amsdbaContext = _serviceScope.ServiceProvider.GetRequiredService<AmsdbaContext>())
    {
        amsdbaContext.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
        return await amsdbaContext.StagingRule.Where(x => x.SectionId == _sectionId).ToListAsync();
    }
}
2个回答

8

我不确定在这里您是否确实需要多个上下文。在EF Core文档中,有这样一个引人注目的警告:

警告

EF Core不支持在同一上下文实例上运行多个并行操作。您应该在开始下一个操作之前始终等待操作完成。通常可以通过在每个异步操作上使用await关键字来实现。

这并不完全准确,或者说,它的措辞有些令人困惑。实际上,您可以在单个上下文实例上运行并行查询。问题在于EF的更改跟踪和对象修复。这些类型的事情不支持同时发生多个操作,因为它们需要有一个稳定的状态来进行工作。但是,这确实限制了您执行某些操作的能力。例如,如果您要运行并行保存/选择查询,则结果可能会混乱。你可能无法得到现在实际存在的东西,或者当它试图创建必要的插入/更新语句时,更改跟踪可能会出错等等。但是,如果您正在执行非原子查询,例如在此处所需的独立表的选择操作,则没有真正的问题,特别是如果您不打算对选择出的实体进行编辑等进一步操作,只是打算将它们返回到一个视图或其他地方。

如果您确实确定需要单独的上下文,最好的方法是在using中使用新的上下文。我以前没有尝试过这个,但是您应该能够将DbContextOptions < AmsdbaContext >注入到正在发生这些操作的类中。由于当服务集合实例化时,它已被注入到您的上下文中,所以它应该已经注册到服务集合中。如果没有,您可以随时构建一个新的上下文:

var options = new DbContextOptionsBuilder()
    .UseSqlServer(connectionString)
    .Build()
    .Options;

在任何一种情况下,执行以下操作:
List<Tbl1> tbl1data;
List<Tbl2> tbl2data;
List<Tbl3> tbl3data;

using (var tbl1Context = new AmsdbaContext(options))
using (var tbl2Context = new AmsdbaContext(options))
using (var tbl3Context = new AmsdbaContext(options))
{
    var tbl1task = tbl1Context.Tbl1.ToListAsync();
    var tbl2task = tbl2Context.Tbl2.ToListAsync();
    var tbl3task = tbl3Context.Tbl3.ToListAsync();

    tbl1data = await tbl1task;
    tbl2data = await tbl2task;
    tbl3data = await tbl3task;
}

最好使用await来获取实际结果。这样,您甚至不需要WaitAll/WhenAll/等等,并且您不会在调用Result时阻塞。由于任务返回热点或已启动,只需在每个任务创建后延迟调用await即可获得并行处理。
只需注意,在usings中选择您需要的所有内容。现在,由于EF Core支持懒加载,如果您正在使用它,则尝试访问未加载的引用或集合属性将触发ObjectDisposedException,因为上下文将不存在。

谢谢你详细的回答,但是如果我需要做10-20个类似的查询怎么办? 我必须重复使用关键词吗? - MustafaOmar
您能详细说明为什么使用await比使用WaitAll/WhenAll更好吗?WhenAny呢?另外,使用单个DbContext是否会导致其跟踪或缓存以某种方式被破坏,并影响稍后调用SaveChanges的上游调用者?如果是这样,我会说警告是正确的,因为默认情况下DbContext的作用域限制在请求范围内,而您的方法进行并行查询不能保证另一个方法将如何处理相同的DbContext实例。 - xr280xr

1
简单的答案是 - 你不需要这样做。你需要一种替代方式来生成DbContext实例。标准方法是在同一个HttpRequest中获取所有请求的DbContext相同的实例。你可能可以覆盖ServiceLifetime,但那会改变所有请求的行为。
你可以注册第二个DbContext (子类、接口) ,使用不同的服务生命周期。即使如此,你仍然需要手动处理创建过程,因为你需要为每个线程调用它一次。
你需要手动创建它们。
标准DI到此结束。它非常缺乏,甚至与旧的MS DI框架相比,在那里你可能可以设置一个单独的处理类,带有一个属性来覆盖创建过程。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接