Skip to content

Allow [Transactional] to select the DbContext for multi-DbContext han… - #3284

Closed
KhaledZaabat wants to merge 3 commits into
JasperFx:mainfrom
KhaledZaabat:feat/transactional-dbcontext-selection
Closed

KhaledZaabat wants to merge 3 commits into
JasperFx:mainfrom
KhaledZaabat:feat/transactional-dbcontext-selection

Conversation

@KhaledZaabat

Copy link
Copy Markdown
Contributor

Allow [Transactional] to select the DbContext for multi-DbContext handlers

Problem

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.DetermineDbContextType throws the
moment it sees more than one DbContext-shaped dependency on a chain:

Cannot determine the DbContext type for ..., multiple DbContext types
detected: AppDbContext, CommonDbContext

There was no way to tell Wolverine which one is "the" transactional context
for that handler.

Solution

[Transactional] gains an optional DbContextType:

public class GrantAccessHandler
{
    [Transactional(typeof(AppDbContext))]
    public static void Handle(GrantAccess message, AppDbContext users, CommonDbContext lookup)
    {
        // users.SaveChanges() is enrolled in the transaction + outbox.
        // lookup is just an ordinary injected read-only dependency.
    }
}
  • Only needed when a chain has more than one DbContext-shaped dependency —
    single-DbContext handlers are unaffected.
  • The type can also be a DbContext abstraction registered via
    WithDbContextAbstraction<TAbstraction, TDbContext>(), so Clean
    Architecture / Application-layer handlers never need to reference the
    concrete EF Core type at all — see USAGE.md.
  • A tag that doesn't match any of the chain's actual dependencies fails
    loudly at startup, naming the offending type, rather than silently
    picking a default.

Changes

  • Wolverine/Attributes/TransactionalAttribute.cs — new DbContextType
    property + Type-based constructor, stored as a chain tag (same pattern
    already used for Mode).
  • Wolverine.EntityFrameworkCore/Codegen/EFCorePersistenceFrameProvider.cs
    — DetermineDbContextType resolves 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 error
for un-tagged multi-DbContext chains is unchanged.

…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>
Copilot AI review requested due to automatic review settings July 2, 2026 01:16

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 optional DbContextType (plus Type constructor) 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.

Comment on lines +1 to +3
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
Comment on lines +22 to +26
// 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.");
KhaledZaabat and others added 2 commits July 4, 2026 21:36
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>
@jeremydmiller

Copy link
Copy Markdown
Member

@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

@jeremydmiller

Copy link
Copy Markdown
Member

Thanks @KhaledZaabat — really solid work here, and the core capability is being merged. I've opened #3295 which supersedes this PR. It keeps your [Transactional(typeof(TDbContext))] mechanism and tests (and preserves you as a co-author on the commit), with two changes from review:

  • Reworked to be explicit-only, no "magic": dropped the registration-side WithTransactionalDbContext<T>() selection and the automatic "single Wolverine-enabled candidate" inference. When a chain has more than one DbContext and no designation, Wolverine now fails fast and tells the user to either drop the automatic transactional middleware or designate the context.
  • Added [Storage(typeof(TDbContext))] as an equivalent, provider-agnostic way to designate the transactional DbContext.

Closing in favor of #3295. Much appreciated! 🙏

jeremydmiller added a commit that referenced this pull request Jul 5, 2026
…election-explicit

Explicit transactional DbContext selection for multi-DbContext handlers (supersedes #3284)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants