Entity Framework Core
Trigger On
- working on
DbContext, migrations, model configuration, or EF queries
- reviewing tracking, loading, performance, or transaction behavior
- porting data access from EF6 or custom repositories to EF Core
- optimizing slow database queries
Documentation
References
- patterns.md - Query patterns, tracking strategies, loading strategies, projections, compiled queries, pagination, and temporal tables
- anti-patterns.md - Common EF Core mistakes including N+1 queries, large contexts, generic repositories, and missing indexes
Workflow
- Prefer EF Core for new development unless a documented gap requires Dapper or raw SQL
- Keep
DbContext lifetime scoped — align with unit of work
- Review query translation — check generated SQL, avoid N+1
- Treat migrations as first-class — reviewable, not throwaway
- Be deliberate about provider behavior — cross-provider but not identical
- Validate with query inspection — not just in-memory mental model
Current Upstream Notes
- EF Core
v10.0.11 is a servicing release. It fixes Azure SQL compatibility-level 170 JSON translation so nested OPENJSON projections retain AS JSON, and keeps EF compatible with .NET 11 MemoryExtensions.Min/Max overload additions. Re-run provider-backed JSON queries and compile against the repository's selected target framework after upgrading.
- The August 2026 EF Core vs EF6 comparison remains the first stop for migration decisions. EF Core is the active cross-platform stack, but EF6-only EDMX/ObjectContext-heavy code should not move without a feature inventory and database-backed equivalence tests.
DbContext Patterns
Basic Configuration
public class AppDbContext : DbContext
{
public DbSet<Product> Products => Set<Product>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
// Entity Configuration (Fluent API)
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> builder)
{
builder.HasKey(p => p.Id);
builder.Property(p => p.Name).HasMaxLength(200).IsRequired();
builder.HasIndex(p => p.Sku).IsUnique();
builder.HasMany(p => p.OrderItems).WithOne(oi => oi.Product);
}
}
Registration with DI
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString)
.EnableSensitiveDataLogging() // Dev only
.EnableDetailedErrors()); // Dev only
// Or with pooling (better performance)
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString));
Query Patterns
Use AsNoTracking for Read-Only
// Bad - tracks entities unnecessarily
var products = await db.Products.ToListAsync();
// Good - no tracking overhead
var products = await db.Products
.AsNoTracking()
.ToListAsync();
Project to DTOs
// Bad - loads entire entity graph
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.ToListAsync();
// Good - loads only needed data
var orders = await db.Orders
.Select(o => new OrderDto
{
Id = o.Id,
CustomerName = o.Customer.Name,
ItemCount = o.Items.Count,
Total = o.Items.Sum(i => i.Price)
})
.ToListAsync();
Avoid N+1 Queries
// Bad - N+1 problem
foreach (var order in orders)
{
var items = await db.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync();
}
// Good - eager loading
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
// Good - split query for large graphs
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
Compiled Queries (EF Core 9)
// Pre-compiled for frequently used queries
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
// Usage
var product = await GetProductById(db, productId);
Migration Patterns
Creating Migrations
# Add migration
dotnet ef migrations add AddProductIndex
# Apply to database
dotnet ef database update
# Generate SQL script
dotnet ef migrations script --idempotent -o migrate.sql
Data Migrations
public partial class AddProductIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_Products_Sku",
table: "Products",
column: "Sku",
unique: true);
// Data migration (if needed)
migrationBuilder.Sql(@"
UPDATE Products
SET NormalizedName = UPPER(Name)
WHERE NormalizedName IS NULL");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_Products_Sku",
table: "Products");
}
}
Anti-Patterns to Avoid
| Anti-Pattern |
Why It's Bad |
Better Approach |
ToList() then filter |
Loads all data to memory |
Filter in query |
| Multiple DbContext per request |
Transaction issues |
Scoped lifetime |
| Lazy loading everywhere |
N+1 queries |
Explicit Include |
| Generic repository wrapper |
Removes query power |
Use DbContext directly |
| Ignoring generated SQL |
Hidden performance issues |
Log and review |
SaveChanges() in loops |
Many roundtrips |
Batch then save |
Performance Best Practices
Index frequently queried columns:
builder.HasIndex(p => p.CreatedAt);
builder.HasIndex(p => new { p.Category, p.Status });
Use pagination:
var page = await db.Products
.OrderBy(p => p.Id)
.Skip(pageSize * pageNumber)
.Take(pageSize)
.ToListAsync();
Batch updates (EF Core 7+):
await db.Products
.Where(p => p.Category == "Obsolete")
.ExecuteDeleteAsync();
await db.Products
.Where(p => p.Category == "Sale")
.ExecuteUpdateAsync(p => p.SetProperty(x => x.Price, x => x.Price * 0.9m));
Minimize network roundtrips:
// Bad - 3 roundtrips
var product = await db.Products.FindAsync(id);
var reviews = await db.Reviews.Where(r => r.ProductId == id).ToListAsync();
var related = await db.Products.Where(p => p.Category == product.Category).ToListAsync();
// Good - 1 roundtrip
var data = await db.Products
.Where(p => p.Id == id)
.Select(p => new
{
Product = p,
Reviews = p.Reviews,
Related = db.Products.Where(r => r.Category == p.Category).Take(5)
})
.FirstOrDefaultAsync();
Concurrency Patterns
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
[ConcurrencyCheck]
public int Version { get; set; }
// Or use RowVersion
[Timestamp]
public byte[] RowVersion { get; set; }
}
// Handle concurrency conflicts
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
// Resolve conflict...
}
Deliver
- EF Core models and queries that match the domain
- safer migrations and lifetime management
- performance-aware data access decisions
- proper indexing and query optimization
Validate
- query behavior is intentional (check SQL logs)
- migrations are reviewable and correct
- no N+1 queries in common paths
- indexes exist for filtered/sorted columns
- DbContext lifetime is scoped properly
- concurrency is handled for critical entities
1---2name: entity-framework-core3description: Design, tune, or review EF Core data access with proper modeling, migrations, query translation, performance, and lifetime management for modern .NET applications. USE FOR: DbContext, migrations, model configuration, EF queries, tracking, loading, performance, transactions, and EF6 migration decisions. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or validation commands when changes are made.4---56# Entity Framework Core78## Trigger On910- working on `DbContext`, migrations, model configuration, or EF queries11- reviewing tracking, loading, performance, or transaction behavior12- porting data access from EF6 or custom repositories to EF Core13- optimizing slow database queries1415## Documentation1617- [EF Core Overview](https://learn.microsoft.com/en-us/ef/core/)18- [Performance](https://learn.microsoft.com/en-us/ef/core/performance/)19- [Efficient Querying](https://learn.microsoft.com/en-us/ef/core/performance/efficient-querying)20- [Migrations](https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/)21- [What's New in EF Core 9](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-9.0/whatsnew)2223### References2425- [patterns.md](references/patterns.md) - Query patterns, tracking strategies, loading strategies, projections, compiled queries, pagination, and temporal tables26- [anti-patterns.md](references/anti-patterns.md) - Common EF Core mistakes including N+1 queries, large contexts, generic repositories, and missing indexes2728## Workflow29301. **Prefer EF Core for new development** unless a documented gap requires Dapper or raw SQL312. **Keep `DbContext` lifetime scoped** — align with unit of work323. **Review query translation** — check generated SQL, avoid N+1334. **Treat migrations as first-class** — reviewable, not throwaway345. **Be deliberate about provider behavior** — cross-provider but not identical356. **Validate with query inspection** — not just in-memory mental model3637## Current Upstream Notes3839- EF Core `v10.0.11` is a servicing release. It fixes Azure SQL compatibility-level 170 JSON translation so nested `OPENJSON` projections retain `AS JSON`, and keeps EF compatible with .NET 11 `MemoryExtensions.Min`/`Max` overload additions. Re-run provider-backed JSON queries and compile against the repository's selected target framework after upgrading.40- The August 2026 EF Core vs EF6 comparison remains the first stop for migration decisions. EF Core is the active cross-platform stack, but EF6-only EDMX/ObjectContext-heavy code should not move without a feature inventory and database-backed equivalence tests.4142## DbContext Patterns4344### Basic Configuration45```csharp46public class AppDbContext : DbContext47{48 public DbSet<Product> Products => Set<Product>();49 public DbSet<Order> Orders => Set<Order>();5051 protected override void OnModelCreating(ModelBuilder modelBuilder)52 {53 modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);54 }55}5657// Entity Configuration (Fluent API)58public class ProductConfiguration : IEntityTypeConfiguration<Product>59{60 public void Configure(EntityTypeBuilder<Product> builder)61 {62 builder.HasKey(p => p.Id);63 builder.Property(p => p.Name).HasMaxLength(200).IsRequired();64 builder.HasIndex(p => p.Sku).IsUnique();65 builder.HasMany(p => p.OrderItems).WithOne(oi => oi.Product);66 }67}68```6970### Registration with DI71```csharp72builder.Services.AddDbContext<AppDbContext>(options =>73 options.UseSqlServer(connectionString)74 .EnableSensitiveDataLogging() // Dev only75 .EnableDetailedErrors()); // Dev only7677// Or with pooling (better performance)78builder.Services.AddDbContextPool<AppDbContext>(options =>79 options.UseSqlServer(connectionString));80```8182## Query Patterns8384### Use AsNoTracking for Read-Only85```csharp86// Bad - tracks entities unnecessarily87var products = await db.Products.ToListAsync();8889// Good - no tracking overhead90var products = await db.Products91 .AsNoTracking()92 .ToListAsync();93```9495### Project to DTOs96```csharp97// Bad - loads entire entity graph98var orders = await db.Orders99 .Include(o => o.Items)100 .Include(o => o.Customer)101 .ToListAsync();102103// Good - loads only needed data104var orders = await db.Orders105 .Select(o => new OrderDto106 {107 Id = o.Id,108 CustomerName = o.Customer.Name,109 ItemCount = o.Items.Count,110 Total = o.Items.Sum(i => i.Price)111 })112 .ToListAsync();113```114115### Avoid N+1 Queries116```csharp117// Bad - N+1 problem118foreach (var order in orders)119{120 var items = await db.OrderItems121 .Where(i => i.OrderId == order.Id)122 .ToListAsync();123}124125// Good - eager loading126var orders = await db.Orders127 .Include(o => o.Items)128 .ToListAsync();129130// Good - split query for large graphs131var orders = await db.Orders132 .Include(o => o.Items)133 .AsSplitQuery()134 .ToListAsync();135```136137### Compiled Queries (EF Core 9)138```csharp139// Pre-compiled for frequently used queries140private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =141 EF.CompileAsyncQuery((AppDbContext db, int id) =>142 db.Products.FirstOrDefault(p => p.Id == id));143144// Usage145var product = await GetProductById(db, productId);146```147148## Migration Patterns149150### Creating Migrations151```bash152# Add migration153dotnet ef migrations add AddProductIndex154155# Apply to database156dotnet ef database update157158# Generate SQL script159dotnet ef migrations script --idempotent -o migrate.sql160```161162### Data Migrations163```csharp164public partial class AddProductIndex : Migration165{166 protected override void Up(MigrationBuilder migrationBuilder)167 {168 migrationBuilder.CreateIndex(169 name: "IX_Products_Sku",170 table: "Products",171 column: "Sku",172 unique: true);173174 // Data migration (if needed)175 migrationBuilder.Sql(@"176 UPDATE Products177 SET NormalizedName = UPPER(Name)178 WHERE NormalizedName IS NULL");179 }180181 protected override void Down(MigrationBuilder migrationBuilder)182 {183 migrationBuilder.DropIndex(184 name: "IX_Products_Sku",185 table: "Products");186 }187}188```189190## Anti-Patterns to Avoid191192| Anti-Pattern | Why It's Bad | Better Approach |193|--------------|--------------|-----------------|194| `ToList()` then filter | Loads all data to memory | Filter in query |195| Multiple DbContext per request | Transaction issues | Scoped lifetime |196| Lazy loading everywhere | N+1 queries | Explicit Include |197| Generic repository wrapper | Removes query power | Use DbContext directly |198| Ignoring generated SQL | Hidden performance issues | Log and review |199| `SaveChanges()` in loops | Many roundtrips | Batch then save |200201## Performance Best Practices2022031. **Index frequently queried columns:**204 ```csharp205 builder.HasIndex(p => p.CreatedAt);206 builder.HasIndex(p => new { p.Category, p.Status });207 ```2082092. **Use pagination:**210 ```csharp211 var page = await db.Products212 .OrderBy(p => p.Id)213 .Skip(pageSize * pageNumber)214 .Take(pageSize)215 .ToListAsync();216 ```2172183. **Batch updates (EF Core 7+):**219 ```csharp220 await db.Products221 .Where(p => p.Category == "Obsolete")222 .ExecuteDeleteAsync();223224 await db.Products225 .Where(p => p.Category == "Sale")226 .ExecuteUpdateAsync(p => p.SetProperty(x => x.Price, x => x.Price * 0.9m));227 ```2282294. **Minimize network roundtrips:**230 ```csharp231 // Bad - 3 roundtrips232 var product = await db.Products.FindAsync(id);233 var reviews = await db.Reviews.Where(r => r.ProductId == id).ToListAsync();234 var related = await db.Products.Where(p => p.Category == product.Category).ToListAsync();235236 // Good - 1 roundtrip237 var data = await db.Products238 .Where(p => p.Id == id)239 .Select(p => new240 {241 Product = p,242 Reviews = p.Reviews,243 Related = db.Products.Where(r => r.Category == p.Category).Take(5)244 })245 .FirstOrDefaultAsync();246 ```247248## Concurrency Patterns249250```csharp251public class Product252{253 public int Id { get; set; }254 public string Name { get; set; }255256 [ConcurrencyCheck]257 public int Version { get; set; }258259 // Or use RowVersion260 [Timestamp]261 public byte[] RowVersion { get; set; }262}263264// Handle concurrency conflicts265try266{267 await db.SaveChangesAsync();268}269catch (DbUpdateConcurrencyException ex)270{271 var entry = ex.Entries.Single();272 var databaseValues = await entry.GetDatabaseValuesAsync();273 // Resolve conflict...274}275```276277## Deliver278279- EF Core models and queries that match the domain280- safer migrations and lifetime management281- performance-aware data access decisions282- proper indexing and query optimization283284## Validate285286- query behavior is intentional (check SQL logs)287- migrations are reviewable and correct288- no N+1 queries in common paths289- indexes exist for filtered/sorted columns290- DbContext lifetime is scoped properly291- concurrency is handled for critical entities