Skip to main content
skill CX Codex

ef-core · codewithmukesh — Claude Code skill: what it does, how to install

Entity Framework Core patterns for .NET 10. Helps agents configure DbContext, run bulk updates, manage migrations, and build fast DTO query projections.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add codewithmukesh/dotnet-claude-kit --skill ef-core -a claude-code
Open on GitHub

What ef-core does

The skill provides .NET 10 database patterns for coding assistants. It directs the agent to organize entity settings via IEntityTypeConfiguration<T>, project queries directly into DTOs with .Select() to avoid over-fetching, and run bulk modifications with ExecuteUpdateAsync and ExecuteDeleteAsync without tracking overhead.

It includes implementations for SaveChangesInterceptor auditing, compiled queries via EF.CompileAsyncQuery, and value converters for strongly-typed record IDs. Wrapping DbContext in another repository or unit of work abstraction is flagged as an anti-pattern because EF Core handles both internally.

Database schema changes follow a strict workflow: generate code with dotnet ef migrations add, inspect the migration manually, and never auto-apply changes in production. For production rollouts, the skill relies on generating idempotent SQL scripts with dotnet ef migrations script --idempotent.

What it helps with

  • project LINQ queries into DTOs to avoid over-fetching and N+1 issues
  • execute bulk updates and deletes directly in the database without change tracking
  • track audit timestamps on entities using a SaveChangesInterceptor
  • generate idempotent SQL migration scripts for production deployments

Example prompts for the agent

  • Configure the Order entity using IEntityTypeConfiguration with indexes and cascade delete
  • Write a paginated query for orders projecting into OrderSummary records
  • Implement an audit interceptor that sets CreatedAt and UpdatedAt on saving changes

How to install ef-core

1Skills CLI (Vercel)

npx skills add codewithmukesh/dotnet-claude-kit --skill ef-core -a claude-code

2Manually: put the files in place

~/.claude/skills/ef-core/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/ef-core/

Step by step, with commands for each platform: How to install skills in Claude Code: five ways

Questions about ef-core

How to install ef-core in Claude Code?

Run npx skills add codewithmukesh/dotnet-claude-kit --skill ef-core -a claude-code. The skill file will be placed in ~/.claude/skills/ef-core/SKILL.md.

What does the ef-core skill do?

It provides .NET 10 patterns for Entity Framework Core, covering DbContext configuration, query projections, interceptors, compiled queries, and bulk operations.

Why does the skill discourage wrapping DbContext in a repository?

EF Core already implements the Unit of Work and Repository patterns through DbContext and DbSet. Adding another repository layer introduces unnecessary abstractions that restrict EF capabilities.

Source text

SKILL.md · MIT
---
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

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

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

### Query Projections (Avoid Over-Fetching)

```csharp
// 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

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

```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);
    }
}

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

```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));
}

// Usage
var order = await OrderQueries.GetById(db, orderId, ct);
```

### Value Converters

```csharp
// 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

```bash
# 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

```csharp
// 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

```csharp
// 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

```csharp
// 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

```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 database
var 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 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 |

© codewithmukesh · MIT · codewithmukesh/dotnet-claude-kit

Similar skills

Everything for Backend and APIs →

ef-core by other authors comes first.

ef-core official github This skill guides Entity Framework Core reviews and suggests practices for models, queries, migrations, security, and tests. Use it when EF Core code needs a focused check. 9,594installs optimizing-ef-core-queries dotnet Reviews EF Core data access in services, repositories, imports and background jobs, and points at queries that run slowly, duplicate rows or hit the database too often. CX Codex +1 5,700installs analyzing-dotnet-performance dotnet Scans C# and .NET code for about 50 performance anti-patterns across async, memory, LINQ, and I/O. Handy when auditing hot paths and memory allocations before release. CX Codex +1 4,900installs efcore-patterns Aaronontheweb A skill for Entity Framework Core work: NoTracking by default, query splitting, migration management and common pitfalls. CX Codex +1 1,600installs dotnet-backend-patterns Seth Hobson Skill for C# and .NET development: patterns for async code, dependency injection, Entity Framework Core, caching, and testing. Use when building backend APIs, MCP servers, or reviewing C# code. CX Codex 19,076installs dotnet-patterns affaan-m C# and ASP.NET Core architectural conventions for AI coding agents. Enforces patterns for dependency injection, immutability, async/await, and error handling. CX Codex +1 4,600installs csharp official github A rule that pins Copilot to one C# style: naming, nullable types, formatting and ASP.NET Core patterns. Useful when the agent writes differently in every file. ★ 40,418repo stars

From the same set46

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor