v10 → v11 Migration Guide
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:
- Removal of
IRepository.SaveChangesAsync - Removal of
AddAndReturnAsyncand thesaveNowparameter - Removal of
ClearChangeTrackingAsync UpdateAsyncsemantics: full replacementDeleteByIdAsyncsemantics: tracked deleterequiresNewreplaced by callback execution- Removal of
PersistenceStrategy/OptimizeForSingleWrite - Removal of
Timeout IDbContextWrapperreplaced byIUnitOfWorkResource- New capabilities:
FlushAsync, savepoints, event hooks - Slimmed-down
UnitOfWorkAttribute IsUowEnabledreplaced by[DisableUnitOfWork]- Read-only action name inference is now opt-in
- Context factory interface consolidation
BypassUnitOfWorkCheckrenamed- Automatic interceptor installation (
UseMiCakeInterceptorsremoved) - Behavioral changes
To migrate quickly, jump straight to the migration checklist.
1. Repository API
Section titled “1. Repository API”1.1 Removal of SaveChangesAsync
Section titled “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:
// Beforeawait _bookRepository.AddAsync(book);await _bookRepository.SaveChangesAsync(); // Competing persistence boundary
// Afterawait _bookRepository.AddAsync(book);await _unitOfWork.CommitAsync(); // Commit at the UoW boundary1.2 Removal of AddAndReturnAsync and saveNow
Section titled “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:
// Beforevar book = await _bookRepository.AddAndReturnAsync(new Book { Title = "x" });
// After (recommended): semantically equivalent, returns the generated keyvar id = await _bookRepository.AddAndGetIdAsync(new Book { Title = "x" }); // Now on IRepositoryawait _unitOfWork.CommitAsync(); // Data is persisted after the commitIn interface-injection scenarios, AddAsync + IUnitOfWork.FlushAsync() is equivalent:
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
Section titled “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
Section titled “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:
// Before: partial update, silently overwrites concurrent changesawait _repo.UpdateAsync(detachedBook);
// After: full replacement; concurrency conflicts throw DbUpdateConcurrencyException at flush/commitawait _repo.UpdateAsync(detachedBook);await _unitOfWork.CommitAsync();Note: load-and-modify remains the preferred workflow; concurrency conflicts now throw at
FlushAsync/CommitAsynctime, not at theUpdateAsynccall.
1.5 DeleteByIdAsync semantics change
Section titled “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:
await _repo.DeleteByIdAsync(id); // Semantics: load → tracked delete → takes effect after UoW commitawait _unitOfWork.CommitAsync();To bypass the lifecycle and physically delete immediately, use the explicit physical deletion API (see 5.8 Physical/batch operations made explicit).
2. Unit of Work API
Section titled “2. Unit of Work API”2.1 BeginAsync(requiresNew: true) removed
Section titled “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:
// Beforevar uow = await _unitOfWorkManager.BeginAsync(requiresNew: true);var repo = _bookRepository; // Outer-scope service — wrongawait repo.AddAsync(book);await uow.CommitAsync();
// Afterawait _unitOfWorkManager.ExecuteRequiresNewAsync(async (sp, ct) =>{ var repo = sp.GetRequiredService<IRepository<Book, int>>(); // 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, useIStandaloneUnitOfWorkExecutor.
2.2 Removal of PersistenceStrategy
Section titled “2.2 Removal of PersistenceStrategy”PersistenceStrategy / OptimizeForSingleWrite are removed. Every writable UoW uses an explicit transaction:
// Beforeawait _uowManager.BeginAsync(new UnitOfWorkOptions { PersistenceStrategy = PersistenceStrategy.OptimizeForSingleWrite });
// Afterawait _uowManager.BeginAsync(); // Default is fine: Lazy activates an explicit transaction// To start the transaction early: UnitOfWorkOptions.ImmediateReason:
OptimizeForSingleWriteallowed 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
Section titled “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
Section titled “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
Section titled “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
// Savepoint example: partial rollback inside a transactionawait _uow.CreateSavepointAsync("step1");// ...execute a batch of operationsawait _uow.RollbackToSavepointAsync("step1"); // Undo only the changes after this point3. ASP.NET Core Boundary
Section titled “3. ASP.NET Core Boundary”3.1 Slimmed-down UnitOfWorkAttribute
Section titled “3.1 Slimmed-down UnitOfWorkAttribute”InitializationMode and CreateOptions() are removed. The attribute now has only two options:
[UnitOfWork(IsReadOnly = true)] // Read-only: write operations fail fast[UnitOfWork(IsolationLevel = IsolationLevel.Serializable)]3.2 IsUowEnabled removed
Section titled “3.2 IsUowEnabled removed”// Before[UnitOfWork(IsUowEnabled = false)]
// After[DisableUnitOfWork]3.3 Read-only action name inference is now opt-in
Section titled “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:
// Startup.cs — re-enable if you depend on the old inference behavior:services.Configure<MiCakeAspNetOptions>(o => o.EnableReadOnlyActionNameInference = true);4. EF Core Integration
Section titled “4. EF Core Integration”4.1 Context factory interface consolidation
Section titled “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:
public class MyFactory<TDbContext> : IEFCoreContextFactory<TDbContext> 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
Section titled “4.2 BypassUnitOfWorkCheck renamed”MiCakeEFCoreOptions.BypassUnitOfWorkCheck is renamed to AllowDbContextAccessWithoutUoW; the default remains false:
// Beforeoptions.BypassUnitOfWorkCheck = true;
// Afteroptions.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
Section titled “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:
// Beforeservices.AddDbContext<AppDbContext>((sp, opt) =>{ opt.UseSqlite(connectionString); opt.UseMiCakeInterceptors(sp); // Removed});
// After — no interceptor-related calls needed:services.AddDbContext<AppDbContext>(opt =>{ opt.UseSqlite(connectionString); opt.UseMiCake(); // Optional: installs the per-context options extension});Requirement: EF Core 9+ (
ConfigureDbContext/IDbContextOptionsConfiguration<TContext>mechanism; built into EF Core 10, verified on EF Core 10).MiCakeDbContextsubclasses do not need to callUseMiCake()—OnConfiguringcalls it automatically.
5. Behavioral Changes
Section titled “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
Section titled “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:
// 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
Section titled “5.2 Nested UoW: shared commit / root rollback”- A nested
CommitAsynconly marks completion; the physical commit happens at the root UoW - A nested
RollbackAsyncmarks the root UoW rollback-only; the root rollback covers all resources
5.3 Multi-resource commit: best-effort + structured results
Section titled “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
Section titled “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
Section titled “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:
// MiCakeEFCoreOptionsoptions.MaxSaveCycles = 32; // Adjust the upper bound as needed5.6 Paging: enforced total order
Section titled “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
Section titled “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 = trueare 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
Section titled “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:
// Before: physical delete directly inside the repository// After: explicit physical operation executorvar executor = sp.GetRequiredService<IEFCorePhysicalOperationExecutor<AppDbContext>>();await executor.ExecuteDeleteAsync<Book>(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
Section titled “6. Migration Checklist”Step 1 — Repository call sites
Section titled “Step 1 — Repository call sites”-
SaveChangesAsync()→IUnitOfWork.CommitAsync() -
AddAndReturnAsync(x)→AddAndGetIdAsync(x)(recommended, now onIRepository);AddAsync(x)+FlushAsync()is also equivalent - Remove the
saveNow:parameter fromAddAsync - Remove
ClearChangeTrackingAsync()calls - Switch immediate physical-delete scenarios to
IEFCorePhysicalOperationExecutor<TDbContext>
Step 2 — UoW usage
Section titled “Step 2 — UoW usage”-
BeginAsync(requiresNew: true)→ExecuteRequiresNewAsync(...), resolving all services from the callback provider - Remove
PersistenceStrategy/OptimizeForSingleWrite/Timeoutusage - 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
Section titled “Step 3 — ASP.NET Core”- Remove
InitializationMode/CreateOptions()/IsUowEnabledfrom[UnitOfWork] -
[UnitOfWork(IsUowEnabled = false)]→[DisableUnitOfWork] - Set
EnableReadOnlyActionNameInference = trueif you rely on GET read-only inference
Step 4 — EF Core registration
Section titled “Step 4 — EF Core registration”- Remove all
UseMiCakeInterceptors(...)calls and customIMiCakeInterceptorFactory - 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<TDbContext>()/AddUowCoreServicescall per context type
Step 5 — Behavioral verification
Section titled “Step 5 — Behavioral verification”- Review direct
DbContextwrites 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
UpdateAsynccall sites: concurrency conflicts now throw at flush/commit - Check handlers that recursively call
SaveChangesAsync: a no-progress loop throwsSaveChangesReentryException
7. New API Quick Reference
Section titled “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<TAggregateRoot, TKey>.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<TDbContext> |
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
Section titled “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
Section titled “Related Documentation”- Repository — v11 repository API in detail
- Unit of Work — v11 UoW API in detail
- Domain Events — event dispatch timing
