How to make parallel async queries in EF Core 3.0 with repository pattern?

末鹿安然 提交于 2020-12-11 09:06:09

问题


I have repository like

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

Constructor of repository (DbContext injection):

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

And call it in EF Core 2.0 like

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

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

await Task.WhenAll(settingsTask, workPositionsTask);

// do other things...

Problem:

With EF Core 3.0 there is InvalidOperationException: a second operation started on this context before a previous operation completed...

DbContext is registered in ConfigureServices like

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

Tutorial says following: Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance.

But! How to use it with repositories in async?


回答1:


How to use it with repositories in async?

You can only have one simultaneous asynchronous request per repository. If you need to have more than one at a time, then you need more than one repository. This may require you to inject a repository factory into your types.




回答2:


Use factory and explicity instantiate context.

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)));

Service class

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();

        }
    }
}

Test class

    //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);

    }

NB: in new .net core 5 you can use buildin AddDbContextFactory instead of create a custom factory as in my example




回答3:


just write:

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

Yeap. EF Core doesn't support multiple parallel operations being run on the same context instance. You should always wait for an operation to complete before beginning the next operation. This is typically done by using the await keyword on each async operation. Look at https://docs.microsoft.com/en-us/ef/core/querying/async



来源:https://stackoverflow.com/questions/58122227/how-to-make-parallel-async-queries-in-ef-core-3-0-with-repository-pattern

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!