efcore-patterns
Entity Framework Coreのベストプラクティスを提供するスキルで、デフォルトのNoTracking設定、ナビゲーションコレクションに対するクエリ分割、マイグレーション管理、専用マイグレーションサービスの構成など、実務で役立つパターンを網羅します。よくある落とし穴を避けるためのガイダンスも含まれており、EF Coreを使った開発の品質と安全性を高めます。
description の原文を見る
Entity Framework Core best practices including NoTracking by default, query splitting for navigation collections, migration management, dedicated migration services, and common pitfalls to avoid.
SKILL.md 本文
Entity Framework Core パターン
このスキルを使用する場合
以下の場合にこのスキルを使用してください:
- 新しいプロジェクトで EF Core をセットアップする
- クエリパフォーマンスを最適化する
- データベースマイグレーションを管理する
- EF Core を .NET Aspire と統合する
- 変更追跡の問題をデバッグする
- 複数のナビゲーションコレクションを効率的に読み込む (クエリ分割)
コア原則
- デフォルトで NoTracking - ほとんどのクエリは読み取り専用; 追跡をオプトインで有効化
- マイグレーションを手動で編集しない - 常に CLI コマンドを使用
- 専用マイグレーションサービス - マイグレーション実行をアプリケーション起動から分離
- ExecutionStrategy でリトライ - 一時的なデータベース障害を処理
- 明示的な更新 - NoTracking 時に、エンティティを明示的に更新対象としてマーク
パターン 1: デフォルトで NoTracking
DbContext を構成して、デフォルトで変更追跡を無効にします。これは読み取り中心のワークロードのパフォーマンスを向上させます。
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
// Disable change tracking by default for better performance on read-only queries
// Use .AsTracking() explicitly for queries that need to track changes
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();
// ✅ 正しい方法 2 - クエリに AsTracking() を使用
var order = await dbContext.Orders
.AsTracking()
.FirstOrDefaultAsync(o => o.Id == orderId);
order.Status = OrderStatus.Shipped;
await dbContext.SaveChangesAsync(); // 動作する!
追跡を使用する場合
| シナリオ | 追跡を使用? | 理由 |
|---|---|---|
| UI にデータを表示 | いいえ | 読み取り専用、更新なし |
| API GET エンドポイント | いいえ | データ返却、変更なし |
| 単一エンティティの更新 | はい、または明示的に Update() | 変更を保存する必要 |
| ナビゲーションを含む複雑な更新 | はい | 追跡が関連性を処理 |
| バッチ操作 | いいえ + ExecuteUpdate | より効率的 |
明示的な Add/Update パターン
public class OrderService
{
private readonly ApplicationDbContext _db;
// CREATE - 常に Add を使用 (追跡設定に関係なく動作)
public async Task<Order> CreateOrderAsync(Order order)
{
_db.Orders.Add(order);
await _db.SaveChangesAsync();
return order;
}
// UPDATE - 明示的に変更済みとしてマーク
public async Task UpdateOrderStatusAsync(Guid orderId, OrderStatus newStatus)
{
var order = await _db.Orders.FirstOrDefaultAsync(o => o.Id == orderId)
?? throw new NotFoundException($"Order {orderId} not found");
order.Status = newStatus;
order.UpdatedAt = DateTimeOffset.UtcNow;
// DbContext が NoTracking をデフォルトで使用するため、明示的にマーク
_db.Orders.Update(order);
await _db.SaveChangesAsync();
}
// DELETE - アタッチと削除
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 を使用 (複数ある場合)
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("Connection string 'appdb' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure")));
// マイグレーション Worker を追加
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("Migration service starting...");
try
{
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
await RunMigrationsAsync(dbContext, stoppingToken);
_logger.LogInformation("Migration service completed successfully.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Migration service failed: {Error}", ex.Message);
throw;
}
finally
{
// マイグレーション完了後にアプリケーションを停止
_hostApplicationLifetime.StopApplication();
}
}
private async Task RunMigrationsAsync(ApplicationDbContext dbContext, CancellationToken ct)
{
// Execution strategy を使用して一時的な障害処理
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
var pendingMigrations = await dbContext.Database.GetPendingMigrationsAsync(ct);
if (pendingMigrations.Any())
{
_logger.LogInformation("Applying {Count} pending migrations...",
pendingMigrations.Count());
await dbContext.Database.MigrateAsync(ct);
_logger.LogInformation("Migrations applied successfully.");
}
else
{
_logger.LogInformation("No pending migrations. Database is up to date.");
}
});
}
}
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 () =>
{
// トランザクションは strategy コールバック内にある必要があります
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 時に Update を忘れる
// ❌ サイレント失敗 - エンティティが追跡されていない
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New Name";
await _db.SaveChangesAsync(); // 何もしない!
// ✅ 明示的な更新
var customer = await _db.Customers.FindAsync(id);
customer.Name = "New 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; // 遅延読み込みがクエリをトリガー
}
// ✅ Eager Loading - 単一クエリ
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 の一貫性がない
// ❌ 非同期コンテキストでのブロッキング呼び出し
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));
バックグラウンドサービス (Scope を作成)
public class MyBackgroundService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// ✅ 各作業単位に対して scope を作成
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
// ... dbContext を使用 ...
}
}
Actor / 長命オブジェクト (ファクトリパターン)
public class OrderActor : ReceiveActor
{
private readonly IDbContextFactory<ApplicationDbContext> _dbFactory;
public OrderActor(IDbContextFactory<ApplicationDbContext> dbFactory)
{
_dbFactory = dbFactory;
ReceiveAsync<GetOrder>(async msg =>
{
// 各操作に対して新しいコンテキストを作成
await using var db = await _dbFactory.CreateDbContextAsync();
var order = await db.Orders.FindAsync(msg.OrderId);
Sender.Tell(order);
});
}
}
// 登録
builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString));
パターン 6: デカルト積爆発を防ぐクエリ分割
Include() で複数のナビゲーションコレクションを読み込む場合、EF Core は単一のクエリを生成するため、デカルト積爆発が発生します。10 個の注文に 10 個のアイテムがある場合、10 + 10 ではなく 100 行になります。
グローバル設定 (ほとんどの場合推奨)
DbContext 設定でクエリ分割をグローバルに有効化:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery);
}));
クエリごとのオーバーライド
より効率的な場合は単一クエリを使用:
// 構造をよく理解している場合は単一クエリを使用
var orders = await dbContext.Orders
.Include(o => o.Items)
.Include(o => o.Payments)
.AsSingleQuery() // グローバルな分割動作をオーバーライド
.ToListAsync();
トレードオフ
| 動作 | 利点 | 欠点 |
|---|---|---|
| SplitQuery | デカルト積爆発なし、大きなコレクション向け | 複数往復、一貫性の問題の可能性 |
| SingleQuery | 単一往復、トランザクション一貫性 | 複数コレクションでデカルト積爆発 |
推奨: グローバルでデフォルトを SplitQuery にし、単一クエリがより効率的であることがわかっているスペシフィッククエリで AsSingleQuery() でオーバーライド。
SingleQuery を優先すべき場合
- 小さく、よく理解されたナビゲーショングラフ (2-3 レベル)
- 関連データが常に必要なクエリ
- パフォーマンスクリティカルなパス (往復コストがデカルト積爆発より低い)
SplitQuery を優先すべき場合
- 大きい、予測不可能なナビゲーショングラフ
- 多対多関係
- すべての関連データが必要でない場合のあるクエリ
EF Core でのテスト
In-Memory プロバイダー (ユニットテストのみ)
// シンプルなユニットテストのみ - 実際のデータベース動作と一致しない
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())
.Options;
using var context = new ApplicationDbContext(options);
TestContainers を使用した実データベース (統合テスト)
詳細は testcontainers-integration-tests スキルを参照してください。
// 実 PostgreSQL をコンテナで使用
var container = new PostgreSqlBuilder()
.WithImage("postgres:16-alpine")
.Build();
await container.StartAsync();
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseNpgsql(container.GetConnectionString())
.Options;
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- aaronontheweb
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/aaronontheweb/dotnet-skills / ライセンス: MIT
関連スキル
superfluid
Superfluidプロトコルおよびそのエコシステムに関するナレッジベースです。Superfluidについて情報を検索する際は、ウェブ検索の前にこちらを参照してください。対応キーワード:Superfluid、CFA、GDA、Super App、Super Token、stream、flow rate、real-time balance、pool(member/distributor)、IDA、sentinels、liquidation、TOGA、@sfpro/sdk、semantic money、yellowpaper、whitepaper
civ-finish-quotes
実質的なタスクが真に完了した際に、文明風の儀式的な引用句を追加します。ユーザーやエージェントが機能追加、リファクタリング、分析、設計ドキュメント、プロセス改善、レポート、執筆タスクといった実際の成果物を完成させるときに、明示的な依頼がなくても使用します。短い返信や小さな修正、未完成の作業には適用しません。
nookplot
Base(Ethereum L2)上のAIエージェント向け分散型調整ネットワークです。エージェントがオンチェーンアイデンティティを登録する、コンテンツを公開する、他のエージェントにメッセージを送る、マーケットプレイスで専門家を雇う、バウンティを投稿・請求する、レピュテーションを構築する、共有プロジェクトで協業する、リサーチチャレンジを解くことでNOOKをマイニングする、キュレーションされたナレッジを備えたスタンドアロンオンチェーンエージェントをデプロイする、またはアグリーメントとリワードで収益を得る場合に利用できます。エージェントネットワーク、エージェント調整、分散型エージェント、NOOKトークン、マイニングチャレンジ、ナレッジバンドル、エージェントレピュテーション、エージェントマーケットプレイス、ERC-2771メタトランザクション、Prepare-Sign-Relay、AgentFactory、またはNookplotが言及された場合にトリガーされます。
web3-polymarket
Polygon上でのPolymarket予測市場取引統合です。認証機能(L1 EIP-712、L2 HMAC-SHA256、ビルダーヘッダー)、注文発注(GTC/GTD/FOK/FAK、バッチ、ポストオンリー、ハートビート)、市場データ(Gamma API、Data API、オーダーブック、サブグラフ)、WebSocketストリーミング(市場・ユーザー・スポーツチャネル)、CTF操作(分割、統合、償却、ネガティブリスク)、ブリッジ機能(入金、出金、マルチチェーン)、およびガスレスリレイトランザクションに対応しています。AIエージェント、自動マーケットメーカー、予測市場UI、またはPolygraph上のPolymarketと統合するアプリケーション構築時に活用できます。
ethskills
Ethereum、EVM、またはブロックチェーン関連のリクエストに対応します。スマートコントラクト、dApps、ウォレット、DeFiプロトコルの構築、監査、デプロイ、インタラクションに適用されます。Solidityの開発、コントラクトアドレス、トークン規格(ERC-20、ERC-721、ERC-4626など)、Layer 2ネットワーク(Base、Arbitrum、Optimism、zkSync、Polygon)、Uniswap、Aave、Curveなどのプロトコルとの統合をカバーします。ガスコスト、コントラクトのデシマル設定、オラクルセキュリティ、リエントランシー、MEV、ブリッジング、ウォレット管理、オンチェーンデータの取得、本番環境へのデプロイ、プロトコル進化(EIPライフサイクル、フォーク追跡、今後の変更予定)といったトピックを含みます。
xxyy-trade
このスキルは、ユーザーが「トークン購入」「トークン売却」「トークンスワップ」「暗号資産取引」「取引ステータス確認」「トランザクション照会」「トークンスキャン」「フィード」「チェーン監視」「トークン照会」「トークン詳細」「トークン安全性確認」「ウォレット一覧表示」「マイウォレット」「AIスキャン」「自動スキャン」「ツイートスキャン」「オンボーディング」「IP確認」「IPホワイトリスト」「トークン発行」「自動売却」「損切り」「利益確定」「トレーリングストップ」「保有者」「トップホルダー」「KOLホルダー」などをリクエストした場合、またはSolana/ETH/BSC/BaseチェーンでXXYYを経由した取引について言及した場合に使用します。XXYY Open APIを通じてオンチェーン取引とデータ照会を実現します。