DEV Community

kirandeepjassal-crypto
kirandeepjassal-crypto

Posted on Originally published at prepstack.co.in

EF Core Interview Questions for Senior Developers in 2026 — Change Tracking, Performance, Concurrency & Migrations

Junior EF Core interviews ask "what is a DbContext." Senior interviews ask why your dashboard fires 200 queries to render one page, what happens when two users save the same row at once, and why calling Migrate() on startup took production down during a rolling deploy.

EF Core makes the easy 80% trivial and hides the hard 20% — change tracking, query translation, concurrency, and schema evolution — right where production breaks. Seniority is knowing what SQL your LINQ actually becomes and what EF is doing on your behalf.

This is 30 questions across change tracking & the DbContext, querying & performance, concurrency & transactions, and migrations & production. Each has a short answer, a deep dive into the mechanism with code, and the senior signal the interviewer is listening for. Every example is from Mattrx — a real multi-tenant marketing-analytics platform on .NET 9 / EF Core 9 over Azure SQL, with Campaigns (~4M rows) and CampaignEvents (~1.2B rows), every query scoped by TenantId.

Block A — change tracking & the DbContext

1. What is change tracking, and how does SaveChanges know what to do?
The DbContext keeps a snapshot of every entity it loaded. On SaveChanges, it diffs current values against that snapshot, computes each entity's state (Added / Modified / Deleted / Unchanged), and emits the minimal INSERT/UPDATE/DELETE batch. Only changed columns go into the UPDATE.

2. What lifetime should DbContext have, and why not a singleton?
Scoped — one per request/unit of work. DbContext is not thread-safe and accumulates tracked entities, so a singleton corrupts state across concurrent requests and leaks memory as the tracker grows unbounded.

3. What does AsNoTracking do, and when do you use it?
It tells EF not to snapshot or track the results — no change-tracking overhead, less memory. Default for read-only queries.

var rows = await _db.Campaigns
    .AsNoTracking()
    .Where(c => c.TenantId == tenantId && c.Status == CampaignStatus.Active)
    .Select(c => new CampaignListItem(c.Id, c.Name, c.UpdatedAt))
    .ToListAsync(ct);
Enter fullscreen mode Exit fullscreen mode

4. How does EF decide INSERT vs UPDATE?
By tracked state, not by whether the entity "looks new." A detached entity (deserialized from an API) has no state until you attach it — then EF guesses from the key, which is the source of countless bugs.

5. What is the identity map (first-level cache)?
Within one DbContext, EF returns the same instance for the same key. Per-context consistency, not a cross-request cache — and why long-lived contexts drift from the DB.

6. Tracking query returns stale data — why?
The identity map won't overwrite an already-tracked entity's values with fresh DB values by default. Use AsNoTracking, a new context, or Reload() for freshness.

7. What's the danger of Update() on a whole entity?
It marks every property Modified, so EF writes all columns — overwriting fields you didn't intend to change and defeating concurrency. Prefer loading + mutating specific properties, or ExecuteUpdate.

8. What is DbContext pooling and when does it help?
AddDbContextPool reuses context instances (resetting state) to cut allocation/GC overhead at high throughput. Catch: you can't inject per-request state into the constructor.

Block B — querying & performance

9. IQueryable vs IEnumerable — where does the query run?
IQueryable builds an expression tree EF translates to SQL and runs on the database. The moment you drop to IEnumerable, the rest executes in memory on the app server. Where on 1.2B rows in SQL vs in C# is the difference between 40ms and an outage.

10. What is the N+1 problem and how do you fix it?

// N+1: one query for campaigns, then one MORE per campaign for its count.
var campaigns = await _db.Campaigns.Where(c => c.TenantId == tenantId).ToListAsync(ct);
foreach (var c in campaigns)
    c.EventCount = await _db.Events.CountAsync(e => e.CampaignId == c.Id, ct);

// Fix: one server-side query that projects the aggregate.
var list = await _db.Campaigns
    .Where(c => c.TenantId == tenantId)
    .Select(c => new CampaignSummary(c.Id, c.Name, c.Events.Count()))
    .ToListAsync(ct);
Enter fullscreen mode Exit fullscreen mode

11. Eager vs lazy vs explicit loading?
Eager (Include) up front when you know you need it. Explicit (.Entry(e).Collection(...).LoadAsync()) on demand, deliberately. Lazy (proxies) transparently on access — the #1 cause of accidental N+1; most teams disable it.

12. Two Includes made the query huge — why?
Cartesian explosion. Multiple collection navigations produce a JOIN whose row count is the product of the collections. Fix with AsSplitQuery().

var campaign = await _db.Campaigns
    .Include(c => c.Events)
    .Include(c => c.Segments)
    .AsSplitQuery()
    .FirstOrDefaultAsync(c => c.Id == id, ct);
Enter fullscreen mode Exit fullscreen mode

13. Projection vs loading full entities?
Select into a DTO fetches only those columns and skips tracking. Loading full entities pulls every column (including blobs) whether you use them or not. For reads, project.

14. What are compiled queries?
EF.CompileAsyncQuery caches the LINQ→SQL translation so a hot query skips expression-tree compilation per call. EF already caches plans; reserve explicit compiled queries for genuinely hot paths you've measured.

15. What is client-side evaluation and why is it dangerous?
When part of your LINQ can't translate to SQL, older EF silently ran it in memory after fetching all rows. EF Core 3.0+ throws instead (except the final projection) — treat that exception as the framework protecting you.

16. When do you drop to raw SQL, and how safely?
FromSql/ExecuteSql when LINQ can't express it. Use parameterized interpolation so EF parameterizes it — never string-concatenate user input (SQL injection).

var rows = await _db.Database
    .SqlQuery<DailyKpi>($@"SELECT ... FROM CampaignEvents
                           WHERE TenantId = {tenantId} AND EventDate >= {from}")
    .ToListAsync(ct);
Enter fullscreen mode Exit fullscreen mode

17. Streaming vs buffering results?
ToListAsync buffers the whole set into memory. AsAsyncEnumerable streams row by row — constant memory for large exports. Buffer small results; stream large ones.

Block C — concurrency & transactions

18. How does optimistic concurrency work in EF Core?
Add a concurrency token (a SQL rowversion). EF appends it to the UPDATE's WHERE; if someone changed the row since you read it, 0 rows match and EF throws DbUpdateConcurrencyException — a lost update becomes a detectable conflict.

public class Campaign
{
    public Guid Id { get; set; }
    public string Name { get; set; } = "";
    [Timestamp] public byte[] RowVersion { get; set; } = default!;
}
// UPDATE Campaigns SET Name=@n WHERE Id=@id AND RowVersion=@original
Enter fullscreen mode Exit fullscreen mode

19. How do you handle DbUpdateConcurrencyException?

try { await _db.SaveChangesAsync(ct); }
catch (DbUpdateConcurrencyException ex)
{
    var entry = ex.Entries.Single();
    var dbValues = await entry.GetDatabaseValuesAsync(ct);
    if (dbValues is null) { /* row was deleted -> handle */ }
    else
    {
        entry.OriginalValues.SetValues(dbValues);   // rebase, then retry
        await _db.SaveChangesAsync(ct);
    }
}
Enter fullscreen mode Exit fullscreen mode

Pick a policy per entity: client wins, store wins, or merge.

20. How do transactions work in EF Core?
A single SaveChanges is already atomic. For multiple saves or mixing EF with raw SQL as one unit, open an explicit BeginTransactionAsync.

await using var tx = await _db.Database.BeginTransactionAsync(ct);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
_db.Outbox.Add(evt);
await _db.SaveChangesAsync(ct);
await tx.CommitAsync(ct);   // both, or neither
Enter fullscreen mode Exit fullscreen mode

21. Does EF Core support pessimistic locking?
Not natively — the model is optimistic. For SELECT ... WITH (UPDLOCK, ROWLOCK) you drop to raw SQL inside a transaction. Default to optimistic unless contention is high.

22. What is an execution strategy, and what's the transaction gotcha?
EnableRetryOnFailure wraps operations in a retrying strategy for transient faults. A retrying strategy can't implicitly retry a user-initiated transaction — wrap that block in strategy.ExecuteAsync(...).

var strategy = _db.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
    await using var tx = await _db.Database.BeginTransactionAsync(ct);
    // ... multiple SaveChanges ...
    await tx.CommitAsync(ct);
});
Enter fullscreen mode Exit fullscreen mode

23. How do you do a bulk update without loading every row?
EF7+ ExecuteUpdate/ExecuteDelete run a set-based UPDATE/DELETE in SQL — no loading, no tracking, one round trip. They bypass the change tracker and don't run SaveChanges interceptors.

await _db.Campaigns
    .Where(c => c.TenantId == tenantId && c.Status == CampaignStatus.Draft)
    .ExecuteUpdateAsync(s => s.SetProperty(c => c.Status, CampaignStatus.Archived), ct);
Enter fullscreen mode Exit fullscreen mode

24. How do you handle transient failures against Azure SQL?
Enable EnableRetryOnFailure so EF retries transient errors with backoff. Pair with idempotent writes/optimistic concurrency so a retry after an ambiguous failure doesn't double-apply.

Block D — migrations, modeling & production

25. How do EF Core migrations work?
dotnet ef migrations add diffs your model against the committed model snapshot (a C# file) and generates an Up/Down migration. database update applies pending ones, recording each in __EFMigrationsHistory.

26. How should you apply migrations in production?
Not with db.Database.Migrate() on startup — multiple instances race, it needs schema-owner permissions at runtime, and nothing is reviewed. Generate an idempotent SQL script in CI and apply it as a gated deploy step.

dotnet ef migrations script --idempotent -o migrate.sql
-> review in PR -> apply as an explicit, gated deploy step
Enter fullscreen mode Exit fullscreen mode

27. Fluent API vs data annotations?
Annotations ([Required], [MaxLength]) are simple and co-located. Fluent API (OnModelCreating) is more powerful (composite keys, relationships, filters, converters) and keeps mapping out of domain classes. Fluent wins where they conflict.

28. What are owned types and value converters?
Owned types map a value object (Money, Address) into the owner's table without its own identity. Value converters translate a property between CLR and DB types (enum as string, encrypted column).

29. What are global query filters and their pitfalls?
HasQueryFilter applies a predicate to every query for an entity — perfect for multi-tenancy and soft-delete. Pitfalls: the context must be scoped, it doesn't apply to raw SQL, and you need IgnoreQueryFilters() for admin/cross-tenant jobs.

modelBuilder.Entity<Campaign>().HasQueryFilter(c => c.TenantId == _tenant.Id);
var all = await _db.Campaigns.IgnoreQueryFilters().Where(...).ToListAsync(ct);
Enter fullscreen mode Exit fullscreen mode

30. What EF Core performance mistakes do you check for in production?
Missing AsNoTracking on reads, N+1 from lazy loading, loading full entities instead of projecting, cartesian explosion from multiple Includes, no pagination, client-side evaluation, Update() writing all columns, and auto-migrate on startup. Verify with the generated SQL (ToQueryString() / logging), not by trusting that it compiled.


The through-line: EF Core is a leaky abstraction on purpose. Project reads into DTOs with AsNoTracking, kill N+1 with one query, split multi-collection includes, make concurrency a rowversion plus a resolution policy, wrap real units of work in explicit transactions with retries, and ship migrations as idempotent reviewed scripts. Answer each question with the generated SQL and the failure it prevents.

This post was originally published on PrepStack. The full original has all the code and senior-signal notes.

Top comments (0)