dotnet-data-access-strategy について
Entity Framework Core、Dapper、Raw ADO.NETを選ぶための.NET開発者向けガイドです。性能やAOT・トリミング対応、併用方法、移行経路を比較できます。
主な機能
- 01性能と機能の比較
- 02AOT・トリミングの検討
- 03EF CoreとDapperの併用方法
- 04移行経路の整理
活用シーン
- 01新しい.NETアプリのデータアクセス選定
- 02選定前のAOT要件の評価
- 03ADO.NET、Dapper、EF Core間でのクエリ移行
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-data-access-strategyインストール後、Claude Code で/dotnet-data-access-strategyを入力して読み込まれたか確認します。
詳細
dotnet-data-access-strategy
.NET アプリケーションで Entity Framework Core、Dapper、生の ADO.NET のどれを選ぶかを判断するためのフレームワークです。パフォーマンスのトレードオフ、機能比較、AOT/トリミングとの互換性、ハイブリッドなアプローチ、移行パスについて説明します。データアクセスコードを書く前に、十分な情報に基づいて技術を選択するために、このスキルを使用してください。
対象外: EF Core の実践的な使い方(DbContext のライフサイクル、マイグレーション、インターセプター)は、[skill:dotnet-efcore-patterns] で扱います。戦略的な EF Core アーキテクチャ(読み取り/書き込みの分離、集約境界、リポジトリの方針)は、[skill:dotnet-efcore-architecture] で扱います。DI コンテナーの仕組みについては、[skill:dotnet-csharp-dependency-injection] を参照してください。非同期パターンについては、[skill:dotnet-csharp-async-patterns] を参照してください。データアクセス層のテストについては、データベースフィクスチャと Testcontainers のパターンを扱う [skill:dotnet-integration-testing] を参照してください。CI/CD パイプラインについては、[skill:dotnet-gha-patterns] と [skill:dotnet-ado-patterns] を参照してください。
相互参照:EF Core の実践的な使い方については [skill:dotnet-efcore-patterns]、戦略的な EF Core パターンについては [skill:dotnet-efcore-architecture]、サービス登録については [skill:dotnet-csharp-dependency-injection]、非同期クエリパターンについては [skill:dotnet-csharp-async-patterns] を参照してください。
選択マトリクス
| 要素 | EF Core | Dapper | Raw ADO.NET |
|---|---|---|---|
| 学習曲線 | 中程度(LINQ、マイグレーション、設定) | 低い(SQL + マッピング) | 低~中程度(SQL + 手動マッピング) |
| 生産性 | 高い(変更追跡、マイグレーション、スキャフォールディング) | 中程度(SQL を記述、自動マッピング) | 低い(すべて手動) |
| クエリパフォーマンス | プロジェクションを使えば良好。変更追跡によるオーバーヘッドあり | ADO.NET に近いパフォーマンス | 実現可能な最速のパフォーマンス |
| 起動時間 | 長め(モデル構築、コンパイル) | 最小限 | 最小限 |
| メモリ割り当て | 多め(変更トラッカー、プロキシオブジェクト) | 少ない(直接マッピング) | 最小 |
| AOT/トリミング | 制限あり(リフレクションへの依存が大きいが、改善中) | ソースジェネレーターを使えば良好 | 完全対応 |
| 変更追跡 | 組み込み | なし | なし |
| マイグレーション | 組み込み | なし(FluentMigrator、DbUp などを使用) | なし |
| LINQ サポート | 完全対応(SQL に変換) | なし(生の SQL) | なし(生の SQL) |
| 一括操作 | ExecuteUpdate/ExecuteDelete(EF Core 7 以降) | 手動でバッチ処理 | 手動でバッチ処理 |
| 複雑なマッピング | 優れている(所有型、TPH/TPT/TPC) | シンプルな POCO マッピング | 手動 |
それぞれを選ぶ場合
EF Core を選ぶ場合
- 標準的なドメインモデルを使った CRUD アプリケーションを構築する
- 変更追跡と自動的な変更検出が必要である
- スキーママイグレーションをコードで管理したい
- チームが生の SQL より LINQ を好む
- .NET Aspire を使って構築する(EF Core は Aspire とファーストクラスで統合されている)
- プロジェクションと
AsNoTracking()を使えばクエリパフォーマンスが許容できる
// EF Core: expressive, type-safe, with change tracking
var order = await db.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == orderId, ct);
order!.Status = OrderStatus.Shipped;
await db.SaveChangesAsync(ct); // Automatic dirty detection
Dapper を選ぶ場合
- パフォーマンスが重要で、SQL を制御する必要がある
- 複雑なクエリ(レポート、分析、複数テーブルの結合)を記述する
- 抽象化を最小限に抑えた軽量なデータアクセスが必要である
- チームが SQL の記述と保守に慣れている
- 現時点で AOT 互換性が必要である(Dapper.AOT ソースジェネレーターを使用)
// Dapper: direct SQL, minimal overhead
await using var connection = new NpgsqlConnection(connectionString);
var orders = await connection.QueryAsync<OrderDto>(
"""
SELECT o.id, o.customer_id, o.status, o.created_at,
COUNT(i.id) AS item_count,
SUM(i.quantity * i.unit_price) AS total
FROM orders o
LEFT JOIN order_items i ON i.order_id = o.id
WHERE o.customer_id = @CustomerId
GROUP BY o.id, o.customer_id, o.status, o.created_at
ORDER BY o.created_at DESC
LIMIT @PageSize
""",
new { CustomerId = customerId, PageSize = pageSize });
Raw ADO.NET を選ぶ場合
- 最大限のパフォーマンスが必須である(サブミリ秒のデータアクセス)
- 接続、コマンド、リーダーのライフサイクルを完全に制御する必要がある
- ライブラリやフレームワークを構築する(アプリケーションレベルの依存関係がない)
- AOT 互換性が必須で、ソースジェネレーターは使用できない
- ストアドプロシージャやデータベース固有の機能を扱う
// Raw ADO.NET: full control, zero abstraction overhead
await using var connection = new NpgsqlConnection(connectionString);
await connection.OpenAsync(ct);
await using var command = connection.CreateCommand();
command.CommandText = "SELECT id, name, price FROM products WHERE category_id = $1";
command.Parameters.AddWithValue(categoryId);
await using var reader = await command.ExecuteReaderAsync(ct);
var products = new List<ProductDto>();
while (await reader.ReadAsync(ct))
{
products.Add(new ProductDto
{
Id = reader.GetInt32(0),
Name = reader.GetString(1),
Price = reader.GetDecimal(2)
});
}
パフォーマンス比較
クエリあたりのおおよそのオーバーヘッド(ADO.NET の生のベースラインとの比較):
| 操作 | ADO.NET | Dapper | EF Core(NoTracking) | EF Core(Tracking) |
|---|---|---|---|---|
| 主キーによる単純な SELECT | 1x | ~1.05x | ~1.3x | ~1.5x |
| 100 行の SELECT | 1x | ~1.1x | ~1.4x | ~2x |
| 単一行の INSERT | 1x | ~1.1x | ~1.5x | ~2x |
| 複雑な JOIN クエリ | 1x | ~1.05x | ~1.3-2x(LINQ の変換に依存) | ~1.5-2.5x |
注:
- これらは大まかな相対比較です。実際の数値は、クエリの複雑さ、データベース、ネットワーク遅延、ハードウェアによって異なります。
- データベースまでのネットワーク遅延は、通常 ORM のオーバーヘッドを大きく上回ります。ネットワーク遅延が 5ms の場合、1ms のクエリは ORM にかかわらず 6ms になります。
Select()によるプロジェクションとAsNoTracking()を使う EF Core は、ほとんどのクエリで Dapper に近いパフォーマンスを発揮します。- パフォーマンスだけを基準に選択する前に、実際のワークロードを測定してください。
AOT およびトリミングとの互換性
EF Core
EF Core は、モデルの構築、変更追跡、クエリ変換でリフレクションに大きく依存しています。AOT への互換性は向上していますが、まだ完全ではありません。
| 機能 | AOT の状況(.NET 9 以降) |
|---|---|
| モデルの構築 | 部分的 — コンパイル済みモデルが必要(dotnet ef dbcontext optimize) |
| クエリ変換 | AOT 非対応(式ツリーのコンパイル) |
| 変更追跡 | AOT 非対応(プロキシ生成、スナップショット作成) |
| マイグレーション | 設計時のみ — 実行時には不要 |
コンパイル済みモデルはモデル構成をビルド時に事前生成し、起動コストを削減してトリミングとの親和性を高めます。
dotnet ef dbcontext optimize \
--project src/MyApp.Infrastructure \
--startup-project src/MyApp.Api \
--output-dir CompiledModels
options.UseNpgsql(connectionString)
.UseModel(AppDbContextModel.Instance); // Pre-compiled model
要点: EF Core の Native AOT サポートは部分的で、バージョンによって異なります。.NET 9 の時点では、コンパイル済みモデルによって起動とトリミングとの親和性が向上しますが、クエリ変換と変更追跡は依然として実行時コード生成に依存しています。AOT 環境へのデプロイで EF Core を採用する前に、対象バージョンの現在の制限事項を確認してください。可能な場合はコンパイル済みモデルを使用して起動時間を短縮しつつ、AOT が重要な処理では Dapper.AOT または ADO.NET に切り替えられるようにしてください。
Dapper
従来の Dapper は、POCO のマッピングに実行時リフレクションと emit を使用します。Dapper.AOT ソースジェネレーターは、トリミングと AOT に対応した代替手段を提供します。
| パッケージ | AOT の状況 |
|---|---|
Dapper(標準) | AOT 非対応(Reflection.Emit を使用) |
Dapper.AOT | AOT 対応(ソース生成マッパー) |
<PackageReference Include="Dapper" Version="2.*" />
<PackageReference Include="Dapper.AOT" Version="1.*" />
// Dapper.AOT generates the mapping code at compile time
// Usage is the same as standard Dapper -- the source generator intercepts calls
[DapperAot] // Attribute enables AOT generation for this class
public sealed class OrderRepository(NpgsqlDataSource dataSource)
{
public async Task<OrderDto?> GetByIdAsync(int id, CancellationToken ct)
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
return await connection.QuerySingleOrDefaultAsync<OrderDto>(
"SELECT id, customer_id, status FROM orders WHERE id = @Id",
new { Id = id });
}
}
生の ADO.NET
完全な AOT およびトリミング対応です。リフレクションやコード生成は不要で、すべてのマッピングを明示的に行います。
AOT の選択ガイド
| 要件 | 推奨 |
|---|---|
| 今すぐ AOT で発行する必要がある | Dapper.AOT または生の ADO.NET |
| ORM を優先し、AOT は不要 | EF Core |
| ORM を優先し、将来的に AOT を予定 | 現時点では EF Core を使用し、サポートの向上に応じて AOT 対応を評価 |
| AOT アプリで利用されるライブラリを構築する | 生の ADO.NET または Dapper.AOT |
ハイブリッドなアプローチ
ほとんどの本番アプリケーションでは、複数のデータアクセス技術を併用すると効果的です。EF Core と Dapper は同じプロジェクト内で共存でき、同じデータベース接続を共有できます。
コマンドには EF Core、クエリには Dapper
// Command: use EF Core for change tracking and validation
public sealed class CreateOrderHandler(WriteDbContext db)
{
public async Task<int> HandleAsync(CreateOrderCommand command, CancellationToken ct)
{
var order = new Order(command.CustomerId);
// ... business logic ...
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return order.Id;
}
}
// Query: use Dapper for complex read-only queries
public sealed class OrderReportHandler(NpgsqlDataSource dataSource)
{
public async Task<IReadOnlyList<OrderReportRow>> HandleAsync(
OrderReportQuery query,
CancellationToken ct)
{
await using var connection = await dataSource.OpenConnectionAsync(ct);
var rows = await connection.QueryAsync<OrderReportRow>(
"""
SELECT
date_trunc('day', o.created_at) AS day,
COUNT(*) AS order_count,
SUM(i.quantity * i.unit_price) AS revenue
FROM orders o
JOIN order_items i ON i.order_id = o.id
WHERE o.created_at >= @StartDate AND o.created_at < @EndDate
GROUP BY date_trunc('day', o.created_at)
ORDER BY day
""",
new { query.StartDate, query.EndDate });
return rows.AsList();
}
}
データベース接続の共有
EF Core のトランザクション内で Dapper のクエリを実行するには、DbContext.Database.GetDbConnection() を使用して、基盤となる DbConnection を取得します。
public async Task ProcessWithBothAsync(int orderId, CancellationToken ct)
{
var connection = db.Database.GetDbConnection();
await db.Database.OpenConnectionAsync(ct);
await using var transaction = await db.Database.BeginTransactionAsync(ct);
// EF Core operation
var order = await db.Orders.FindAsync([orderId], ct);
order!.Status = OrderStatus.Processing;
await db.SaveChangesAsync(ct);
// Dapper operation on the same connection and transaction
await connection.ExecuteAsync(
"""
INSERT INTO audit_log (entity_type, entity_id, action, timestamp)
VALUES (@Type, @Id, @Action, @Timestamp)
""",
new { Type = "Order", Id = orderId, Action = "StatusChange",
Timestamp = DateTimeOffset.UtcNow },
transaction: transaction.GetDbTransaction());
await transaction.CommitAsync(ct);
}
NpgsqlDataSource の登録
PostgreSQL で Dapper を使用する場合は、NpgsqlDataSource を DI にシングルトンとして登録します(内部で接続プーリングを管理します)。
builder.Services.AddNpgsqlDataSource(
builder.Configuration.GetConnectionString("DefaultConnection")!);
Npgsql.DependencyInjection パッケージは AddNpgsqlDataSource() を提供します。これは EF Core とも連携し、UseNpgsql() で登録済みのデータソースを受け取ることができます。
builder.Services.AddDbContext<AppDbContext>((sp, options) =>
options.UseNpgsql(sp.GetRequiredService<NpgsqlDataSource>()));
移行パス
生の ADO.NET から Dapper へ
Dapper は、既存の ADO.NET コードを IDbConnection の拡張メソッドでラップします。移行は段階的に行えます。
DbDataReaderのループをQueryAsync<T>()の呼び出しに置き換えます。command.Parameters.AddWithValue()を匿名オブジェクトに置き換えます。- スキーマの変更は不要で、新たな依存関係も
DapperNuGet パッケージ以外にはありません。
Dapper から EF Core へ
- EF Core パッケージを追加し、エンティティ構成を含む
DbContextを作成します。 - 既存のデータベースから初期マイグレーションを生成します:
dotnet ef dbcontext scaffold。 - 新機能で Dapper クエリを段階的に EF Core に置き換えます。
- 複雑なレポート用クエリには Dapper を使い続けます。ハイブリッド構成で問題ありません。
EF Core から Dapper/ADO.NET へ(パフォーマンスが重要な処理)
- プロファイリング(OpenTelemetry トレース、データベースのクエリ統計)を通じて、負荷の高い処理を特定します。
- 特定のクエリを Dapper に置き換え、同じ接続を共有します。
- 変更追跡の利点がある CRUD 操作には EF Core を使い続けます。
パッケージ一覧
| パッケージ | 用途 | NuGet |
|---|---|---|
Microsoft.EntityFrameworkCore | EF のコアフレームワーク | Microsoft.EntityFrameworkCore |
Microsoft.EntityFrameworkCore.Design | CLI ツール(マイグレーション、スキャフォールディング) | 設計時のみ |
Npgsql.EntityFrameworkCore.PostgreSQL | PostgreSQL 用 EF Core プロバイダー | Npgsql.EntityFrameworkCore.PostgreSQL |
Microsoft.EntityFrameworkCore.SqlServer | SQL Server 用 EF Core プロバイダー | Microsoft.EntityFrameworkCore.SqlServer |
Microsoft.EntityFrameworkCore.Sqlite | SQLite 用 EF Core プロバイダー | Microsoft.EntityFrameworkCore.Sqlite |
Dapper | マイクロ ORM | Dapper |
Dapper.AOT | Dapper 用の AOT 対応ソースジェネレーター | Dapper.AOT |
Npgsql.DependencyInjection | NpgsqlDataSource の DI 登録 | Npgsql.DependencyInjection |
Npgsql | PostgreSQL 用 ADO.NET プロバイダー | Npgsql |
Microsoft.Data.SqlClient | SQL Server 用 ADO.NET プロバイダー | Microsoft.Data.SqlClient |
FluentMigrator | コードベースのマイグレーション(EF 以外) | FluentMigrator |
DbUp | SQL スクリプトベースのマイグレーション(EF 以外) | dbup |
主な原則
- パフォーマンスベンチマークではなく、実際のニーズに基づいて選択します。ほとんどのアプリケーションでは、データベースまでのネットワークレイテンシーが ORM のオーバーヘッドを大きく上回ります。
- .NET アプリケーションでは EF Core がデフォルトの選択肢です。生産性、安全性、マイグレーション機能を備えています。プロファイリングで特定のボトルネックが見つかったら、Dapper で最適化します。
- ハイブリッド構成が現実的な選択です。コマンドには EF Core、複雑なクエリには Dapper を使用します。接続とトランザクションは共有できます。
- 必要な場合は AOT 互換性が重要です。現時点で AOT による発行が必須要件なら、Dapper.AOT または生の ADO.NET を使用します。EF Core の AOT サポートは改善されていますが、まだ不完全です。
- 時期尚早な最適化は避けます。まず EF Core から始め、
AsNoTracking()とSelect()によるプロジェクションを使い、Dapper を導入する前に計測します。 - マイグレーションは実質的な生産性向上機能です。Dapper を選ぶ場合は、マイグレーション戦略(FluentMigrator、DbUp、または手動スクリプト)を別途計画します。
エージェントが陥りやすい落とし穴
- パフォーマンスだけを理由に Dapper を推奨しないでください。 その前に、
AsNoTracking()とSelect()によるプロジェクションを使った EF Core で、パフォーマンス要件を満たせるか確認してください。EF Core を適切に使用すれば、差はほとんどないことがよくあります。 - AOT 発行するアプリケーションでは、標準の
Dapperを使わないでください。Reflection.Emitを使用するため、AOT と互換性がありません。AOT のシナリオでは、[DapperAot]属性とDapper.AOTを使用してください。 - 必要な NuGet パッケージを忘れずに列挙してください。 EF Core プロバイダー(例:
Npgsql.EntityFrameworkCore.PostgreSQL)と Dapper パッケージの両方を、明示的に参照する必要があります。パッケージ参照を含めずにコードを生成すると、プロジェクトがコンパイルできない場合があります。 - DI に登録されたサービスで、
NpgsqlConnectionのインスタンスを手動で新規作成しないでください。 接続プーリングを管理するNpgsqlDataSource(AddNpgsqlDataSource()で登録)を使用してください。接続を手動で作成すると、プール管理を迂回してしまいます。 - 同じ論理トランザクション内で、EF Core と Dapper を別々の接続で使用しないでください。 接続を共有するには
DbContext.Database.GetDbConnection()を使用し、トランザクションを共有するにはtransaction.GetDbTransaction()を使用してください。 - EF Core の LINQ がすべての C# 式を SQL に変換できると思い込まないでください。 サポートされていない式は、古いバージョンでは暗黙的にクライアント側で評価され、新しいバージョンでは例外が発生します。開発時に
ToQueryString()で生成された SQL を確認してください。