Skip to content

DbContext abstractions with ef core transactions#2919

Merged
jeremydmiller merged 4 commits into
JasperFx:mainfrom
XL1TTE:t2p1
May 28, 2026
Merged

DbContext abstractions with ef core transactions#2919
jeremydmiller merged 4 commits into
JasperFx:mainfrom
XL1TTE:t2p1

Conversation

@XL1TTE
Copy link
Copy Markdown
Contributor

@XL1TTE XL1TTE commented May 27, 2026

Enable developers to use an abstraction layer above DbContext by calling WithDbContextAbstraction<TAbstraction, TDbContext>().
This approach allows you to implement IUnitOfWork with any number of IRepository interfaces per DbContext and use this abstraction in handlers instead of directly depending on DbContext.

@jeremydmiller
Copy link
Copy Markdown
Member

jeremydmiller commented May 27, 2026

@XL1TTE Going to lead with:

  1. Yes, this comes up, and I guess
  2. I'm deeply unenthusiastic
  3. I don't think folks should use that
  4. Custom IUnitOfWork implementations are an abomination :-)

And also the very antithesis of how Wolverine is meant to be used. As a bridge for existing code though, sure, I get that.

@XL1TTE
Copy link
Copy Markdown
Contributor Author

XL1TTE commented May 28, 2026

@jeremydmiller It's primarily about Repository abstraction rather than Unit of Work for me. The challenge arises when packages have intrusive designs that force you to pollute your domain models with persistence-related attributes. This creates a design dilemma:

  1. Avoid the library entirely - but this limits your tooling choices
  2. Add persistence attributes to domain models - but this couples your domain logic to infrastructure concerns
  3. Create separate data access entities - anemic projections of your rich domain models, with a mapping layer between them

If you choose option 3, you inevitably need a bidirectional mapping layer (domain ↔ persistence), which introduces complexity and maintenance overhead.

The real pain point emerges when you use DbContext directly without a repository abstraction, because you come up with ugly code like this in every single handler:

var orderEntity = db.Orders.Find(id);
var order = orderEntity.ToDomain(); // Mapping

// business operations on order

orderEntity.ApplyUpdate(order); // Mapping 
db.SaveChanges();

I get it why repository abstraction is opinionated with modern efCore, but the cases like this one above is actually kind a difficult for me to work around w/o them.

P.S: WithDbContextAbstraction will only work if DbContext itself implements the interface, which means that it will not support abstractions that wraps around DbContext for example.

P.P.S: Would be happy to hear some insights about that.

@XL1TTE XL1TTE marked this pull request as ready for review May 28, 2026 04:53
@jeremydmiller
Copy link
Copy Markdown
Member

@jeremydmiller
Copy link
Copy Markdown
Member

@XL1TTE I'll take this in, but to be more idiomatic in Wolverine, you could use the EF Core transaction middleware and other integration and write code like this:

public static void Handle(UpdateOrder command, [Entity] Order orderEntity)
{
    // do whatever to mutate the Order
}

// Wolverine takes care of  both loading the Order and SaveChangesAsync & the outbox for that matter

The Wolverine way leads to simpler code, business logic being decoupled from infrastructure, more testable code, and less layering hoops to jump through

@jeremydmiller jeremydmiller merged commit df89e32 into JasperFx:main May 28, 2026
23 checks passed
jeremydmiller added a commit that referenced this pull request May 28, 2026
Follow-up to #2919: docs + extended scenario tests for DbContext abstractions
outofrange-consulting pushed a commit to outofrange-consulting/wolverine that referenced this pull request May 28, 2026
…ext abstractions

Reviewer-requested follow-up to PR JasperFx#2919 (DbContext abstractions support
via ).

Adds three new scenarios in
src/Persistence/EfCoreTests/dbContext_abstraction_scenarios.cs:

  1. multi-DbContext, mixed abstraction - one DbContext registered through
     an abstraction (IOrderRepository -> OrdersDbContext) and a second
     DbContext used directly (CustomersDbContext). One Wolverine host;
     both handlers transact through the EF Core middleware in parallel.

  2. multiple abstractions for the SAME DbContext - StoreDbContext
     implements IItemRepository AND IOrderInsightRepository. Two separate
     handlers, each depending on a different abstraction, both
     successfully commit through the same physical DbContext.

  3. multiple abstractions used IN THE SAME handler - the test handler
     takes BOTH IItemRepository and IOrderInsightRepository. The assertion
     proves that at runtime both parameters resolve to the *same scoped*
     StoreDbContext via ReferenceEquals on the casts. That's the contract
     the cast frame the merged PR emits relies on: one DbContext in
     scope, viewed through different interfaces. The forwarding-factory
     DI shape (services.AddScoped<TAbs>(sp => sp.GetRequiredService<TDb>()))
     is what makes this work; AddScoped<TAbs, TImpl>() would create
     separate DbContext instances per registered interface and the cast
     would land on a different one than the handler's reference.

Schemas are dropped + recreated with raw DDL before each scenario starts
(mirroring the workaround documented in
Bug_DurableLocalQueue_ancillary_store_routing for JasperFxGH-2618 - EF Core's
EnsureCreatedAsync is a no-op when the database already exists, so the
user-defined tables never get materialised on the shared Wolverine
integration-tests Postgres unless we issue DDL ourselves).

The new file lives in a sub-namespace
so its IOrderRepository / OrderEntity / etc. fixtures don't collide with
the unrelated identically-named fixtures in Bug_252_codegen_issue.cs
under the parent namespace.

Also adds documentation:
  - new  section in
    docs/guide/durability/efcore/transactional-middleware.md
  - Five  blocks in the new test file (the registration
    shape, the simple-handler shape, the multi-abstraction registration,
    the multi-abstraction handler, and the mixed-DbContext registration),
    expanded by mdsnippets so every code block in the docs lives in a
    passing test.

Local: full wolverine.slnx -c Release builds clean (0 warnings, 0 errors).
All three scenarios pass (Postgres locally on port 5433).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 28, 2026
Bug-fix + feature release on top of 6.1.0 — 13 PRs.

Notable additions:
- Custom Result<T> handler-return-value support (Phases 0+1+2+3, #2952, refs #2221)
- DbContext abstractions for EF Core transaction middleware (#2919 + docs/tests #2954)
- Outgoing Envelope pooling at MessageRouter.RouteForPublish (#2956, closes #2955)
  — ~-504 B/op on transport-bound sends per the CritterStackScalability
  WolverineTransportBenchmarks harness

Bug fixes: scheduled-cascade loss from [ReadAggregate]/[DocumentExists]
handlers (#2941), ancillary-store inbox routing (#2944), Postgres queue-name
length (#2942), MySQL node-record quoting (#2940), Pulsar batched-partition
ack KeyNotFoundException (#2883/#2950), remote-node agent reply timeout
(#2949), and additional resource-disposal cleanup (#2894 from
dmytro-pryvedeniuk).

Polecat bumped 4.1.1 -> 4.2.1 (#2947); Marten + JasperFx families unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants