DeveloperHA / Learn / EF Core
EF Core learning path

EF Core patterns that prevent surprises

Use DbContext, projections, migrations and concurrency deliberately in real .NET applications.

Practical30–45 min4 lessons

Outcome: Know when to track entities, when to project, how to review migrations and how to make concurrency visible.

Your local progress0 / 4 complete

EF Core is productive because it maps application code to SQL, but the database is still the source of truth. These cards help you see the boundary.

Work through the path

Open a card when you need it. Marking a lesson complete is optional and stays only in this browser.

1Choose tracking based on intent

DbContext5 min

Track entities when you intend to modify them; use no-tracking queries for read-only projections.

Change tracking lets EF Core detect modifications and persist them through SaveChanges. It also has memory and identity-resolution costs that are unnecessary for many read endpoints.

A projection to a DTO is often clearer than loading a large entity graph and mapping it afterward.

Try it

Compare generated SQL and selected columns for an entity query and a DTO projection.

Watch outAsNoTracking is not a universal speed button; measure the query and materialization cost.
Review the SQL shape →
Example
var rows = await db.Orders
    .AsNoTracking()
    .Where(o => o.CustomerId == customerId)
    .Select(o => new OrderListItem(o.Id, o.CreatedAt, o.TotalAmount))
    .ToListAsync(cancellationToken);

2Find N+1 queries before production

Query behavior6 min

Loading a related collection inside a loop can create one query per row; project or include deliberately instead.

N+1 is a behavior problem. A page that loads 100 orders and then queries related data 100 more times will usually become slow as data grows.

Use a projection for the response shape, or Include when you genuinely need an entity graph. Inspect generated SQL and logs.

Try it

Turn on EF Core command logging in development and count SQL statements for one request.

Watch outInclude is not automatically better than projection; it can load more rows than the endpoint needs.
Format the SQL you inspect →
Example
var summaries = await db.Orders
    .AsNoTracking()
    .Select(o => new OrderSummary(
        o.Id, o.Customer.DisplayName,
        o.Lines.Sum(line => line.Quantity)))
    .ToListAsync(cancellationToken);

3Treat migrations as deployment code

Schema changes6 min

A migration can lock tables, rewrite data or fail on existing values, so review it like application code.

Generated migrations are a starting point. Read the operations, check impact on real row counts and decide whether expand-and-contract is safer.

Deploy a schema both old and new application versions can understand before removing an old column or constraint.

Try it

Take a migration that changes a populated column and write down what old application instances do during deployment.

Watch outDo not run destructive production migrations automatically just because they were generated successfully.
Map the database types →
Example
// Migration 1: add a nullable column
// Deploy code that writes old and new values
// Backfill in controlled batches
// Migration 2: enforce the new invariant
// Remove the old column after readers move

4Make lost updates visible

Concurrency6 min

A concurrency token lets EF Core detect that another writer changed a row since you read it.

Without an explicit strategy, the last writer can overwrite someone else. A rowversion or equivalent token turns that silent conflict into an exception you can handle.

The correct response depends on the domain: reload, merge values or apply a server-side rule.

Try it

Open the same record in two contexts, update it in both and observe the second SaveChanges behavior.

Watch outA concurrency token detects conflicts; it does not automatically merge business changes.
Check the SQL type mapping →
Example
public sealed class Order
{
    public Guid Id { get; set; }
    public decimal TotalAmount { get; set; }
    public byte[] Version { get; set; } = [];
}

modelBuilder.Entity<Order>()
    .Property(o => o.Version).IsRowVersion();

No lesson matches that phrase.

Related tools

Turn the concept into a working example with the browser-based tools.

Need a second pair of eyes?

When the quick answer turns into an architecture or delivery decision, use a fixed-price review or talk to DeveloperHA.

See packages All learning paths