Entity Framework Core

Entity Framework Core interview questions with answers

Questions about how EF Core behaves in real applications: performance traps, safe migrations and concurrency. Each answer explains the reasoning and shows a short example.

Intermediate · 12 questions · Tap a question to see the answer.

  1. Q1. What lifetime should a DbContext have in an ASP.NET Core app, and why is sharing one instance across threads a problem?

    Register it as Scoped with AddDbContext, so each HTTP request gets its own instance. A DbContext is a lightweight unit of work, not thread-safe, and it tracks every entity it loads.

    A shared or Singleton context fails with 'A second operation was started on this context instance' under concurrent use, and its change tracker grows without limit. In background services, create a scope or use IDbContextFactory<T> to create short-lived contexts.

    builder.Services.AddDbContextFactory<AppDbContext>(o => o.UseSqlServer(conn));
    
    // in a singleton/background service
    await using var db = await factory.CreateDbContextAsync(ct);

    Official documentation

  2. Q2. What does change tracking do, and when should you use AsNoTracking()?

    By default, entities returned by a query are tracked: EF Core keeps a snapshot and detects changes at SaveChanges. That costs memory and CPU.

    For read-only queries (API GETs, reports), use AsNoTracking() (or AsNoTrackingWithIdentityResolution() if you need one instance per key). For whole read-heavy contexts, set QueryTrackingBehavior.NoTracking as the default.

    var products = await db.Products
        .AsNoTracking()
        .Where(p => p.IsActive)
        .ToListAsync(ct);

    Official documentation

  3. Q3. Compare eager, explicit and lazy loading of related data.

    Eager loading (Include/ThenInclude) loads related data in the same query. Explicit loading (db.Entry(order).Collection(o => o.Lines).LoadAsync()) loads it later on demand. Lazy loading (proxies) loads a navigation automatically the first time it is accessed.

    Lazy loading easily causes N+1 queries, especially during JSON serialization, so most web APIs avoid it and use projections or Include instead.

    var orders = await db.Orders
        .Include(o => o.Customer)
        .Include(o => o.Lines).ThenInclude(l => l.Product)
        .Where(o => o.Id == id)
        .ToListAsync(ct);

    Official documentation

  4. Q4. A query with two collection Includes returns slowly and transfers far more data than expected. What is happening?

    Including two collections in one query joins them, so each order row is repeated for every combination of lines × payments: a cartesian explosion.

    Use AsSplitQuery() to load each collection with its own SQL query, or project only the data you need. Split queries cost extra round trips and can see inconsistent data if rows change between them; you can set split queries as the default for the context.

    var orders = await db.Orders
        .Include(o => o.Lines)
        .Include(o => o.Payments)
        .AsSplitQuery()
        .ToListAsync(ct);

    Official documentation

  5. Q5. Why is projecting to a DTO with Select often better than returning entities from an API?

    Select makes EF Core fetch only the columns you use, joins related data in SQL without Include, and returns untracked results. It also avoids exposing your database model and serialization cycles.

    Entity graphs returned directly from an API often cause over-fetching, lazy loading surprises, or cycles in JSON.

    var dto = await db.Orders
        .Where(o => o.Id == id)
        .Select(o => new OrderDto(o.Id, o.Total, o.Customer.Name, o.Lines.Count))
        .SingleOrDefaultAsync(ct);

    Official documentation

  6. Q6. How should you apply EF Core migrations to a production database?

    Not with Database.Migrate() at app startup: several instances may race, and the app needs schema-change permissions. Generate an idempotent SQL script (dotnet ef migrations script --idempotent) or a migration bundle (dotnet ef migrations bundle) and run it as a deployment step.

    Review the generated SQL, especially for renames (EF may drop and add a column) and data-losing changes.

    dotnet ef migrations bundle --self-contained -r linux-x64
    ./efbundle --connection "$CONNECTION_STRING"

    Official documentation

  7. Q7. Two users edit the same product at the same time and one silently overwrites the other. How does EF Core prevent this?

    Add a concurrency token, for example a SQL Server rowversion column mapped with [Timestamp]. EF Core adds it to the UPDATE's WHERE clause; if another user changed the row, zero rows are affected and EF throws DbUpdateConcurrencyException.

    Catch it and decide: reload and show the user the new values, retry, or merge.

    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; } = "";
        [Timestamp] public byte[] Version { get; set; } = [];
    }

    Official documentation

  8. Q8. How do you update or delete thousands of rows efficiently in EF Core 7+?

    Use ExecuteUpdateAsync and ExecuteDeleteAsync. They translate to a single UPDATE or DELETE statement and run immediately without loading entities into memory.

    They bypass the change tracker, so tracked entities in the same context aren't updated, and they don't run in the same transaction as SaveChanges unless you open one explicitly.

    await db.Orders
        .Where(o => o.Status == Status.Draft && o.CreatedAt < cutoff)
        .ExecuteDeleteAsync(ct);
    
    await db.Products
        .Where(p => p.CategoryId == id)
        .ExecuteUpdateAsync(s => s.SetProperty(p => p.Price, p => p.Price * 1.1m), ct);

    Official documentation

  9. Q9. When and how do you use raw SQL with EF Core safely?

    Use it for queries LINQ can't express well or for tuned SQL. FromSql with an interpolated string turns each interpolated value into a parameter, so it is safe from SQL injection; you can compose LINQ on top of it.

    FromSqlRaw with string concatenation is not safe. SqlQuery<T> (EF Core 8+) maps results to unmapped types such as DTOs.

    var term = "%" + search + "%";
    var blogs = await db.Blogs
        .FromSql(quot;SELECT * FROM dbo.Blogs WHERE Name LIKE {term}")
        .Where(b => b.IsActive)
        .ToListAsync(ct);

    Official documentation

  10. Q10. A LINQ query throws 'could not be translated' at runtime. Why, and how do you fix it?

    EF Core must translate the whole Where/OrderBy to SQL. Calling a custom C# method or an unsupported function inside it can't be translated, and since EF Core 3.0 it throws instead of silently loading everything into memory.

    Rewrite the condition with translatable operations, move the custom logic after a projection to the final Select, or explicitly switch to client evaluation with AsEnumerable() after filtering as much as possible in SQL.

    Official documentation

  11. Q11. How do you map a value object like an Address that has no identity of its own?

    Use an owned type (OwnsOne) or, in EF Core 8+, a complex type (ComplexProperty). Both store the properties in the owner's table (Address_Street, Address_City...).

    Complex types are the better fit for true value objects: they have no key and can be shared by value. Owned types are still entity types with a hidden key, and can also map to a separate table or JSON column.

    modelBuilder.Entity<Customer>().ComplexProperty(c => c.Address);

    Official documentation

  12. Q12. Your API runs the same EF Core query thousands of times per second. What can reduce EF overhead?

    Use DbContext pooling (AddDbContextPool), which reuses context instances instead of creating new ones, and compiled queries (EF.CompileAsyncQuery), which skip LINQ translation on each call.

    Measure first: usually indexes, projections and no-tracking matter far more. Pooled contexts must not keep per-request state in fields.

    private static readonly Func<AppDbContext, int, Task<Product?>> ById =
        EF.CompileAsyncQuery((AppDbContext db, int id) =>
            db.Products.AsNoTracking().FirstOrDefault(p => p.Id == id));

    Official documentation

Original questions written to explain concepts; not taken from any certification exam. Spotted a mistake? See our corrections policy.