Communityコーディング&開発github.com

efcore-patterns

Entity Framework Core のベストプラクティス:デフォルトのNoTracking、ナビゲーションコレクションのクエリ分割、マイグレーション管理、専用マイグレーションサービス、よくある落とし穴などを紹介します。

efcore-patterns とは?

このスキルは、Entity Framework Core を使用する開発者向けの包括的なベストプラクティスを提供します。主なテーマとして、デフォルトの変更追跡無効化(NoTracking)による読み取りクエリのパフォーマンス最適化、ナビゲーションコレクションの効率的な読み込みのためのクエリ分割、EF Core CLI を使用した安全なマイグレーション管理、.NET Aspire と統合した専用マイグレーションサービスの実装、一時的なデータベース障害を処理するための ExecutionStrategy の活用、そして実行更新(ExecuteUpdate/ExecuteDelete)によるバルク操作の最適化が含まれます。また、変更追跡の競合や N+1 クエリ問題などの一般的な落とし穴とその回避策についても詳しく説明します。このスキルは、Claude Code や Cursor などの AI エージェントが EF Core プロジェクトを設定・保守する際に、パフォーマンスが高く信頼性のあるデータアクセスコードを生成するのに役立ちます。

対応~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Aaronontheweb/dotnet-skills/tree/main/skills/efcore-patterns

Installed? Explore more コーディング&開発 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

お気に入りのAIに質問する

このエージェントスキルを事前に読み込んだ状態で新しいチャットを開きます。

ドキュメント

Entity Framework Core パターン

このスキルを使用するタイミング

以下の場合にこのスキルを使用します:

  • 新しいプロジェクトで EF Core をセットアップするとき
  • クエリパフォーマンスを最適化するとき
  • データベースマイグレーションを管理するとき
  • EF Core を .NET Aspire と統合するとき
  • 変更追跡の問題をデバッグするとき
  • 複数のナビゲーションコレクションを効率的に読み込むとき(クエリ分割)

基本原則

  1. デフォルトの NoTracking - ほとんどのクエリは読み取り専用です。追跡は明示的にオプトインします
  2. マイグレーションを手動で編集しない - 常に CLI コマンドを使用します
  3. 専用マイグレーションサービス - マイグレーションの実行をアプリケーションの起動から分離します
  4. 再試行のための ExecutionStrategy - 一時的なデータベース障害を処理します
  5. 明示的な更新 - NoTracking の場合、エンティティを明示的に更新対象としてマークします

パターン 1: デフォルトの NoTracking

DbContext を構成して、デフォルトで変更追跡を無効にします。これにより、読み取りが多いワークロードのパフォーマンスが向上します。

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
        // 読み取り専用クエリのパフォーマンス向上のため、デフォルトで変更追跡を無効にする
        // 変更を追跡する必要があるクエリには、明示的に .AsTracking() を使用する
        ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
    }

    public DbSet<Order> Orders => Set<Order>();
    public DbSet<Customer> Customers => Set<Customer>();
}

NoTracking が有効な場合

読み取り専用クエリは通常通り動作します:

// ✅ 高速な読み取り - 追跡のオーバーヘッドなし
var orders = await dbContext.Orders
    .Where(o => o.Status == OrderStatus.Pending)
    .ToListAsync();

書き込みには明示的な処理が必要です:

// ❌ 誤り - エンティティが追跡されていないため、SaveChanges は何もしない
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // 何も起きない!

// ✅ 正しい - エンティティを明示的に更新対象としてマークする
var order = await dbContext.Orders.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
dbContext.Orders.Update(order); // エンティティ全体を変更済みとしてマーク
await dbContext.SaveChangesAsync();

// ✅ これも正しい - クエリに AsTracking() を使用する
var order = await dbContext.Orders
    .AsTracking()
    .FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // 動作する!

追跡を使用するタイミング

シナリオ追跡を使用するか?理由
UI にデータを表示するいいえ読み取り専用、更新なし
API GET エンドポイントいいえデータを返すのみ、変更なし
単一エンティティの更新はい または 明示的な Update()変更を保存する必要がある
ナビゲーションを含む複雑な更新はい追跡がリレーションシップを処理する
バッチ操作いいえ + ExecuteUpdateより効率的

明示的な追加/更新パターン

public class OrderService
{
    private readonly ApplicationDbContext _db;

    // 作成 - 常に Add を使用する(追跡設定に関係なく動作する)
    public async Task<Order> CreateOrderAsync(Order order)
    {
        _db.Orders.Add(order);
        await _db.SaveChangesAsync();
        return order;
    }

    // 更新 - 明示的に変更済みとしてマークする
    public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
    {
        var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
            ?? throw new NotFoundException($"注文 {orderId} が見つかりません");

        order.Status = newStatus;
        order.UpdatedAt = DateTimeOffset.UtcNow;

        // DbContext がデフォルトで NoTracking を使用するため、明示的に変更済みとしてマークする
        _db.Orders.Update(order);
        await _db.SaveChangesAsync();
    }

    // 削除 - アタッチして削除する
    public async Task DeleteOrderAsync(Guid orderId)
    {
        var order = new Order { Id = orderId };
        _db.Orders.Remove(order);
        await _db.SaveChangesAsync();
    }
}

パターン 2: マイグレーションを手動で編集しない

重要: EF Core CLI コマンドを使用してマイグレーションを管理してください。以下のことは絶対に行わないでください:

  • マイグレーションファイルを手動で編集する(Up()/Down() 内のカスタム SQL を除く)
  • マイグレーションファイルを直接削除する
  • マイグレーションファイルの名前を変更する
  • マイグレーションをプロジェクト間でコピーする

マイグレーションの作成

# 新しいマイグレーションを作成する
dotnet ef migrations add AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# 特定の DbContext を指定する場合(複数の DbContext がある場合)
dotnet ef migrations add AddCustomerTable \
    --context ApplicationDbContext \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

マイグレーションの削除

# 最後のマイグレーションを削除する(まだ適用されていない場合)
dotnet ef migrations remove \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# 絶対にしないでください:
# rm Migrations/20240101_AddCustomerTable.cs  # ❌ 悪い例!

マイグレーションの適用

# 保留中のすべてのマイグレーションを適用する
dotnet ef database update \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# 特定のマイグレーションまで適用する
dotnet ef database update AddCustomerTable \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

# 以前のマイグレーションにロールバックする
dotnet ef database update PreviousMigrationName \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

SQL スクリプトの生成

# すべてのマイグレーションの SQL スクリプトを生成する
dotnet ef migrations script \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api \
    --output migrations.sql

# べき等なスクリプトを生成する(複数回実行しても安全)
dotnet ef migrations script \
    --idempotent \
    --project src/MyApp.Infrastructure \
    --startup-project src/MyApp.Api

パターン 3: Aspire を使用した専用マイグレーションサービス

マイグレーションの実行をメインアプリケーションから分離し、専用のマイグレーションサービスを使用します。これにより以下が保証されます:

  • アプリケーションの起動前にマイグレーションが完了する
  • 関心事の明確な分離
  • テスト環境での制御されたシード

プロジェクト構成

src/
├── MyApp.AppHost/           # Aspire オーケストレーション
├── MyApp.Api/               # メインアプリケーション
├── MyApp.Infrastructure/    # DbContext とマイグレーション
└── MyApp.MigrationService/  # 専用マイグレーションランナー

MigrationService Program.cs

using MyApp.Infrastructure.Data;
using MyApp.MigrationService;
using Microsoft.EntityFrameworkCore;

var builder = Host.CreateApplicationBuilder(args);

// Aspire サービスのデフォルトを追加
builder.AddServiceDefaults();

// PostgreSQL DbContext を追加
var connectionString = builder.Configuration.GetConnectionString("appdb")
    ?? throw new InvalidOperationException("接続文字列 'appdb' が見つかりません。");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString, npgsqlOptions =>
        npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));

// マイグレーションワーカーを追加
builder.Services.AddHostedService<MigrationWorker>();

var host = builder.Build();
host.Run();

MigrationWorker.cs

public class MigrationWorker : BackgroundService
{
    private readonly IServiceProvider _serviceProvider;
    private readonly IHostApplicationLifetime _hostApplicationLifetime;
    private readonly ILogger<MigrationWorker> _logger;

    public MigrationWorker(
        IServiceProvider serviceProvider,
        IHostApplicationLifetime hostApplicationLifetime,
        ILogger<MigrationWorker> logger)
    {
        _serviceProvider = serviceProvider;
        _hostApplicationLifetime = hostApplicationLifetime;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("マイグレーションサービスを開始しています...");

        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();

            await RunMigrationsAsync(dbContext, stoppingToken);

            _logger.LogInformation("マイグレーションサービスが正常に完了しました。");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "マイグレーションサービスが失敗しました: {Error}", ex.Message);
            throw;
        }
        finally
        {
            // マイグレーション完了後にアプリケーションを停止する
            _hostApplicationLifetime.StopApplication();
        }
    }

    private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
    {
        // 一時的な障害処理のために実行戦略を使用
        var strategy = dbContext.Database.CreateExecutionStrategy();

        await strategy.ExecuteAsync(async () =>
        {
            var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);

            if (pendingMigrations.Any())
            {
                _logger.LogInformation("{Count} 個の保留中のマイグレーションを適用しています...",
                    pendingMigrations.Count());

                await dbContext.Database.MigrateAsync(ct);

                _logger.LogInformation("マイグレーションが正常に適用されました。");
            }
            else
            {
                _logger.LogInformation("保留中のマイグレーションはありません。データベースは最新です。");
            }
        });
    }
}

AppHost の構成

var builder = DistributedApplication.CreateBuilder(args);

var postgres = builder.AddPostgres("postgres");
var db = postgres.AddDatabase("appdb");

// マイグレーションが最初に実行され、その後終了する
var migrations = builder.AddProject<Projects.MyApp_MigrationService>("migrations")
    .WaitFor(db)
    .WithReference(db);

// API はマイグレーションの完了を待機する
var api = builder.AddProject<Projects.MyApp_Api>("api")
    .WaitForCompletion(migrations)  // 重要:マイグレーションが完了するのを待つ
    .WithReference(db);

パターン 4: 一時的な障害のための ExecutionStrategy

一時的に失敗する可能性のある操作には、常に CreateExecutionStrategy() を使用します:

public async Task UpdateWithRetryAsync(Guid id, Action<Order> update)
{
    var strategy = _dbContext.Database.CreateExecutionStrategy();

    await strategy.ExecuteAsync(async () =>
    {
        var order = await _dbContext.Orders
            .AsTracking()
            .FirstOrDefaultAsync(o => o.Id == id);

        if (order is null) return;

        update(order);
        await _dbContext.SaveChangesAsync();
    });
}

重要: CreateExecutionStrategy() をユーザーが開始したトランザクションと一緒に使用することはできません。再試行付きのトランザクションが必要な場合:

var strategy = _dbContext.Database.CreateExecutionStrategy();

await strategy.ExecuteAsync(async () =>
{
    // トランザクションは戦略コールバックの内部になければならない
    await using var transaction = await _dbContext.Database.BeginTransactionAsync();

    try
    {
        // ... 操作 ...
        await _dbContext.SaveChangesAsync();
        await transaction.CommitAsync();
    }
    catch
    {
        await transaction.RollbackAsync();
        throw;
    }
});

パターン 5: ExecuteUpdate/ExecuteDelete を使用したバルク操作

バルク操作には、エンティティをロードする代わりに EF Core 7 以降の ExecuteUpdateAsync と ExecuteDeleteAsync を使用します:

// ❌ 低速 - すべてのエンティティをメモリにロードする
var expiredOrders = await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ToListAsync();

foreach (var order in expiredOrders)
{
    order.Status = OrderStatus.Expired;
}
await _db.SaveChangesAsync();

// ✅ 高速 - 単一の SQL UPDATE ステートメント
await _db.Orders
    .Where(o => o.ExpiresAt < DateTimeOffset.UtcNow)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(o => o.Status, OrderStatus.Expired)
        .SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow));

// ✅ 高速 - 単一の SQL DELETE ステートメント
await _db.Orders
    .Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate)
    .ExecuteDeleteAsync();

よくある落とし穴

1. NoTracking 時の更新を忘れる

// ❌ 暗黙の失敗 - エンティティが追跡されていない
var customer = await _db.Customers.FindAsync(id);
customer.Name = "新しい名前";
await _db.SaveChangesAsync(); // 何もしない!

// ✅ 明示的な更新
var customer = await _db.Customers.FindAsync(id);
customer.Name = "新しい名前";
_db.Customers.Update(customer);
await _db.SaveChangesAsync();

2. N+1 クエリ問題

// ❌ N+1 クエリ - 注文ごとに 1 つのクエリ
var customers = await _db.Customers.ToListAsync();
foreach (var customer in customers)
{
    var orders = customer.Orders; // 遅延ロードがクエリをトリガー
}

// ✅ 一括読み込み - 単一のクエリ
var customers = await _db.Customers
    .Include(c => c.Orders)
    .ToListAsync();

3. 複数の DbContext インスタンスによる追跡の競合

// ❌ 追跡の競合 - エンティティが異なるコンテキストで追跡されている
var order1 = await _db1.Orders.AsTracking().FindAsync(id);
var order2 = await _db2.Orders.AsTracking().FindAsync(id);
order2.Status = OrderStatus.Shipped;
await _db2.SaveChangesAsync(); // 例外がスローされるか、予期しない動作をする可能性がある

// ✅ 単一のコンテキストを使用するか、最初にデタッチする
_db1.Entry(order1).State = EntityState.Detached;

4. Async を一貫して使用しない

// ❌ async コンテキストでのブロッキング呼び出し
var orders = _db.Orders.ToList(); // スレッドをブロックする

// ✅ すべて非同期で
var orders = await _db.Orders.ToListAsync();

5. ループ内でのクエリ

// ❌ 反復ごとにクエリ
foreach (var orderId in orderIds)
{
    var order = await _db.Orders.FindAsync(orderId);
    // 注文を処理
}

// ✅ 単一のクエリ
var orders = await _db.Orders
    .Where(o => orderIds.Contains(o.Id))
    .ToListAsync();

DI における DbContext のライフタイム

ASP.NET Core(デフォルトで Scoped)

// Scoped = HTTP リクエストごとに 1 つのインスタンス
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseNpgsql(connectionString));

…

[翻訳のため省略]

Individual skills in this repo

This repo contains 20 individual skills — each has its own dedicated page.

akka-hosting-actor-patterns

Patterns for building entity actors with Akka.Hosting - GenericChildPerEntityParent, message extractors, cluster sharding abstraction, akka-reminders, and ITimeProvider. Supports both local testing and clustered production modes.

akka-net-aspire-configuration

Configure Akka.NET with .NET Aspire for local development and production deployments. Covers actor system setup, clustering, persistence, Akka.Management integration, and Aspire orchestration patterns.

akka-net-best-practices

Critical Akka.NET best practices including EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution patterns, and cluster/local mode abstractions for testability.

akka-net-management

Akka.Management for cluster bootstrapping, service discovery (Kubernetes, Azure, Config), health checks, and dynamic cluster formation without static seed nodes.

akka-net-testing-patterns

Write unit and integration tests for Akka.NET actors using modern Akka.Hosting.TestKit patterns. Covers dependency injection, TestProbes, persistence testing, and actor interaction verification. Includes guidance on when to use traditional TestKit.

api-design

Design stable, compatible public APIs using extend-only design principles. Manage API compatibility, wire compatibility, and versioning for NuGet packages and distributed systems.

aspire-configuration

Configure Aspire AppHost to emit explicit app config via environment variables; keep app code free of Aspire clients and service discovery.

aspire-integration-testing

Write integration tests using .NET Aspire

aspire-service-defaults

Create a shared ServiceDefaults project for Aspire applications. Centralizes OpenTelemetry, health checks, resilience, and service discovery configuration across all services.

crap-analysis

Analyze code coverage and CRAP (Change Risk Anti-Patterns) scores to identify high-risk code. Use OpenCover format with ReportGenerator for Risk Hotspots showing cyclomatic complexity and untested code paths.

csharp-concurrency-patterns

Choosing the right concurrency abstraction in .NET - from async/await for I/O to Channels for producer/consumer to Akka.NET for stateful entity management. Avoid locks and manual synchronization unless absolutely necessary.

database-performance

Database access patterns for performance. Separate read/write models, avoid N+1 queries, use AsNoTracking, apply row limits, and never do application-side joins. Works with EF Core and Dapper.

dependency-injection-patterns

Organize DI registrations using IServiceCollection extension methods. Group related services into composable Add* methods for clean Program.cs and reusable configuration in tests.

dotnet-devcert-trust

Diagnose and fix .NET HTTPS dev certificate trust issues on Linux. Covers the full certificate lifecycle from generation to system CA bundle inclusion, with distro-specific guidance for Ubuntu, Fedora, Arch, and WSL2.

dotnet-local-tools

Managing local .NET tools with dotnet-tools.json for consistent tooling across development environments and CI/CD pipelines.

dotnet-project-structure

Modern .NET project structure including .slnx solution format, Directory.Build.props, central package management, SourceLink, version management with RELEASE_NOTES.md, and SDK pinning with global.json.

dotnet-slopwatch

Use Slopwatch to detect LLM reward hacking in .NET code changes. Run after every code modification to catch disabled tests, suppressed warnings, empty catch blocks, and other shortcuts that mask real problems.

ilspy-decompile

Understand implementation details of .NET code by decompiling assemblies. Use when you want to see how a .NET API works internally, inspect NuGet package source, view framework implementation, or understand compiled .NET binaries.

mailpit-integration

Test email sending locally using Mailpit with .NET Aspire. Captures all outgoing emails without sending them. View rendered HTML, inspect headers, and verify delivery in integration tests.

marketplace-publishing

Workflow for publishing skills and agents to the dotnet-skills Claude Code marketplace. Covers adding new content, updating plugin.json, validation, and release tagging.

関連スキル