跳转到内容

v10 → v11 迁移指南

本指南帮助您将 MiCake 应用从 v10 升级到 v11。v11 将 Unit of Work 确立为唯一持久化所有者,并改为自动安装 EF Core 拦截器,涉及仓储 API、UoW API、ASP.NET Core 边界与 EF Core 集成四个层面的破坏性变更。

升级后请对照下列变更逐项迁移:

  1. 移除 IRepository.SaveChangesAsync
  2. 移除 AddAndReturnAsyncsaveNow 参数
  3. 移除 ClearChangeTrackingAsync
  4. UpdateAsync 语义:完整替换
  5. DeleteByIdAsync 语义:跟踪式删除
  6. requiresNew 改为回调执行
  7. 移除 PersistenceStrategy / OptimizeForSingleWrite
  8. 移除 Timeout
  9. IDbContextWrapper 改为 IUnitOfWorkResource
  10. 新增 FlushAsync、保存点、事件钩子
  11. UnitOfWorkAttribute 精简
  12. IsUowEnabled 改为 [DisableUnitOfWork]
  13. 只读 action 推断改为 opt-in
  14. 上下文工厂接口合并
  15. BypassUnitOfWorkCheck 更名
  16. 拦截器自动安装(UseMiCakeInterceptors 移除)
  17. 行为变更

若要快速完成迁移,可直接跳转到迁移清单

仓储方法不再自行保存或提交。变更仅修改 UoW 的跟踪状态,持久化由 UoW 统一拥有。 将每个 repo.SaveChangesAsync() 替换为环境 UoW 上的 CommitAsync()

// 迁移前
await _bookRepository.AddAsync(book);
await _bookRepository.SaveChangesAsync(); // 竞争性持久化边界
// 迁移后
await _bookRepository.AddAsync(book);
await _unitOfWork.CommitAsync(); // 在 UoW 边界统一提交

原来使用 AddAndReturnAsync 的地方,建议迁移到新加入接口的 AddAndGetIdAsync(...)——它保持了「添加 + 立即获取数据库生成键」的原有语义: 添加聚合根后自动调用 FlushAsync 填充生成键并返回 TKey。flush 发生在环境可写 UoW 事务内,不提交,仅在 UoW 提交后持久化;需要环境可写 UoW,否则抛出 InvalidOperationException

// 迁移前
var book = await _bookRepository.AddAndReturnAsync(new Book { Title = "x" });
// 迁移后(推荐):语义等价,返回生成的键
var id = await _bookRepository.AddAndGetIdAsync(new Book { Title = "x" }); // 已加入 IRepository 接口
await _unitOfWork.CommitAsync(); // 提交后数据持久化

接口注入场景亦可使用 AddAsync + IUnitOfWork.FlushAsync() 组合,行为等价:

var book = new Book { Title = "x" };
await _bookRepository.AddAsync(book);
await _unitOfWork.FlushAsync(); // 生成键,book.Id 已填充,事务未提交
// ...后续操作,最后统一 CommitAsync()

AddAsync(..., saveNow: true) 中的 saveNow 参数同样移除,按上述方式处理。

不再提供。清理变更跟踪属于 EF Core 关注点,需要时通过 DbContext 直接分离条目。

未跟踪实例的 UpdateAsync 现在是完整分离聚合替换:写入实例的全部属性。 配置的并发令牌被保留,因此过期实例会以 DbUpdateConcurrencyException 暴露冲突, 而不再静默覆盖:

// 迁移前:部分更新,静默覆盖并发变更
await _repo.UpdateAsync(detachedBook);
// 迁移后:完整替换;并发冲突在 flush/commit 时抛出 DbUpdateConcurrencyException
await _repo.UpdateAsync(detachedBook);
await _unitOfWork.CommitAsync();

注意:load-and-modify 仍是首选工作流;并发冲突现在在 FlushAsync / CommitAsync 时抛出,而不是在 UpdateAsync 调用时。

现在先加载聚合到稳定的 UoW 上下文,再执行跟踪式删除——软删除、审计、 领域事件、回滚语义与 DeleteAsync 完全一致:

await _repo.DeleteByIdAsync(id); // 语义:load → tracked delete → UoW 提交后生效
await _unitOfWork.CommitAsync();

需要绕过生命周期立即物理删除时,改用显式物理删除 API (见 5.8 物理/批量操作)。

2.1 BeginAsync(requiresNew: true) 已移除

Section titled “2.1 BeginAsync(requiresNew: true) 已移除”

requiresNew 布尔参数被隔离回调执行取代。ExecuteRequiresNewAsync 创建独立 DI 作用域, 回调必须从传入的 IServiceProvider 解析仓储/DbContext;提交、回滚、释放自动完成:

// 迁移前
var uow = await _unitOfWorkManager.BeginAsync(requiresNew: true);
var repo = _bookRepository; // 外层作用域服务——错误
await repo.AddAsync(book);
await uow.CommitAsync();
// 迁移后
await _unitOfWorkManager.ExecuteRequiresNewAsync(async (sp, ct) =>
{
var repo = sp.GetRequiredService<IRepository<Book, int>>(); // 从回调 provider 解析
await repo.AddAsync(book);
// 成功自动提交,失败自动回滚,作用域自动释放
});

注意:捕获外层 scoped 服务会触发所有权校验失败;无外层 UoW 时调用会抛出 InvalidOperationException。后台作业等无环境 UoW 的场景改用 IStandaloneUnitOfWorkExecutor

PersistenceStrategy / OptimizeForSingleWrite 已移除。每个可写 UoW 都使用显式事务

// 迁移前
await _uowManager.BeginAsync(new UnitOfWorkOptions { PersistenceStrategy = PersistenceStrategy.OptimizeForSingleWrite });
// 迁移后
await _uowManager.BeginAsync(); // 默认即可:Lazy 激活显式事务
// 需要提前激活事务时:UnitOfWorkOptions.Immediate

原因OptimizeForSingleWrite 允许 EF 隐式事务在 post-save 生命周期处理完成前提交, 可能造成“数据已持久化但返回错误”的矛盾结果。

UnitOfWorkOptions.Timeout 已移除——它原本就没有运行时效果。超时改用 EF/provider 的命令、锁、事务超时配置。

被 provider 无关的 IUnitOfWorkResource 取代。仅影响自定义持久化 provider 集成方; 普通应用无需迁移。

IUnitOfWork 新增:

  • FlushAsync() — 按注册顺序激活并 flush 全部资源,返回受影响行数,不提交
  • 保存点 — CreateSavepointAsync / RollbackToSavepointAsync / ReleaseSavepointAsync
  • MarkAsCompletedAsync() — 只读边界跳过提交
  • 事务事件 — OnCommitting / OnCommitted / OnRollingBack / OnRolledBack
  • IAsyncDisposable
// 保存点示例:事务内部分回滚
await _uow.CreateSavepointAsync("step1");
// ...执行一批操作
await _uow.RollbackToSavepointAsync("step1"); // 只撤销该点之后的变更

InitializationModeCreateOptions() 已移除。属性现在只有两个选项:

[UnitOfWork(IsReadOnly = true)] // 只读:写操作快速失败
[UnitOfWork(IsolationLevel = IsolationLevel.Serializable)]
// 迁移前
[UnitOfWork(IsUowEnabled = false)]
// 迁移后
[DisableUnitOfWork]

3.3 只读 action 名称推断改为 opt-in

Section titled “3.3 只读 action 名称推断改为 opt-in”

以前 GET action 自动按名称推断为只读;现在默认关闭,显式元数据始终优先:

// Startup.cs —— 依赖旧推断行为时重新开启:
services.Configure<MiCakeAspNetOptions>(o => o.EnableReadOnlyActionNameInference = true);

非泛型 IEFCoreContextFactoryIEFCoreAnchoredContextFactory、无参 GetDbContextWrapper() 已合并为单一公共契约。自定义工厂实现只需实现两个方法:

public class MyFactory<TDbContext> : IEFCoreContextFactory<TDbContext>
where TDbContext : DbContext
{
public TDbContext GetDbContext() { /* ... */ }
public EFCoreDbContextWrapper GetOrCreateWrapperFor(DbContext context) { /* ... */ }
}

框架会通过适配器自动调用你的实现,无需了解内部视图。

MiCakeEFCoreOptions.BypassUnitOfWorkCheck 更名为 AllowDbContextAccessWithoutUoW, 默认仍为 false

// 迁移前
options.BypassUnitOfWorkCheck = true;
// 迁移后
options.AllowDbContextAccessWithoutUoW = true;

注意:该选项现在仅放宽上下文解析(返回无 UoW 集成的 standalone wrapper, 用于只读 filter/middleware)。无 UoW 的写入整体按 Permissive 策略放行, 不再需要、也不应依赖此选项放行写入。

UseMiCakeInterceptors 全部 3 个重载、IMiCakeInterceptorFactory 及其实现已移除。 拦截器由模块的 ConfigureDbContext 配置器自动挂载——只需在容器中注册 DbContext:

// 迁移前
services.AddDbContext<AppDbContext>((sp, opt) =>
{
opt.UseSqlite(connectionString);
opt.UseMiCakeInterceptors(sp); // 已移除
});
// 迁移后 —— 无需任何拦截器相关调用:
services.AddDbContext<AppDbContext>(opt =>
{
opt.UseSqlite(connectionString);
opt.UseMiCake(); // 可选:安装 per-context options extension
});

要求:EF Core 9+ConfigureDbContext / IDbContextOptionsConfiguration<TContext> 机制,EF Core 10 内置,已在 EF Core 10 上验证)。 MiCakeDbContext 子类无需调用 UseMiCake()——OnConfiguring 会自动调用。

以下变更不涉及 API 删除,但会改变运行时行为,迁移后必须验证。

以前会拒绝无 UoW 的直接 DbContext 写入;现在按原生 EF Core 语义放行(隐式事务, 无回滚/生命周期保证)。仓储/UoW 路径始终受守卫:

// 无环境 UoW 时:
await dbContext.SaveChangesAsync(); // 放行,等同原生 EF(无 MiCake 保证)

需要事务保证时,开启 UoW 或使用 IStandaloneUnitOfWorkExecutor

  • 嵌套 CommitAsync 只标记完成,物理提交发生在根 UoW
  • 嵌套 RollbackAsync 标记 UoW rollback-only,根回滚覆盖全部资源

5.3 多资源提交:best-effort + 结构化结果

Section titled “5.3 多资源提交:best-effort + 结构化结果”

多资源提交按注册顺序确定性执行;部分失败抛出 PartialUnitOfWorkCommitException, 携带逐资源的结构化、非敏感结果(ID、类型、状态)。跨资源原子性需自行选择 outbox 或补偿工作流。

回滚或清理失败时抛出 UnitOfWorkBoundaryException,同时携带主异常 + 回滚失败 + 清理失败,便于诊断。

生命周期/领域事件处理器中递归调用 SaveChangesAsync 不再是原始递归: 嵌套保存被合并为同一事务内的保存周期,事件按实例去重。无进展或达到 MaxSaveCycles(默认 16)抛出 SaveChangesReentryException 并标记 UoW rollback-only:

// MiCakeEFCoreOptions
options.MaxSaveCycles = 32; // 按需调整上界

分页前必须产生全序:无调用方排序时按每个主键属性升序;有排序时缺失的主键属性 作为最后升序 ThenBy 追加;无键实体被拒绝。依赖旧隐式顺序的调用点请提供显式排序。

5.7 启动校验:DbContext 生命周期与执行策略

Section titled “5.7 启动校验:DbContext 生命周期与执行策略”
  • DbContext 必须注册为 scoped 或 pooled;singleton/transient 启动即失败并给出指引
  • RetriesOnFailure = true 的执行策略对环境可写 UoW 被拒绝(需应用拥有的 可重放边界,如独立重试执行器)

绕过聚合生命周期的删除改用显式 API,要求在环境可写 UoW 内,且保持在 UoW 事务中:

// 迁移前:仓储内直接物理删除
// 迁移后:显式物理删除执行器
var executor = sp.GetRequiredService<IEFCorePhysicalOperationExecutor<AppDbContext>>();
await executor.ExecuteDeleteAsync<Book>(b => b.PublishedYear < 2000);

直接使用 EF ExecuteUpdate / ExecuteDelete / ExecuteSqlRaw:UoW 内被守卫并绑定事务, UoW 外按原生语义放行。

  • SaveChangesAsync()IUnitOfWork.CommitAsync()
  • AddAndReturnAsync(x)AddAndGetIdAsync(x)(推荐,已加入 IRepository 接口);也可用 AddAsync(x) + FlushAsync()(行为等价)
  • 移除 AddAsyncsaveNow: 参数
  • 移除 ClearChangeTrackingAsync() 调用
  • 立即物理删除场景改用 IEFCorePhysicalOperationExecutor<TDbContext>
  • BeginAsync(requiresNew: true)ExecuteRequiresNewAsync(...),全部从回调 provider 解析服务
  • 移除 PersistenceStrategy / OptimizeForSingleWrite / Timeout 用法
  • 后台作业/无环境 UoW 操作改用 IStandaloneUnitOfWorkExecutor.ExecuteAsync(...)
  • 确保创建的 UoW 被释放(支持异步释放;ASP.NET 边界自动处理)
  • 移除 [UnitOfWork] 上的 InitializationMode / CreateOptions() / IsUowEnabled
  • [UnitOfWork(IsUowEnabled = false)][DisableUnitOfWork]
  • 依赖 GET 只读推断时设置 EnableReadOnlyActionNameInference = true
  • 移除所有 UseMiCakeInterceptors(...) 调用与自定义 IMiCakeInterceptorFactory
  • 确认 DbContext 注册为 scoped 或 pooled
  • BypassUnitOfWorkCheckAllowDbContextAccessWithoutUoW(仅只读场景保留)
  • 不要注册自定义 IUnitOfWorkAmbientAccessor(框架 singleton)
  • 每种上下文类型仅保留一个 UseEFCore<TDbContext>() / AddUowCoreServices 调用
  • 审查无 UoW 的直接 DbContext 写入——现在会放行,需要保证时包进 UoW
  • 检查依赖隐式顺序的分页调用点,提供显式排序
  • 检查 UpdateAsync 调用点:并发冲突现在在 flush/commit 时抛出
  • 检查递归调用 SaveChangesAsync 的处理器:无进展循环抛 SaveChangesReentryException
API 用途
IUnitOfWork.FlushAsync() 激活事务 + 按注册顺序 flush;返回受影响行数;不提交
IRepository<TAggregateRoot, TKey>.AddAndGetIdAsync(...) AddAndReturnAsync推荐迁移目标:添加 + FlushAsync 填充生成键并返回 TKey(需环境可写 UoW)
IUnitOfWorkManager.ExecuteRequiresNewAsync(...) 隔离内部边界(取代 requiresNew
IStandaloneUnitOfWorkExecutor 无环境 UoW 的隔离作用域执行;成功提交、失败回滚
IUnitOfWorkResource provider 无关资源契约(取代 IDbContextWrapper
保存点三件套 事务内部分回滚;创建后注册的资源被拒绝
IEFCorePhysicalOperationExecutor<TDbContext> 显式物理删除(绕过聚合生命周期)
UnitOfWorkBoundaryException 主异常 + 回滚/清理失败合并
PartialUnitOfWorkCommitException best-effort 多资源提交的结构化结果
SaveChangesReentryException 无进展 / MaxSaveCycles 重入失败
UseMiCake()(options builder) 仅安装 options 的 EF Core 集成入口
[DisableUnitOfWork] 为 action/controller 退出 ASP.NET UoW 边界
MiCakeEFCoreOptions.MaxSaveCycles(默认 16) 重入循环上界
MiCakeEFCoreOptions.AllowDbContextAccessWithoutUoW 只读 filter/middleware 放宽上下文解析

问:安装拦截器还需要调用什么吗? 不需要。在容器中注册 DbContext 即可——模块的 ConfigureDbContext 配置器自动挂载 拦截器与 options。要求 EF Core 9+。

问:UoW 外直接 SaveChangesAsync 以前会失败,现在呢? 按原生 EF Core 语义放行(Permissive 策略),无 MiCake 事务/回滚/生命周期保证。 需要保证时开启 UoW 或使用 IStandaloneUnitOfWorkExecutor

问:Timeout 没了,超时怎么设? 使用 EF/provider 的命令、锁、事务超时配置。

问:为什么移除 OptimizeForSingleWrite 它允许隐式事务在 post-save 生命周期处理前提交,post-save 失败会在数据已持久化后 返回错误。现在每个可写 UoW 使用显式事务(Lazy 首写前激活,或 Immediate)。

问:UpdateAsync 现在抛 DbUpdateConcurrencyException 分离替换保留并发令牌,过期实例暴露冲突而非静默覆盖。建议 load-and-modify。

问:requiresNew 回调捕获外层服务还能用吗? 不能——所有权校验拒绝外层作用域捕获的上下文/仓储。全部从回调 IServiceProvider 解析。