Entity Framework Core
DbContext
Async Programming
.NET
SaveChangesAsync

.Net EF core DbContext.Save during multiple Async functions

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

DbContext.SaveChangesAsync() works well in asynchronous code, but one rule matters more than anything else: a single DbContext instance is not thread-safe and must not run multiple operations at the same time. Most problems in “multiple async functions” scenarios come from sharing one context across parallel work.

The Safe Mental Model

Think of one DbContext as one unit of work. You can call asynchronous EF Core methods on it, but you must await each operation before starting the next one on the same instance.

Safe:

csharp
1public async Task UpdateOrderAsync(AppDbContext db, int id)
2{
3    var order = await db.Orders.FindAsync(id);
4    order!.Status = "Processed";
5    await db.SaveChangesAsync();
6}

Unsafe parallel use of the same context:

csharp
1public async Task BrokenAsync(AppDbContext db)
2{
3    var task1 = db.Orders.CountAsync();
4    var task2 = db.Customers.CountAsync();
5
6    await Task.WhenAll(task1, task2);
7}

That second example can throw because both queries try to use the same context concurrently.

async Does Not Mean “Safe to Share”

It is easy to think that because a method is async, the context can be passed around freely to many asynchronous functions. That is not the rule. The real rule is:

  • asynchronous use is fine
  • overlapping use on the same DbContext instance is not

This is why code such as Task.WhenAll around multiple EF Core calls often breaks when those calls share one scoped context.

Await Sequentially on One Context

If the operations belong to one transaction or unit of work, keep them sequential:

csharp
1public async Task ProcessAsync(AppDbContext db, int orderId)
2{
3    var order = await db.Orders.FindAsync(orderId);
4    var customer = await db.Customers.FindAsync(order!.CustomerId);
5
6    order.Status = "Processed";
7    customer!.LastOrderProcessedAt = DateTime.UtcNow;
8
9    await db.SaveChangesAsync();
10}

This is a normal pattern. One context tracks the changes, and one SaveChangesAsync() persists them at the end.

Use Separate Contexts for Real Parallelism

If you truly need parallel database work, create separate contexts:

csharp
1public async Task<int[]> CountInParallelAsync(IDbContextFactory<AppDbContext> factory)
2{
3    var task1 = Task.Run(async () =>
4    {
5        await using var db = await factory.CreateDbContextAsync();
6        return await db.Orders.CountAsync();
7    });
8
9    var task2 = Task.Run(async () =>
10    {
11        await using var db = await factory.CreateDbContextAsync();
12        return await db.Customers.CountAsync();
13    });
14
15    return await Task.WhenAll(task1, task2);
16}

Each parallel task gets its own context instance. That is the correct pattern when concurrency is actually required.

Save Once When It Makes Sense

Another common mistake is calling SaveChangesAsync() after every small update inside a larger workflow. Often it is cleaner and cheaper to:

  • load entities
  • modify them
  • save once at the end
csharp
1public async Task CreateInvoiceAsync(AppDbContext db)
2{
3    db.Invoices.Add(new Invoice { Number = "INV-001" });
4    db.AuditEntries.Add(new AuditEntry { Message = "Invoice created" });
5
6    await db.SaveChangesAsync();
7}

This reduces round-trips and keeps the unit of work coherent.

Use Transactions for Multi-Step Consistency

If several async steps must succeed or fail together, combine sequential context usage with a transaction:

csharp
1public async Task TransferAsync(AppDbContext db, int fromId, int toId, decimal amount)
2{
3    await using var tx = await db.Database.BeginTransactionAsync();
4
5    var from = await db.Accounts.FindAsync(fromId);
6    var to = await db.Accounts.FindAsync(toId);
7
8    from!.Balance -= amount;
9    to!.Balance += amount;
10
11    await db.SaveChangesAsync();
12    await tx.CommitAsync();
13}

This keeps the logic async while preserving consistency without resorting to unsafe shared parallel operations.

Common Pitfalls

  • Starting multiple EF Core operations concurrently on the same DbContext.
  • Passing one scoped context through several async methods and then wrapping them in Task.WhenAll.
  • Calling SaveChangesAsync() repeatedly when one save at the end of the unit of work would do.
  • Mistaking asynchronous code for thread-safe code.
  • Using one context for long-running background or parallel jobs instead of creating separate instances.

Summary

  • A single DbContext instance must not be used concurrently.
  • Asynchronous EF Core code is fine as long as each operation is awaited before the next begins on that context.
  • Use separate contexts when you truly need parallel database work.
  • Save once per unit of work when practical instead of saving after every tiny change.
  • Add transactions when several async steps must commit together consistently.

Course illustration
Course illustration

All Rights Reserved.