Entity Framework DbContext is Not Thread-Safe with C# Code Examples – Part 2

awjanthiel

Entity Framework DbContext is Not Thread-Safe. C# Code Examples – Part 2.

In the first part of this series, we discussed the fundamental fact that the DbContext in Entity Framework is not thread-safe. Sharing the same DbContext instance across multiple threads can easily lead to unpredictable behavior, data corruption, or runtime exceptions. In this continuation, we dive deeper into practical C# scenarios, strategies to work safely with DbContext in multi-threaded environments, and better design patterns to avoid concurrency pitfalls.

Why DbContext is Not Thread-Safe

The DbContext maintains internal state such as change tracking, connection management, and caching during its lifecycle. Concurrent operations on shared state without synchronization introduce race conditions. Since DbContext is lightweight but stateful, the Entity Framework team designed it for usage in a single-threaded unit of work, typically per web request or service call.

Attempting to run multiple asynchronous operations in parallel on the same DbContext instance can cause exceptions such as “A second operation started on this context before a previous operation completed.” This error is a built-in safeguard to detect unsafe parallel access.

Safe Usage Patterns with C# Examples

1. One DbContext Per Thread or Scope

Best practice is to create and use a new DbContext instance for each logical operation scope, such as a web request or background task. Avoid passing the same DbContext instance into multiple threads or parallel tasks.

using(var context = new MyDbContext())
{
    var users = context.Users.Where(u => u.IsActive).ToList();
    // Operations with context are confined to this thread/scope
}

2. Avoid Parallel Operations on One Instance

The following example shows how NOT to use DbContext. Concurrent database calls on the same instance will throw exceptions.

var context = new MyDbContext();

var task1 = context.Users.ToListAsync();
var task2 = context.Products.ToListAsync();

await Task.WhenAll(task1, task2); // This will throw InvalidOperationException

This happens because EF Core does not support multiple concurrent operations on one DbContext.

3. Use DbContextFactory for Parallel Queries

To safely run parallel queries, use IDbContextFactory<T> to create separate DbContext instances for each parallel task. This isolates the operations, preventing threading issues.

public class MyService
{
    private readonly IDbContextFactory<MyDbContext> _contextFactory;

    public MyService(IDbContextFactory<MyDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public async Task LoadDataInParallel()
    {
        var task1 = Task.Run(async () =>
        {
            using var context = _contextFactory.CreateDbContext();
            return await context.Users.ToListAsync();
        });

        var task2 = Task.Run(async () =>
        {
            using var context = _contextFactory.CreateDbContext();
            return await context.Products.ToListAsync();
        });

        await Task.WhenAll(task1, task2);

        var users = await task1;
        var products = await task2;
    }
}

Each task gets its own DbContext instance, so no thread-safety conflicts occur.

Additional Considerations

Async Usage: Always await asynchronous EF Core calls before starting another operation on the same DbContext instance. Sequentially awaiting operations prevents overlapping database calls.

Dependency Injection: When using DI in ASP.NET Core, configure DbContext lifetimes appropriately. Scoped lifetime is recommended per web request. Avoid using singleton lifetime for DbContext.

Long-Running Operations: For background services or multi-threaded scenarios, prefer creating distinct DbContext instances per thread or background task using factories.

Summary

The DbContext class from Entity Framework Core is built to be used in a single-threaded manner. Sharing a single instance across threads or performing parallel operations on it is unsafe and causes exceptions.

To handle multi-threaded or parallel operations involving database queries:

  • Create separate DbContext instances per thread or task
  • Use IDbContextFactory<T> for creating isolated contexts
  • Await all async operations before starting new work on the same instance
  • Follow dependency injection best practices for DbContext lifetime management

By following these practices, you ensure stable, consistent, and thread-safe database interactions in your C# applications using Entity Framework Core.