Ef Core

codewithmukesh/dotnet-claude-kit/skills/ef-core

by codewithmukesh23300897f4d1No license754 starsListed Oct 8, 2026Updated Oct 8, 2026Repository updated 2 months ago

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".

Instructions only

EF Core (.NET 10)

Core Principles

  1. 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.
  2. 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.
  3. Queries should be projections — Use .Select() to project into DTOs instead of loading full entities. This avoids over-fetching and N+1 issues.
  4. 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.

csharp
// Persistence/AppDbContext.cspublic 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.cspublic 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

csharp
// Program.csbuilder.Services.AddDbContext<AppDbContext>(options =>    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));

Query Projections (Avoid Over-Fetching)

csharp
// GOOD — project to DTO, only loads needed columnspublic 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

csharp
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.

csharp
// Update without loading entitiesawait 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 entitiesawait 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.

csharp
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);    }}
// Registrationbuilder.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.

csharp
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));}
// Usagevar order = await OrderQueries.GetById(db, orderId, ct);

Value Converters

csharp
// Store enum as stringbuilder.Property(o => o.Status)    .HasConversion<string>()    .HasMaxLength(50);
// Strongly-typed IDspublic readonly record struct OrderId(Guid Value);
builder.Property(o => o.Id)    .HasConversion(id => id.Value, value => new OrderId(value));

Migrations Workflow

bash
# Create a migrationdotnet 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 databasedotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api
# Generate SQL script for productiondotnet ef migrations script --idempotent --output migrations.sql

Global Query Filters

csharp
// Soft delete filterbuilder.HasQueryFilter(o => !o.IsDeleted);
// Multi-tenant filterbuilder.HasQueryFilter(o => o.TenantId == _tenantProvider.TenantId);
// Bypass when neededvar allOrders = await db.Orders.IgnoreQueryFilters().ToListAsync(ct);

Anti-patterns

Don't Wrap DbContext in a Repository

csharp
// BAD — unnecessary abstraction that limits EF Core's powerpublic interface IOrderRepository{    Task<Order?> GetByIdAsync(Guid id);    Task AddAsync(Order order);    Task SaveChangesAsync();}
// GOOD — use DbContext directly in handlerspublic 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

csharp
// BAD — lazy loading causes N+1 queries and hides data accessbuilder.Services.AddDbContext<AppDbContext>(options =>    options.UseLazyLoadingProxies()); // DON'T
// GOOD — explicit loading with Include or projectionvar orders = await db.Orders    .Include(o => o.Items)    .Where(o => o.CustomerId == customerId)    .ToListAsync(ct);

Don't Use .ToListAsync() Then Filter in Memory

csharp
// 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 databasevar pending = await db.Orders    .Where(o => o.Status == OrderStatus.Pending)    .ToListAsync(ct);

Don't Forget to Await Async Methods

csharp
// BAD — missing await, returns before save completespublic void Handle(CreateOrder.Command command){    db.Orders.Add(order);    db.SaveChangesAsync(); // Fire-and-forget BUG}
// GOODpublic async Task Handle(CreateOrder.Command command, CancellationToken ct){    db.Orders.Add(order);    await db.SaveChangesAsync(ct);}

Decision Guide

ScenarioRecommendation
Standard CRUDDbContext with projections
Bulk updates (100+ rows)ExecuteUpdateAsync / ExecuteDeleteAsync
Hot-path read queryCompiled query
Complex reporting queryRaw SQL with FromSqlInterpolated or Dapper
Audit trailsSaveChangesInterceptor
Multi-tenancyGlobal query filter
Soft deletesGlobal query filter + interceptor
Strongly-typed IDsValue converter
Production migrationIdempotent SQL script, never auto-migrate

Source and attribution

Source:codewithmukesh/dotnet-claude-kitinskills/ef-coreat commit2330089

License: No license

Content belongs to its original authors. SourceWeft indexes it from a public repository.

Report or request removal

More from codewithmukesh/dotnet-claude-kit

Wrap Up

codewithmukesh

Captures end-of-session work, pending tasks and learnings into a handoff file, and reloads it at session start.

Productivity & Workflow754updated 2 months ago

Workflow Mastery

codewithmukesh

Claude Code workflow mastery for .NET developers. Covers parallel execution with git worktrees, plan mode strategy, verification loops, auto-formatting hooks, permission setup for dotnet CLI, prompting techniques, subagent patterns, and context discipline — token budget management, MCP-first navigation, lazy loading, and subagent isolation — all adapted for the .NET ecosystem. Load this skill when setting up Claude Code for a .NET project, optimizing workflows, running parallel sessions, when context is running low or sessions feel sluggish, when exploring a large codebase efficiently, or when the user mentions "productivity", "workflow", "parallel", "worktree", "plan mode", "permissions", "hooks", "10x", "setup Claude Code", "speed up development", "context", "tokens", "budget", "running out of context", "too many files", or "large codebase". Inspired by tips from Boris Cherny (creator of Claude Code) and the Anthropic team.

Awaiting classification754updated 2 months ago

Vertical Slice

codewithmukesh

Guides .NET developers in structuring applications with Vertical Slice Architecture, covering feature folders, endpoint grouping and handler patterns.

Software Development754updated 2 months ago

Testing

codewithmukesh

Testing strategy for .NET 10 applications. Covers xUnit v3, WebApplicationFactory for integration tests, Testcontainers for real database testing, Verify for snapshot testing, and the AAA pattern. Load this skill when writing tests, setting up test infrastructure, reviewing test coverage, or when the user mentions "test", "xUnit", "WebApplicationFactory", "Testcontainers", "integration test", "unit test", "bUnit", "snapshot test", "Verify", "test coverage", "AAA pattern", "WireMock", or "FakeTimeProvider".

Awaiting classification754updated 2 months ago

Tdd

codewithmukesh

Guided test-driven development workflow for .NET 10 using xUnit v3, WebApplicationFactory, Testcontainers, and Verify snapshots. Follows the strict red-green-refactor cycle. Use when: "TDD", "test-driven", "let's TDD this", "red green refactor", "write the test first", or when building a feature with clear acceptance criteria.

Awaiting classification754updated 2 months ago

Spec

codewithmukesh

Turns a vague feature idea into an agreed, persisted specification file through structured questioning rounds.

Productivity & Workflow754updated 2 months ago
Ef Core · skills/ef-core Agent Skill | SourceWeft