Entity Framework Core & Data Access
16 questions found
What is Entity Framework Core, and how does it relate to the Repository and Unit of Work patterns?
Beginner
EF Core is an object-relational mapper (ORM) that lets you work with a relational database using C# objects (entities) and LINQ queries instead of writing raw SQL by hand, translating LINQ expressions into SQL and mapping query results back into strongly-typed objects. A DbContext itself already implements much of the Unit of Work pattern (tracking changes, committing them together via SaveChanges), and a DbSet<T> serves as a Repository-like abstraction over a table, which is why many teams skip additional Repository/UoW layers on top of EF Core as often redundant.
public class AppDbContext : DbContext {
public DbSet<Product> Products { get; set; }
}
var product = await context.Products.FindAsync(1);
product.Price = 29.99m;
await context.SaveChangesAsync(); // Unit of Work: commits all tracked changes together
Real-world example
A team initially wraps EF Core in a custom Repository/UnitOfWork layer for testability, then later removes it after realizing DbContext already provides equivalent abstraction and the extra layer just added indirection without meaningful benefit.
Common follow-ups: When would an additional Repository layer over EF Core still be justified?;How does DbContext's change tracking actually implement Unit of Work internally?
Dependency Injection;Caching (In-Memory
Distributed & Redis)
What is the difference between EF Core's Code-First (migrations) approach and Database-First (scaffolding) approach?
Intermediate
Code-First defines your data model as C# classes first, then uses migrations to generate and apply corresponding database schema changes -- ideal for new projects where the application drives the schema. Database-First (via `dotnet ef dbcontext scaffold`) instead reverse-engineers C# entity classes and a DbContext from an existing database schema, appropriate when integrating with a pre-existing database you don't control or fully own the schema for.
# Code-First: define classes, generate migrations
dotnet ef migrations add InitialCreate
dotnet ef database update
# Database-First: scaffold classes from existing DB
dotnet ef dbcontext scaffold "Server=...;Database=Legacy;" Microsoft.EntityFrameworkCore.SqlServer
Real-world example
A greenfield microservice uses Code-First migrations to evolve its schema alongside application development, while a reporting service connecting to a legacy, DBA-controlled data warehouse uses Database-First scaffolding to generate matching entity classes.
Common follow-ups: How do you keep scaffolded classes in sync when the source database schema changes?;Can you mix both approaches within the same solution?
CI/CD
Publishing & Deployment;Data Types & Structures
How does EF Core's change tracking work, and what is the difference between tracked and no-tracking queries?
Advanced
By default, EF Core's DbContext tracks every entity returned by a query, recording a snapshot of its original values so SaveChanges can detect and generate SQL for exactly what changed. AsNoTracking() disables this tracking for a query, improving performance and reducing memory usage for read-only scenarios (like displaying data with no intent to update it), since the context doesn't need to maintain change-detection snapshots for entities that will never be modified and saved back.
// Tracked (default): needed if you plan to modify and SaveChanges
var product = await context.Products.FirstAsync(p => p.Id == 1);
product.Price = 19.99m;
await context.SaveChangesAsync();
// No-tracking: faster for read-only display scenarios
var products = await context.Products.AsNoTracking().ToListAsync();
Real-world example
A reporting dashboard querying thousands of records for read-only display uses AsNoTracking() throughout, measurably reducing memory usage and query time compared to the default tracked behavior which was unnecessary for a page that never saves changes back.
Common follow-ups: How much memory/performance overhead does tracking actually add per entity?;When would AsNoTrackingWithIdentityResolution be preferable to plain AsNoTracking?
Memory Management & Garbage Collection;Diagnostics & Performance
What is the N+1 query problem in EF Core, and how does eager loading with Include() solve it?
Intermediate
The N+1 problem occurs when code queries a list of N entities, then separately queries related data for each one individually inside a loop, resulting in 1 initial query plus N additional queries (N+1 total) instead of a single efficient join -- Include() (and ThenInclude() for nested relations) instructs EF Core to eagerly load related entities as part of the original query via a SQL JOIN, collapsing what would be N+1 round-trips into one.
// N+1 problem: 1 query for orders, then N queries for each order's customer
var orders = await context.Orders.ToListAsync();
foreach (var order in orders) {
Console.WriteLine(order.Customer.Name); // lazy-loads Customer, one query per order!
}
// Fixed with eager loading: exactly 1 query total
var orders = await context.Orders.Include(o => o.Customer).ToListAsync();
Real-world example
A dashboard page loading noticeably slowly under load is diagnosed via query logging to be issuing over 200 separate database round-trips for a list of 200 orders; adding a single .Include(o => o.Customer) collapses this to one query and dramatically improves response time.
Common follow-ups: How does query logging help you detect N+1 problems in existing code?;What's the difference between eager, lazy, and explicit loading strategies?
Diagnostics & Performance;RESTful Web APIs & Controllers
How do EF Core migrations work, and what is the recommended workflow for applying them safely in production?
Advanced
`dotnet ef migrations add <Name>` compares your current model against the last migration's snapshot and generates a new migration class (with Up/Down methods describing the schema change), which `dotnet ef database update` (or Migrate() called programmatically) then applies. For production, the recommended safe workflow separates migration generation (done during development, reviewed in code review like any other code) from migration application (run as a distinct, monitored deployment step, ideally with a database backup taken immediately before, rather than calling context.Database.Migrate() automatically on every application startup, which risks concurrent migration attempts from multiple app instances).
dotnet ef migrations add AddProductDiscountColumn
# Review the generated Up()/Down() methods in the migration file before committing
# Production: apply as a distinct pipeline step, not on every app startup
dotnet ef database update --connection "$PROD_CONNECTION_STRING"
Real-world example
A team learns the hard way after multiple simultaneously-starting container replicas all tried to run Database.Migrate() at once, causing a migration lock conflict -- they switch to running migrations as a single, separate pipeline step before deploying application instances.
Common follow-ups: Why is calling Database.Migrate() on every app startup risky in a scaled-out deployment?;How do you generate a SQL script from migrations for DBA review instead of applying directly?
CI/CD
Publishing & Deployment;.NET CLI
SDK & Project Structure (csproj)
What is the difference between IQueryable<T> and IEnumerable<T> when writing EF Core queries, and why does it matter for performance?
Intermediate
IQueryable<T> represents a composable, not-yet-executed query expression tree that EF Core translates into SQL, meaning additional LINQ operations (Where, OrderBy, Take) chained onto it are incorporated into the actual SQL query sent to the database. IEnumerable<T> represents an already-materialized in-memory sequence, so calling .AsEnumerable() or accidentally forcing early materialization (like calling .ToList() too early) causes subsequent LINQ operations to execute in application memory instead of the database, often processing far more data than necessary and losing the database's ability to optimize the query (like using indexes).
// Efficient: filtering happens in SQL (WHERE clause)
var expensive = context.Products.Where(p => p.Price > 100).ToList();
// Inefficient: loads ALL products into memory first, THEN filters in C#
var expensive = context.Products.ToList().Where(p => p.Price > 100).ToList();
Real-world example
A performance review catches a query that materializes an entire 500,000-row table into memory via an early .ToList() before applying a filter that should have been part of the SQL WHERE clause, fixed by simply reordering the LINQ chain.
Common follow-ups: How can you tell from code review whether a LINQ chain will execute in SQL or in memory?;What operations force premature materialization?
Diagnostics & Performance;Comprehensions & Generators
How do you handle optimistic concurrency conflicts in EF Core using a concurrency token (like RowVersion)?
Advanced
Marking a property with [ConcurrencyCheck] or, more commonly, using a dedicated [Timestamp]/RowVersion byte[] column, causes EF Core to include that column's original value in the WHERE clause of UPDATE/DELETE statements -- if another process modified the row in the meantime (changing the RowVersion), zero rows match, and EF Core throws a DbUpdateConcurrencyException, letting your application detect and handle the conflict (e.g., by reloading and asking the user to retry) instead of silently overwriting someone else's concurrent changes.
public class Product {
public int Id { get; set; }
public decimal Price { get; set; }
[Timestamp] public byte[] RowVersion { get; set; }
}
try {
await context.SaveChangesAsync();
} catch (DbUpdateConcurrencyException) {
// Another user modified this row concurrently -- reload and prompt for resolution
}
Real-world example
An inventory management system uses RowVersion-based concurrency tokens on stock quantities so that two warehouse staff simultaneously updating the same product's count get a clear conflict error instead of one person's update silently overwriting the other's.
Common follow-ups: What's the difference between optimistic and pessimistic concurrency control?;How do you resolve a DbUpdateConcurrencyException programmatically (client-wins vs store-wins)?
Exception Handling;Multiple Inheritance & MRO
How does EF Core map inheritance hierarchies to database tables, and what are the three main strategies (TPH, TPT, TPC)?
Intermediate
Table-Per-Hierarchy (TPH, the default) stores all types in a single table with a discriminator column identifying each row's actual type -- simplest and most performant for queries, but can lead to many nullable columns. Table-Per-Type (TPT) creates a separate table per class in the hierarchy, joined via shared primary keys -- normalized but requires joins for queries. Table-Per-Concrete-Type (TPC) creates a fully independent table per concrete class with all inherited properties duplicated -- avoids joins entirely but duplicates schema and complicates cross-hierarchy queries.
public abstract class Animal { public int Id { get; set; } public string Name { get; set; } }
public class Dog : Animal { public string Breed { get; set; } }
public class Cat : Animal { public bool IsIndoor { get; set; } }
// TPH (default): one 'Animals' table with a 'Discriminator' column and nullable Breed/IsIndoor columns
modelBuilder.Entity<Animal>().HasDiscriminator<string>("AnimalType");
Real-world example
A pet management system's Animal hierarchy uses the default TPH strategy for simplicity, accepting some nullable columns, since query performance (avoiding joins) matters more than schema normalization purity for this particular domain.
Common follow-ups: What are the trade-offs that would push you toward TPT or TPC instead of the TPH default?;How does the discriminator column get automatically populated?
Multiple Inheritance & MRO;Data Types & Structures
How would you optimize an EF Core query that's generating inefficient SQL, and what tools help identify the actual generated SQL?
Advanced
Use context.Database.Log or the built-in logging (configuring the Microsoft.EntityFrameworkCore.Database.Command category to Information/Debug level) to see the exact SQL EF Core generates, or use EF Core's ToQueryString() method on an IQueryable to inspect the SQL without executing it -- common optimizations include adding AsNoTracking() for read-only queries, using Select() to project only needed columns instead of loading full entities, using Include() strategically to avoid N+1 while avoiding over-fetching with excessive includes, and adding appropriate database indexes for frequently filtered/sorted columns.
var query = context.Products.Where(p => p.Category == "Electronics").OrderBy(p => p.Price);
Console.WriteLine(query.ToQueryString()); // prints the exact generated SQL without executing
// Optimized: project only needed columns
var summaries = await context.Products
.Where(p => p.Category == "Electronics")
.Select(p => new { p.Id, p.Name, p.Price }) // avoids loading full entity
.ToListAsync();
Real-world example
A team diagnosing a slow product listing endpoint uses ToQueryString() to discover EF Core was selecting all 40 columns of a wide Product table when the UI only displayed 3 fields, fixing it with a targeted Select() projection.
Common follow-ups: How do you enable EF Core's built-in query logging in appsettings.json?;What's the performance difference between projecting with Select() versus loading full entities?
Logging;Diagnostics & Performance
What is the difference between SaveChanges() and SaveChangesAsync(), and why does it matter for a web application's scalability?
Intermediate
SaveChangesAsync() performs the database write operation asynchronously, releasing the calling thread back to the thread pool to handle other work while waiting for the database round-trip to complete, rather than blocking that thread for the duration of the I/O operation -- essential for a web application's scalability under concurrent load, since blocking threads on synchronous SaveChanges() calls can lead to thread pool exhaustion under sufficient concurrent traffic, exactly like any other blocking I/O call.
// Blocks a thread pool thread for the entire database round-trip
context.SaveChanges();
// Releases the thread during the I/O wait, scales much better under load
await context.SaveChangesAsync();
Real-world example
A high-traffic API's controller actions consistently use SaveChangesAsync() throughout, part of a broader 'async all the way' discipline that lets the application handle significantly more concurrent requests on the same hardware compared to synchronous data access.
Common follow-ups: What's the actual mechanism by which async I/O avoids blocking a thread?;Are there any scenarios where synchronous SaveChanges is actually preferable?
Concurrency (asyncio/threading/multiprocessing);ASP.NET Core Middleware & Request Pipeline