agilelabs-fx-docs main data-access/migrate-efcore-to-dapper.md

EF Core 迁移到 Dapper

本页定义 AgileLabs Framework 项目从 EF Core 收敛到 Dapper / SQL 的标准迁移规范。目标不是一次性推翻所有存量代码,而是给出一条对人和 AI Agent 都可执行的收敛路径。

为什么迁移

当前文档已经把 Dapper / SQL 定义为默认推荐方案,原因主要有三类:

  • SQL、事务、并发和影响行数都是显式行为,更适合代码审查和 AI 生成后的人工校验。
  • EF Core 的真实行为分散在实体、配置类、DbContext、Filter、SaveChanges() 扩展和导航属性里,问题排查链更长。
  • 当项目同时混用 CrudRepository、自定义 DbContext 和 Dapper 时,维护成本通常会继续扩大,而不是自然下降。

适用场景

  • 项目已经存在 AgileLabDbContextCrudRepositoryAutoCommiterFilterAttribute 或 EF Core migration。
  • 团队希望停止继续扩大 EF Core 使用面,并把新增模块收敛到 Dapper。
  • 当前项目需要更强的 SQL 可控性、并发控制可见性和 AI 协作可审查性。

不建议立刻整体迁移的情况

  • 当前发布窗口极短,无法接受较大数据访问面调整。
  • 模块高度依赖导航属性、复杂 Change Tracking,且业务边界尚未明确。
  • 团队还没有完成 Repository 边界、DTO 边界和事务边界梳理。

这些情况下也不要继续新增 EF Core 面,而应先冻结扩张,再逐步准备迁移。

迁移目标

  • 新模块不再新增 DbContextCrudRepository 和 EF Core migration。
  • 查询、分页、报表、列表页统一收敛到 Dapper Repository。
  • 写入逻辑显式表达 SQL、事务、并发条件和影响行数校验。
  • 审计字段、租户信息和操作者身份不再隐式依赖 SaveChanges() 扩展。

分阶段迁移路径

flowchart LR
    A[冻结新增 EF Core] --> B[先迁查询与分页]
    B --> C[再迁写入与事务]
    C --> D[收敛并发与审计字段]
    D --> E[移除 DbContext 主路径依赖]

阶段 1:冻结新增 EF Core

  • 不再新增 RegisterDbContext<TDbContext>() 接入。
  • 不再给新模块增加 CrudRepository、新的实体配置类和新的 EF Core migration。
  • 新需求默认直接落到 Dapper Repository。

阶段 2:先迁查询与分页

  • 优先迁移列表页、详情页、报表和分页查询。
  • 这些能力最容易从 LINQ 收敛到显式 SQL,也最容易独立验收。
  • 迁移后确保 Controller 或 AppService 只依赖业务 Repository,不直接持有 SQL。

阶段 3:再迁写入与事务

  • 把新增、修改、删除逻辑迁到 Repository 中的显式 SQL。
  • 明确 where 条件、租户条件、版本条件和影响行数判断。
  • 涉及多步写入时,统一改用 TransScopeAsync 之类的显式事务入口。

阶段 4:收敛并发与审计字段

  • ts、版本号、更新时间、更新人等字段回填逻辑显式落在参数组装或项目级基类中。
  • 不再依赖 SaveChanges() 的隐式审计行为兜底。
  • 所有需要当前用户、租户、时区的逻辑都显式从 WorkContext 取值。

阶段 5:移除 EF Core 主路径依赖

  • 业务主路径不再依赖 CrudRepository
  • 新功能不再依赖 AutoCommiterFilterAttribute
  • 历史 DbContext 保留到可接受的最小范围后,再决定是否继续清退。

Repository 重构规范

迁移时,先改边界,再改实现。推荐规则:

  • Controller / AppService 只调用业务语义方法,例如 GetOrderListAsyncApproveOrderAsync
  • Repository 内封装 SQL、参数对象、事务和并发判断。
  • 不把 DbConnectionIDbTransaction、SQL 字符串散落到控制器和服务层。
  • DTO、查询参数对象、写入命令对象和数据库实体概念要分清,不继续复用 EF Core Entity 当成所有层通用模型。

查询迁移模板

EF Core 查询通常长这样:

var query = await _crudRepository.GetQueryAsync<OrderEntity>();
var list = await query
    .Where(x => x.BuyerId == buyerId)
    .OrderByDescending(x => x.CreateTime)
    .Select(x => new OrderListItemDto { Id = x.Id, OrderNo = x.OrderNo })
    .ToListAsync();

迁到 Dapper 后,目标形态应该更接近:

public async Task<IEnumerable<OrderListItemDto>> GetOrderListAsync(string buyerId, CancellationToken cancellationToken)
{
    const string sql = @"
select id, order_no as OrderNo
from orders
where buyer_id = @buyerId
order by create_time desc";

    return await GetListAsync<OrderListItemDto>(sql, new { buyerId }, cancellationToken: cancellationToken);
}

迁移要求:

  • SQL 字段选择显式列出,不依赖整表实体投影。
  • 排序、过滤和分页规则在 SQL 中可见。
  • 返回 DTO 只保留调用方真实需要的字段。

写入迁移模板

EF Core 写入通常是:

var entity = await context.Set<OrderEntity>().FirstAsync(x => x.Id == id);
entity.Status = OrderStatus.Approved;
await context.SaveChangesAsync();

迁到 Dapper 后,目标形态应该更接近:

public async Task ApproveOrderAsync(Guid id, long ts, string accountId, CancellationToken cancellationToken)
{
    const string sql = @"
update orders
set status = @status,
    update_time = @updateTime,
    updated_by = @updatedBy,
    ts = @nextTs
where id = @id and ts = @ts";

    var affectedRows = await ExecuteNoQueryAsync(sql, new
    {
        id,
        ts,
        status = "Approved",
        updateTime = DateTimeOffset.UtcNow,
        updatedBy = accountId,
        nextTs = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
    }, cancellationToken: cancellationToken);

    if (affectedRows != 1)
    {
        throw new ApiException("订单更新失败或版本已变化");
    }
}

迁移要求:

  • where 条件显式包含主键和版本字段。
  • 审计字段显式赋值。
  • 影响行数不是 1 时立即走失败分支,不依赖 ORM 状态机推断。

分页迁移模板

  • 原来通过 LINQ + Skip/Take 做分页的查询,统一迁成显式分页 SQL。
  • 如果已经使用 SqlBuilder 或项目分页基类,优先继续复用,避免每个分页查询都手工拼不同风格。
  • 分页的 selectcountorder by 要一起显式维护,避免“数据页和总数条件不一致”。

事务迁移规范

  • 需要多步写入时,统一在 Repository 边界使用 TransScopeAsync
  • 不在 Controller 里手工控制事务。
  • 只有明确存在跨 SQL 语句的一致性需求时才开事务,避免把所有写入都机械放进大事务。

并发与审计字段迁移规范

  • ts、版本号或其他乐观锁字段纳入 where 条件。
  • create_timeupdate_timecreated_byupdated_by 统一显式赋值。
  • 操作者身份、租户、语言或时区相关信息统一从 WorkContext 读取。
  • 如果项目已有 Dapper 基类,优先把这些通用逻辑沉到项目级基类,不要在每个 Repository 重复散写。

验收标准

每一批迁移完成后,至少检查这些点:

  • 行为一致:核心查询结果、排序、过滤条件与原功能一致。
  • 写入一致:更新、删除、审批等关键写入路径行为不变。
  • 并发可控:版本冲突可以被稳定识别,而不是静默覆盖。
  • 审计完整:create_timeupdate_timecreated_byupdated_by 等字段仍正确落库。
  • 边界清晰:Controller / AppService 不直接持有 SQL、连接或事务对象。
  • 路径收敛:新代码不再继续依赖 CrudRepository 或新增 EF Core migration。

常见坑

  • 只迁查询,不迁写入,导致同一业务仍然双轨混用。
  • 把 EF Core Entity 直接当成 Dapper DTO 继续跨层复用。
  • 写入 SQL 忘记带租户条件或版本字段。
  • 迁移后仍保留 AutoCommiterFilterAttribute 兜底,造成行为理解混乱。
  • 发现 EF Core 不顺手后,直接把 SQL 写进 Controller,而不是收敛到业务 Repository。

AI Agent 执行规则

  • 未经明确说明,不新增 DbContextCrudRepository、EF Core migration。
  • 修存量 EF Core 代码时,先判断本次改动是否可以直接迁到 Dapper。
  • 新增数据访问代码时,默认使用 Dapper Repository 结构输出。
  • 如果本次任务无法完整迁移,也要在实现中避免继续扩大 EF Core 面。

相关页面