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

Posted

技术标签:

【中文标题】在 ASP.NET Core 中使用 DbContext 注入并行 EF Core 查询【英文标题】:Parallel EF Core queries with DbContext injection in ASP.NET Core 【发布时间】:2019-03-22 05:30:34 【问题描述】:

我正在编写一个 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();
    

【问题讨论】:

【参考方案1】:

我不确定您是否真的需要多个上下文。您已经注意到,在 EF Core 文档中,有一个明显的警告:

警告

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

这并不完全准确,或者更确切地说,它只是措辞有些混乱。您实际上可以在单个上下文实例上运行并行查询。该问题与 EF 的更改跟踪和对象修复有关。这些类型的事物不支持同时发生多个操作,因为它们在工作时需要有一个稳定的状态才能工作。然而,这实际上只是限制了你做某些事情的能力。例如,如果您要运行并行保存/选择查询,结果可能会出现乱码。您可能无法取回现在实际存在的内容,或者在尝试创建必要的插入/更新语句等时更改跟踪可能会变得混乱。但是,如果您正在执行非原子查询,例如在独立表上进行选择正如您希望在这里做的那样,没有真正的问题,特别是如果您不打算对您选择的实体进行进一步的操作,例如编辑您选择的实体,而只是计划将它们返回到视图或其他内容。

如果您确实确定需要单独的上下文,最好的办法是使用 using 来更新您的上下文。我之前实际上没有尝试过,但是您应该能够将DbContextOptions&lt;AmsdbaContext&gt; 注入到您正在发生这些操作的类中。它应该已经在服务集合中注册,因为它是在服务集合实例化时注入到您的上下文中的。如果没有,您可以随时构建一个新的:

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/etc。而且您不会阻止对Result 的呼叫。由于任务返回热,或者已经开始,只需推迟调用 await 直到每个都被创建就足以为您购买并行处理。

请注意在使用中选择所需的所有内容。现在 EF Core 支持延迟加载,如果您使用延迟加载,尝试访问尚未加载的引用或集合属性将触发 ObjectDisposedException,因为上下文将消失。

【讨论】:

感谢您的详细回答,但如果我需要进行 10-20 次类似的查询怎么办?我必须使用重复关键字吗??【参考方案2】:

简单的答案是 - 你没有。您需要另一种方法来生成 dbcontext 实例。标准方法是在同一个 HttpRequest 中对 DbContext 的所有请求获取相同的实例。您可以覆盖 ServiceLifetime,但这会改变所有请求的行为。

您可以注册具有不同服务生命周期的第二个 DbContext(子类、接口)。即使这样,您也需要手动处理创建,因为您需要为每个线程调用一次。

您可以手动创建它们。

标准 DI 到此结束。即使与旧的 MS DI 框架相比,它也很缺乏,在旧框架中,您可以建立一个带有属性的单独处理类来覆盖创建。

【讨论】:

以上是关于在 ASP.NET Core 中使用 DbContext 注入并行 EF Core 查询的主要内容,如果未能解决你的问题,请参考以下文章

在 ASP.NET Core 6.0 中使用 Serilog

如何在 ASP.Net Core 中使用 IHostedService

ASP.NET Core Web 应用程序系列- 在ASP.NET Core中使用Autofac替换自带DI进行批量依赖注入(MVC当中应用)

asp.net core中托管SPA应用

如何使用 EF Core 在 ASP.NET Core 中取消应用迁移

ASP.NET Core中使用Swagger