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

Tom*_*ing 5 c# dependency-injection entity-framework-core asp.net-core

我编写了一个 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()
Run Code Online (Sandbox Code Playgroud)

ASP.NET 核心项目:

启动.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();
}
Run Code Online (Sandbox Code Playgroud)

控制器的动作方法:

[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...;
}
Run Code Online (Sandbox Code Playgroud)

.NET 核心库项目:

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;
}
Run Code Online (Sandbox Code Playgroud)

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

Chr*_*att 6

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

警告

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

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

如果你真的确定你需要单独的上下文,你最好的选择是使用 using 来更新你的上下文。我之前实际上没有尝试过,但是您应该能够将DbContextOptions<AmsdbaContext>这些操作注入到您的班级中。它应该已经在服务集合中注册,因为它在服务集合实例化时注入到您的上下文中。如果没有,您总是可以构建一个新的:

var options = new DbContextOptionsBuilder()
    .UseSqlServer(connectionString)
    .Build()
    .Options;
Run Code Online (Sandbox Code Playgroud)

在任何一种情况下,那么:

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;
}
Run Code Online (Sandbox Code Playgroud)

最好使用await来获得实际结果。这样,您甚至不需要WaitAll/ WhenAll/etc。并且您不会阻止对Result. 由于任务返回热值,或者已经开始,只需推迟调用 await 直到每个任务都被创建就足以购买并行处理。

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


小智 1

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

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

  • 您手动创建它们。

标准DI到这里就结束了。即使与较旧的 MS DI 框架相比,它也是相当缺乏的,在旧的 MS DI 框架中,您可以使用一个属性来建立一个单独的处理类来覆盖创建。