Learn / .NET API Engineering Path
EF Core Write Transactions and Concurrency
Make write paths safe under retries, concurrency conflicts, and partial failures with explicit transaction boundaries.
Course: .NET API Engineering Path. Level: Advanced. Topic: Typed backend.
Stage: advanced - Advanced data access - Write safety, transactions, and concurrency. Treat writes as production workflows with transaction ownership, idempotency, and explicit conflict behavior.
Outcomes
- Wrap multi-step writes in clear transactions.
- Handle optimistic concurrency conflicts.
- Keep idempotency decisions visible in review.
- Create a DbContext and entity mappings.
- Use migrations to evolve a relational schema.
- Write efficient queries and keep persistence concerns contained.
Concepts
- transaction
- row version
- idempotency key
- SaveChangesAsync
- Data Access
- DbContext is a unit of work
- Query boundaries and performance
- Modern C# and .NET foundations
- Guided practice
Concept flow
Show how write safety, transactions, and concurrency moves from trigger to implementation outcome in .NET APIs.
- Endpoint
- Command handler
- Transaction
- DbContext
- Outbox
- Conflict response
Session flow
- Model transaction (concept, 11 min) — Name the decisions behind transaction before writing code.
- Wrap multi-step writes in clear transactions.
- Explain where transaction belongs in order management API.
- Build the vertical slice (walkthrough, 19 min) — Implement the smallest useful slice in Orders/ConfirmOrderHandler.cs.
- Handle optimistic concurrency conflicts.
- Connect row version to a working example.
- Verify and harden (exercise, 13 min) — Write a test for stale updates.
- Keep idempotency decisions visible in review.
- Record one risk or follow-up before moving on.
- Practical EF Core Data Access: DbContext is a unit of work (concept, 50 min) — EF Core tracks entity changes inside a DbContext. In ASP.NET Core, a DbContext is usually scoped to one request. You query, modify entities, and call SaveChangesAsync to persist changes inside that unit of work.
- Retained source example: DbContext and entity
public sealed class CatalogDbContext(DbContextOptions<CatalogDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(product => product.Id);
entity.Property(product => product.Name).HasMaxLength(200).IsRequired();
entity.Property(product => product.Price).HasPrecision(18, 2);
});
}
}
public sealed class Product
{
public Guid Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
}
Explicit mapping protects the database schema from accidental defaults such as unlimited strings or imprecise money columns.
- Practical EF Core Data Access: Query boundaries and performance (walkthrough, 50 min) — EF Core LINQ queries are translated to SQL when possible. Keep filtering, sorting, and paging in the database. Use AsNoTracking for read-only queries and project to DTOs when you do not need full tracked entities.
- Retained source example: Read model projection
var page = await db.Products
.AsNoTracking()
.Where(product => product.Price >= minimumPrice)
.OrderBy(product => product.Name)
.Select(product => new ProductListItem(product.Id, product.Name, product.Price))
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
The query projects only the fields needed by the response and avoids change tracking for read-only data.
- Practical EF Core Data Access: practices and mistakes (review, 5 min) — Retained guidance from the legacy PTesting lesson.
- Practice: Keep migrations in source control and review generated schema changes.
- Practice: Use parameterized LINQ or SQL APIs rather than string-concatenated SQL.
- Practice: Add indexes based on query patterns, not guesses.
- Practice: Use transactions around multi-step changes that must commit atomically.
- Avoid: Returning IQueryable from application services and leaking persistence decisions upward.
- Avoid: Loading entire tables into memory before filtering.
- Avoid: Ignoring N+1 query patterns when navigating relationships.
- Avoid: Using the EF in-memory provider as a substitute for relational integration tests.
- Practical EF Core Data Access: references (review, 2 min) — Original references retained from the legacy library.
- Entity Framework Core: https://learn.microsoft.com/ef/core/
- EF Core performance: https://learn.microsoft.com/ef/core/performance/
Code example
C# in Orders/ConfirmOrderHandler.cs.
public async Task<Results<NoContent, Conflict>> ConfirmAsync(Guid id, byte[] version, CancellationToken ct)
{
var order = await db.Orders.SingleOrDefaultAsync(item => item.Id == id, ct);
if (order is null) return TypedResults.Conflict();
db.Entry(order).Property("RowVersion").OriginalValue = version;
order.Confirm();
try
{
await db.SaveChangesAsync(ct);
return TypedResults.NoContent();
}
catch (DbUpdateConcurrencyException)
{
return TypedResults.Conflict();
}
}
Walkthrough examples
- EF Core Write Transactions and Concurrency in a order management API — A team is extending a production-grade ASP.NET Core order API and needs this lesson's pattern to be clear enough for review, testing, and future maintenance.
- File: Orders/ConfirmOrderHandler.cs
- File: src/Orders.Application/ef-core-write-transactions-and-concurrency.cs
- File: tests/Orders.Api.Tests/ef-core-write-transactions-and-concurrency.Tests.cs
- File: docs/dotnet-advanced/ef-core-write-transactions-and-concurrency.md
- Start from the provided C# snippet and make the intent visible in names and boundaries.
- Apply the checklist item "Transaction boundary is named" before adding extra behavior.
- Write down how the implementation changes when row version fails or becomes slow.
- DbContext and entity — Explicit mapping protects the database schema from accidental defaults such as unlimited strings or imprecise money columns.
- Retained source code:
public sealed class CatalogDbContext(DbContextOptions<CatalogDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>(entity =>
{
entity.HasKey(product => product.Id);
entity.Property(product => product.Name).HasMaxLength(200).IsRequired();
entity.Property(product => product.Price).HasPrecision(18, 2);
});
}
}
public sealed class Product
{
public Guid Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
}
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Read model projection — The query projects only the fields needed by the response and avoids change tracking for read-only data.
- Retained source code:
var page = await db.Products
.AsNoTracking()
.Where(product => product.Price >= minimumPrice)
.OrderBy(product => product.Name)
.Select(product => new ProductListItem(product.Id, product.Name, product.Price))
.Skip(pageIndex * pageSize)
.Take(pageSize)
.ToListAsync(cancellationToken);
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
- Migration workflow — Migrations record schema changes in source control and apply them to a database deliberately.
- Retained source code:
dotnet ef migrations add AddProducts
dotnet ef database update
- Explain the expected behavior.
- Compare the example with the canonical PTLearn implementation.
Practice
- Add a row-version field to one write model.
- Map concurrency failure to a typed conflict response.
- Write a test for stale updates.
- Choose the read-only optimization: Which EF Core method should you add for a read-only query that does not modify returned entities?
- Starter code: db.Products.____().Where(...)
- Expected output: AsNoTracking
- Hint: It disables change tracking for entities returned by the query.
- Reference solution: Use AsNoTracking for read-only queries.
- Accepted answers: AsNoTracking | AsNoTracking()
Checklist
- Transaction boundary is named
- Concurrency conflict is handled
- Retry/idempotency decision is documented
- Write test covers stale state
Quiz prompts
- What is the most reviewable response to a stale row-version update? — Concurrency conflicts are expected business failures and deserve explicit contract behavior.
- A teammate wants to hide transaction inside a convenient helper. What should you check first? — Place transaction at the boundary that keeps order management API behavior explicit, testable, and reviewable.
- Which artifact best proves this .NET APIs lesson is ready for review? — Production-ready learning needs evidence: a test, trace, command, screenshot, or log that catches the risk again.
- Advanced data access: a teammate says the happy path works, but "Transaction boundaries" is still implicit. What should you ask for before merging? — Transaction boundaries belongs in the advanced stage only when the decision is visible, testable, and tied to a realistic failure mode.
- A reviewer has five minutes to evaluate this advanced .NET APIs slice. Which evidence is strongest? — Upgrade the order write path with a transaction boundary, stale-update conflict handling, and an idempotency decision note.
- Why project to DTOs in database queries? — Projection improves API boundaries and can reduce database and network work.
Flashcards
- Advanced data access: what decision does "Transaction boundaries" force you to make? Name where a multi-step write starts, commits, rolls back, and emits follow-up work. Evidence prompt: Wrap one multi-step order write in a transaction and document what happens after rollback.
- Advanced data access: what decision does "Optimistic concurrency" force you to make? Use row versions or equivalent tokens to detect stale writes without locking every reader. Evidence prompt: Write a stale update test and map the conflict to a typed API response.
- Advanced data access: what decision does "Idempotency review" force you to make? Decide which commands can be retried safely and which need explicit idempotency keys. Evidence prompt: Add an idempotency note for one command and name the duplicate-request behavior.
- In .NET APIs, what should you remember about transaction? transaction matters here because it supports "Wrap multi-step writes in clear transactions.".
- In .NET APIs, what should you remember about row version? row version matters here because it supports "Handle optimistic concurrency conflicts.".
- In .NET APIs, what should you remember about idempotency key? idempotency key matters here because it supports "Keep idempotency decisions visible in review.".
- In .NET APIs, what should you remember about SaveChangesAsync? SaveChangesAsync matters here because it supports "Wrap multi-step writes in clear transactions.".
Labs
- Ship a ef core write transactions and concurrency slice — Extend a production-grade ASP.NET Core order API with a small but reviewable feature that proves the lesson's architecture in code.
- Upgrade the order write path with a transaction boundary, stale-update conflict handling, and an idempotency decision note.
- Wrap one multi-step order write in a transaction and document what happens after rollback.
- Write a stale update test and map the conflict to a typed API response.
- Add an idempotency note for one command and name the duplicate-request behavior.
- Add a row-version field to one write model.
- Map concurrency failure to a typed conflict response.
- The lab demonstrates the advanced data access outcome without skipping earlier contract evidence.
- Each subtopic has a concrete artifact: Transaction boundaries, Optimistic concurrency, Idempotency review.
- The review notes explain how this level changes ownership, verification, or operational risk.
Challenge
- Review-ready ef core write transactions and concurrency (Core) — Upgrade the order write path with a transaction boundary, stale-update conflict handling, and an idempotency decision note.
- All checklist items are either implemented or documented with a reason.
- The change can be understood from Orders/ConfirmOrderHandler.cs plus one short note.
- The concept diagram names ownership, failure handling, and verification points.
- A teammate could run the verification steps without asking for hidden context.
Canonical lesson URL