【问题标题】:How to make parallel async queries in EF Core 3.0 with repository pattern?如何使用存储库模式在 EF Core 3.0 中进行并行异步查询?
【发布时间】:2019-09-26 17:51:18
【问题描述】:

我有类似的存储库

public interface IEmployeeRepository
{
  Task<EmployeeSettings> GetEmployeeSettings(int employeeId);
  Task<ICollection<DepartmentWorkPosition>> GetWorkPositions(int employeeId);
}

存储库的构造函数(DbContext 注入):

public EmployeeRepository(EmployeeDbContext dbContext)
{
  _dbContext = dbContext;
}

并在 EF Core 2.0 中调用它

var settingsTask = _employeeRepository
    .GetEmployeeSettings(employeeId.Value);

var workPositionsTask = _employeeRepository
    .GetWorkPositions(employeeId.Value);

await Task.WhenAll(settingsTask, workPositionsTask);

// do other things...

问题:

在 EF Core 3.0 中存在 InvalidOperationException:在前一个操作完成之前在此上下文上启动了第二个操作...

DbContext 是在 ConfigureServices 中注册的

services.AddDbContext<EmployeeDbContext>(ServiceLifetime.Transient);

教程如下: Entity Framework Core 不支持在同一个 DbContext 实例上运行多个并行操作。

但是!如何在异步存储库中使用它?

【问题讨论】:

    标签: c# repository-pattern ef-core-3.0


    【解决方案1】:

    如何在异步存储库中使用它?

    每个存储库只能同时有一个异步请求。如果您一次需要多个存储库,那么您需要多个存储库。这可能需要您将存储库 factory 注入到您的类型中。

    【讨论】:

    • 但是为什么呢?等待来自服务器的数据的两个并行查询有什么问题?使用不同的上下文意味着性能下降,因为上下文在创建时会初始化一些东西——我猜池化可以解决这个问题。
    • @MihaMarkic:我可能是错的,但我相信原因是单个数据库连接一次只能用于流式传输一个结果集。要流式传输另一个,则需要另一个连接。有一种方法可以让每个连接同时生成多个结果集,但我认为 EF 不支持。
    • 我也这么认为。但是,旧的 ado.net 良好做法是打开一个连接,做你必须做的事情并在之后立即处理它 - 所以它会回到池中。我想知道是什么决定促使 EF 团队使用一个根本没有释放的连接(猜测)。
    • EF 提供商决定如何管理连接,我相信所有大牌提供商都使用连接池。
    • 多个活动结果集一次应该处理多个结果,我的猜测是每个结果的客户端物化不能是多线程的或一次执行一个排队......很遗憾,因为这在 2.2 中有效,并且可以很容易地同时启动多个可能长时间运行的查询。
    【解决方案2】:

    使用工厂和显式实例化上下文。

    Startup.cs

    //classical dbcontext registration
    services.AddDbContext<TestDB>(
                options => options.UseSqlServer(
                    Configuration.GetConnectionString("Test")));
    
    //factory
    //in case we want parallellize more queries at the same request, we can't use the same connection. So, because dbcontext is instantiate at request time this would  generate exception, so we need to use factory and explicit "using" to explicitly manage dbcontext lifetime
    var optionsBuilder = new DbContextOptionsBuilder<TestDB>();
            optionsBuilder.UseSqlServer(Configuration.GetConnectionString("Test"));
            services.AddSingleton(s => new Func<TestDB>(() => new TestDB(optionsBuilder.Options)));
    

    服务类

    public class TestService
    {
        private readonly TestDB _testDb;
        private readonly Func<TestDB> _testDbfunct;
    
        public TestService(TestDB testDb, Func<TestDB> testDbfunct)
        {
            _testDb = testDb;
            _testDbfunct = testDbfunct;
        }
    
        //mixed classical request dbcontext and factory approaches
        public async Task<string> TestMultiple(int id, bool newConnection = false) //we need to add optional newConnection parameter and the end of other parameters
        {
            //use request connection (_testDb) if newconnection is false, otherwise instantiate a new connection using factory. null inside "using" means that "using" is not used
            //use newconnection = true if you want run parallel queries, so you need different connection for each one
            TestDB testDb = _testDb;
            using (newConnection ? testDb = _testDbfunct() : null)
            {
                return await (from t in testDb.Table where t.id == id select t.code).FirstOrDefaultAsync();
    
            }
        }
    }
    

    测试类

        //instantiate dbcontext for each call, so we can parallellize
        [TestMethod]
        public async Task TestMultiple()
        { 
            //test1 and test2 starts in parallel without test2 that need to wait the end of test1. For each one a Task in returned
            var test1 = _testService.TestMultiple(1,true);
            var test2 = _testService.TestMultiple(2,true);
    
            //wait test1 and test2 return
            string code1 = await test1;
            string code2 = await test2;
    
        }
        
        //use request dbcontext
        [TestMethod]
        public async Task TestClassic()
        {
            string code = await _testService.TestMultiple(3);
    
        }
    

    注意:在新的 .net core 5 中,您可以使用内置 AddDbContextFactory 而不是像我的示例中那样创建自定义工厂

    【讨论】:

    • 这里没有并行代码。您等待两种测试方法。
    • 嗨。我在他们都开始之后等待(第二个开始不要等待第一个完成)。 Yon 也可以这样写: await Task.WhenAll(test1, test2) 并且你没有异常,因为使用了 2 个不同的上下文;
    • 我很想看到一个 .Net 5.0 DBContextFactory 解决方案。你能添加一个新代码的例子吗?
    【解决方案3】:

    只写:

    var settings = await _employeeRepository.GetEmployeeSettings(employeeId.Value);
    var workPositions = await _employeeRepository.GetWorkPositions(employeeId.Value);
    

    是的。 EF Core 不支持在同一个上下文实例上运行多个并行操作。在开始下一个操作之前,您应该始终等待操作完成。这通常通过在每个异步操作上使用 await 关键字来完成。 看https://docs.microsoft.com/en-us/ef/core/querying/async

    【讨论】:

    • 否决:问题不是如何让这两个查询运行,而是如何并行运行它们
    • 没有办法并行使用。请阅读我的评论。
    猜你喜欢
    • 2020-02-18
    • 2023-03-19
    • 2020-06-22
    • 1970-01-01
    • 2016-09-22
    • 1970-01-01
    • 2017-03-21
    • 1970-01-01
    • 2020-03-13
    相关资源
    最近更新 更多