Kafka Support Improvements in Wolverine 6.13

Wolverine 6.13 dropped yesterday with quite a few refinements to our existing integration with Kafka in Wolverine. We had, of course, probably fallen into the trap of just trying to make Kafka behave like a Rabbit MQ analogue. It was already on my radar to get enable more idiomatic Kafka capabilities, and a community request this week was a little impetus to finally go do this. I will admit that we could possibly stand to reorganize the Kafka related documentation inside of https://wolverinefx.io, but for right now, I’m hoping we can declare victory on our Kafka integration story for a bit.

Smarter Batch Sending

I’m beyond embarrassed over this one. For whatever reason, at some point we had disabled real batch sending with Kafka during some kind of troubleshooting and a user spotted that in the past couple weeks.That major oversight is now addressed. That should increase outbound throughput to Kafka from a Wolverine application by a potentially considerable amount (~20X improvement according to our benchmarking). Ouch, but hey, it’s better now!

#3150 — Kafka: commit-strategy overhaul with CommitMode replaces that with an explicit, opt-in strategy that defaults to the idiomatic non-blocking path:

opts.UseKafka(connectionString)
    .ConfigureListeners(l => l.CommitOffsets(CommitMode.StoreThenAutoFlush));

The four modes:

ModeWhat it doesWhen to reach for it
StoreThenAutoFlush (default)EnableAutoOffsetStore=false + StoreOffset per completed message; Kafka’s background committer flushes on AutoCommitIntervalMsThe new default — idiomatic Kafka throughput
PerMessageSynchronous commit of the message’s own offsetStrict at-least-once on low-volume topics
BatchCount(n)Commit watermark every N messagesHigh-volume topics where you want a tunable lever
BatchInterval(t)Commit watermark every T elapsedBursty traffic

A subtle but important correctness fix rides along: CompleteAsync and the DLQ paths now commit the message’s specific TopicPartitionOffset (offset + 1), not the consumer’s global position. That was a prerequisite for every concurrency feature below.

If you’d already set EnableAutoCommit=true on the Kafka client, Wolverine now respects that and issues no manual commits at all — the previous transport blanket-overrode it.

And: in-flight-safe watermarks for every mode

In Wolverine’s default buffered listener (handlers running at MaxDegreeOfParallelism), messages can complete out of order. The original Batch strategy tracked an in-flight watermark; the new StoreThenAutoFlush and PerMessage strategies initially did not, which meant a fast-completing offset 11 could advance the committed position past a still-in-flight offset 10 — and on a crash, that 10 would be silently dropped.

#3161 — in-flight-safe offset watermark for all commit strategies routes all three manual strategies through a per-partition OffsetWatermark. The committable position is now the lowest still-in-flight offset, or high-water + 1 when nothing is in flight. It never advances past in-flight work, it’s monotonic across re-seeks, and it tolerates the offset gaps that compacted or read_committed transactional topics produce.


Scale-out, the way Kafka actually wants you to do it

The next two PRs make Kafka’s own group coordinator the recommended path to scale Wolverine handlers across nodes.

#3139 — Cooperative-sticky rebalancing + static membership

Two opt-in knobs that any production Kafka deployment will recognize:

opts.UseKafka(connectionString)
    .UseCooperativeStickyAssignment()  // incremental rebalances
    .UseStaticMembership();             // POD_NAME → HOSTNAME → machine name
  • UseCooperativeStickyAssignment() sets partition.assignment.strategy = CooperativeSticky, so a rebalance only moves the partitions that need to move — the rest of the group keeps working uninterrupted.
  • UseStaticMembership() sets group.instance.id so a rolling restart of the same pod doesn’t churn the partition map. Instance id is resolved from POD_NAME → HOSTNAME → machine name (the k8s StatefulSet idiom), and Wolverine logs the resolved id at startup so you can verify per-node uniqueness.

Both are opt-in so you don’t break a live rolling upgrade by silently switching assignment strategies. The Kafka docs section now spells out the two-step rolling onto cooperative-sticky.

#3140 — Opt-in intra-partition concurrency by key

The second concurrency lever. Within a single partition assigned to your node, process messages with different keys concurrently while preserving strict ordering per key:

opts.ListenToKafkaTopic("orders")
    .ProcessConcurrentlyByKey(PartitionSlots: 8);

The trick is that this reuses Wolverine’s existing durable sharded execution — it forces the durable inbox, persists each envelope in consumption order, commits the Kafka offset on persist (the specific-offset fix from #3150), and shards inbox processing by the message key. The inbox is the reliability boundary, so a crash or rebalance can’t lose in-flight work.

#3146 — First-class AutoOffsetReset + ephemeral hot-tail

Cold start and live-tail consumption are now first-class:

opts.ListenToKafkaTopic("metrics").BeginAtEarliest();   // or .BeginAtLatest()
opts.ListenToKafkaTopic("events").TailFromLatest();     // broadcast/fan-out

TailFromLatest() is the interesting one — the listener joins a unique per-process consumer group ({ServiceName}-hot-tail-{guid}) at the tail with EnableAutoCommit=true. Every node receives every message, no commits, no replay. This is the Kafka-local equivalent of a broadcast subscription, and it’s perfect for cache invalidation, ephemeral notifications, or dashboards. The trade-off (throwaway consumer groups left behind on the broker) is called out in the docs.


Replay, finally as a discrete operation

#3147 — Bounded one-shot replay via Assign lets you replay a window of a topic’s history back through the normal Wolverine handler pipeline without disturbing the live consumer group:

// Programmatic
await host.ReplayKafkaTopicAsync(new KafkaReplayRequest
{
    Topic = "orders",
    FromTimestamp = DateTimeOffset.UtcNow.AddHours(-2),
});
# CLI
dotnet run -- kafka-replay orders --from-timestamp 2026-06-18T10:00:00Z

Under the covers, KafkaReplay spins up a throwaway Assign()-based consumer with a unique group id and EnableAutoCommit=false, resolves per-partition start/end from explicit offsets or OffsetsForTimes, seeks to the start, and feeds every record through runtime.Pipeline.InvokeAsync — the same envelope mapping and handlers as live consumption. Each partition pauses at its end boundary. The live group’s committed offsets are untouched.

Live seek of a running group-subscribed listener and a CritterWatch control pane are explicit follow-ups.


Non-blocking tiered retries

This is the one a lot of users have been asking for. #3148 — Non-blocking tiered retry topics via OnException DSL:

opts.OnException<TransientException>()
    .MoveToKafkaRetryTopic(1.Seconds(), 30.Seconds(), 5.Minutes());

On a matching failure the message is produced to a tiered fixed-delay retry topic ({source}.retry.{delay}), the source offset is committed so the partition keeps flowing — no head-of-line blocking, and a delayed consumer reprocesses it through the normal handler pipeline once the tier delay elapses. After the last tier it lands in the existing Kafka DLQ. Tier, attempt, and exception metadata travel in headers.

Two design notes worth calling out:

  • The continuation self-guards: if a non-Kafka listener somehow hits this rule it falls back to a normal inline retry, so the policy can never cross transports. The Kafka transport scans opts.Policies.Failures at ConnectAsync and warns at startup if non-Kafka listeners are present.
  • The core got one small generic hook — IFailureActions.ContinueWith(IContinuationSource) — so transport-specific continuations can plug into the standard error DSL discoverably. This was the gap Pulsar’s resiliency support had to work around; that pattern is now first-class.

Exactly-once building blocks (the cheap ones)

#3149 — Idempotent producer + read_committed + EOS docs ships the cheap, opt-in pieces and — just as importantly — documents Wolverine’s actual exactly-once story so you reach for the right tool:

opts.UseKafka(connectionString)
    .UseIdempotentProducer()  // producer→broker dedupe
    .UseReadCommitted();      // skip records from aborted Kafka txns

The new docs section leads with the durable inbox/outbox as the recommended path for DB-backed apps — that’s effectively-once across DB + Kafka, which Kafka transactions can’t span — then covers the idempotent producer, read_committed, the handler-idempotency reality, and a clear non-goal callout pointing DB-free Kafka→Kafka EOS users at Kafka Streams.

A transactional read-process-write EOS engine remains an explicit non-goal for Wolverine.


One small but annoying bug

#3151 — Fix ExtendConsumerConfiguration inheritance, contributed by @Ferchke7: a regression from a recent PR where ExtendConsumerConfiguration() created an empty topic-level ConsumerConfig, which Kafka then preferred over the parent, silently dropping any global consumer settings configured via UseKafka(...).ConfigureClient(...). Now the topic config is layered properly: parent → existing topic → extension callback.


Where we are vs. where this leaves the .NET Kafka story

After yesterday, Wolverine has:

  • ✅ Idiomatic non-blocking commits, four selectable strategies, in-flight-safe watermarks
  • ✅ Native scale-out via cooperative-sticky + static membership
  • ✅ Second-tier concurrency by message key within a partition
  • ✅ First-class cold-start / hot-tail consumption
  • ✅ Bounded replay through the normal handler pipeline, without touching the live group
  • ✅ Non-blocking tiered retry topics wired into the standard error DSL
  • ✅ Idempotent producer + read_committed + an honest EOS story built on the durable inbox/outbox

The remaining gap the umbrella tracks is the transactional read-process-write EOS engine — explicitly a non-goal — and we’d rather just focus on Wolverine having a great transactional inbox/outbox integration with Kafka and all of our supported messaging options.

Upgrade to Wolverine 6.13.0 (6.13.1 was completely unrelated to the Kafka support), tweak nothing, and you’ll already see the throughput bump from the new default commit strategy. Then pick the levers that match your topic shape.


— Jeremy

The Codebase Is the Prompt: Wolverine, Vertical Slices, and AI-Assisted Development

I’m taking a little bit of time today to start writing up a “strategery” document on the JasperFx / Critter Stack approach to AI in the near future. One of the arguments I’m wanting to lean into quite heavily to call Wolverine AI-friendly is how it compresses code in its approach to “Vertical Slice Architecture” and how its focus on the “A-Frame Architecture” to testability has also made Wolverine AI friendly as well.

Formal layered architecture approaches like the Clean Architecture or other Hexagonal Architecture flavors have dominated the .NET landscape as the accepted “best practices” approach for years. It was a well intentioned strategy to improve maintainability through enforcing some level of loose coupling. Today though, there’s a new factor that’s annoyingly dominating seemingly all software development discourse: the AI coding agent.

An agent doesn’t get tired, but it does have a finite context window, and it pays — in tokens, in latency, and in accuracy — for every irrelevant file it has to load to understand one feature. The structure of your codebase is now, effectively, part of the prompt. And it turns out that the architecture that’s easiest for an agent to reason about is a vertical slice approach, which is conveniently enough, what I’d prefer to do anyway and something that Wolverine is very good at.

I want to make the case that the Critter Stack — and Wolverine specifically — is the best foundation and approach in .NET for vertical slice architecture in the age of coding agents, precisely because of how aggressively it compresses the code you have to write and read for each feature as well as the structural consistency you’ll get by following our recommended idioms.

Why layered architectures fight the agent

Picture the canonical “clean” layered solution: controllers in one project, application services in another, a pile of IRequest/IRequestHandler types, repository interfaces and their implementations, DTOs, mapping profiles, and a domain project underneath it all. To change one behavior — say, how a shipment gets created — an agent has to go find the controller, the request, the handler, the validator, the repository interface, the repository implementation, and probably a mapping profile or two. They live in six or seven directories, scattered among dozens of unrelated features that share those same folders.

Every one of those files has to be pulled into the agent’s context before it can safely make a change. Most of what it loads is irrelevant to the task. The signal-to-noise ratio in the context window collapses, and that’s exactly the condition under which agents start guessing — inventing abstractions you didn’t ask for, “fixing” error cases that can’t happen, and drifting away from the intent of the change. The architecture that was supposed to manage complexity ends up manufacturing context pollution.

This isn’t a hypothetical. It’s become the dominant theme in writing about AI-ready codebases over the last year, usually under the banner of locality of reference: keep everything a feature needs in one place, and the agent loads only what’s relevant. The conclusion practitioners keep arriving at is that feature-organized, vertical-slice code is simply easier — and cheaper — for an agent to work in than layer-organized code.

Vertical slices, and the ceremony problem

Vertical slice architecture, as Jimmy Bogard originally framed it, organizes code around features instead of technical layers. A slice owns its whole pathway: take the input, do the work, produce the output, all in one place. It’s no accident that the .NET community tends to conflate VSA with MediatR — they share an originator, and MediatR’s request-per-handler model nudges you naturally toward one self-contained unit per use case.

For the record, FubuMVC (Wolverine’s predecessor) also promoted what we now call “vertical slices,” but as Jimmy told me, does it matter if nobody used the tool? (ouch).

But here’s the thing, the “just use MediatR” approach still carries a surprising amount of ceremony. For one feature you typically write a request record, the IRequest<T> marker, a handler class implementing IRequestHandler<,>, constructor injection for every dependency, an explicit SaveChangesAsync, a separate call through IMediator/IPublisher to raise follow-on events, a Program.cs registration, and a pipeline behavior to wire up validation. The slice is co-located, which is good. But it’s not small. There’s a lot of structural code surrounding the two or three lines that actually express the business decision — and the agent has to read all of it.

If fewer files and tighter context are what make vertical slices good for AI, then the natural question is: how few artifacts can a slice actually be reduced to?

Wolverine takes the slice to its logical conclusion

This is where Wolverine earns its place in the conversation. Wolverine is, in effect, vertical slice architecture compressed about as far as the language allows. Consider a typical “create a shipment” slice the MediatR way:

public record CreateShipment(Guid OrderId, string Carrier) : IRequest<ShipmentCreated>;
public class CreateShipmentValidator : AbstractValidator<CreateShipment>
{
public CreateShipmentValidator() => RuleFor(x => x.Carrier).NotEmpty();
}
public class CreateShipmentHandler : IRequestHandler<CreateShipment, ShipmentCreated>
{
private readonly IDocumentSession _session;
private readonly IPublisher _publisher;
public CreateShipmentHandler(IDocumentSession session, IPublisher publisher)
{
_session = session;
_publisher = publisher;
}
public async Task<ShipmentCreated> Handle(CreateShipment request, CancellationToken ct)
{
var shipment = new Shipment(request.OrderId, request.Carrier);
_session.Store(shipment);
await _session.SaveChangesAsync(ct);
var @event = new ShipmentCreated(shipment.Id, request.OrderId);
await _publisher.Publish(@event, ct);
return @event;
}
}

Plus the controller that calls it, plus the MediatR and validation-pipeline registration in Program.cs.

Here’s the same slice in Wolverine:

public record CreateShipment(Guid OrderId, string Carrier);
public class CreateShipmentValidator : AbstractValidator<CreateShipment>
{
public CreateShipmentValidator() => RuleFor(x => x.Carrier).NotEmpty();
}
public static class CreateShipmentHandler
{
[Transactional]
public static ShipmentCreated Handle(CreateShipment command, IDocumentSession session)
{
var shipment = new Shipment(command.OrderId, command.Carrier);
session.Store(shipment);
// The returned event is published by Wolverine as a cascading message.
// No IMediator. No manual SaveChangesAsync. No pipeline wiring.
return new ShipmentCreated(shipment.Id, command.OrderId);
}
}

Look at what disappeared. No marker interfaces — Wolverine discovers handlers by convention, so the type carries no IRequest/IRequestHandler noise. No constructor and no fields — dependencies arrive by method injection, declared right where they’re used. No manual transaction management — [Transactional] (or a global auto-transaction policy) lets Wolverine and Marten manage the unit of work and use the document session as a transactional outbox. No separate publish call — returning a value is publishing it, as a cascading message. The validator is still a plain FluentValidation validator, but it’s discovered and run by Wolverine’s middleware; there’s no pipeline behavior to hand-wire.

And also, the Wolverine version also integrates a transactional outbox capability for durable execution. Now that there is so much interest in modular monoliths right now as well, I think it’s going to be important for folks to consider asynchronous workflows between modules within the same system. Conveniently enough, Wolverine has very strong support for that through its in process queueing, transactional outbox for durability, and built in Open Telemetry tracing for visibility into the asynchronous workflows. MediatR and all the tools it has inspired typically do not have any of that.

What’s left is almost entirely the business decision — and that’s the whole point!

It compresses even further on the HTTP edge. With Wolverine.Http, the endpoint is the handler — there’s no controller calling a mediator calling a handler:

public static class CreateShipmentEndpoint
{
[WolverinePost("/shipments")]
[Transactional]
public static ShipmentCreated Post(CreateShipment command, IDocumentSession session)
{
var shipment = new Shipment(command.OrderId, command.Carrier);
session.Store(shipment);
return new ShipmentCreated(shipment.Id, command.OrderId);
}
}

And if you’re doing event sourcing with Marten, the aggregate handler workflow collapses the load-decide-append-save dance into a single method that receives the current aggregate state and returns the resulting events:

public static class ShipOrderHandler
{
[AggregateHandler]
public static OrderShipped Handle(ShipOrder command, Order order)
{
if (order.HasShipped)
throw new InvalidOperationException("Order has already shipped");
return new OrderShipped(command.OrderId, DateTimeOffset.UtcNow);
}
}

Wolverine fetches and rehydrates the Order from its event stream, hands it to you, appends whatever events you return, and commits — transactionally — without you writing any of that plumbing. The slice is the decision and nothing else.

Mediator tools were valuable when you were using them within ASP.Net Core MVC architectures where MVC controllers organized around domain entities (think InvoiceController) had a tendency to get very bloated. MediatR absolutely provided value in those days for teams to mitigate and control the accidental complexity from that type of MVC controller approach. In my opinion though, using a “mediator” should be completely unnecessary with Wolverine.

Wolverine can of course be used as just a “mediator” too, but I tend to recommend against that in most cases.

Why compression is the feature for AI

Tightly co-located slices are good for agents; small slices are better. Three reasons it compounds:

The whole slice fits in context. When a feature is one record, one validator, and one short static handler, the agent can load the entire unit of work and still have headroom for the task. It never has to reconstruct a flow from fragments strewn across layers, which is precisely the situation where agents hallucinate.

There’s far less surface to get wrong. Boilerplate isn’t free for an agent — it’s more code to generate correctly, more interfaces to implement consistently, more registration to remember. Every artifact Wolverine removes is an artifact the agent can’t fumble. Returning a cascading message can’t drift the way a hand-written IPublisher.Publish call can be forgotten or mis-ordered.

It’s cheaper to operate. This one is easy to overlook. Fewer tokens loaded per task is a direct, recurring cost reduction every single time an agent touches the code — whether that’s a developer’s Copilot session, a Claude Code run, or an automated diagnostic agent. In a world where you may be paying per token to run agents against your system, the most compressed codebase is also the cheapest one to keep an agent working in.

And by the way, our new curated AI Skills for the Critter Stack will help you settle into idiomatic vertical slice usage with Wolverine and Marten that will fit well into AI usage. And it so happens that you can purchase access to those AI Skills from the JasperFx website🙂

Compression alone isn’t the whole story

I want to be honest about where “just write tiny handlers” stops being sufficient, because the failure mode is real. When every part is small and stateless, the burden of knowing how the parts wire together — what conventions discover a handler, what a cascading return actually does, what middleware runs around your method — doesn’t vanish. It shifts somewhere. If it shifts into the agent’s context as guesswork, you’ve traded one problem for another.

The answer is conventions plus documented context. Wolverine’s behavior is convention-driven, which means it’s learnable and, more importantly, encodable. This is exactly why we ship AI skill files for the Critter Stack: they give the agent the macrostructure that the compressed slice deliberately leaves implicit — handler discovery rules, the cascading-message model, when and how the transactional middleware applies, the idiomatic shape we actually want. The skills are the constitution; the slices are the code. Together they give an agent a codebase that is both minimal to read and unambiguous to extend. Compressed code without the conventions documented is just terse code. Compressed code with the conventions encoded is an architecture an agent can work in confidently.

Two practical notes in the same honest spirit. Wolverine generates the glue code around your handlers, and that generated code is a benefit for AI — the agent writes the small handler; the framework produces the plumbing it would otherwise have to read and reproduce — but your conventions should tell the agent plainly that generated code is not to be hand-edited, and explain the model so it doesn’t fight the generator. And the classic VSA critique about duplication at scale still applies: resist the urge to grow a sprawling shared “services” layer the moment two slices rhyme. Wolverine’s middleware and compound-handler patterns are the right place to absorb genuinely shared concerns without rebuilding the layered architecture you just escaped.

It’s still in flight, but we’ve put work into our forthcoming CritterWatch tool to give you visualizations of how messages flow between systems or even between handlers within the same system. I’ll be recording a video on that some time next week.

The takeaway

The industry is converging on vertical slices as the AI-friendly way to organize code, and it’s converging there for sound reasons: locality, focus, and a clean context window. Wolverine is the most thorough expression of that idea in .NET. It strips a slice down to the business decision, removes the ceremony that an agent would otherwise have to read and reproduce, and — paired with skill files that encode the conventions — gives a coding agent a codebase that is small, coherent, and cheap to reason about.

If your architecture is now part of the prompt, the move is to make that prompt as short and as clear as it can be. That’s been the Wolverine philosophy from the start. It just happens to be exactly what the agents want too.

Wolverine Hit Five Million Downloads

I’m having an aggravating day at work today, so just indulge me in writing this up for fun whilst I’m waiting for some very slow CI builds to finish…

If you’re not familiar with Wolverine, it’s a series of application frameworks for server side .NET development including asynchronous messaging, asynchronous processing via in process messaging, an alternative HTTP endpoint framework inside of ASP.Net Core, and a “mediator” if that’s really all you need. What’s going to set Wolverine apart from other tools that overlap in capability is our relentless emphasis on low ceremony code and testability in your application code. I would happily argue that Wolverine with or without Marten to form the full “Critter Stack” is the best solution in .NET for both a “Vertical Slice Architecture” style and the new “Modular Monolith” idea.

From supporting folks the past couple years using Wolverine to build modular monoliths, I can tell you that the modular monolith approach is far more complicated than I think many people are recognizing online. I’m not saying that it’s the wrong approach, just suggesting that it’s not a silver bullet.

The main WolverineFx Nuget passed 5 million downloads today, so a little celebratory blog post seems appropriate. Granted, that’s a rounding error compared to some of the more successful OSS tools out there in .NET, but our trajectory is bending upwards quite a bit because that makes a million downloads in the past six weeks alone.

Why the sudden interest and download numbers? I hope that at least some of that growth Wolverine finally getting a little more visibility from .NET content creators on LinkedIn and YouTube. Admittedly some of that download growth is probably just due to the absurd number of releases we’ve made in the past six months. The release cadence has been from a combination of:

  • We get a ton of community pull requests and involvement in bug reports, suggestions, and requests. The Critter Stack community does a great job of writing up actionable issues with reproduction projects too, and that makes it a lot easier to crank through reported issues.
  • I prefer smaller releases rather than letting things build up
  • JasperFx Software has a policy of trying to address fixes or features requested by our clients quickly rather than waiting for the “next scheduled release cycle”
  • For better or worse, AI has made it possible for us to burn through a huge amount of back log issues and long standing ideas that wouldn’t have been feasible to do otherwise

For some context, I visited my son in the first week of December in Boston as he was wrapping up at Northeastern. We’re both history buffs, so we were naturally discussing the American Revolution as we did sight seeing in Boston. At the time, I was making a big effort to burn down the backlog of issues for Wolverine and the GitHub issue and pull request numbers were at that time in the mid-1700’s. Being a history nerd, I had fun talking about what historical events were happening as Wolverine work proceeded from the years during colonial times to the America Revolution to the Napoleonic Wars to the US Civil War and suddenly through the entire tumultuous 20th century. Six months later we’re in far out SciFi times as we cracked 3,000 issues and pull requests last week.

All that being said, yes, I would really like the release cadence to slow down and I’m hopeful that happens once we get past the inevitable slate of issues with the structural changes in the recent Wolverine 6.0 release.

Anyway, Wolverine is clearly trending in a positive direction for adoption right now. This is especially positive to me because Wolverine has taken an extraordinary length of time and effort to get here.

Wolverine is the latest in a lineage of OSS projects dating back to the earliest efforts for FubuMVC starting in ’08 during the tail end of the ALT.Net movement — with my two biggest disappointments in my career being the failure of FubuMVC and the utter implosion of the main ALT.Net community in a cloud of negativity.

When I launched a project called “Jasper” a decade ago, I set out with a long laundry list of lessons learned from FubuMVC about how to make the next attempt at an application framework more successful, and well, that failed too. Wolverine rebooted “Jasper” with the full intention of being a complement to Marten that was already successful. This time though, the integration with Marten got us some early users, and that got us into a virtuous cycle of feedback leading to improvements leading to more community leading to more feedback and you get the point.

Just to end somewhere, if you want one single actionable way to make an OSS project be more successful I’ll tell you to get feedback from users and use that to continuously improve. I just can’t give you a reliable recipe for doing that other than luck.

Wolverine and Marten Just Got Better for F# Folks

Polecat 4.0 shares many more internals with Marten and I’m hopeful that it’s also much better for F# developers as well.

Wolverine and more so Marten do already have F# users, but we just made the deployment story a lot better in both tools for F# developers. One of the key components of Wolverine especially has been our usage of runtime code generation and compilation using Roslyn, which is how Wolverine is able to adapt to your application code instead of forcing you to write adapters to our specific interfaces or abstractions like basically every other application framework in .NET.

That’s the special sauce in Wolverine that allows your application code to be far simpler than it would be with other application frameworks, but it comes at the cost of Roslyn being a beast for memory consumption (sometimes, but not always), the size of the binaries shipped, and cold start times (again, sometimes). We’ve long had the ability in both Marten and Wolverine to pre-generate the Wolverine or Marten adapter code ahead of time and let it be compiled into the application itself to side step the Roslyn runtime issues. But in a story I’m sure is aggravatingly familiar for F# folks, that was only useful for C# projects as we could only generate C# code (and Roslyn only compiles C# code at runtime as far as I know, but feel free to correct me on that one).

Marten 9.0 helped things for everybody by completely eliminating its usage of the Roslyn code compilation. Wolverine 6.0 improved F# usage (with follow ups including 6.3.0 today!) helped as well by supporting the pre-generation of all Wolverine adapter code in F# (message handlers, HTTP endpoint handlers, and gRPC endpoint handlers) with this:

dotnet run -- codegen write --language fsharp

Better yet, Wolverine 6.0 now let’s you use the Roslyn compiler business as strictly a development time only dependency and omit those binaries completely in production deployments. As long as you’re pre-generating the code as shown above (many people like to do that in Docker image initialization), you can deploy a lean, mean, even AOT compliant set of binaries while happily coding with F#!

Last Thoughts

I’m hopeful that these changes make Marten and Wolverine better for folks building and deploying systems with F#.

As we’ve been able to burn down so much of our backlog and other issues, I’ve had time to turn my attention to making our tools better for people who don’t code the exact same way I do. For example, we’ve invested a lot in the last year for the EF Core integration with Wolverine. Just this week we’ve made some progress toward making Wolverine better when folks insist on using more runtime IoC trickery that we would recommend. Along those lines, this post talks about how we hopefully got better for F# developers.

Just so I don’t have to have this conversation yet again, yes, we’re aware of Source Generators in .NET, and no, we don’t believe that it’s remotely possible to replace our usage of Roslyn in Wolverine with Source Generators without Wolverine becoming a much lesser tool because of how much runtime information we use to do the code generation. We have started using far more Source Generators in other elements of the Critter Stack though.

CLI Improvements in the Critter Stack and AI Stuff

There’s and important Wolverine 6.1.0 enhancement release this week that follows up on the big Critter Stack 2026 wave with even more improvements to our “cold start” performance. Specifically today, I’d actually like to talk about how we improved and extended the command line diagnostics tools in the Critter Stack to make it faster and more useful — especially in our new world order of AI assisted software development.

If you’ll pay attention to a coding agent at work, you’ll notice that it’s doing a lot of brute force work through successive command line calls on your system. It turns out that exposing diagnostic information about your system or database or really any system state through command line tools that generate easily parseable information to stdout turns out to be a great way to enable AI agents. You could even say (with a cringe) that:

Now though, the AI utilization of command line tools brings us to some new needs:

  • It would be awfully nice to optimize the command line tools for faster cycles now that it’s a machine trying to utilize the output rather than a human who probably won’t hardly notice some minor delays due to command discovery and application bootstrapping
  • The command line output in some cases now needs to be optimized for terse, easily parsed data that will be read by the AI agent

Alright, back to the Critter Stack. Buried all the way at the bottom of our stack is a command line parser that originated with FubuMVC about 15 years ago. The value of our particular CLI tooling is that it allows you to utilize custom commands discovered in assemblies within your system. We’ve depended on that pretty heavily over the years to introduce quite a few built in utilities for managing dependencies, environment checks, projection rebuilds, and database management.

This seems to be a good point to give a shoutout to Spectre Console that we use internally to make our output a lot prettier than it would be otherwise.

Here’s a little visualization of the various commands hanging out across the Critter Stack:

And of course, many folks will utilize our command line discovery to create their own CLI commands for any number of their own custom diagnostics, batch job runners, or data loading tasks that can be baked directly into their application.

All good so far? It’s been a very useful subsystem for us, but the auto-magic wiring of commands from all these assemblies in your system was enabled by assembly scanning to discover concrete command types upfront.

For the recent “Critter Stack 2026” wave of releases, we switched the command discovery to relying on a source generator (JasperFx.SourceGenerator) to build in the discovery and even more of the parsing code up front to reduce the time it takes a JasperFx CLI enabled application to spin up and be ready to work. This was done quite purposely to optimize the cycle time for AI agent usage, especially if you’re running from pre-compiled code. We also made some optimizations to the command parsing itself too.

Step 1: Get the Big Picture with describe

For the exhaustive flag-by-flag reference, see the Command Line Integration and Diagnostics guides.

First off, if you have either Marten, Polecat, or Wolverine active in your codebase, you can add this line at the very bottom of your Program file (or Program.Main() method, same difference) that opts into the JasperFx command line runner:

// Opt into JasperFx for command line parsing to unlock the built in
// diagnostics and utility tools within your Wolverine application
// And this *exact* signature is important so that the exit code is
// correctly handled to denote failures!
return await app.RunJasperFxCommands(args);

You can verify that the JasperFx is enabled by:

dotnet run help

And also, dotnet run help [command name] to see the specific arguments and flag usage of any specific command.

Wolverine and Marten are a configuration-heavy with plenty of options that impact behavior. Conventions, policies, middleware, transports, and explicit routing all layer together, which is powerful — but it can leave you asking “what is my app actually doing?” For that, we have this command that’s writes out a textual report

dotnet run describe

 describe prints a series of tabular reports about the running configuration:

  • Wolverine Options — the basics, including which assembly Wolverine thinks is your application assembly and which extensions loaded
  • Listeners — every configured listening endpoint and local queue, with how each is configured
  • Message Routing — where known, published message types are routed
  • Sending Endpoints — configured endpoints that send messages externally
  • Error Handling — a preview of the active message-failure policies
  • HTTP Endpoints — all Wolverine HTTP endpoints (only when WolverineFx.Http is in use)
  • Marten or Polecat configuration

This is also the report that I will most often ask you to paste when you need help online.

Step 2: See the Code Wolverine Generates

Wolverine generates the adapter code around your handlers and HTTP endpoints at startup. When you want to see it — what middleware ran, how dependencies resolve, where transactions wrap — write it out or preview it:

# Write all generated code to /Internal/Generated
dotnet run -- codegen write

# Or just dump it to the terminal
dotnet run -- codegen preview

codegen covers the whole app, which can be noisy. When you want to understand a single entry point, reach for codegen-preview under the wolverine-diagnostics parent command :

# A message handler (fully-qualified, short, or handler class name — fuzzy matched)
dotnet run -- wolverine-diagnostics codegen-preview --handler CreateOrder

# An HTTP endpoint (requires Wolverine.Http; format "METHOD /path")
dotnet run -- wolverine-diagnostics codegen-preview --route "POST /api/orders"

# A proto-first gRPC service (requires Wolverine.Grpc)
dotnet run -- wolverine-diagnostics codegen-preview --grpc Greeter

The output is identical to codegen preview, but scoped to one handler, so the signal-to-noise ratio is far higher. This command was 100% meant for AI tool usage, but it’s hopefully helpful for human usage. We have been investing in JasperFx’s curated AI skills to “know” how to use these tools to troubleshoot Critter Stack application building inside of AI coding agent work.

Conveniently enough, you are now able to purchase access to our curated AI Skills online — but these are also bundled into JasperFx Software support plans.

We’re going to add an interactive mode to the new wolverine-diagnostics tool soon for easier human usage.

Step 3: Understand Where and Why Messages Route

describe shows you the routing table. When you need to focus on one message type — or understand why it routes the way it does — use describe-routing :

# One message type (full name, short name, or fuzzy match)
dotnet run -- wolverine-diagnostics describe-routing CreateOrder

# The complete routing topology
dotnet run -- wolverine-diagnostics describe-routing --all

For a single type you get the local handler, a routes table (destination, local vs. external, Buffered/Durable/Inline mode, outbox enrollment, serializer, and how each route was resolved), and any message-level attributes such as [DeliverWithin].

The most useful flag is --explain , which walks Wolverine’s route-source chain in order and shows what each source produced and which terminating source short-circuited the rest:

dotnet run -- wolverine-diagnostics describe-routing CreateOrder --explain

# Same explanation as structured JSON, for tooling or AI agents
dotnet run -- wolverine-diagnostics describe-routing CreateOrder --json

This is the command-line surface over the IWolverineRuntime.ExplainRoutingFor(Type) API. The text output is deliberately stable and labeled so it reads well for humans and parses cleanly for automated tooling. See Troubleshooting Message Routing for the programmatic side.

Step 4: Troubleshoot Handler Discovery

If you expected Wolverine to find a handler but it isn’t running, ask Wolverine to explain its discovery decision for that type:

# By handler class name (also accepts a fully-qualified name or a fuzzy partial match)
dotnet run -- wolverine-diagnostics describe-handlers CreateOrderHandler

The argument is matched against the types in your application, and if it matches more than one type you get a report for each. Every report tells you whether the type’s assembly is being scanned, which type-level include/exclude rules HIT or MISS, and — per method — whether it satisfies the handler naming and signature conventions. It’s the command-line surface over WolverineOptions.DescribeHandlerMatch(Type), so you don’t have to drop a temporary Console.WriteLine(...) into your bootstrapping code (see Troubleshooting Handler Discovery).

Step 5: Check Your Infrastructure

Wolverine’s transports and the durable inbox/outbox register self-diagnosing environment checks — can I reach the database? the broker? are the IoC registrations valid?

dotnet run -- check-env

To create, inspect, or tear down the stateful infrastructure Wolverine needs (queues, tables, topics):

dotnet run -- resources setup     # also: check, list, teardown, statistics

resources setup is a great way to provision a clean environment before a test run.

Step 6: Inspect and Recover Message Storage

For applications using the durable inbox/outbox, the storage command administers the message store:

dotnet run -- storage counts     # incoming / outgoing / scheduled / dead-letter / handled
dotnet run -- storage clear
dotnet run -- storage rebuild                                   # --file to emit the schema script
dotnet run -- storage release --exception-type Some.Exception   # replay dead-lettered messages

storage counts is the quick “is anything backing up?” check, and release re-queues dead-lettered envelopes (optionally filtered to a single exception type). To purge inbox rows already marked Handled:

dotnet run -- clear-handled

Step 7: Export a Full Snapshot with capabilities

dotnet run -- capabilities wolverine.json

Writes a complete JSON description of the application — settings, message types, message store, messaging endpoints, even configured event stores. It’s useful for support, for feeding external tooling, and for detecting unintended configuration drift between deployments.

Bonus: Generate OpenAPI Offline (Wolverine.Http)

If you use WolverineFx.Http, you can generate the OpenAPI document without starting the host — no database or broker required, which makes it CI-friendly:

dotnet run -- openapi --list                  # list document names from AddOpenApi()
dotnet run -- openapi -d v1 -o swagger.json    # generate a document to a file
dotnet run -- openapi --route "GET /orders/{id}"

Cheat Sheet

CommandWhat it answers
describeWhat’s my whole configuration?
codegen write / previewWhat code is Wolverine generating?
wolverine-diagnostics codegen-preview…for one handler / endpoint / gRPC service
wolverine-diagnostics describe-routing [--explain] [--json]Where — and why — does a message route?
wolverine-diagnostics describe-handlers <TypeName>Why is (or isn’t) a type discovered as a handler?
check-envCan I connect to my infrastructure?
resources setup / check / teardownProvision or inspect stateful resources
storage counts / clear / rebuild / releaseInbox/outbox state and recovery
clear-handledPurge handled inbox rows
capabilities <file>.jsonFull machine-readable app snapshot
openapiGenerate OpenAPI docs offline (Wolverine.Http)

Where to Go Next

Wait, how does this work with Aspire?

But wait, what if you’re using Aspire as a de facto replacement for docker compose locally such that your applications can’t really run without Aspire being started up first? How is that going to work?

Um, I need to have a better answer for that myself. Let me get back to you on that one!

So, what’s JasperFx Software’s game plan for AI?

We’re going to throw all the spaghetti up against the wall!

More seriously, we’re going to bet on the command line usage described here + AI Skills combination as our first step. We’re also building quite a bit of MCP support into the CritterWatch commercial tooling that will hopefully be generally available in the next couple weeks. The last bit of planned spaghetti is some spec driven development experimentation we’re doing off in the background that’s going down a Behavior Driven Development strategy rather than trying for any kind of user interface modelling approach.

Higher Performance Dynamic Consistency Boundary Development with Marten 9.0

To try to explain “Dynamic Consistency Boundary” usage in Event Sourcing, I’d contrast it to “traditional” Event Sourcing where events are only organized into a stream of related events. For example, all the events related to a single invoice in an invoicing system are an example of an event stream. DCB came about because Axon IQ has weak consistency and couldn’t support transactions across multiple streams the way that Marten or Polecat can it’s often impossible to model a system where every operation only involves a single event stream. To that end, folks created the idea of “Dynamic Consistency Boundary” Event Sourcing where events are more organized by tags and the event stores that support DCB are able to enforce transactional boundaries based on an event tag query (think: all the events of these types that are related to either this class id, student id, or instructor id) so that systems can be much more flexible over time.

Marten has had support for the Dynamic Consistency Boundary approach (DCB) to Event Sourcing for a little while. The Marten 9.0 release last week added a new, potentially more performant option for DCB using the PostgreSQL HSTORE extension — which is supported by all the major cloud providers plus specialized cloud providers for managed PostgreSQL like Neon and Supabase.

Unfortunately, we don’t yet have a way to retroactively switch from “classic” DCB to the HSTORE style DCB in an existing application, but let’s say that you’re starting:

  • A greenfield application
  • A problem domain where the event stream boundaries either aren’t clear upfront or you think will never cleanly line up neatly in terms of event streams

You might want to adopt DCB style Event Sourcing from the get go, then use the HSTORE flavor of Marten DCB to be more performant. To get started, just opt into that style of DCB storage like this:

var builder = Host.CreateApplicationBuilder();
builder.Services.AddMarten(opts =>
{
opts.Connection(builder.Configuration.GetConnectionString("postgres"));
// Marten does need to know about the possible tag types
// upfront, and we rely on value types for this
opts.Events.RegisterTagType<StudentId>("student");
opts.Events.RegisterTagType<CourseId>("course");
// This is all you need to do, but this does assume
// that the HSTORE extension is available
opts.Events.DcbStorageMode = DcbStorageMode.HStore;
});

The HSTORE style of DCB is a big performance improvement if you are querying events by two or more tag values at a time, which I’d probably argue is the only time DCB is worthwhile to use from a logical structure perspective anyway:)

I do owe the Critter Stack community a better YouTube video and blog post on using DCB, but for now, I’ll actually send you to the Wolverine documentation on command handlers using DCB with Marten for more examples and context.

Summary

I was the technical lead of a very successful software project for supply chain management in the early 2000’s. The most popular feature within that early web application was a last minute throw in report that I did as a favor for our business contact that wasn’t even part of our original specification. Once the system was live and folks found out about that report, we actually had to add new servers to the application cluster to keep up with the unexpected load just because that one single report was so popular with supply chain analysts.

My only point there is that I’m not always sure what features will actually resonate with users. Sometimes you know based on reported friction that a new feature will eliminate a pain point, but with the DCB support in Marten and Wolverine, I flat out don’t know. DCB is very popular in the Event Sourcing community outside of the Critter Stack, and it was clear we had to have that feature set just to be competitive from an adoption perspective, but I’m not seeing a lot of interest in DCB from our existing community as Marten is much more able to handle more flexible transactional consistency across event streams than specialist Event Store databases seem to be able to do.

But, if DCB is something you’re interested in or just works much easier for your mental model of how the domain should be modeled in your system, Marten has you covered!

Sneak Peek at Critter Stack 2026

This is in lieu of the “official” release post I’ll make early next week after the Memorial Day holiday when I have enough rest and energy to do something much better by hand. Until then, here’s a peek at the new releases we’ll be announcing early next week — but with some published updates to our AI Skills tomorrow morning that will help you do the migrations!

The whole stack just shipped to NuGet — a coordinated major release across every Critter Stack project, built on a shared JasperFx 2.0 foundation and targeting .NET 9 + .NET 10. Headline themes: AOT-friendlyfaster cold-start, and Marten ↔ Polecat dedupe onto shared infrastructure.

(Quick notes for now — a full writeup written by an actual human being with benchmarks lands next week.)


JasperFx 2.0 (foundation)

The shared base for the whole stack — also ships JasperFx.Events 2.0 + JasperFx.RuntimeCompiler 5.0.

  • AOT-compatible core; runtime Roslyn split into JasperFx.RuntimeCompiler so Static-mode apps drop it and the trimmer removes Roslyn.
  • Source generators for projection dispatch (FEC-free), CLI command discovery, and options descriptions — less runtime reflection.
  • Cold-start wins (delegate caching, deterministic/byte-stable codegen) + consolidated event-store & async-daemon abstractions.

📖 Docs: https://shared-libs.jasperfx.net · 🏷️ Release: https://github.com/JasperFx/jasperfx/releases/tag/2.0.0 · 🧭 Migration: https://shared-libs.jasperfx.net/migration-guide.html

Weasel 9.0 (database layer)

  • Database-primitive foundation on JasperFx 2.0; the consolidation home for IStorageOperationOperationRoleBulkInsertMode, and the SQL Server advisory lock (dedupe pillar).
  • Postgres / SQL Server / Sqlite / MySql / Oracle / EF Core providers, all 9.0.0.

📖 Docs: https://weasel.jasperfx.net · 🏷️ Release: https://github.com/JasperFx/weasel/releases/tag/v9.0.0 · 🧭 Migration: https://weasel.jasperfx.net/migration-guide.html

Marten 9.0

  • No more runtime code generation — Roslyn is gone. Closed-shape document/event storage + Marten.SourceGenerator compiled queries. No codegen write step for Marten; AOT-publishable in Static mode.
  • Best-perf defaults flipped on: Quick append w/ server timestamps, advanced async tracking, bigint events, lightweight sessions, System.Text.Json. One-line revert with opts.RestoreV8Defaults().
  • IRevisioned.Version stays int (V8-compatible); new ILongVersioned for long. DCB gains optional HSTORE tag storage + identity-less boundary aggregates.

📖 Docs: https://martendb.io · 🏷️ Release: https://github.com/JasperFx/marten/releases/tag/V9.0.0 · 🧭 Migration: https://martendb.io/migration-guide.html

Polecat 4.0

  • SQL Server / EF Core-rooted event sourcing on the shared foundation — source-generator-first and AOT-clean end to end.
  • Adopted the lifted JasperFx.Events async-daemon abstractions; folded SqlServerAppLock into Weasel.SqlServer.AdvisoryLock; SingleTenant lock-ids now align with Marten.
  • Ships in lockstep with Marten 9.

📖 Docs: https://polecat.jasperfx.net · 🏷️ Release: https://github.com/JasperFx/polecat/releases/tag/V4.0.0 · 🧭 Migration: https://polecat.jasperfx.net/migration-guide.html

Wolverine 6.0

  • ⚠️ Runtime codegen decoupled from core. Apps in the default Dynamic mode must add WolverineFx.RuntimeCompilation (dev/test), or pre-generate via codegen write + Static mode (prod). This is the #1 upgrade gotcha — see the migration guide.
  • ⚠️ ServiceLocationPolicy.NotAllowed is the new default — restructure registrations or call opts.RestoreV5Defaults() to revert.
  • AOT-clean; Tier-1 cold-start static handler registry. Newtonsoft extracted to WolverineFx.NewtonsoftIForwardsTo<T> discovery is now explicit.

📖 Docs: https://wolverinefx.net · 🏷️ Release: https://github.com/JasperFx/wolverine/releases/tag/V6.0.0 · 🧭 Migration: https://wolverinefx.net/guide/migration.html


Upgrading? Bump the whole stack in lockstep — JasperFx 2.0 / Weasel 9.0 / Marten 9.0 / Polecat 4.0 / Wolverine 6.0. Each migration guide above has an at-a-glance table of breaking changes. The big perf/benchmark writeup is coming next week. 🚀

Wolverine Middleware and Some Random Observations

This post is ostensibly about a sample usage of Wolverine middleware, but I’m going to meander a bit.

I’m working pretty hard this week trying to make some serious progress on CritterWatch, our long planned production monitoring and management console for the “Critter Stack.” At one point late in the day I was working to troubleshoot some failing tests in the CritterWatch codebase with a harness like this:

CritterWatch itself and a test Wolverine application were both running in memory as separate IHost instances. CritterWatch heavily utilizes the Wolverine SignalR messaging transport for communication between its Vue.js based user interface and the CritterWatch server. Part of Wolverine’s SignalR integration is a SignalR Client Transport option that makes it easy for us to use WebSockets communication in integration tests to mimic the client, but live completely in the world of strong typed C# objects.

The integration tests in question were trying to:

  1. Send a message through the SignalR client to mimic commands from the user interface
  2. Which would be relayed from CritterWatch to the right monitored Wolverine service
  3. Which would execute a command against its database, then send a response message back to CritterWatch
  4. Which would relay that response message right back to the original caller through SignalR

The test was failing in a very annoying way by timing out waiting for the ultimate response message to come all the way back with no real feedback about why it was failing.

More on this in a bit, but some of the handler code and automated testing code was written somewhat naively by AI agents and I have some thoughts.

As I wrote about recently in On Debugging Problems, I frequently start debugging efforts by formulating a theory about the most likely cause of the problem and trying to take a quick way to either prove or disprove that theory. This time I happened to be exactly right as I found this code:

public class ReplayMessagesHandler : MessageHandler<ReplayMessages>
{
protected override async Task HandleAsync(ReplayMessages command, MessageContext context,
CancellationToken cancellation)
{
// This, unsurprisingly, was the smoking gun
if (!LicenseGuard.IsOperationAllowed())
{
return;
}
// Other stuff...
}
}

After dithering back and forth on this, we landed on the idea of making CritterWatch a “freemium” model where all the advanced features require an installed license and I retrofitted the license protection with a little bit of help from my friend Claude — and wouldn’t you know it, in all the constant sprinting on the user interface, the test harness didn’t have a license applied so it could test through the message handler above. Easy fix to bring the tests back to green, but I wanted to improve the license guard usage by utilizing Wolverine middleware and that might be a great example for a blog post!

Then I remembered that the way that message handler is built completely sidesteps Wolverine middleware, so hold that thought.

The first problem was that we weren’t getting any obvious indication in the test harness that the test was failing because the license file wasn’t applied during tests. That’s an easy thing to fix by just changing the guard clause to this:

if (!LicenseGuard.IsOperationAllowed())
{
throw new LicenseRequiredException();
}

And a bit of corresponding error handling configuration for Wolverine to know to discard these messages rather than let them go to the dead letter queue or waste any time retrying:

// Just throw the message away if this happens
options.OnException<LicenseRequiredException>().Discard();

By throwing an exception — and I’m not too worried about using an exception here for flow control because after all, you’re doing something naughty if you manage to hit that message handler — I knew that would be automatically written out to any test failures with the Wolverine tracked session testing helpers that these tests were using even though the exceptions are happening in asynchronous message handling and would be handled internally by Wolverine.

The key point here is that it is very often important in your test automation strategy to think about how you can report contextual information about test failures that will help developers troubleshoot said failures.

Alright, so let’s pretend that I’m working with normal Wolverine message handlers and middleware strategies are available. In that case I can get the license guard out of the message handler code as a cross cutting concern. A way to do that is to design a new [RequiresLicense] attribute that will add middleware for the license guard to any handler class or method decorated with that attribute. Here’s the Wolverine flavor of that strategy:

public class RequiresLicenseAttribute : ModifyChainAttribute
{
// This method will be called at the beginning
public static void Validate()
{
if (!LicenseGuard.IsOperationAllowed())
{
throw new LicenseRequiredException();
}
}
// This is the actual middleware application. This will make Wolverine add
// a call to the static Validate() method in the code it generates around
// a Wolverine message handler
public override void Modify(IChain chain, GenerationRules rules, IServiceContainer container)
{
chain.Middleware.Insert(0, new MethodCall(typeof(RequiresLicenseAttribute), nameof(Validate)));
}
}

With that attribute marking up either the handler class or the main message handler method, Wolverine is going to do some “code weaving” so that this line of code will appear on the first line of in the generated code that Wolverine builds around your handler code:

Wolverine.CritterWatch.Handlers.RequiresLicenseAttribute.Validate();

Just to make sure this is clear, Wolverine does not use any kind of Reflection at runtime but instead “bakes” the middleware application in on the first usage of the handler or even completely ahead of time in production usage.

Wolverine’s Configuration vs Runtime Model

It’s admittedly a goofy model that is quite different than basically every other “Russian Doll” tool out there where there’s usually some kind of wrapping model like MediatR’s IPipelineBehavior<TRequest, TResponse>. Wolverine’s model was intentionally designed to avoid the bloated object allocations that tools like MediatR accidentally cause when folks get a little slap happy with middleware usage. Wolverine’s model is also designed to minimize the dreadful exception stack traces that many application frameworks that support middleware create for you by doing so much object wrapping and delegation *cough* ASP.Net Core *cough*.

I had an extremely popular professor in college for all our heat transfer classes who had a legendary drinking game that had been passed down for generations (if Dr. Chapman gets chalk on his pants, take a drink). If there was a drinking game for me, it would be “if Jeremy quotes the original C2 Wiki or links to a Martin Fowler post…”

Wolverine’s middleware strategy also varies quite a bit from a MediatR or most other is our usage of an internal “Semantic Model” that is built up at configuration and bootstrapping time, then compiled into a runtime model:

At bootstrapping time, we build up a configuration model for how every discovered message handler or HTTP endpoint method will be handled. That same model also models the application of middleware, post processors, error handling for message handlers, and a ton of HTTP specific elements for each message handler or HTTP endpoint. In order, the configuration is built up by:

  1. Built in Wolverine policies like “an HTTP endpoint method that returns a string will write out content type text/plain go first
  2. User defined policies go next, and can override anything from the Wolverine policies
  3. Attributes on the message handler or HTTP endpoint type and method apply individual overrides on specific message handlers or HTTP endpoint handling. We do this through the base ModifyChainAttribute or ModifyHttpChainAttribute classes that expose a method that directly modifies the HandlerChain configuration model for each message handler or likewise the HttpChain model for an HTTP endpoint. Attributes of course win out over policies
  4. Lastly, Wolverine has a way to expose direct manipulation of the underlying HandlerChain model for individual handlers, and the more explicit mechanisms always win out over any kind of policies

This HandlerChain model is also built up with knowledge of your application’s IoC container and through that, the dependencies of any given message handler or HTTP endpoint method. Wolverine can use this to selectively apply middleware. For example:

  • Wolverine’s Fluent Validation middleware doesn’t apply if there are no matching validators for a message type and can effectively inline a single or collection of validators otherwise. No runtime probing of the IoC containers like you see in many validation middleware approaches out there
  • In a system using multiple EF Core DbContext types, Wolverine can choose the right one for a given Saga type and generate the most efficient code possible to use that DbContext without having to use any kind of wrappers or runtime IoC tricks
  • In a system that uses both EF Core and Marten, Wolverine can tell from the dependencies of a single handler if it should use Marten based or EF Core based transactional middleware

The key point here is that the “Semantic Model” usage and the way we do configuration in Wolverine allows you a great deal of control over the application of middleware in a fine grained way and this is frequently valuable.

NServiceBus also has their BehaviorGraph concept that is the same “Semantic Model” concept I’m discussing here that allows either Wolverine or NServiceBus users to fine tune the application of middleware to specific messaging handlers based on user or framework defined conventions. The similarity is not the slightest bit coincidental because NServiceBus’s model was taken from FubuMVC that was the spiritual predecessor of Wolverine.

About the AI Thing

Ages ago I read a quote from Martin Fowler (take a drink) something to the effect of:

The only way to know how far a new tool or technique can go is to take it too far, then back off a bit

I’m in my “back off” phase for AI assisted development after feeling completely blown away at first by how much I was able to accomplish. Up above, I explained that I ran into some trouble because of code written naively by AI that I had not reviewed well enough. I’ve also been the victim or perpetrator of several AI coding related problems in the past couple months alone that have let regression bugs slip out into the wild.

I don’t think that anybody is going all the way back to coding completely by hand, but for my part, I think that AI tempts you into trying to develop faster than you should and that you need to exert more control over the code than I apparently had before this week. I guess my only main takeaway is to slow down, not make too many risky changes just because an AI tool made it easy, and if you are responsible for code used by other people, make sure you have eyeballs on it.

I miss long form blogging

If you’ll allow me a little diversion, I used to enjoy technical blogging when that was the way that developers communicated online. Before social media took off, I would take time to formulate and craft a blog post to explain some idea I had or to share something I’d learned that I self importantly thought other developers should know too. At one point I had an hour and change commute on a train every morning, and used to occasionally write a series of mini essays I called a “Train of Thought” for topics that were on my mind but not worthy of a long form blog post by themselves. As I thought tonight about writing up a little blog post sample of Wolverine middleware, it occurred to me that that was going to touch on several other topics and that reminded me of that magic little time when I enjoyed writing technical blog posts.

Then came Twitter of course, and that acted as a release valve that let you blurt things out without ever building up a cohesive, long form post and everything changed forever. Now, of course, what was Twitter isn’t nearly as important, there’s basically no technical content on BluSky, Mastodon has “Linux on the Desktop” energy, and LinkedIn posts are nothing but non stop self-promotion. Younger developers are on Twitch or cranking out YouTube videos. I still blog, but I’m admittedly almost completely focused on promoting the “Critter Stack” tools or JasperFx Software and it’s not the same at all. For what it’s worth, I enjoyed just sitting down tonight and trying to write something by hand.

Like Vertical Slice Architecture? Meet Wolverine.Http!

Before you read any of this, just know that it’s perfectly possible to mix and match Wolverine.HTTP, MVC Core controllers, and Minimal API endpoints in the same application.

Edit: The documentation links were all wrong when I pushed this late at night of course, so:

If you’ve built ASP.NET Core applications of any size, you’ve probably run into the same friction: MVC controllers that balloon with constructor-injected dependencies, or Minimal API handlers that accumulate scattered app.MapGet(...) calls across multiple files. And if you’ve reached for a Mediator library to impose some structure, you’ve added a layer of abstraction that — while familiar — brings its own ceremony and a seam that can make unit testing harder than it should be.

Wolverine.HTTP is a different model. It’s a first-class HTTP framework built on top of ASP.NET Core that’s designed from the ground up for vertical slice architecture, has built-in transactional outbox support, and delivers a middleware story that is arguably more powerful than IEndpointFilter. And it doesn’t need a separate “Mediator” library because the Wolverine HTTP endpoints very naturally support a “Vertical Slice” style without so many moving parts as the average “check out my vertical slice architecture template!” approach online.

Moreover, Wolverine.HTTP has first class support for resilient messaging through Wolverine’s transactional outbox and asynchronous messaging. No other HTTP endpoint library in .NET has any such smooth integration.

What Is Vertical Slice Architecture?

The core idea is organizing code by feature rather than by technical layer. Instead of a Controllers/ folder, a Services/ folder, and a Repositories/ folder that all have to be navigated to understand one feature, you co-locate everything that belongs to a single use case: the request type, the handler, and any supporting types.

The payoff is locality. When a bug is filed against “create order”, you open one file. When a feature is deleted, you delete one file. There’s no hunting across layers.

Wolverine.HTTP is a natural fit for this style. A Wolverine HTTP endpoint is just a static class — no base class, no constructor injection, no framework coupling. The framework discovers it by scanning for [WolverineGet][WolverinePost][WolverinePut][WolverineDelete], and [WolverinePatch] attributes.

And because of the world we live in now, I have to mention that there is already plenty of anecdotal evidence that AI assisted coding works better with the “vertical slice” approach than it does against heavily layered approaches.

Getting Started

Install the NuGet package:

dotnet add package WolverineFx.Http

Wire it up in Program.cs:

var builder = WebApplication.CreateBuilder(args);
builder.Host.UseWolverine();
builder.Services.AddWolverineHttp();
var app = builder.Build();
app.MapWolverineEndpoints();
return await app.RunJasperCommands(args;

A Complete Vertical Slice

Here’s what a full feature slice looks like with Wolverine.HTTP. Request type, response type, and handler all in one place:

// The request
public record CreateTodo(string Name);
// The response
public record TodoCreated(int Id);
// The handler — a plain static class, no base class required
public static class CreateTodoEndpoint
{
[WolverinePost("/todoitems")]
public static async Task<IResult> Post(
CreateTodo command,
IDocumentSession session) // injected by Wolverine from the IoC container
{
var todo = new Todo { Name = command.Name };
session.Store(todo);
return Results.Created($"/todoitems/{todo.Id}", todo);
}
}

Compare that to what this would look like in MVC Core with a service layer and constructor injection. The Wolverine version is shorter, has no framework coupling in the handler method itself, and every dependency is explicit in the method signature. There’s no hidden state, and the method is trivially unit-testable in isolation.

For reading data, it’s even cleaner:

public static class TodoEndpoints
{
[WolverineGet("/todoitems")]
public static Task<IReadOnlyList<Todo>> Get(IQuerySession session)
=> session.Query<Todo>().ToListAsync();
[WolverineGet("/todoitems/{id}")]
public static Task<Todo?> GetTodo(int id, IQuerySession session, CancellationToken cancellation)
=> session.LoadAsync<Todo>(id, cancellation);
[WolverineDelete("/todoitems/{id}")]
public static void Delete(int id, IDocumentSession session)
=> session.Delete<Todo>(id);
}

No controller. No service interface. No repository abstraction. Just the feature.

No Separate Mediator Needed

One of the most common patterns in .NET vertical slice architecture is using a Mediator library like MediatR to dispatch commands from controllers to handlers. Wolverine makes this unnecessary — it handles both HTTP routing and in-process message dispatch with the same execution pipeline.

If you’re coming from MediatR, the key difference is that there’s no IRequest<T> base type to implement, no IRequestHandler<TRequest, TResponse> to wire up, and no _mediator.Send(command) call to thread through your controllers. The HTTP endpoint is the handler. When you also want to dispatch a message for async processing, you just return it from the method (more on that below).

See our converting from MediatR guide for a detailed side-by-side comparison.

If you’re coming from MVC Core controllers or Minimal API, we have migration guides for both:

The Outbox: The Feature That Changes Everything

Here is where Wolverine.HTTP really pulls ahead. In any event-driven architecture, HTTP endpoints frequently need to do two things atomically: save data to the database and publish a message or event. If you do these as two separate operations and something crashes between them, you’ve lost a message — or worse, written corrupted state.

The standard solution is a transactional outbox: write the message to the same database transaction as the data change, then have a background process deliver it reliably.

With plain IMessageBus in a Minimal API handler, you’re responsible for the outbox mechanics yourself. With Wolverine.HTTP, the outbox is automatic. Any message returned from an endpoint method is enrolled in the same transaction as the handler’s database work.

The simplest pattern uses tuple return values. Wolverine recognizes any message types in the return tuple and routes them through the outbox:

public static class CreateTodoEndpoint
{
[WolverinePost("/todoitems")]
public static (Todo todo, TodoCreated created) Post(
CreateTodo command,
IDocumentSession session)
{
var todo = new Todo { Name = command.Name };
session.Store(todo);
// Both the HTTP response (Todo) and the outbox message (TodoCreated)
// are committed in the same transaction. No message is lost.
return (todo, new TodoCreated(todo.Id));
}
}

The Todo becomes the HTTP response body. The TodoCreated message goes into the outbox and is delivered durably after the transaction commits. The database write and the message write are atomic — no coordinator needed.

If you need to publish multiple messages, use OutgoingMessages:

[WolverinePost("/orders")]
public static (OrderCreated, OutgoingMessages) Post(CreateOrder command, IDocumentSession session)
{
var order = new Order(command);
session.Store(order);
var messages = new OutgoingMessages
{
new OrderConfirmationEmail(order.CusmerId),
new ReserveInventory(order.Items),
new NotifyWarehouse(order.Id)
};
return (new OrderCreated(order.Id), messages);
}

All four database and message operations commit together. This is the kind of correctness that is genuinely difficult to achieve with raw IMessageBus calls in Minimal API, and it comes for free in Wolverine.HTTP.

Middleware: Better Than IEndpointFilter

ASP.NET Core Minimal API introduced IEndpointFilter as its extensibility hook — a way to run logic before and after an endpoint handler. It works, but it has a few rough edges: you write a class that implements an interface with a single InvokeAsync method that receives an EndpointFilterInvocationContext, and you have to dig values out by index or type from the context object. It’s not especially readable, and composing multiple filters is verbose.

Wolverine.HTTP’s middleware model is different. Middleware is just a class with Before and After methods that can take any of the same parameters the endpoint handler can take — including the request body, IoC services, HttpContext, and even values produced by earlier middleware. Wolverine generates the glue code at compile time (via source generation), so there’s no runtime reflection and no boxing.

Here’s a stopwatch middleware that times every request:

public class StopwatchMiddleware
{
private readonly Stopwatch _stopwatch = new();
public void Before() => _stopwatch.Start();
public void Finally(ILogger logger, HttpContext context)
{
_stopwatch.Stop();
logger.LogDebug(
"Request for route {Route} ran in {Duration}ms",
context.Request.Path,
_stopwatch.ElapsedMilliseconds);
}
}

A middleware method can also return IResult to conditionally stop the request. If the returned IResult is WolverineContinue.Result(), processing continues. Anything else — Results.Unauthorized()Results.NotFound()Results.Problem(...) — short-circuits the handler and writes the response immediately:

public class FakeAuthenticationMiddleware
{
public static IResult Before(IAmAuthenticated message)
{
return message.Authenticated
? WolverineContinue.Result() // keep going
: Results.Unauthorized(); // stop here
}
}

This same pattern powers Wolverine’s built-in FluentValidation middleware — every validation failure becomes a ProblemDetails response with no boilerplate in the handler itself.

The IHttpPolicy interface lets you apply middleware conventions across many endpoints at once:

public class RequireApiKeyPolicy : IHttpPolicy
{
public void Apply(IReadOnlyList<HttpChain> chains, GenerationRules rules, IServiceContainer container)
{
foreach (var chain in chains.Where(c => c.Method.Tags.Contains("api")))
{
chain.Middleware.Insert(0, new MethodCall(typeof(ApiKeyMiddleware), nameof(ApiKeyMiddleware.Before)));
}
}
}

Policies are registered during bootstrapping:

app.MapWolverineEndpoints(opts =>
{
opts.AddPolicy<RequireApiKeyPolicy>();
})

ASP.NET Core Middleware: Everything Still Works

Wolverine.HTTP is built on top of ASP.NET Core, not around it. Every piece of standard ASP.NET Core middleware works exactly as you’d expect — Wolverine endpoints are just routes in the middleware pipeline.

Authentication and Authorization work via the standard [Authorize] and [AllowAnonymous] attributes:

public static class OrderEndpoints
{
[WolverineGet("/orders")]
[Authorize]
public static Task<IReadOnlyList<Order>> GetAll(IQuerySession session)
=> session.Query<Order>().ToListAsync();
[WolverinePost("/orders")]
[Authorize(Roles = "admin")]
public static (Order, OrderCreated) Post(CreateOrder command, IDocumentSession session)
{
// ...
}
}

You can also require authorization on a set of routes at bootstrapping time:

app.MapWolverineEndpoints(opts =>
{
opts.ConfigureEndpoints(chain =>
{
chain.Metadata.RequireAuthorization();
});
});

Output caching via [OutputCache]:

[WolverineGet("/products/{id}")]
[OutputCache(Duration = 60)]
public static Task<Product?> Get(int id, IQuerySession session)
=> session.LoadAsync<Product>(id)

Rate limiting via [EnableRateLimiting]:

builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("per-user", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
options.RejectionStatusCode = 429;
});
app.UseRateLimiter();
// In your endpoint class:
[WolverinePost("/api/orders")]
[EnableRateLimiting("per-user")]
public static (Order, OrderCreated) Post(CreateOrder command, IDocumentSession session)
{
// ...
}

The UseRateLimiter() call in the pipeline hooks standard ASP.NET Core rate limiting middleware, and the [EnableRateLimiting] attribute wires up the policy exactly as it does for Minimal API or MVC — no Wolverine-specific configuration required.

OpenAPI / Swagger Support

Wolverine.HTTP integrates with Swashbuckle and the newer Microsoft.AspNetCore.OpenApi package. Endpoints are discovered as standard ASP.NET Core route metadata, so Swagger UI works out of the box. You can use [Tags][ProducesResponseType], and [EndpointSummary] to enrich the generated spec:

[Tags("Orders")]
[WolverinePost("/api/orders")]
[ProducesResponseType<Order>(201)]
[ProducesResponseType(400)]
public static (CreationResponse<Guid>, OrderStarted) Post(CreateOrder command, IDocumentSession session)
{
// ...
}

Summary

Wolverine.HTTP gives you a cleaner foundation for vertical slice architecture in .NET:

  • No Mediator library needed — Wolverine handles both HTTP routing and in-process dispatch in the same pipeline
  • Discoverability built in for vertical slices — which is an advantage over Minimal API + Mediator style “vertical slices”
  • Lower ceremony than MVC controllers — static classes, method injection, no base types
  • Built-in outbox — messages returned from endpoints commit atomically with the database transaction
  • Better middleware than IEndpointFilter — Before/After methods with full dependency injection and IResult for conditional short-circuiting
  • Full ASP.NET Core compatibility — authentication, authorization, rate limiting, output caching, and all other middleware work without changes

If you’re starting a new project or looking to reduce complexity in an existing one, Wolverine.HTTP is worth a close look.

EF Core is Better with Wolverine

TL;DR: Wolverine has a pretty good development and production time story for developers using EF Core and that is constantly being improved.

Wolverine was explicitly restarted 3-4 years back specifically to combine with Marten as a complete end to end solution for Event Sourcing and CQRS with asynchronous messaging support. While that “Critter Stack” strategy has definitely paid off, vastly more .NET developers and systems are using EF Core as their primary persistence mechanism. And since I’d personally like to see Wolverine get much more usage and see JasperFx Software continue to grow, we’ve made a serious effort to improve the development time experience with EF Core and Wolverine.

To get started using EF Core with Wolverine, install this Nuget:

dotnet add package WolverineFx.EntityFrameworkCore

I should say, that’s not expressly necessary, but all of the development time accelerators, middleware, and transactional inbox/outbox integration we’re about to utilize require that library.

Let’s just get started with a simple Wolverine bootstrapping configuration that is going to use a single EF Core DbContext (for now, Wolverine happily supports using multiple DbContext types in a single application) and SQL Server for the Wolverine message persistence we’ll need for transactional outbox support later:

var builder = Host.CreateApplicationBuilder();
var connectionString = builder.Configuration.GetConnectionString("sqlserver")!;
// Register a DbContext or multiple DbContext types as normal
builder.Services.AddDbContext<ItemsDbContext>(
x => x.UseSqlServer(connectionString),
// This is actually a significant performance gain
// for Wolverine's sake
optionsLifetime:ServiceLifetime.Singleton);
// Register Wolverine
builder.UseWolverine(opts =>
{
// You'll need to independently tell Wolverine where and how to
// store messages as part of the transactional inbox/outbox
opts.PersistMessagesWithSqlServer(connectionString);
// Adding EF Core transactional middleware, saga support,
// and EF Core support for Wolverine storage operations
opts.UseEntityFrameworkCoreTransactions();
});
// Rest of your bootstrapping...

With that in place, let’s look at a simple message handler that uses our ItemsDbContext:

public static class CreateItemCommandHandler
{
public static ItemCreated Handle(
// This would be the message
CreateItemCommand command,
// Any other arguments are assumed
// to be service dependencies
ItemsDbContext db)
{
// Create a new Item entity
var item = new Item
{
Name = command.Name
};
// Add the item to the current
// DbContext unit of work
db.Items.Add(item);
// This event being returned
// by the handler will be automatically sent
// out as a "cascading" message
return new ItemCreated
{
Id = item.Id
};
}
}

In the handler above, you’ll notice there’s no synchronous calls at all, and that’s because we’ve turned on Wolverine’s transactional middleware for EF Core that will handle the actual transaction management. You’ll also notice that we’re using Wolverine’s cascading messages syntax to kick out an ItemCreated domain event upon the successful completion of this handler. With the EF Core transactional middleware, that is also handling any integration with Wolverine’s transactional outbox for reliable messaging. Absolutely nothing else for you to do in that handler to enable any of that behavior, and we can shove off some of the typically ugly async/await mechanics into Wolverine itself while keeping our actual application behavior cleaner.

Now let’s go a little farther and utilize some Wolverine optimizations for our EF Core usage and change the service registration up above to this:

// If you're okay with this, this will register the DbContext as normally,
// but make some Wolverine specific optimizations at the same time
builder.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(
x => x.UseSqlServer(connectionString), "wolverine");

That version of the integration optimizes application performance by fine tuning the service lifetimes in a way that improves Wolverine’s internal usage of the DbContext type, and adds direct mappings for Wolverine’s internal inbox and outbox storage. By using a “Wolverine optimized DbContext” like this, Wolverine is able to improve your system’s performance by allowing EF Core to batch the SQL commands for your application code and Wolverine’s transactional outbox storage in a single database round trip — and that’s important because the single most common killer of performance in enterprise applications is database chattiness!

So that’s the bare bones basics, now let’s look at some recent improvements in Wolverine for…

Development Time Usage with EF Core

We’ve invested a lot of time recently in trying to make EF Core easier to work with at development time with Wolverine. Coming from Marten where our database migrations have an “it should just work” model that quietly configures the database to match your application configuration at runtime for quick iteration at development time.

With the Wolverine.EntityFrameworkCore library, you can get that same behavior with EF Core through this option:

builder.UseWolverine(opts =>
{
opts.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(
x => x.UseSqlServer(connectionString));
// Diff the DbContext against the live DB at startup and apply missing DDL.
opts.UseEntityFrameworkCoreWolverineManagedMigrations();
// This will make Wolverine do any necessary database migration
// work happen at application startup
opts.Services.AddResourceSetupOnStartup();
});

To be clear, with this setup, you can change your EF Core mappings, then restart the application or an IHost in testing and your application will automatically detect any database differences from the configuration and quietly apply a patch for you on application startup. This enables a much faster iteration cycle than EF Core Migrations do in my opinion.

The Weasel docs go deeper on the diff engine, opt-outs, and how it handles schemas.

Another feature in Marten that our community utilizes very heavily is the ability to quickly reset the state of a database in tests. I’ve also occasionally used the Respawn library for the same kind of ability when developing closer to the metal of a relational database to do the same. In a recent version of Wolverine, we’ve added similar abilities to our EF Core support including a version of Marten’s IInitialData concept to help you reset data in tests:

public class SeedItems : IInitialData<ItemsDbContext>
{
public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
{
context.Items.Add(new Item { Name = "Seed" });
await context.SaveChangesAsync(cancellation);
}
}
builder.Services.AddInitialData<ItemsDbContext, SeedItems>();

And to see that in usage:

[Fact]
public async Task ordering_flow()
{
await _host.ResetAllDataAsync<ItemsDbContext>();
// arrange ... act ... assert
}

The ResetAllDataAsync<T>() method will look through a DbContext object to see all the tables it maps to, and delete all the data in those tables. It does take into account foreign key relationships to order its operations. After the data is wiped out, each IInitialData<T> registered in your system will be applied to lay down baseline data.

While this feature will surely have to be enhanced if many people start using it, this is already helping us make the Wolverine internal EF Core testing a lot more reliable and easier to use.

Declarative Persistence with EF Core

The next usage is special to Wolverine. A lot of times in simpler HTTP endpoints or command handlers you simply need to load an entity by its identity or primary key. And frequently, you’ll need to apply some repetitive validation that the entity exists in the first place. For that common need, Wolverine has its declarative persistence helpers like the [Entity] attribute shown below that can automatically load an entity through EF Core by its identity on the incoming command type implied by some naming conventions like this sample below:

The mapping of the identity can be explicitly mapped as well of course, and the pre-generated code always reveals Wolverine’s behavior around handlers or HTTP endpoint methods.

public class ItemsDbContext : DbContext
{
public DbSet<BacklogItem> BacklogItems { get; set; }
public DbSet<Sprint> Sprints { get; set; }
}
public record CommitToSprint(Guid BacklogItemId, Guid SprintId);
public static class CommitToSprintHandler
{
public static object[] Handle(
CommitToSprint command,
// There's a naming convention here about how
// Wolverine "knows" the id for the BacklogItem
// from the incoming command
[Entity(Required = true)] BacklogItem item,
[Entity(Required = true)] Sprint sprint
)
{
return item.CommitTo(sprint);
}
}

In the code above, Wolverine “knows” that the ItemsDbContext persists both the BacklogItems and Sprint entities, so it’s generating code around your handler to load these entities through ItemsDbContext. We can also tell Wolverine to automatically stop handling or in HTTP usage return a 400 ProblemDetails response if either of the requested entities are missing in the database. This helps keep Wolverine handler or HTTP endpoint code simpler by eliminating asynchronous code and letting you write more and more business or workflow logic in pure functions that are easy to test.

In the code above, the EF Core transactional middleware is calling ItemsDbContext.SaveChangesAsync() for you, and the automatic EF Core change tracking will catch the change to the BacklogItem.

And now, I think this is cool, Wolverine has its own new mechanism to batch up the two queries above through a custom EF Core futures query mechanism so that the handler above can fetch both the BacklogItem and the Sprint entity in one database round trip.

But wait, there’s more!

At the risk of making this blog post way too long, here’s more ways that Wolverine can make EF Core usage more successful: