--- title: v10 → v11 Migration Guide description: "A complete migration guide for upgrading MiCake from v10 to v11: all breaking changes, migration steps, and a new API quick reference" --- This guide helps you upgrade your MiCake application from v10 to v11. v11 establishes the **Unit of Work as the single persistence owner** and switches to **automatic EF Core interceptor installation**, with breaking changes across four areas: repository API, UoW API, the ASP.NET Core boundary, and EF Core integration. Work through the following changes item by item after upgrading: 1. [Removal of `IRepository.SaveChangesAsync`](#11-removal-of-savechangesasync) 2. [Removal of `AddAndReturnAsync` and the `saveNow` parameter](#12-removal-of-addandreturnasync-and-savenow) 3. [Removal of `ClearChangeTrackingAsync`](#13-removal-of-clearchangetrackingasync) 4. [`UpdateAsync` semantics: full replacement](#14-updateasync-semantics-change) 5. [`DeleteByIdAsync` semantics: tracked delete](#15-deletebyidasync-semantics-change) 6. [`requiresNew` replaced by callback execution](#21-beginasyncrequiresnew-true-removed) 7. [Removal of `PersistenceStrategy` / `OptimizeForSingleWrite`](#22-removal-of-persistencestrategy) 8. [Removal of `Timeout`](#23-removal-of-timeout) 9. [`IDbContextWrapper` replaced by `IUnitOfWorkResource`](#24-idbcontextwrapper-removed) 10. [New capabilities: `FlushAsync`, savepoints, event hooks](#25-new-capabilities) 11. [Slimmed-down `UnitOfWorkAttribute`](#31-unitofworkattribute-slimmed-down) 12. [`IsUowEnabled` replaced by `[DisableUnitOfWork]`](#32-isuowenabled-removed) 13. [Read-only action name inference is now opt-in](#33-read-only-action-name-inference-is-now-opt-in) 14. [Context factory interface consolidation](#41-context-factory-interface-consolidation) 15. [`BypassUnitOfWorkCheck` renamed](#42-bypassunitofworkcheck-renamed) 16. [Automatic interceptor installation (`UseMiCakeInterceptors` removed)](#43-automatic-interceptor-installation) 17. [Behavioral changes](#5-behavioral-changes) To migrate quickly, jump straight to the [migration checklist](#6-migration-checklist). ## 1. Repository API ### 1.1 Removal of `SaveChangesAsync` Repository methods no longer save or commit by themselves. They only modify the UoW tracking state; persistence is owned entirely by the UoW. Replace every `repo.SaveChangesAsync()` with `CommitAsync()` on the ambient UoW: ```csharp // Before await _bookRepository.AddAsync(book); await _bookRepository.SaveChangesAsync(); // Competing persistence boundary // After await _bookRepository.AddAsync(book); await _unitOfWork.CommitAsync(); // Commit at the UoW boundary ``` ### 1.2 Removal of `AddAndReturnAsync` and `saveNow` For code that used `AddAndReturnAsync`, **migrate to `AddAndGetIdAsync(...)`, now on the interface** — it preserves the original "add + immediately obtain the database-generated key" semantics: after adding the aggregate root it automatically calls `FlushAsync` to fill the generated key and returns `TKey`. The flush happens inside the ambient writable UoW transaction and does **not commit**; data is persisted only when the UoW commits. It requires an ambient writable UoW, otherwise it throws `InvalidOperationException`: ```csharp // Before var book = await _bookRepository.AddAndReturnAsync(new Book { Title = "x" }); // After (recommended): semantically equivalent, returns the generated key var id = await _bookRepository.AddAndGetIdAsync(new Book { Title = "x" }); // Now on IRepository await _unitOfWork.CommitAsync(); // Data is persisted after the commit ``` In interface-injection scenarios, `AddAsync` + `IUnitOfWork.FlushAsync()` is equivalent: ```csharp var book = new Book { Title = "x" }; await _bookRepository.AddAsync(book); await _unitOfWork.FlushAsync(); // Generated key filled into book.Id; transaction not committed // ...continue, then finally CommitAsync() ``` The `saveNow` parameter of `AddAsync(..., saveNow: true)` is removed as well; handle it the same way. ### 1.3 Removal of `ClearChangeTrackingAsync` No longer provided. Clearing change tracking is an EF Core concern; detach entries directly through the `DbContext` when needed. ### 1.4 `UpdateAsync` semantics change `UpdateAsync` on an untracked instance is now a **full detached aggregate replacement**: all properties of the instance are written. Configured concurrency tokens are preserved, so stale instances surface conflicts via `DbUpdateConcurrencyException` instead of being silently overwritten: ```csharp // Before: partial update, silently overwrites concurrent changes await _repo.UpdateAsync(detachedBook); // After: full replacement; concurrency conflicts throw DbUpdateConcurrencyException at flush/commit await _repo.UpdateAsync(detachedBook); await _unitOfWork.CommitAsync(); ``` > **Note**: load-and-modify remains the preferred workflow; concurrency conflicts now throw at `FlushAsync` / `CommitAsync` time, not at the `UpdateAsync` call. ### 1.5 `DeleteByIdAsync` semantics change It now loads the aggregate into a stable UoW context first, then performs a **tracked delete** — soft delete, audit, domain events, and rollback semantics are identical to `DeleteAsync`: ```csharp await _repo.DeleteByIdAsync(id); // Semantics: load → tracked delete → takes effect after UoW commit await _unitOfWork.CommitAsync(); ``` To bypass the lifecycle and physically delete immediately, use the explicit physical deletion API (see [5.8 Physical/batch operations made explicit](#58-physicalbatch-operations-made-explicit)). ## 2. Unit of Work API ### 2.1 `BeginAsync(requiresNew: true)` removed The `requiresNew` boolean parameter is replaced by **isolated callback execution**. `ExecuteRequiresNewAsync` creates a separate DI scope; the callback must resolve repositories/DbContexts from the passed-in `IServiceProvider`; commit, rollback, and disposal are handled automatically: ```csharp // Before var uow = await _unitOfWorkManager.BeginAsync(requiresNew: true); var repo = _bookRepository; // Outer-scope service — wrong await repo.AddAsync(book); await uow.CommitAsync(); // After await _unitOfWorkManager.ExecuteRequiresNewAsync(async (sp, ct) => { var repo = sp.GetRequiredService>(); // Resolve from the callback provider await repo.AddAsync(book); // Commits automatically on success, rolls back on failure, scope disposed automatically }); ``` > **Note**: capturing outer scoped services fails the ownership check; calling without an outer UoW throws `InvalidOperationException`. For scenarios without an ambient UoW such as background jobs, use `IStandaloneUnitOfWorkExecutor`. ### 2.2 Removal of `PersistenceStrategy` `PersistenceStrategy` / `OptimizeForSingleWrite` are removed. Every writable UoW uses an **explicit transaction**: ```csharp // Before await _uowManager.BeginAsync(new UnitOfWorkOptions { PersistenceStrategy = PersistenceStrategy.OptimizeForSingleWrite }); // After await _uowManager.BeginAsync(); // Default is fine: Lazy activates an explicit transaction // To start the transaction early: UnitOfWorkOptions.Immediate ``` > **Reason**: `OptimizeForSingleWrite` allowed EF implicit transactions to commit before post-save lifecycle processing finished, which could produce the contradictory result "data persisted but error returned". ### 2.3 Removal of `Timeout` `UnitOfWorkOptions.Timeout` is removed — it had **no runtime effect** anyway. Configure timeouts via EF/provider command, lock, and transaction timeout settings. ### 2.4 `IDbContextWrapper` removed Replaced by the provider-agnostic `IUnitOfWorkResource`. This only affects custom persistence provider integrations; regular applications need no migration. ### 2.5 New capabilities `IUnitOfWork` now provides: - `FlushAsync()` — activates and flushes all resources in registration order; returns the number of affected rows; does not commit - Savepoints — `CreateSavepointAsync` / `RollbackToSavepointAsync` / `ReleaseSavepointAsync` - `MarkAsCompletedAsync()` — skip the commit at a read-only boundary - Transaction events — `OnCommitting` / `OnCommitted` / `OnRollingBack` / `OnRolledBack` - `IAsyncDisposable` ```csharp // Savepoint example: partial rollback inside a transaction await _uow.CreateSavepointAsync("step1"); // ...execute a batch of operations await _uow.RollbackToSavepointAsync("step1"); // Undo only the changes after this point ``` ## 3. ASP.NET Core Boundary ### 3.1 Slimmed-down `UnitOfWorkAttribute` `InitializationMode` and `CreateOptions()` are removed. The attribute now has only two options: ```csharp [UnitOfWork(IsReadOnly = true)] // Read-only: write operations fail fast [UnitOfWork(IsolationLevel = IsolationLevel.Serializable)] ``` ### 3.2 `IsUowEnabled` removed ```csharp // Before [UnitOfWork(IsUowEnabled = false)] // After [DisableUnitOfWork] ``` ### 3.3 Read-only action name inference is now opt-in Previously GET actions were inferred as read-only by name; now this is **off by default**, and explicit metadata always takes precedence: ```csharp // Startup.cs — re-enable if you depend on the old inference behavior: services.Configure(o => o.EnableReadOnlyActionNameInference = true); ``` ## 4. EF Core Integration ### 4.1 Context factory interface consolidation The non-generic `IEFCoreContextFactory`, `IEFCoreAnchoredContextFactory`, and the parameterless `GetDbContextWrapper()` are consolidated into a single public contract. Custom factory implementations only need to implement two methods: ```csharp public class MyFactory : IEFCoreContextFactory where TDbContext : DbContext { public TDbContext GetDbContext() { /* ... */ } public EFCoreDbContextWrapper GetOrCreateWrapperFor(DbContext context) { /* ... */ } } ``` The framework invokes your implementation automatically through an adapter — no need to understand the internal view. ### 4.2 `BypassUnitOfWorkCheck` renamed `MiCakeEFCoreOptions.BypassUnitOfWorkCheck` is renamed to `AllowDbContextAccessWithoutUoW`; the default remains `false`: ```csharp // Before options.BypassUnitOfWorkCheck = true; // After options.AllowDbContextAccessWithoutUoW = true; ``` > **Note**: this option now only relaxes **context resolution** (returns a standalone wrapper without UoW integration, for read-only filters/middleware). Writes **without** a UoW are allowed overall under the Permissive strategy — this option is no longer needed and should not be relied on to allow writes. ### 4.3 Automatic interceptor installation All 3 overloads of `UseMiCakeInterceptors`, `IMiCakeInterceptorFactory`, and its implementation are removed. Interceptors are **mounted automatically** by the module's `ConfigureDbContext` configurator — just register the DbContext in the container: ```csharp // Before services.AddDbContext((sp, opt) => { opt.UseSqlite(connectionString); opt.UseMiCakeInterceptors(sp); // Removed }); // After — no interceptor-related calls needed: services.AddDbContext(opt => { opt.UseSqlite(connectionString); opt.UseMiCake(); // Optional: installs the per-context options extension }); ``` > **Requirement**: EF Core **9+** (`ConfigureDbContext` / `IDbContextOptionsConfiguration` mechanism; built into EF Core 10, verified on EF Core 10). > `MiCakeDbContext` subclasses do not need to call `UseMiCake()` — `OnConfiguring` calls it automatically. ## 5. Behavioral Changes The following changes do not remove APIs, but they change runtime behavior and must be verified after migration. ### 5.1 Writes without a UoW: Permissive Direct `DbContext` writes without a UoW used to be rejected; they are now **allowed** with native EF Core semantics (implicit transaction, no rollback/lifecycle guarantees). The repository/UoW paths remain guarded: ```csharp // Without an ambient UoW: await dbContext.SaveChangesAsync(); // Allowed, same as native EF (no MiCake guarantees) ``` For transactional guarantees, open a UoW or use `IStandaloneUnitOfWorkExecutor`. ### 5.2 Nested UoW: shared commit / root rollback - A nested `CommitAsync` only marks completion; the **physical commit happens at the root UoW** - A nested `RollbackAsync` marks the **root** UoW rollback-only; the root rollback covers all resources ### 5.3 Multi-resource commit: best-effort + structured results Multi-resource commits execute deterministically in registration order; partial failures throw `PartialUnitOfWorkCommitException` carrying per-resource structured, non-sensitive results (ID, type, status). For cross-resource atomicity, choose an outbox or compensation workflow yourself. ### 5.4 Rollback/cleanup failures are no longer lost Rollback or cleanup failures throw `UnitOfWorkBoundaryException` carrying the primary exception + rollback failure + cleanup failure for easy diagnosis. ### 5.5 SaveChanges reentry: bounded state machine Recursive `SaveChangesAsync` calls inside lifecycle/domain-event handlers are no longer raw recursion: nested saves are merged into save cycles within the same transaction, and events are deduplicated per instance. When there is no progress or the `MaxSaveCycles` limit (default **16**) is reached, `SaveChangesReentryException` is thrown and the UoW is marked rollback-only: ```csharp // MiCakeEFCoreOptions options.MaxSaveCycles = 32; // Adjust the upper bound as needed ``` ### 5.6 Paging: enforced total order Paging now requires a **total order**: without caller-provided ordering, every primary key property is appended ascending; with ordering, missing primary key properties are appended as a final ascending `ThenBy`; keyless entities are rejected. Call sites relying on the old implicit order should provide explicit ordering. ### 5.7 Startup validation: DbContext lifetime and execution strategy - DbContext must be registered as **scoped or pooled**; singleton/transient fails at startup with guidance - Execution strategies with `RetriesOnFailure = true` are **rejected** for ambient writable UoWs (an application-owned replayable boundary, such as a dedicated retry executor, is required) ### 5.8 Physical/batch operations made explicit Deletions that bypass the aggregate lifecycle use an explicit API, which requires an ambient writable UoW and stays inside the UoW transaction: ```csharp // Before: physical delete directly inside the repository // After: explicit physical operation executor var executor = sp.GetRequiredService>(); await executor.ExecuteDeleteAsync(b => b.PublishedYear < 2000); ``` Using EF `ExecuteUpdate` / `ExecuteDelete` / `ExecuteSqlRaw` directly: guarded and bound to the transaction inside a UoW; allowed with native semantics outside a UoW. ## 6. Migration Checklist ### Step 1 — Repository call sites - [ ] `SaveChangesAsync()` → `IUnitOfWork.CommitAsync()` - [ ] `AddAndReturnAsync(x)` → **`AddAndGetIdAsync(x)` (recommended, now on `IRepository`)**; `AddAsync(x)` + `FlushAsync()` is also equivalent - [ ] Remove the `saveNow:` parameter from `AddAsync` - [ ] Remove `ClearChangeTrackingAsync()` calls - [ ] Switch immediate physical-delete scenarios to `IEFCorePhysicalOperationExecutor` ### Step 2 — UoW usage - [ ] `BeginAsync(requiresNew: true)` → `ExecuteRequiresNewAsync(...)`, resolving all services from the callback provider - [ ] Remove `PersistenceStrategy` / `OptimizeForSingleWrite` / `Timeout` usage - [ ] Switch background jobs/no-ambient-UoW operations to `IStandaloneUnitOfWorkExecutor.ExecuteAsync(...)` - [ ] Ensure created UoWs are disposed (async disposal supported; the ASP.NET Core boundary handles it automatically) ### Step 3 — ASP.NET Core - [ ] Remove `InitializationMode` / `CreateOptions()` / `IsUowEnabled` from `[UnitOfWork]` - [ ] `[UnitOfWork(IsUowEnabled = false)]` → `[DisableUnitOfWork]` - [ ] Set `EnableReadOnlyActionNameInference = true` if you rely on GET read-only inference ### Step 4 — EF Core registration - [ ] Remove all `UseMiCakeInterceptors(...)` calls and custom `IMiCakeInterceptorFactory` - [ ] Make sure DbContext is registered as **scoped or pooled** - [ ] `BypassUnitOfWorkCheck` → `AllowDbContextAccessWithoutUoW` (read-only scenarios only) - [ ] Do not register a custom `IUnitOfWorkAmbientAccessor` (framework singleton) - [ ] Keep exactly one `UseEFCore()` / `AddUowCoreServices` call per context type ### Step 5 — Behavioral verification - [ ] Review direct `DbContext` writes without a UoW — they are now **allowed**; wrap them in a UoW when guarantees are needed - [ ] Check paging call sites relying on implicit order and provide explicit ordering - [ ] Check `UpdateAsync` call sites: concurrency conflicts now throw at flush/commit - [ ] Check handlers that recursively call `SaveChangesAsync`: a no-progress loop throws `SaveChangesReentryException` ## 7. New API Quick Reference | API | Purpose | |---|---| | `IUnitOfWork.FlushAsync()` | Activates the transaction and flushes all resources in registration order; returns affected rows; no commit | | `IRepository.AddAndGetIdAsync(...)` | **Recommended migration target** for `AddAndReturnAsync`: add + FlushAsync to fill the generated key and return `TKey` (requires an ambient writable UoW) | | `IUnitOfWorkManager.ExecuteRequiresNewAsync(...)` | Isolated inner boundary (replaces `requiresNew`) | | `IStandaloneUnitOfWorkExecutor` | Isolated scope execution without an ambient UoW; commits on success, rolls back on failure | | `IUnitOfWorkResource` | Provider-agnostic resource contract (replaces `IDbContextWrapper`) | | Savepoint trio | Partial rollback inside a transaction; resources registered after creation are rejected | | `IEFCorePhysicalOperationExecutor` | Explicit physical delete (bypasses the aggregate lifecycle) | | `UnitOfWorkBoundaryException` | Primary exception + rollback/cleanup failures combined | | `PartialUnitOfWorkCommitException` | Structured results of best-effort multi-resource commits | | `SaveChangesReentryException` | No-progress / `MaxSaveCycles` reentry failure | | `UseMiCake()` (options builder) | EF Core integration entry point that only installs options | | `[DisableUnitOfWork]` | Opt out of the ASP.NET UoW boundary for an action/controller | | `MiCakeEFCoreOptions.MaxSaveCycles` (default 16) | Reentry loop upper bound | | `MiCakeEFCoreOptions.AllowDbContextAccessWithoutUoW` | Relaxes context resolution for read-only filters/middleware | ## 8. FAQ **Q: Do I still need to call anything to install interceptors?** No. Just register the DbContext in the container — the module's `ConfigureDbContext` configurator mounts interceptors and options automatically. Requires EF Core 9+. **Q: Direct `SaveChangesAsync` outside a UoW used to fail. What about now?** It is allowed with native EF Core semantics (Permissive strategy) — no MiCake transaction/rollback/lifecycle guarantees. Open a UoW or use `IStandaloneUnitOfWorkExecutor` when guarantees are needed. **Q: `Timeout` is gone. How do I set a timeout?** Use EF/provider command, lock, and transaction timeout settings. `Timeout` never took effect anyway. **Q: Why was `OptimizeForSingleWrite` removed?** It allowed implicit transactions to commit before post-save lifecycle processing finished, so a post-save failure could return an error after data was already persisted. Every writable UoW now uses an explicit transaction (Lazy before the first write, or Immediate). **Q: Does `UpdateAsync` now throw `DbUpdateConcurrencyException`?** Detached replacement preserves concurrency tokens, so stale instances surface conflicts instead of being silently overwritten. Load-and-modify is recommended. **Q: Can the `requiresNew` callback still capture outer services?** No — the ownership check rejects contexts/repositories captured from the outer scope. Resolve everything from the callback's `IServiceProvider`. ## Related Documentation - [Repository](/en/domain-driven/repository/) — v11 repository API in detail - [Unit of Work](/en/domain-driven/unit-of-work/) — v11 UoW API in detail - [Domain Events](/en/domain-driven/domain-event/) — event dispatch timing