关于 dotnet-data-access-strategy
面向 .NET 开发者的数据访问技术选型指南,比较 Entity Framework Core、Dapper 和原始 ADO.NET,并涵盖性能、AOT、混合方案及迁移路径。
主要功能
- 01比较性能与功能
- 02说明 AOT 和裁剪取舍
- 03介绍 EF Core 与 Dapper 混用
- 04梳理技术迁移路径
适用场景
- 01为新的 .NET 应用选择数据访问方案
- 02选型前评估 AOT 需求
- 03在 ADO.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]。数据访问层测试请参阅 [skill:dotnet-integration-testing],了解数据库 fixture 和 Testcontainers 模式。CI/CD 管道请参阅 [skill:dotnet-gha-patterns] 和 [skill:dotnet-ado-patterns]。
交叉引用:[skill:dotnet-efcore-patterns] 介绍 EF Core 的战术性用法,[skill:dotnet-efcore-architecture] 介绍 EF Core 的战略模式,[skill:dotnet-csharp-dependency-injection] 介绍服务注册,[skill:dotnet-csharp-async-patterns] 介绍异步查询模式。
决策矩阵
| 因素 | EF Core | Dapper | 原始 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 应用
- 需要更改跟踪和自动脏数据检测
- 希望在代码中管理架构迁移
- 团队更倾向于使用 LINQ,而非原始 SQL
- 使用 .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 });
选择原始 ADO.NET 的情况
- 最高性能不容妥协(数据访问低于 1 毫秒)
- 需要完全控制连接、命令和读取器的生命周期
- 构建库或框架(不依赖应用层)
- 必须兼容 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) |
|---|---|---|---|---|
| 按 PK 执行简单 SELECT | 1x | ~1.05x | ~1.3x | ~1.5x |
| SELECT 100 行 | 1x | ~1.1x | ~1.4x | ~2x |
| 插入单行 | 1x | ~1.1x | ~1.5x | ~2x |
| 复杂 JOIN 查询 | 1x | ~1.05x | ~1.3-2x(取决于 LINQ 转换) | ~1.5-2.5x |
注意:
- 这些只是粗略的相对比较——实际数值取决于查询复杂度、数据库、网络延迟和硬件。
- 到数据库的网络延迟通常远超 ORM 开销。一个耗时 1ms、网络延迟为 5ms 的查询,无论使用哪种 ORM 都需要 6ms。
- 对于大多数查询,EF Core 配合
Select()投影和AsNoTracking()的性能接近 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 使用运行时反射和 Emit 来映射 POCO。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();
}
}
共享数据库连接
使用 DbContext.Database.GetDbConnection() 获取底层 DbConnection,以便在 EF Core 事务中执行 Dapper 查询:
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
使用 Dapper 连接 PostgreSQL 时,在 DI 中将 NpgsqlDataSource 注册为单例(它会在内部管理连接池):
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 | 与 AOT 兼容的 Dapper 源生成器 | 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 开销。
- EF Core 是 .NET 应用程序的默认选择,它提供开发效率、安全性和迁移功能。通过性能分析发现特定瓶颈后,再使用 Dapper 优化。
- 混合使用是务实之选:命令使用 EF Core,复杂查询使用 Dapper。二者共享连接和事务。
- 需要时才考虑 AOT 兼容性:如果当前发布 AOT 是硬性要求,请使用 Dapper.AOT 或原始 ADO.NET。EF Core 的 AOT 支持正在改进,但尚不完整。
- 不要过早优化:从 EF Core 开始,使用
AsNoTracking()和Select()投影,并在引入 Dapper 前进行测量。 - 迁移是提升开发效率的实际功能:如果选择 Dapper,请单独规划迁移策略(FluentMigrator、DbUp 或手动脚本)。
Agent 注意事项
- 不要仅因性能而推荐 Dapper,应先检查 EF Core 搭配
AsNoTracking()和Select()投影是否满足性能要求。正确使用 EF Core 时,两者的差异往往微不足道。 - 不要在以 AOT 发布的应用程序中使用标准
Dapper——它使用Reflection.Emit,与 AOT 不兼容。对于 AOT 场景,请使用带有[DapperAot]特性的Dapper.AOT。 - 不要忘记列出所需的 NuGet 包——EF Core 提供程序(例如
Npgsql.EntityFrameworkCore.PostgreSQL)和 Dapper 包都必须显式引用。生成代码却不包含包引用的 Agent 会导致项目无法编译。 - 不要在通过 DI 注册的服务中手动创建新的
NpgsqlConnection实例——请使用通过AddNpgsqlDataSource()注册的NpgsqlDataSource,它会管理连接池。手动创建连接会绕过连接池管理。 - 不要在同一个逻辑事务中通过不同连接混用 EF Core 和 Dapper——请使用
DbContext.Database.GetDbConnection()共享连接,并使用transaction.GetDbTransaction()共享事务。 - 不要假定 EF Core LINQ 会将所有 C# 表达式转换为 SQL——在较早版本中,不受支持的表达式会静默地在客户端求值;在较新版本中则会引发异常。开发期间请使用
ToQueryString()检查生成的 SQL。