mcpField
E
ef-core

ef-core

github.com/codewithmukeshデータ・データベース7913.0k 回インストール12 週間前に更新
リポジトリ

ef-core について

ef-core は、EF Core(.NET 10)でデータアクセス層を構築する開発者向けのスキルです。クエリ、永続化、マイグレーション、モデル設計を扱い、射影クエリとマイグレーションのレビューを重視します。

主な機能

  1. 01DbContext の設定と登録
  2. 02射影クエリとページングのパターン
  3. 03一括操作とコンパイル済みクエリ
  4. 04インターセプターとグローバルクエリフィルター
  5. 05マイグレーション手順とアンチパターン

活用シーン

  1. 01CRUD データアクセス層の構築
  2. 02頻繁に実行する読み取りクエリの調整
  3. 03論理削除やマルチテナントの管理
  4. 04データベースマイグレーションの確認と適用

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill ef-core

インストール後、Claude Code で/ef-coreを入力して読み込まれたか確認します。

詳細

EF Core (.NET 10)

基本原則

  1. EF CoreをデフォルトのORMとして使用します — 特別な理由(極端なパフォーマンス要件、外部キー制約のないレガシーデータベース)がない限り、EF Coreを使用してください。ADR-003を参照してください。
  2. DbContextは作業単位です — 別のUoW抽象化でラップしないでください。EF Coreには、Unit of WorkパターンとRepositoryパターンがすでに内部的に実装されています。
  3. クエリではプロジェクションを使用します — エンティティ全体を読み込む代わりに、.Select()を使用してDTOにプロジェクションしてください。これにより、過剰なデータ取得やN+1問題を防げます。
  4. マイグレーションはコードです — ほかのソースコードと同様に扱ってください。レビューとテストを行い、本番環境では決して自動適用しないでください。

パターン

DbContextの構成

エンティティの構成を分離し、見つけやすくするために、IEntityTypeConfiguration<T>を使用してください。

// 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);
    }
}
登録
// Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
クエリのプロジェクション(過剰なデータ取得を回避)
// 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);
}
ページネーション
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

パフォーマンス向上のため、変更追跡をバイパスする一括操作です。

// 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);
インターセプター

監査証跡や論理削除など、横断的な関心事にはインターセプターを使用してください。

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>()));
コンパイル済みクエリ

同じ形のクエリを頻繁に実行するホットパスで使用してください。

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);
値コンバーター
// 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));
マイグレーションのワークフロー
# 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
グローバルクエリフィルター
// 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);

アンチパターン

DbContextをリポジトリでラップしない
// 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);
    }
}
遅延読み込みを使用しない
// 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);
.ToListAsync()の後にメモリ上でフィルターしない
// 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);
非同期メソッドのawaitを忘れない
// 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);
}

判断ガイド

シナリオ推奨事項
標準的なCRUDプロジェクションを使用したDbContext
一括更新(100行以上)ExecuteUpdateAsync / ExecuteDeleteAsync
ホットパスの読み取りクエリコンパイル済みクエリ
複雑なレポートクエリFromSqlInterpolatedを使用した生SQL、またはDapper
監査証跡SaveChangesInterceptor
マルチテナンシーグローバルクエリフィルター
論理削除グローバルクエリフィルター + インターセプター
強く型付けされたID値コンバーター
本番環境のマイグレーション冪等性のあるSQLスクリプトを使用し、自動マイグレーションは行わない

コメント