Repository navigation
Allow [Transactional] to select the DbContext for multi-DbContext han… - #3284
KhaledZaabat wants to merge 3 commits into
Conversation
…dlers A handler chain can only have one transactional DbContext, but until now Wolverine's EF Core middleware threw as soon as it saw more than one DbContext-shaped dependency, with no way to disambiguate - blocking the common case of one write-side DbContext plus a read-only lookup DbContext in the same handler. [Transactional] now accepts a DbContextType, which can be the concrete DbContext or a type registered via WithDbContextAbstraction<,>() so Clean Architecture handlers never need to reference the concrete DbContext. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds a way to disambiguate which EF Core DbContext should be treated as the single transactional/outbox-enrolled context for handler chains that have multiple DbContext-shaped dependencies (including support for registered DbContext abstractions).
Changes:
- Extend
[Transactional]with an optionalDbContextType(plusTypeconstructor) and store it as a chain tag. - Update EF Core transaction frame selection to honor the tag (including resolving registered DbContext abstractions) before throwing for ambiguous multi-DbContext chains.
- Add EF Core integration tests covering concrete-type selection, abstraction selection, and mismatched tag failures.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/Wolverine/Attributes/TransactionalAttribute.cs | Adds DbContextType support and tags the chain for later resolution by persistence providers. |
| src/Persistence/Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs | Resolves the tagged DbContext (including abstractions) to select the transactional context and improves ambiguity guidance. |
| src/Persistence/EfCoreTests/transactional_dbcontext_selection_tests.cs | Introduces coverage for selecting the transactional DbContext in multi-DbContext handlers and for the error path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| using IntegrationTests; | ||
| using JasperFx; | ||
| using JasperFx.CodeGeneration.Frames; |
| // than one DbContext-shaped dependency (EFCorePersistenceFrameProvider.cs:497-501) - it has no way | ||
| // to know which one is "the" transactional context for this chain. `[Transactional(typeof(TDbContext))]` | ||
| // disambiguates that per-chain. | ||
| public class transactional_dbcontext_selection_tests |
| // lookups and must never be wrapped in a transaction. Today `EFCorePersistenceFrameProvider. | ||
| // DetermineDbContextType(IChain, ...)` throws `InvalidOperationException` the moment it sees more | ||
| // than one DbContext-shaped dependency (EFCorePersistenceFrameProvider.cs:497-501) - it has no way | ||
| // to know which one is "the" transactional context for this chain. `[Transactional(typeof(TDbContext))]` | ||
| // disambiguates that per-chain. |
| { | ||
| throw new InvalidOperationException( | ||
| $"Cannot determine the {nameof(DbContext)} type for {chain.Description}, multiple {nameof(DbContext)} types detected: {contextTypes.Select(x => x.Name).Join(", ")}"); | ||
| $"Cannot determine the {nameof(DbContext)} type for {chain.Description}, multiple {nameof(DbContext)} types detected: {contextTypes.Select(x => x.Name).Join(", ")}. Use [Transactional(typeof(YourDbContext))] to select which one is transactional for this handler."); |
Follow-up to the [Transactional(typeof(T))] work: a handler chain with more than one DbContext-shaped dependency can now resolve its transactional context without any DbContext reference or attribute on the handler - required for Clean Architecture and modular monoliths. EFCorePersistenceFrameProvider.DetermineDbContextType now resolves in this precedence: (1) the [Transactional] tag, (2) registration selections made with the new WithTransactionalDbContext<T>() on EFCoreTransactionConfiguration (overloads: default, Assembly, and Func<IChain,bool>; T may be a concrete DbContext or a registered abstraction), then (3) an automatic tiebreaker that picks the single Wolverine-enabled candidate - a plain AddDbContext<> read-only lookup context is never a transactional candidate. Mismatched or conflicting selections fail fast at startup. Fully additive: the automatic tiebreaker only changes chains that previously threw the ambiguity error, and every selection mechanism is opt-in. - EFCoreTransactionConfiguration: WithTransactionalDbContext<T>() overloads - EFCorePersistenceFrameProvider: RegisterTransactionalSelection, resolveRegistrationSelection, isWolverineEnabled tiebreaker, new precedence - EfCoreTests/transactional_dbcontext_registration_selection_tests.cs - docs: "Selecting the Transactional DbContext" section Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@KhaledZaabat Some of what you wrote for the documentation isn't right. Hey, I think I'm going to go for a superseding pull request based on this, but I'll leave you as a co-author and reuse the tests |
|
Thanks @KhaledZaabat — really solid work here, and the core capability is being merged. I've opened #3295 which supersedes this PR. It keeps your
Closing in favor of #3295. Much appreciated! 🙏 |
…election-explicit Explicit transactional DbContext selection for multi-DbContext handlers (supersedes #3284)
Allow
[Transactional]to select the DbContext for multi-DbContext handlersProblem
A Wolverine handler chain can only have one transactional DbContext. But a
handler is often perfectly valid with two DbContext-shaped dependencies —
one it writes through (must be enrolled in the transaction + outbox), and
one it only reads from (e.g. a shared read-only lookup database, or a
different bounded context/module in a modular monolith).
Today,
EFCorePersistenceFrameProvider.DetermineDbContextTypethrows themoment it sees more than one DbContext-shaped dependency on a chain:
There was no way to tell Wolverine which one is "the" transactional context
for that handler.
Solution
[Transactional]gains an optionalDbContextType:single-DbContext handlers are unaffected.
WithDbContextAbstraction<TAbstraction, TDbContext>(), so CleanArchitecture / Application-layer handlers never need to reference the
concrete EF Core type at all — see
USAGE.md.loudly at startup, naming the offending type, rather than silently
picking a default.
Changes
Wolverine/Attributes/TransactionalAttribute.cs— newDbContextTypeproperty +
Type-based constructor, stored as a chain tag (same patternalready used for
Mode).Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs—
DetermineDbContextTyperesolves that tag before the ambiguity check,including resolving through registered DbContext abstractions.
EfCoreTests/transactional_dbcontext_selection_tests.cs— coverage for:disambiguation with a concrete type, disambiguation through a registered
abstraction, and the mismatched-tag error path.
Backward compatibility
Fully additive. Existing single-DbContext handlers and existing
[Transactional(Mode = ...)]usages are unaffected; the ambiguity errorfor un-tagged multi-DbContext chains is unchanged.