ef-core
codewithmukesh/dotnet-claude-kit
Entity Framework Core patterns for .NET 10: DbContext, migrations, interceptors, compiled queries, and query optimization.
What is ef-core?
Covers Entity Framework Core best practices for .NET 10, including DbContext configuration, migrations workflow, interceptors, compiled queries, ExecuteUpdateAsync/ExecuteDeleteAsync, value converters, and query optimization techniques. Use this skill when working with databases, writing LINQ queries, managing schema changes, or when the user mentions EF Core, DbContext, migrations, or database performance.
- Configure DbContext with IEntityTypeConfiguration for clean, discoverable entity mappings
- Write projection-based queries with .Select() to avoid over-fetching and N+1 problems
- Implement bulk updates and deletes with ExecuteUpdateAsync and ExecuteDeleteAsync without loading entities
- Use interceptors for cross-cutting concerns like audit trails and soft deletes
- Compile hot-path queries with EF.CompileAsyncQuery for repeated query shapes
- Apply value converters for enums, strongly-typed IDs, and custom type mappings
How to install ef-core
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill ef-core- Entity Framework Core NuGet packages installed (e.g., Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.SqlServer or .Npgsql)
- DbContext class set up in your .NET project
- Database connection string configured in appsettings.json or Program.cs
How to use ef-core
- 1.Create entity configuration classes implementing IEntityTypeConfiguration<T> and apply them in OnModelCreating
- 2.Write queries using .Select() projections to DTOs instead of loading full entities
- 3.Use .Include() for explicit eager loading or split queries to avoid N+1 issues
- 4.Apply ExecuteUpdateAsync and ExecuteDeleteAsync for bulk operations that bypass change tracking
- 5.Register interceptors in AddDbContext for audit trails, soft deletes, or other cross-cutting concerns
- 6.Use EF.CompileAsyncQuery for frequently-executed queries with the same shape
- 7.Create and review migrations with dotnet ef migrations add, then apply with dotnet ef database update
Use cases
- Projecting entities to DTOs to load only required columns and improve query performance
- Implementing pagination with proper ordering and skip/take patterns
- Bulk updating order statuses or soft-deleting expired records without entity tracking overhead
- Adding audit timestamps automatically via SaveChangesInterceptor
- Caching compiled queries for frequently-executed database lookups
- Backend developers building .NET 10 applications with relational databases
- Teams adopting Entity Framework Core as their primary ORM
- Developers optimizing database queries and reducing N+1 problems
- Architects designing clean data access layers with minimal abstraction overhead
ef-core FAQ
No. DbContext already implements Unit of Work and Repository patterns internally. Use it directly in your handlers to avoid unnecessary abstraction that limits EF Core's power.
Use .Select() projections to load only needed columns, or use .Include() for explicit eager loading. Avoid lazy loading and filtering in memory after .ToListAsync().
Use ExecuteUpdateAsync for bulk updates that don't require entity tracking or business logic—it bypasses change tracking and executes directly in the database for better performance.
Implement a SaveChangesInterceptor that sets CreatedAt and UpdatedAt properties on entities implementing IAuditable before SaveChangesAsync completes.
No. Lazy loading hides data access patterns and causes N+1 queries. Use explicit .Include() or projection-based queries instead.
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
name: ef-core description: > Entity Framework Core patterns for .NET 10. Covers DbContext configuration, migrations workflow, interceptors, compiled queries, ExecuteUpdateAsync, ExecuteDeleteAsync, value converters, and query optimization. Load this skill when working with databases, writing queries, managing schema changes, or when the user mentions "EF Core", "Entity Framework", "DbContext", "migration", "LINQ query", "database", "SQL", "N+1", "Include", "split query", "value converter", "interceptor", or "compiled query".
EF Core (.NET 10)
Core Principles
- EF Core is the default ORM — Use it unless you have a specific reason not to (extreme perf, legacy DB without FK constraints). See ADR-003.
- DbContext is a unit of work — Don't wrap it in another UoW abstraction. EF Core already implements Unit of Work and Repository patterns internally.
- Queries should be projections — Use
.Select()to project into DTOs instead of loading full entities. This avoids over-fetching and N+1 issues. - Migrations are code — Treat them like any other source code. Review them, test them, never auto-apply in production.
Patterns
DbContext Configuration
Use IEntityTypeConfiguration<T> to keep entity configs separate and discoverable.
// Persistence/AppDbContext.cs
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
public DbSet<Order> Orders => Set<Order>();
public DbSet<Product> Products => Set<Product>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
}
}
// Persistence/Configurations/OrderConfiguration.cs
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.HasKey(o => o.Id);
builder.Property(o => o.Total)
.HasPrecision(18, 2);
builder.HasMany(o => o.Items)
.WithOne()
.HasForeignKey(i => i.OrderId)
.OnDelete(DeleteBehavior.Cascade);
builder.HasIndex(o => o.CustomerId);
builder.HasIndex(o => o.CreatedAt);
}
}
Registration
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
Query Projections (Avoid Over-Fetching)
// GOOD — project to DTO, only loads needed columns
public async Task<OrderResponse?> GetOrderAsync(Guid id, CancellationToken ct)
{
return await db.Orders
.Where(o => o.Id == id)
.Select(o => new OrderResponse(
o.Id,
o.Total,
o.CreatedAt,
o.Items.Select(i => new OrderItemResponse(i.ProductName, i.Quantity, i.Price)).ToList()))
.FirstOrDefaultAsync(ct);
}
Pagination
public async Task<PagedList<OrderSummary>> ListOrdersAsync(int page, int pageSize, CancellationToken ct)
{
var query = db.Orders
.OrderByDescending(o => o.CreatedAt)
.Select(o => new OrderSummary(o.Id, o.CustomerName, o.Total, o.Status));
var totalCount = await query.CountAsync(ct);
var items = await query
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync(ct);
return new PagedList<OrderSummary>(items, totalCount, page, pageSize);
}
ExecuteUpdateAsync / ExecuteDeleteAsync
Bulk operations that bypass change tracking for better performance.
// Update without loading entities
await db.Orders
.Where(o => o.Status == OrderStatus.Pending && o.CreatedAt < cutoff)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.Status, OrderStatus.Expired)
.SetProperty(o => o.UpdatedAt, clock.GetUtcNow()),
ct);
// Delete without loading entities
await db.Orders
.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < archiveCutoff)
.ExecuteDeleteAsync(ct);
Interceptors
Use interceptors for cross-cutting concerns like audit trails and soft deletes.
public class AuditInterceptor(TimeProvider clock) : SaveChangesInterceptor
{
public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken ct = default)
{
var context = eventData.Context;
if (context is null) return ValueTask.FromResult(result);
var now = clock.GetUtcNow();
foreach (var entry in context.ChangeTracker.Entries<IAuditable>())
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.CreatedAt = now;
entry.Entity.UpdatedAt = now;
break;
case EntityState.Modified:
entry.Entity.UpdatedAt = now;
break;
}
}
return ValueTask.FromResult(result);
}
}
// Registration
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options
.UseNpgsql(connectionString)
.AddInterceptors(sp.GetRequiredService<AuditInterceptor>()));
Compiled Queries
Use for hot-path queries that execute frequently with the same shape.
public class OrderQueries
{
public static readonly Func<AppDbContext, Guid, CancellationToken, Task<Order?>> GetById =
EF.CompileAsyncQuery((AppDbContext db, Guid id, CancellationToken ct) =>
db.Orders
.Include(o => o.Items)
.FirstOrDefault(o => o.Id == id));
}
// Usage
var order = await OrderQueries.GetById(db, orderId, ct);
Value Converters
// Store enum as string
builder.Property(o => o.Status)
.HasConversion<string>()
.HasMaxLength(50);
// Strongly-typed IDs
public readonly record struct OrderId(Guid Value);
builder.Property(o => o.Id)
.HasConversion(id => id.Value, value => new OrderId(value));
Migrations Workflow
# Create a migration
dotnet ef migrations add AddOrderIndex --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
# Review the generated migration — ALWAYS review before applying
# Check for data loss, index strategy, constraint names
# Apply to development database
dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
# Generate SQL script for production
dotnet ef migrations script --idempotent --output migrations.sql
Global Query Filters
// Soft delete filter
builder.HasQueryFilter(o => !o.IsDeleted);
// Multi-tenant filter
builder.HasQueryFilter(o => o.TenantId == _tenantProvider.TenantId);
// Bypass when needed
var allOrders = await db.Orders.IgnoreQueryFilters().ToListAsync(ct);
Anti-patterns
Don't Wrap DbContext in a Repository
// BAD — unnecessary abstraction that limits EF Core's power
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id);
Task AddAsync(Order order);
Task SaveChangesAsync();
}
// GOOD — use DbContext directly in handlers
public class Handler(AppDbContext db)
{
public async Task<Order?> Handle(GetOrder.Query query, CancellationToken ct)
{
return await db.Orders.FindAsync([query.Id], ct);
}
}
Don't Use Lazy Loading
// BAD — lazy loading causes N+1 queries and hides data access
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseLazyLoadingProxies()); // DON'T
// GOOD — explicit loading with Include or projection
var orders = await db.Orders
.Include(o => o.Items)
.Where(o => o.CustomerId == customerId)
.ToListAsync(ct);
Don't Use .ToListAsync() Then Filter in Memory
// BAD — loads ALL orders, filters in C#
var orders = await db.Orders.ToListAsync(ct);
var pending = orders.Where(o => o.Status == OrderStatus.Pending);
// GOOD — filter in the database
var pending = await db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync(ct);
Don't Forget to Await Async Methods
// BAD — missing await, returns before save completes
public void Handle(CreateOrder.Command command)
{
db.Orders.Add(order);
db.SaveChangesAsync(); // Fire-and-forget BUG
}
// GOOD
public async Task Handle(CreateOrder.Command command, CancellationToken ct)
{
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
}
Decision Guide
| Scenario | Recommendation |
|---|---|
| Standard CRUD | DbContext with projections |
| Bulk updates (100+ rows) | ExecuteUpdateAsync / ExecuteDeleteAsync |
| Hot-path read query | Compiled query |
| Complex reporting query | Raw SQL with FromSqlInterpolated or Dapper |
| Audit trails | SaveChangesInterceptor |
| Multi-tenancy | Global query filter |
| Soft deletes | Global query filter + interceptor |
| Strongly-typed IDs | Value converter |
| Production migration | Idempotent SQL script, never auto-migrate |
Related skills
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

error-handling
Result pattern, ProblemDetails, and global exception handling for .NET 10 APIs

health-check
Multi-dimensional health assessment for .NET projects with letter grades (A-F) using Roslyn MCP tools.

httpclient-factory
IHttpClientFactory and resilience patterns for .NET 10 HTTP clients

instinct-system
Unified learning system: confidence-scored instincts, user corrections, and discoveries for Claude agents.

logging
Health checks, correlation IDs, and observability wiring for .NET 10 services.

messaging
Asynchronous messaging patterns for .NET: Wolverine and MassTransit, outbox, sagas, and broker configuration.