EF Core
DbContext and DbSet, the change tracker and AsNoTracking, loading strategies, the N+1 problem, migrations, and optimistic concurrency.
11 questions
JuniorTheoryVery commonWhat is a DbContext in the object-relational mapper EF Core, and what does a DbSet<T> property represent?
What is a DbContext in the object-relational mapper EF Core, and what does a DbSet<T> property represent?
DbContext is the session with the database: it holds the model, the connection and the change tracker, and SaveChanges flushes the pending edits in one go. Each DbSet<T> property is the queryable collection of one entity type and the entry point for Add and Remove on it.
Common mistakes
- ✗Treating
DbContextas thread-safe and sharing one instance across concurrent requests - ✗Thinking an edit to a tracked entity reaches the database before
SaveChangesis called - ✗Seeing
DbSet<T>as an in-memory list rather than a queryable over a table
Follow-up questions
- →Why does
AddDbContextregister the context with a scoped lifetime? - →What happens when you query the same entity twice through one
DbContext?
MiddleTheoryVery commonWhat does the change tracker of the object-relational mapper EF Core do, and what does AsNoTracking skip?
What does the change tracker of the object-relational mapper EF Core do, and what does AsNoTracking skip?
A tracked query puts each entity into the ChangeTracker with a snapshot of its original values and resolves identity, so one key yields one instance. SaveChanges diffs the snapshots against the current values to build the UPDATEs. AsNoTracking skips the snapshot and identity resolution — cheaper reads, but the entities cannot be saved back.
Common mistakes
- ✗Using
AsNoTracking, then editing the entity and callingSaveChanges, expecting anUPDATE - ✗Thinking tracking caches rows so that a repeated query avoids the database
- ✗Missing that identity resolution is what makes one key return one instance
Follow-up questions
- →When does
AsNoTrackingWithIdentityResolutionearn its extra cost? - →How do
AttachandUpdatemake a detached entity saveable again?
JuniorTheoryCommonHow do you register a DbContext so a service receives one, and what lifetime does it get?
How do you register a DbContext so a service receives one, and what lifetime does it get?
builder.Services.AddDbContext<AppDbContext>(...) registers the context and configures its provider. The registration is scoped: the container builds one instance per request and disposes it with the scope. A service takes AppDbContext as a constructor parameter, never newing one.
Common mistakes
- ✗Registering the context as a singleton, sharing one change tracker across requests
- ✗Creating a
DbContextwithnewinstead of taking it from the container - ✗Assuming two injections of
AppDbContextin one request are different instances
Follow-up questions
- →What breaks when a singleton service takes a scoped
DbContextin its constructor? - →What does
AddDbContextPoolchange about how instances are reused?
MiddleTheoryCommonWhy is a DbContext an implicit implementation of the persistence pattern Unit of Work?
Why is a DbContext an implicit implementation of the persistence pattern Unit of Work?
It accumulates every add, update and delete made to its tracked entities and commits them together: one SaveChanges opens a transaction, writes them all, and rolls everything back on failure. That is why AddDbContext registers it as scoped — one context per request means one unit of work per request, disposed with the scope.
Common mistakes
- ✗Calling
SaveChangesafter every single entity change, losing the one-transaction batch - ✗Registering
DbContextas aSingletonand sharing one unit of work across requests - ✗Assuming a failed
SaveChangesleaves part of the batch applied in the database
Follow-up questions
- →What happens to the change tracker's state when
SaveChangesthrows? - →When do you need an explicit transaction around several
SaveChangescalls?
MiddleTheoryCommonWhat is the N+1 query problem in the object-relational mapper EF Core, and how do you eliminate it?
What is the N+1 query problem in the object-relational mapper EF Core, and how do you eliminate it?
One query loads N parents, and then touching each parent's navigation issues one more query per parent — N+1 round trips instead of one. Load the related data in the same query: Include for the whole graph, or a Select projection that pulls only the columns you need. Lazy loading is the usual cause, so turn it off in an API.
Common mistakes
- ✗Leaving lazy loading on and iterating navigations inside a loop
- ✗Believing lazy loading batches the extra queries into a single round trip
- ✗Reaching for
Includewhen aSelectprojection would fetch far less data
Follow-up questions
- →When does
AsSplitQuerybeat a singleIncludejoin? - →How would you spot N+1 in the logs or in a captured SQL trace?
MiddleTheoryCommonHow do eager, explicit and lazy loading differ when a related entity is fetched in EF Core?
How do eager, explicit and lazy loading differ when a related entity is fetched in EF Core?
Eager loading pulls the relation inside the original query, with Include. Explicit loading fetches it later on demand, through an Entry(...).Collection(...).Load() call — one extra round trip that you asked for. Lazy loading fetches it the moment a proxied navigation is read: implicit, and the classic source of N+1.
Common mistakes
- ✗Confusing explicit with lazy loading — one is an intentional call, the other fires on access
- ✗Leaving lazy-loading proxies enabled in an API and shipping N+1 by accident
- ✗Expecting a navigation to be populated with no
Include, noLoad()and no proxy
Follow-up questions
- →What must an entity's navigation look like for lazy-loading proxies to work?
- →Why does a lazy navigation throw once its
DbContexthas been disposed?
MiddleTheoryCommonWhat does the command dotnet ef migrations add actually do, and what does it not do?
What does the command dotnet ef migrations add actually do, and what does it not do?
It builds the model from your DbContext, diffs it against the model snapshot left by the previous migration, and generates a C# migration class with Up and Down plus an updated snapshot. It never touches the database — the SQL runs later, on dotnet ef database update or a Migrate() call at startup.
Common mistakes
- ✗Thinking the command touches the database rather than only generating code
- ✗Hand-editing or deleting the model snapshot, so the next diff produces a wrong migration
- ✗Expecting the migration to be recorded in the history table before it is applied
Follow-up questions
- →Why does the model snapshot file matter for the next migration's diff?
- →How do you turn a migration into a SQL script for a database administrator to review?
SeniorTheoryOccasionalWhat cost does a compiled query remove from a LINQ query, and when is it worth using?
What cost does a compiled query remove from a LINQ query, and when is it worth using?
Every query is compiled: the provider walks the expression tree, computes a cache key from it, and looks the plan up in its query cache. A compiled query does that once and hands you a delegate, so the tree walk and the cache lookup vanish from every later call. It pays off for hot, frequently executed queries of a fixed shape — the structure cannot vary per call.
Common mistakes
- ✗Expecting a compiled query to cache the results rather than the query plan
- ✗Compiling a query whose shape changes from call to call
- ✗Compiling cold or rare queries and getting no measurable gain at all
Follow-up questions
- →Why must a compiled query take its
DbContextas a parameter of the delegate? - →Does a compiled query still track its results, and how would you drop that cost?
SeniorTheoryOccasionalHow does a version-marker column rowversion give optimistic concurrency, and what happens on a conflict?
How does a version-marker column rowversion give optimistic concurrency, and what happens on a conflict?
Configure the property with IsRowVersion(): the value read with the entity is added to the WHERE clause of the generated UPDATE. If another transaction changed the row meanwhile, the version no longer matches, zero rows are affected, and SaveChanges throws DbUpdateConcurrencyException. You then reload the current values and retry, merge, or surface the conflict.
Common mistakes
- ✗Catching the concurrency exception and calling
SaveChangesagain on the same stale entity - ✗Reading optimistic concurrency as a lock rather than a conflict detected at write time
- ✗Deciding how to resolve the conflict without reloading the current database values
Follow-up questions
- →How do the entry's original values and the fetched database values help you merge a conflict?
- →When would you make a concurrency token out of a business column instead of a
rowversion?
SeniorDebuggingOccasionalAn endpoint built on a LINQ query is far slower than its SQL — find the cause
An endpoint built on a LINQ query is far slower than its SQL — find the cause
The log shows one query for the 50 orders and then one query per order for its lines — 51 round trips. That is N+1, caused by a navigation resolved lazily inside the loop; each command is fast, so the SQL is not the problem, the count of them is. Load the lines in the same query with Include or a Select projection, turn lazy loading off, and add AsNoTracking to drop the tracking cost on a read-only endpoint.
Common mistakes
- ✗Reading fast individual commands as proof the query is healthy, ignoring how many of them there are
- ✗Blaming a missing index when the round-trip count is what costs the time
- ✗Assuming an untranslatable
Whereis silently evaluated on the client — since 3.0 it throws
Follow-up questions
- →Which log category or diagnostic would you switch on first to see this?
- →How much does tracking still cost once the N+1 is gone, and how would you measure that?
SeniorDesignRareA schema change must ship to a live API that never stops serving. The Customers.Name column has to become FullName and be made non-nullable. The service runs on several instances behind a load balancer and is deployed by a rolling update, so for a while the old build and the new build both talk to the same database. The table holds tens of millions of rows, and the pipeline applies EF Core migrations automatically before new instances start taking traffic. Downtime, a lock that blocks writes for more than a moment, and a failed deploy that cannot be rolled back are all unacceptable. Design the migration: how many deployments it takes, what each one changes in the schema and in the code, how the existing rows are backfilled, and how each step stays reversible.
A schema change must ship to a live API that never stops serving. The Customers.Name column has to become FullName and be made non-nullable. The service runs on several instances behind a load balancer and is deployed by a rolling update, so for a while the old build and the new build both talk to the same database. The table holds tens of millions of rows, and the pipeline applies EF Core migrations automatically before new instances start taking traffic. Downtime, a lock that blocks writes for more than a moment, and a failed deploy that cannot be rolled back are all unacceptable. Design the migration: how many deployments it takes, what each one changes in the schema and in the code, how the existing rows are backfilled, and how each step stays reversible.
Never rename in place — expand, then contract, over three deploys. Deploy 1 adds a nullable FullName, writes both columns and still reads Name. Backfill the old rows in small batches so no long lock is taken. Deploy 2 reads FullName and still writes both, once the backfill is verified. Deploy 3 makes FullName non-nullable and drops Name. Every step is additive, works with both builds live, and can be rolled back on its own.
Common mistakes
- ✗Renaming or dropping a column in the same deploy that changes the code reading it
- ✗Backfilling tens of millions of rows in one
UPDATE, holding a long lock on the table - ✗Making the new column non-nullable before every existing row has a value
Follow-up questions
- →How do you make each of the three deployments individually reversible?
- →How does the readiness probe keep traffic off an instance whose schema step is unfinished?