EF Core 迁移到 Dapper
本页定义 AgileLabs Framework 项目从 EF Core 收敛到 Dapper / SQL 的标准迁移规范。目标不是一次性推翻所有存量代码,而是给出一条对人和 AI Agent 都可执行的收敛路径。
为什么迁移
当前文档已经把 Dapper / SQL 定义为默认推荐方案,原因主要有三类:
- SQL、事务、并发和影响行数都是显式行为,更适合代码审查和 AI 生成后的人工校验。
- EF Core 的真实行为分散在实体、配置类、
DbContext、Filter、SaveChanges()扩展和导航属性里,问题排查链更长。 - 当项目同时混用
CrudRepository、自定义DbContext和 Dapper 时,维护成本通常会继续扩大,而不是自然下降。
适用场景
- 项目已经存在
AgileLabDbContext、CrudRepository、AutoCommiterFilterAttribute或 EF Core migration。 - 团队希望停止继续扩大 EF Core 使用面,并把新增模块收敛到 Dapper。
- 当前项目需要更强的 SQL 可控性、并发控制可见性和 AI 协作可审查性。
不建议立刻整体迁移的情况
- 当前发布窗口极短,无法接受较大数据访问面调整。
- 模块高度依赖导航属性、复杂 Change Tracking,且业务边界尚未明确。
- 团队还没有完成 Repository 边界、DTO 边界和事务边界梳理。
这些情况下也不要继续新增 EF Core 面,而应先冻结扩张,再逐步准备迁移。
迁移目标
- 新模块不再新增
DbContext、CrudRepository和 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 只调用业务语义方法,例如
GetOrderListAsync、ApproveOrderAsync。 - Repository 内封装 SQL、参数对象、事务和并发判断。
- 不把
DbConnection、IDbTransaction、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或项目分页基类,优先继续复用,避免每个分页查询都手工拼不同风格。 - 分页的
select、count、order by要一起显式维护,避免“数据页和总数条件不一致”。
事务迁移规范
- 需要多步写入时,统一在 Repository 边界使用
TransScopeAsync。 - 不在 Controller 里手工控制事务。
- 只有明确存在跨 SQL 语句的一致性需求时才开事务,避免把所有写入都机械放进大事务。
并发与审计字段迁移规范
- 把
ts、版本号或其他乐观锁字段纳入 where 条件。 create_time、update_time、created_by、updated_by统一显式赋值。- 操作者身份、租户、语言或时区相关信息统一从
WorkContext读取。 - 如果项目已有 Dapper 基类,优先把这些通用逻辑沉到项目级基类,不要在每个 Repository 重复散写。
验收标准
每一批迁移完成后,至少检查这些点:
- 行为一致:核心查询结果、排序、过滤条件与原功能一致。
- 写入一致:更新、删除、审批等关键写入路径行为不变。
- 并发可控:版本冲突可以被稳定识别,而不是静默覆盖。
- 审计完整:
create_time、update_time、created_by、updated_by等字段仍正确落库。 - 边界清晰:Controller / AppService 不直接持有 SQL、连接或事务对象。
- 路径收敛:新代码不再继续依赖
CrudRepository或新增 EF Core migration。
常见坑
- 只迁查询,不迁写入,导致同一业务仍然双轨混用。
- 把 EF Core Entity 直接当成 Dapper DTO 继续跨层复用。
- 写入 SQL 忘记带租户条件或版本字段。
- 迁移后仍保留
AutoCommiterFilterAttribute兜底,造成行为理解混乱。 - 发现 EF Core 不顺手后,直接把 SQL 写进 Controller,而不是收敛到业务 Repository。
AI Agent 执行规则
- 未经明确说明,不新增
DbContext、CrudRepository、EF Core migration。 - 修存量 EF Core 代码时,先判断本次改动是否可以直接迁到 Dapper。
- 新增数据访问代码时,默认使用 Dapper Repository 结构输出。
- 如果本次任务无法完整迁移,也要在实现中避免继续扩大 EF Core 面。