It’s apparently time to play another game of .NET developers feeling consternation because of an OSS project’s attempt to be more sustainable. JasperFx Software, the company I founded around the “Critter Stack” tools is committed to an “Open Core” model. What that means for us is that:
The main libraries and tools like Marten and Wolverine will remain MIT licensed, i.e. free and open
JasperFx also has its commercial AI Skills offerings for the Critter Stack and now our commercial CritterWatch tool. With another set of commercial tools related to AI assisted development and Event Modeling coming soon (those will be part of the same license as CritterWatch)
At this point I think that JasperFx Software has already established that we have a viable business model and that we’re going to be able to sustain our “Open Core” model going forward.
We still get occasional friction from potential clients and users that we’ll do the same OSS “rug pull” that some other projects did by adopting a commercial license on their newer versions even though I’ve said “Open Core” in public a half a thousand times. I do not particularly enjoy that level of cynicism about OSS that frequently crops up in the ,NET community.
What I would say to folks out there is that tools like Marten and Wolverine are just not viable as side projects. A huge amount of our functionality in Marten for scalability only existed after I founded JasperFx and started working directly with clients every day. Wolverine’s leadership election which undergirds a lot of our advanced blue/green deployment support and scalability would not have been possible to build and constantly curate without me being full time on the Critter Stack. I would ask our users to have some awareness for how much time it takes to evolve and maintain their OSS tools.
Yes, the existence of AI tools makes it tempting to think you can just vibe code replacements for your 3rd party dependencies over a rainy weekend, but you have to also understand how much hardening widely used OSS tools get from being beaten up by users and having to adapt to a world of technical irregularities like database outages, Rabbit MQ quietly dropping connections, database overloading, network hiccups, database administrators unexpectedly sending a kill signal to a PostgreSQL database that turns out to create gaps in sequences (and wasn’t that one fun), and not to mention all the crazy edge cases we’ve had to face from Kubernetes doing Kubernetes things.
To sum this all up, you can’t just vibe code replacements for quite a bit of this, these kinds of tools achieve deep quality through a lot of usage, feedback, and adaptation over time — and all of that takes a lot of time and a long attention span.
Hell, I’ve personally had to make several improvements to code subsystems in Marten and Wolverine in the last month that I thought were “done” and as stable as they could possibly be because new users in new circumstances proved otherwise
And just because I might get asked about this, my friend Ian Cooper wrote about this too, but maybe coming from a different perspective as an OSS maintainer. I partially agree with some of that and I’ll respectfully disagree with other parts and just leave it at that.
I’m obviously sympathetic to the Polly maintainers, and based on this exchange with one of the creators of the OSMF, my initial inclination is to pay the OSMF fee from JasperFx Software because of our commercialization of Marten, Polecat, and Fisherthrough support plans (those projects are still MIT licensed folks!) and the transitive dependency that CritterWatch has on Polly through those other libraries:
Like I said, I’m sympathetic to the Polly maintainers, and we’ll try to be above board with them here. But, if there’s even the slightest bit of hesitation from our current or potential customers about the Polly license, we’ll replace our relatively small usage of Polly with something new in our foundational JasperFx library and remove Polly entirely. I’m not enthusiastic about doing that because the Polly.Core dependency is in our public API and pulling that out would require us to either do a major version release or cheat on SemVer rules — which we really hate to do without very good reason.
I after all have a fiduciary responsibility to my “shareholders” to make JasperFx a sustainable financial success.
Wolverine has its own resiliency features, so Polly isn’t a concern there at least. Our document database and event store applications do use Polly for resiliency against transient errors though, and that’s what would need to change. I’d guess that most of our users don’t even realize that’s there, so maybe the switchover won’t be that big a deal if we decide to go that way.
Wolverine 6.30 is out. It is a big release with one clear headline: a new endpoint mode, EndpointMode.NativeAck, plus the supporting cast it needed to be trustworthy — lease renewal, an additional idempotency guard, partitioning, and a five-node chaos harness to address a reported issue. This release came with feature requests from JasperFx Software clients, and just to remind folks, being a JasperFx support client means that like Homer below, you have the secret handshake to get your Critter Stack needs and requests to the top of our priority list:
There is also a crop of transport fixes, two long-standing multi-tenancy gaps closed, and a couple of new HTTP and event sourcing conveniences. And inevitably, we also smuggled in some support for our soon forthcoming “Event Modeling” visualization support across the Critter Stack and more for CritterWatch visualization.
You’ve obviously detected that this post was largely drafted by AI of course, but the other side effect of AI usage is that we actually find many more little issues as we work and this 6.30 release includes quite a few bug fixes that spilled out of AI assisted efforts to improve our continuous integration safety nets and an embarrassing number of combinatorial bugs in the existing code that were discovered during the “Native Acks” effort.
Let’s take the tour.
🚀 The Fourth Endpoint Mode
This mode came about from interactions with a JasperFx client who needed to optimize the throughput of listening endpoints but still needed an “at least once” delivery guarantee. The existing Durable mode gives you the delivery guarantee and parallelization, but the extra database overhead was problematic for a flood of small messages that needed to be handled quickly. The Buffered mode gives you the maximal throughput, but a lesser delivery guarantee and is mostly suitable for “fire and forget” messages. Our Inline mode gives you the “at least once” guarantee without the database overhead of Durable, but limited parallelism and throughput. Our client used a different configuration change in Wolverine that alleviated their message pressure, but this new mode was going to be the next thing we tried for them if they needed it.
Wolverine has had three listener modes for a long time, and if you have configured a busy endpoint you have probably felt the shape of the tradeoff. You want parallelism. You want group-ordered processing. You do not want to lose messages when a pod gets rescheduled. And you would very much like to not stand up a database to get all three.
Until 6.30 you could have any two.
Broker ack timing
Loss window
Parallelism
Group partitioning
DB cost
Inline
after handler success
none
ListenerCount only
none
none
NativeAck
after handler success
none
MaximumParallelMessages
✔️
none
BufferedInMemory
at receipt, before the handler
crash loses buffered messages
MaximumParallelMessages
✔️
none
Durable
after the inbox insert
none
MaximumParallelMessages
✔️
inbox insert + mark-handled
NativeAck fills the empty cell: Buffered’s throughput and partitioning with Inline’s no-loss guarantee, and no database involvement.
The mechanism is exactly what the name says. The broker delivery is held unacknowledged while the message flows through an in-memory, optionally group-partitioned execution block, and is settled natively from the completion continuation — acked on handler success, nacked or dead-lettered on terminal failure. Nothing is written to a database. Nothing is acknowledged ahead of its handler.
Three consequences follow, and all three are the point rather than side effects:
Back pressure is the broker’s prefetch window, not BufferingLimits. The broker stops delivering once its unacked ceiling is reached, so there is no BackPressureAgent at all.
A dying node loses nothing. Anything queued but not yet completed is still unacknowledged, so closing the channel or crashing hands every one of those deliveries back to the broker.
Shutdown redelivers, but redelivery is not duplicate execution. More on that below, because it is the part everyone gets wrong — including, until this release, our own documentation.
The guarantee, stated exactly
I want to be precise here rather than reassuring, because this is the sort of claim people build on:
Protection against intra-group concurrency is the hard guarantee. Strict sequential processing in original delivery order is not.
The sequential lane per group slot structurally guarantees that no two messages sharing a group id execute concurrently on the owning node. Original-order processing is not guaranteed under failure, requeue, or broker redelivery — a failed or redelivered message re-enters its lane later, never concurrently. That is the honest contract for native-ack retry semantics on every broker. If you need strict order under failure, keep the durable inbox.
📊 What a Redeployment Actually Costs
This was from the community.
Earlier versions of the docs said a rolling deploy produced “duplicate deliveries bounded by the prefetch depth.” That was honest reasoning. It was also never measured — and when we finally measured it, the figure turned out to be the wrong one to quote. It is the bound on redeliveries, and it overstates handler-visible duplicates by more than an order of magnitude.
So #3713 built the harness: a five node cluster, group-partitioned across five RabbitMQ slots, under a sustained flood deep enough that the broker was holding a full unacked window at every disruption, reading the broker’s own messages_unacknowledged at that instant. Ranges are across four consecutive runs.
Scenario
Unacked at disruption
Duplicate executions
Rate
Steady state, no disruption
0
0
0%
Rolling deploy, all 5 nodes drained and replaced
180
0
0%
One node killed outright mid-flood
180
3–4
~0.1%
Two hard kills plus two rolling replacements
180
7–9
~0.05%
Two findings matter more than the headline percentage:
A graceful rolling deploy costs zero duplicate executions. Draining settles the handlers that were already running, so nothing runs twice. The prefetch window is redelivered — those messages simply had not executed yet, so they run for the first time, and your handler cannot tell them from any other first delivery.
A hard kill costs about one duplicate per busy lane, not one per unacked message. Only handlers that were mid-flight when the connection died can run twice, and that population is the partition slot count, not the prefetch depth. Three or four per killed node against an unacked window of 180 — a factor of roughly 45, and it barely moved between runs.
Handlers still need to be idempotent; at-least-once is the contract and 0.1% of a flood is not a small number of messages. But size that work against in-flight lanes rather than against prefetch. The full writeup is in What a redeployment actually costs.
🔌 Transport Support: Opt-In and Default-Closed
Wolverine actually supports more messaging technology options than any other messaging tool in .NET, and man, that’s not always a blessing.
A transport must settle each delivery individually and tolerate settling out of order, because the execution block completes messages in handler-completion order rather than delivery order. Not every broker can express that, so support is opt-in and default-closed — calling ProcessInParallelWithNativeAcks() on a transport that has not claimed the mode throws at configuration time rather than degrading silently.
Seven transports qualified and shipped in this release:
Kafka cannot and is out of scope. A cumulative offset commit has no way to express a gap. This is not an oversight to be fixed later — it is what the storage model means.
Two transports also refuse the mode for particular endpoints whose own settings contradict it, again at bootstrap rather than at runtime. Pulsar’s AcknowledgeCumulative() reintroduces exactly the gap-less commit problem that disqualifies Kafka. And an SQS FIFO queue exists to guarantee ordering within a message group, which native-ack lanes deliberately do not preserve — and which partitioning by group id does not rescue, because SQS blocks a message group behind its own in-flight head. Both combinations are rejected by name.
Brokers that put a clock on an unsettled delivery
This is about protecting you from problems that arise from long executing messages when using any kind of native broker acknowledgement. This isn’t an issue at all in our Durable mode, but becomes an issue using any other endpoint mode. We had to get more serious about this with the advent or our new NativeAck mode in 6.30.
On SQS, Azure Service Bus, JetStream and Pub/Sub, an unsettled delivery is on a timer — visibility timeout, lock duration, AckWait. Wolverine renews that clock for every delivery still sitting in a lane, for as long as it sits there (#4048).
This is unconditional under this mode and not something you opt into. Lane queue time is unbounded by design, so an un-renewed native-ack endpoint would be a duplicate-delivery generator by construction rather than merely at risk under a slow handler. A transport that declares such a clock but does not implement renewal is refused at startup.
SQS is a good illustration of how far the per-transport tuning goes — MaxNumberOfMessages defaults down under this mode:
Instead of the usual 10, a native-ack endpoint receives twice the number of lanes that can be busy at once, clamped to the SQS maximum. Under every other mode the surplus messages in a batch are deleted before their handlers run, so a full batch is free and saves API calls. Here each one sits in a lane holding an unsettled delivery to renew and to redeliver on a crash. Setting the property explicitly always wins.
🛡️ The In-Memory Idempotency Guard
The durable inbox deduplicates on the primary key of its incoming table. NativeAck has no such table and is at-least-once by design, so 6.30 adds the non-durable analogue (#3710): an opt-in, bounded, in-memory set of the message ids this process has already handled on this endpoint.
opts.ListenToRabbitQueue("webhooks")
.ProcessInParallelWithNativeAcks()
.PartitionProcessingByGroupId(PartitionSlots.Five)
// Opt in. Both arguments are optional; these are the defaults.
.WithInMemoryIdempotency(window: 5.Minutes(), maxTracked: 100_000);
Read the limits before you rely on it. The guard is per process and in memory, and three consequences follow — none of which is a bug:
A restart forgets everything. The very deploy that produces the redelivery burst also empties the guard on the node that starts up. It protects a running process against a redelivery it saw itself.
A second node never knew. With competing consumers, a redelivery can land on a different node than the original.
Eviction is generational, not exact. An id is remembered for at least half the window and at most the whole window — less if a flood of unique ids hits maxTracked first.
The promise is at-least-once delivery with best-effort deduplication, not exactly-once. If you need hard deduplication across restarts and nodes, that is what the durable inbox is for. Memory is bounded by construction — two rotating hash sets, no per-entry timestamps, no LRU bookkeeping, roughly single-digit megabytes at the 100,000 default. Details in In-Memory Idempotency Guard.
⚠️ The Fix You Might Actually Be Affected By
Buried in the supporting work is #3712, and it is worth pulling out because it may already apply to you.
Wolverine now validates listener configuration coherence at bootstrap instead of silently ignoring settings that the chosen mode cannot honor. In writing those checks we discovered that RabbitMQ queues default to Inline — and Inline supports neither parallelism nor group partitioning.
Which means: if you built a sharded topology with PublishToShardedRabbitQueues() and did not explicitly add ConfigureListening(x => x.BufferedInMemory()), your partitioning was silently doing nothing. The configuration was accepted and ignored. Now it is rejected with an error that says so.
A companion fix (#4022) catches the related case where a local queue configured with ProcessInline() was accepted at configuration time and then threw a message-less NotSupportedException from deep inside agent startup.
The partitioning guide covers the topology options, including the NativeAck variant added in #3709 — partitioned clustering without the durable inbox, across all ten transports that support the sharded topology.
🏢 Multi-Tenancy: Conjoined EF Core Under a Marten-Owned Store
#4044 is a small feature with a genuinely sneaky root cause.
If Marten owns your message storage through IntegrateWithWolverine(), Wolverine’s message store is built from Marten’s NpgsqlDataSource and never sees a connection string. And NpgsqlDataSource.ConnectionStringdeliberately omits the password — so there is no string the conjoined DbContext could be configured with that the database would actually accept. The obvious fix (pull the connection string off the data source) produces an authentication failure at first use.
So there is a new DbDataSource overload that carries the credentials through intact:
var builder = Host.CreateApplicationBuilder();
var configuration = builder.Configuration;
builder.UseWolverine(opts =>
{
// Marten owns the message storage here, so Wolverine's message store is built
// from Marten's NpgsqlDataSource rather than from a connection string
opts.Services.AddMarten(m =>
{
m.Connection(configuration.GetConnectionString("main")!);
}).IntegrateWithWolverine();
// ...which means the conjoined DbContext has to be configured from that same
// DbDataSource. NpgsqlDataSource.ConnectionString deliberately omits the password,
// so the connection string overload cannot authenticate in this setup
opts.Services.AddDbContextWithWolverineManagedConjoinedTenancy<ConjoinedItemsDbContext>(
(builder, dataSource) =>
{
builder.UseNpgsql((NpgsqlDataSource)dataSource);
}, AutoCreate.CreateOrUpdate);
});
Registering the connection string overload in this setup now fails fast at startup with an error naming this overload, rather than surfacing later as an authentication failure.
A second defect on the same path is fixed too: IntegrateWithWolverine() never registered the tenant partitioning provider, so PartitionPerTenant() failed outright. Both are covered in With Marten Owning the Message Store.
🌐 HTTP and Event Sourcing
Marten concurrency conflicts as 409
This was a JasperFx client request.
An endpoint using [WriteAggregate] can lose an optimistic concurrency race — two clients posting to the same aggregate at once. Without a handler that escapes as an unhandled 500, even though nothing went wrong: optimistic concurrency did its job. 409 Conflict is the honest status.
6.30 ships a documented, tested recipe (#3764). The OnException middleware convention is all you need — but there are two exception types, and the second is easy to miss:
public static class MartenConcurrencyExceptionMiddleware
{
// Marten's optimistic concurrency failures -- EventStreamUnexpectedMaxEventIdException from
// the event store, and document level concurrency violations -- all derive from
// JasperFx.ConcurrencyException, so one handler covers them
public static ProblemDetails OnException(ConcurrencyException ex)
{
return new ProblemDetails
{
Status = 409,
Title = "Conflict",
Detail = ex.Message
};
}
// StreamLockedException does NOT derive from ConcurrencyException -- it is a MartenException --
// so the FetchForExclusiveWriting path needs its own handler. Catching only ConcurrencyException
// silently misses it
public static ProblemDetails OnException(StreamLockedException ex)
{
return new ProblemDetails
{
Status = 409,
Title = "Conflict",
Detail = ex.Message
};
}
}
That second handler is the whole reason this is a documented recipe rather than a one-liner in a FAQ. Marten.Exceptions.StreamLockedException — what FetchForExclusiveWriting throws on a contended stream — derives from MartenException, notConcurrencyException. A recipe that catches only the latter silently leaves the exclusive locking path returning 500s. See Recipe: Marten Concurrency Conflicts as 409.
[StreamState] and [StreamEvents]
New parameter attributes for handlers whose read is the raw stream rather than the folded aggregate — timeline views, audit endpoints, anything [ReadModel] cannot express (#3627).
They are store-agnostic across Marten, Polecat and Fisher, and on Marten both fetches are batched into a single round trip.
Event Model slices per route
HttpChainDescriptor and GrpcRpcDescriptor now carry the slice the route is, so a consumer walking endpoint by endpoint sees it next to the route rather than only through the assembled model (#4000).
🐛 Transport Fixes
The AI tools are sometimes good about finding combinatorial or lurking bugs while doing other work. It’s annoying, but I always ask Claude to file issues for any unrelated problems it finds while doing any work — then immediately turn around and try to address them.
A good crop this time, and several of them share a theme worth naming: silent failure. Each of these was doing the wrong thing without reporting anything.
Pulsar
Requeue, scheduled retry and dead-letter routing were simply unimplemented (#3797).
A global native-resiliency failure rule was swallowing every user-configured error policy in the entire application (#4079, also reported as #4075). A plain local queue in a UsePulsar() host got exactly one attempt. If you use Pulsar and have ever wondered why an OnException policy seemed inert, this is why.
Hot-tail listeners silently dropped deferred messages in every mode (#4060).
GCP Pub/Sub
Listener shutdown could hang waiting on in-flight callbacks (#4065).
Exhausting MaxTotalAckExtension silently delivered a concurrent duplicate rather than reporting anything (#4066).
PubsubTopicOptions.OrderBy gained a configuration surface (#4087, docs).
Redis
DeleteStreamEntryOnAck silently never acked on Redis < 8.2, where XACKDEL is unsupported (#4058).
Ack reliability
A shared ack-attempt budget across stacked retry blocks, plus terminal-failure classification for Azure Service Bus and SQS so a permanent settle failure stops rather than burning the whole budget (#4012 — partially delivered; the remaining items are tracked there for 6.31).
⬆️ Upgrading
This release is additive.EndpointMode.NativeAck is opt-in per endpoint and default-closed per transport, and MaximumBrokerRedeliveries defaults to off.
Requires JasperFx 2.55.0.
The one thing to look at before upgrading is #3712, described above — the new listener coherence validation will reject at startup a configuration it previously accepted and ignored. That is a change in behavior, but the configuration it rejects was never doing what it looked like it was doing.
What’s Next
NativeAck is probably overdue and another option for Wolverine usage. We’ll be releasing a new version of our curated AI Skills this week that builds in decision making about endpoint usage in Wolverine applications.
I’m mostly out this week on a family vacation and this is the most ambitious blog post I’m going to be up to until I’m back:)
Wolverine.HTTP (in 6.17.0 last week) added support for the HTTP QUERY method (RFC 10008) through a single new [WolverineQuery] attribute. In this post let’s walk through what it is, why you’d reach for it, and how it behaves inside Wolverine’s middleware model.
To me, the new QUERY verb seems pretty logical and probably something that could have been added a long time ago.
What is the QUERY method, and why should I care?
If you’ve ever built a search endpoint with oodles of optional search criteria, you’ve probably felt the tension. Search criteria want to be a body — nested filters, arrays of facets, date ranges, a big structured DTO. But “read-only, cacheable, idempotent” wants to be a GET. And GET famously does not carry a request body in any way you can rely on.
Plenty of other folks cram everything into an ever-growing query string (and start bumping into URL length limits and gnarly encoding), or you POST your search — quietly giving up the semantic promise that this call is safe and idempotent, and confusing every proxy, cache, and reader of your API along the way.
QUERY is the method that resolves that tension. It is safe and idempotent — like GET — but it is allowed to carry a request body — like POST. It’s purpose-built for exactly the search/query endpoints whose criteria are too large or too structured to encode in a URL.
The API surface: one attribute
The entire feature is a single attribute, [WolverineQuery], that sits right alongside the verb attributes you already know — [WolverineGet], [WolverinePost], [WolverinePut], and friends. There’s no new fluent method to learn and no configuration to flip on.
That’s it. The SearchRequest binds from the request body exactly as it would for a POST endpoint — same JSON deserialization, same everything. The only difference on the wire is the HTTP method, which flows straight through to ASP.NET Core as route metadata.
Middleware rules are dependency-based, not verb-based
I think it’s a little bit weird to be using messaging from within a GET or QUERY endpoints because of “query-command separation,” but there are exception cases and that means Wolverine has to support this.
This is the part I want to be very precise about, because it’s the most common wrong assumption.
You might expect a “safe” verb like QUERY to be automatically exempt from transactional or outbox middleware. It is not — and that’s by design. Wolverine has never keyed its middleware decisions off the HTTP verb; it keys them off the dependencies your handler actually takes:
Outbox middleware is applied when your handler depends on IMessageBus / IMessageContext.
Transactional middleware is applied when your handler takes a persistence dependency like Marten’s IDocumentSession or an EF Core DbContext.
So the /search endpoint above stays free of transactional middleware because it takes no persistence dependency — not because it’s a QUERY. The rule cuts both ways. Take an IDocumentSession on a QUERY endpoint under AutoApplyTransactions() and you’ll get transactional middleware wrapped around it, exactly as you would on a POST:
usingMarten;
usingWolverine.Http;
// Taking an IDocumentSession attracts AutoApplyTransactions on a QUERY endpoint
// exactly as it would on a POST — there is no verb-based exemption.
The practical guidance: for a QUERY endpoint that reads the database and should stay non-transactional, take Marten’s read-only IQuerySession instead of an IDocumentSession — or, on EF Core, decorate the endpoint with [NonTransactional].
⚠️ One caveat: OpenAPI 3.1
QUERY only became a first-class operation in OpenAPI 3.2. The OpenAPI 3.1 document produced by the Swashbuckle / Microsoft.OpenApi stack can’t represent it, and naively handing it a QUERY operation throws and breaks document generation for your whole application.
So — matching ASP.NET Core’s own behavior on OpenAPI 3.1 — Wolverine gracefully omits QUERY endpoints from the generated OpenAPI document rather than break generation for everything else. Your QUERY endpoints are fully routable and functional; they’re simply not described in the OpenAPI 3.1 output. First-class OpenAPI docs can follow once the underlying stack emits 3.2.
Testing QUERY endpoints (and an honest Alba caveat)
There is a new major release underway for Alba and I fully expect QUERY support to be part of that. While Alba doensn’t yet have first class QUERY support, you can temporarily use the test server’s HttpClient:
Host.GetTestServer() comes from the same Alba/TestServer plumbing your other tests already use — you’re just hand-building the one request whose verb Alba can’t spell for you.
You’ll often also want to assert on routing and middleware wiring directly, without an HTTP round trip. Because the interesting behaviors here are about metadata and middleware, those checks read cleanly against the endpoint graph:
// The QUERY endpoint is gracefully omitted, not thrown on.
doc.Paths.ContainsKey("/search").ShouldBeFalse();
The bottom line
QUERY support in Wolverine.HTTP is deliberately small: one attribute, no new configuration surface, and it reuses the same body binding and the same dependency-based middleware rules you already rely on for every other verb. There’s a tiny bit of logic in Wolverine’s internals that make it a little different than GET of course, but as a user of Wolverine.HTTP all you really care about is that one single [WolverineQuery] attribute. If you’ve been POST-ing your searches and feeling slightly dirty about it, [WolverineQuery] is the verb you’ve been wanting to not feel dirty about how you’re coding.
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!
EnableAutoOffsetStore=false + StoreOffset per completed message; Kafka’s background committer flushes on AutoCommitIntervalMs
The new default — idiomatic Kafka throughput
PerMessage
Synchronous commit of the message’s own offset
Strict at-least-once on low-volume topics
BatchCount(n)
Commit watermark every N messages
High-volume topics where you want a tunable lever
BatchInterval(t)
Commit watermark every T elapsed
Bursty 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.
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.
The second concurrency lever. Within a single partition assigned to your node, process messages with different keys concurrently while preserving strict ordering per key:
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.
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:
# 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.
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.
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.
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:
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:
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:
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.
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.
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).
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.
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.
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!
returnawaitapp.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.
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):
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}"
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.
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:
// 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 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!
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-friendly, faster 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.
Database-primitive foundation on JasperFx 2.0; the consolidation home for IStorageOperation, OperationRole, BulkInsertMode, and the SQL Server advisory lock (dedupe pillar).
Postgres / SQL Server / Sqlite / MySql / Oracle / EF Core providers, all 9.0.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.
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.
⚠️ 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.Newtonsoft; IForwardsTo<T> discovery is now explicit.
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. 🚀