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
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.
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
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.
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
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.
// 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
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.
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.