Multiple concurrent queries in EF Core

November 1st 2024 EF Core .NET

I recently had to convince someone that the Entity Framework Core DbContext doesn't support running multiple queries in parallel even if the underlying connection does. Since I couldn't immediately find the documentation proving my point, I created a sample project which I decided to polish a little and share publicly.

I created a contrived test which makes multiple asynchronous queries in parallel without awaiting them individually using LINQ:

[TestCase(false)]
[TestCase(true)]
public async Task ParallelQueriesWithSingleDbContext(bool multipleActiveResultSets)
{
    using var dbContext = CreateDbContext(multipleActiveResultSets);

    var action = async () =>
        await Task.WhenAll(
            usernames.Select(username =>
                dbContext.Users.SingleOrDefaultAsync(user => user.Username == username)
            )
        );

    await action.Should().ThrowAsync<InvalidOperationException>();
}

The CreateDbContext method creates a new DbContext with multiple active result sets enabled or disabled in the connection string depending on the input parameter:

private SampleDbContext CreateDbContext(bool multipleActiveResultSets)
{
    var connectionStringBuilder = new SqlConnectionStringBuilder(
        msSqlContainer.GetConnectionString()
    )
    {
        InitialCatalog = databaseName,
        MultipleActiveResultSets = multipleActiveResultSets
    };

    var optionsBuilder = new DbContextOptionsBuilder<SampleDbContext>().UseSqlServer(
        msSqlContainer.GetConnectionString()
    );

    return new SampleDbContext(optionsBuilder.Options);
}

The test passes in both cases, meaning that an InvalidOperationException was thrown even when the database connection allowed multiple active statements because multiple active result sets were enabled:

System.InvalidOperationException: A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.

The link in the error message points at the documentation page which makes it completely clear that a single DbContext instance doesn't support running multiple queries in parallel:

Entity Framework Core does not support multiple parallel operations being run on the same DbContext instance. This includes both parallel execution of async queries and any explicit concurrent use from multiple threads. Therefore, always await async calls immediately, or use separate DbContext instances for operations that execute in parallel.

If you really need to do this (unlike me in my contrived example), you need to use a separate DbContext instance for each query:

[TestCase(false)]
[TestCase(true)]
public async Task ParallelQueriesWithMultipleDbContexts(bool multipleActiveResultSets)
{
    var users = await Task.WhenAll(
        usernames.Select(async username =>
        {
            using var dbContext = CreateDbContext(multipleActiveResultSets);

            return await dbContext.Users.SingleOrDefaultAsync(user =>
                user.Username == username
            );
        })
    );

    users.Should().HaveCount(2);
    users.Should().NotContainNulls();
}

I'm creating instances with explicit constructor calls in my tests. In a real application, you will typically be using the single DbContext instance injected into your class. To be able to create multiple instances, you need to register with dependency injection and inject DbContextFactory instead.

You can find full sample code in my GitHub repository. I'm using Testcontainers to set up Microsoft SQL Server on the fly in Docker and shut it down immediately after running the tests.

DbContext in Entity Framework Core is not thread safe and as such doesn't support running multiple queries in parallel. If you need to do that, you have to create a separate DbContext for each query.

Get notified when a new blog post is published (usually every Friday):

Copyright
Creative Commons License