| name | entity-framework-core |
| description | 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. |
| compatibility | Requires EF Core 7+ (preferably 8/9 for latest features). |
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
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);
}
}
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()
.EnableDetailedErrors());
builder.Services.AddDbContextPool<AppDbContext>(options =>
options.UseSqlServer(connectionString));
Query Patterns
Use AsNoTracking for Read-Only
var products = await db.Products.ToListAsync();
var products = await db.Products
.AsNoTracking()
.ToListAsync();
Project to DTOs
var orders = await db.Orders
.Include(o => o.Items)
.Include(o => o.Customer)
.ToListAsync();
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
foreach (var order in orders)
{
var items = await db.OrderItems
.Where(i => i.OrderId == order.Id)
.ToListAsync();
}
var orders = await db.Orders
.Include(o => o.Items)
.ToListAsync();
var orders = await db.Orders
.Include(o => o.Items)
.AsSplitQuery()
.ToListAsync();
Compiled Queries (EF Core 9)
private static readonly Func<AppDbContext, int, Task<Product?>> GetProductById =
EF.CompileAsyncQuery((AppDbContext db, int id) =>
db.Products.FirstOrDefault(p => p.Id == id));
var product = await GetProductById(db, productId);
Migration Patterns
Creating Migrations
dotnet ef migrations add AddProductIndex
dotnet ef database update
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);
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:
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();
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; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.Single();
var databaseValues = await entry.GetDatabaseValuesAsync();
}
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