mcpField
D
dotnet-data-access-strategy

dotnet-data-access-strategy

github.com/wshaddix开发8687 次安装35 周前更新
代码仓库

关于 dotnet-data-access-strategy

面向 .NET 开发者的数据访问技术选型指南,比较 Entity Framework Core、Dapper 和原始 ADO.NET,并涵盖性能、AOT、混合方案及迁移路径。

主要功能

  1. 01比较性能与功能
  2. 02说明 AOT 和裁剪取舍
  3. 03介绍 EF Core 与 Dapper 混用
  4. 04梳理技术迁移路径

适用场景

  1. 01为新的 .NET 应用选择数据访问方案
  2. 02选型前评估 AOT 需求
  3. 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 CoreDapper原始 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.NETDapperEF Core(NoTracking)EF Core(Tracking)
按 PK 执行简单 SELECT1x~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,不要求 AOTEF 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 扩展方法。迁移可以逐步进行:

  1. 将 DbDataReader 循环替换为 QueryAsync<T>() 调用。
  2. 将 command.Parameters.AddWithValue() 替换为匿名对象。
  3. 无需更改架构,除 Dapper NuGet 包外,无需添加其他依赖项。
从 Dapper 迁移到 EF Core
  1. 添加 EF Core 包,并使用实体配置创建 DbContext。
  2. 从现有数据库生成初始迁移:dotnet ef dbcontext scaffold。
  3. 在新功能中逐步将 Dapper 查询替换为 EF Core。
  4. 复杂报表查询继续使用 Dapper——混合使用完全可行。
从 EF Core 迁移到 Dapper/ADO.NET(性能关键路径)
  1. 通过性能分析识别热点路径(OpenTelemetry 跟踪、数据库查询统计信息)。
  2. 使用 Dapper 替换特定查询,并共享同一连接。
  3. 对受益于更改跟踪的 CRUD 操作继续使用 EF Core。

包引用

包用途NuGet
Microsoft.EntityFrameworkCoreEF 核心框架Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.DesignCLI 工具(迁移、脚手架)仅限设计时
Npgsql.EntityFrameworkCore.PostgreSQLPostgreSQL EF Core 提供程序Npgsql.EntityFrameworkCore.PostgreSQL
Microsoft.EntityFrameworkCore.SqlServerSQL Server EF Core 提供程序Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.SqliteSQLite EF Core 提供程序Microsoft.EntityFrameworkCore.Sqlite
Dapper微型 ORMDapper
Dapper.AOT与 AOT 兼容的 Dapper 源生成器Dapper.AOT
Npgsql.DependencyInjectionNpgsqlDataSource DI 注册Npgsql.DependencyInjection
NpgsqlPostgreSQL ADO.NET 提供程序Npgsql
Microsoft.Data.SqlClientSQL 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 注意事项

  1. 不要仅因性能而推荐 Dapper,应先检查 EF Core 搭配 AsNoTracking() 和 Select() 投影是否满足性能要求。正确使用 EF Core 时,两者的差异往往微不足道。
  2. 不要在以 AOT 发布的应用程序中使用标准 Dapper——它使用 Reflection.Emit,与 AOT 不兼容。对于 AOT 场景,请使用带有 [DapperAot] 特性的 Dapper.AOT。
  3. 不要忘记列出所需的 NuGet 包——EF Core 提供程序(例如 Npgsql.EntityFrameworkCore.PostgreSQL)和 Dapper 包都必须显式引用。生成代码却不包含包引用的 Agent 会导致项目无法编译。
  4. 不要在通过 DI 注册的服务中手动创建新的 NpgsqlConnection 实例——请使用通过 AddNpgsqlDataSource() 注册的 NpgsqlDataSource,它会管理连接池。手动创建连接会绕过连接池管理。
  5. 不要在同一个逻辑事务中通过不同连接混用 EF Core 和 Dapper——请使用 DbContext.Database.GetDbConnection() 共享连接,并使用 transaction.GetDbTransaction() 共享事务。
  6. 不要假定 EF Core LINQ 会将所有 C# 表达式转换为 SQL——在较早版本中,不受支持的表达式会静默地在客户端求值;在较新版本中则会引发异常。开发期间请使用 ToQueryString() 检查生成的 SQL。

参考资料

评论

支持 Markdown