如何在使用仓储模式的EF Core 3.0中进行并行异步查询?

6

我有一个类似这样的代码库

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: a second operation started on this context before a previous operation completed...

DbContext 在 ConfigureServices 中已注册,如下:

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

教程中提到: Entity Framework Core 不支持在同一 DbContext 实例上运行多个并行操作。
但是!如何在异步情况下使用它与仓库?
3个回答

2

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

每个存储库只能同时进行一个异步请求。如果您需要同时进行多个请求,则需要多个存储库。这可能需要您将存储库工厂注入到您的类型中。


但是为什么呢?两个并行查询等待服务器数据有什么问题吗?使用不同的上下文意味着性能会受到影响,因为上下文在创建时会初始化一些东西 - 我想连接池可以解决这个问题。 - Miha Markic
@MihaMarkic:我可能错了,但我认为原因是单个数据库连接一次只能用于流式传输一个结果集。要流式传输另一个结果集,需要另一个连接。有一种方法可以在一个连接上拥有多个同时的结果集,但我不认为EF支持这种方式。 - Stephen Cleary
我也这么认为。然而,旧的好的ado.net实践是打开一个连接,完成你需要做的事情,然后立即释放它 - 这样它就会返回到池中。我想知道EF团队决定采用不释放单个连接的原因(猜测)。 - Miha Markic
EF提供程序确定如何管理连接,我相信所有知名的提供程序都使用连接池。 - Stephen Cleary
多个活动结果集应该同时处理多个结果,我猜测客户端对每个结果的物化不能进行多线程或排队以进行一次执行...这很遗憾,因为在2.2中可以工作,并且使得同时启动多个可能运行时间较长的查询变得非常容易。 - Bob Provencher
另一种可能的方法是配置DbContextPool以每个查询租用/释放DbContext。 - Arsync

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

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而不是像我示例中创建自定义工厂。

这里没有并行代码。您需要等待两个测试方法。 - Gert Arnold
嗨。我等待它们同时开始(第二个开始不等第一个完成)。 你也可以写成:await Task.WhenAll(test1, test2),因为使用了两个不同的上下文,所以不会出现异常。 - Alessandro Lazzara
我非常希望能看到一个 .Net 5.0 的 DBContextFactory 解决方案。您能否添加一些新代码的示例? - Pangamma

-3

只需编写:

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

没错,EF Core 不支持在同一上下文实例上运行多个并行操作。在开始下一个操作之前,您应该始终等待操作完成。通常通过在每个异步操作上使用 await 关键字来实现此目的。 请参阅 https://learn.microsoft.com/zh-cn/ef/core/querying/async


1
被踩:问题不是如何使这两个查询运行,而是如何并行运行它们。 - dyesdyes
没有可以并行使用的方法。请阅读我的评论。 - Andrei Kostyrin
只要使用不同的上下文实例,即使它们是相同类型的上下文,查询也可以并行运行。 - reasonet

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