Ten Days of Critter Stack Releases

Most of the big improvements in this blog post came from JasperFx client engagements. Reach out any time to sales@jasperfx.net and we’ll happily chat with you about how we can help your shop succeed with whatever technical challenges you might have!

Let me put a stake into the ground here and say that you simply cannot (yet) vibe code yourself an equivalent of the “Critter Stack” because so much of our deep quality is the direct result of adapting to real life usages and problems over years of constant usage and continuous improvement. In the past we’ve worked with JasperFx clients or the community on issues caused by database maintenance shutdowns, way too many Kubernetes related things as pods spin up and down, and all kinds of unexpected real like episodes that have all directly led to real improvements in the tools. We’ve faced issues from sudden system surges due to unexpectedly big system inputs like import files. We’ve had to endlessly harden MartenPolecat, and Wolverine against infrastructure hiccups and random disconnects our users have faced in real life usage. You simply cannot get that level of built in quality by “rolling your own” over a long weekend.

The last ten days have been one of the heaviest release stretches in the history of the critter stack. Every repository in the family shipped, and the three headline stories are all about the same thing: what happens when a system is under real load, on real hardware, at real scale, and something goes wrong.

Here’s what moved:

PackageWhere we were on July 17Where we are today
Wolverine6.20.06.23.1
Marten9.16.09.20.0
Polecat5.1.05.7.0
Weasel9.16.49.19.0
JasperFx / JasperFx.Events2.28.02.36.2

That’s 4 Wolverine releases, 5 Marten releases, 6 Polecat releases, 3 Weasel releases, and 9 JasperFx releases in ten days. Below are the three things I most want you to know about, followed by everything else.


1. Marten’s Async Daemon Tells You Why It Stopped

This work was inspired by helping a JasperFx client troubleshoot issues last week

The single most frustrating failure mode in an event-sourced system is the silent one: a projection stops advancing, and the only evidence is a chart that flatlines. The daemon knew perfectly well what happened — it just had nowhere to put it.

Classified shard failures

This will be exposed through the CritterWatch user interface and MCP tools in the 1.0 RC release

The async daemon now classifies why a shard is paused or stopped and persists it, so a monitoring tool polling the database sees exactly what an in-process observer sees:

var states = await store.Storage.Database.AllProjectionProgress();
foreach (var state in states.Where(x => x.Failure != null))
{
    // ApplyEvent, EventSerialization, UnknownEventType, ProgressionOutOfOrder, or Other
    Console.WriteLine($"{state.ShardName}: {state.Failure!.Category} on {state.Failure.Event}");
}

ShardFailure is a plain, serializable record — category, the failing event’s sequence and type, the exception message and detail — deliberately not an Exception, so it survives the trip to a monitoring UI. Extended progression tracking grew four new columns to carry it (failure_categoryfailure_event_sequencefailure_event_typefailure_event_tenant_id), and failure_category stores the enum name rather than its ordinal so reordering the enum in a future release can never silently re-label rows an older deployment wrote.

The distinction between categories is the whole point. EventSerialization means a stored body won’t deserialize — you need a serializer or data fix. UnknownEventType means an event alias resolves to no known .NET type in this deployment — usually a missing registration or a rollback past the point where that event type was introduced. Those are different problems with different fixes, and the daemon now says which one you have. A shard that recovers clears its failure columns on the next successful start, so a supervisor built on this doesn’t keep alerting on something you fixed an hour ago.

Graceful shutdown and the drain timeout

This improvement will also help a great deal when Marten/Wolverine decides to rebalance work across a cluster of nodes as you might be scaling up or down

When a shard is stopped, the daemon doesn’t simply cancel it — it drains: lets the in-flight page of events finish applying, then flushes the progression row so the next start picks up exactly where this one left off. If that drain gets cut short, the shard restarts against a stale progression row and throws ProgressionProgressOutOfOrderException. That’s now bounded, per shard, and configurable:

// The default is 5 seconds
opts.Projections.StopAndDrainTimeout = 30.Seconds();

The motivating case is a database-per-tenant deployment with thousands of (projection × tenant) shards all trying to drain inside a Kubernetes termination grace window. A per-shard bound only helps if the process lives long enough to spend it, so pair a raised StopAndDrainTimeout with HostOptions.ShutdownTimeout and the pod’s terminationGracePeriodSeconds. Full write-up in Graceful Shutdown and the Drain Timeout.

The high-water health check became more effective

This part really only impacts users using the new per-tenant event store partitioning — but that’s going to be one of our answers for extreme scalability needs*

The high-water health check previously probed every database in a multi-tenanted store on every probe — a connection fan-out that gets ugly at a few hundred shard databases, and outright wrong when daemon distribution is spread across nodes and a node ends up probing databases it doesn’t host:

Services.AddHealthChecks().AddMartenHighWaterHealthCheck(
    staleThreshold: TimeSpan.FromSeconds(30),

    // Only probe the databases this node actually owns
    databaseFilter: db => LocallyOwnedDatabaseIdentifiers.Contains(db.Identifier),

    // Assert even under DaemonMode.ExternallyManaged (i.e. Wolverine-managed distribution)
    includeExternallyManaged: true);

Under UseTenantPartitionedEvents the check now evaluates the per-tenant HighWaterMark:<tenant> progression rows too, using the liveness heartbeat signal (the sequence-gap fallback is store-global and can’t be applied per tenant).

More reliable integration testing against asynchronous projections

This gobbledygook should really translate to “automated testing against asynchronous projections just got faster and more reliable”

Two long-tail concurrency bugs went with it: the high-water agent’s lost-wakeup race was closed rather than narrowed (jasperfx#572), and a WaitForShardState race against an already-published state was fixed (jasperfx#568). On the PostgreSQL side, Marten 9.20 added an allocation fence so an idle advisory-lock session can no longer hold gap skips open forever (marten#4953) — a fix that had a direct Wolverine counterpart, more on that below.


2. Wolverine’s Agent Assignment Got Its Hard Lesson

We’ve had confirmation from a JasperFx client that these changes made a dramatic improvement in how Wolverine behaved in a hugely complicated system, but I expect this to be an improvement for plenty of other users as well

This one started as an incident report (from a pretty extremely complicated usage well beyond what most people will ever experience) and turned into a nine-part fix.

The setup: a Wolverine cluster distributing thousands of Marten subscription and projection agents across nodes, under rebuild load. The symptom: Wolverine basically panicked and continuously tried to start, stop, and re-assign agents to diffent nodes because it couldn’t tell if anything was healthy or not. Nodes were being ejected while very much alive, resurrecting under new identities, and the leader re-sent the same assignments forever while nothing actually started. The database was taking roughly 96,000 telemetry inserts an hour from the churn alone — into the very database the rebuild was already saturating.

Nine separate defects fed that livelock. All of them are fixed:

The heartbeat was starved by its own work. The node heartbeat was written as the first step of the health-check loop, which also drained agent commands serially. A leader spending sixty seconds burning reply timeouts while starting thousands of subscription agents therefore delayed its own next heartbeat past StaleNodeTimeout — looking dead to its peers precisely when it was doing the most work. The heartbeat now runs on its own independent loop, so no amount of slow command work can starve it.

Resurrection restored a skeleton, not a node. When a peer deleted a still-live node’s row, every store blindly re-inserted a skeleton: fresh node number, empty capabilities, no assignments. A capability-less node is a candidate for nothing, so a 3-node cluster silently shrank to 2 for event-subscription work. MarkHealthCheckAsync now reports existence without ever inserting, and the controller re-registers with the node’s real number, its captured capabilities, and its agent assignment rows — implemented across all nine persistence stores (PostgreSQL, SQL Server, MySQL, SQLite, Oracle, RavenDB, CosmosDB, and the two in-memory/multi-tenanted wrappers).

A 2,100-agent batch answered by a 30-second timeout. Assignments went out as one mega-batch and were started serially on the receiving node — where each Marten subscription-agent start is a daemon shard spin-up with database round trips. That is hours of serial work answered by a 30-second reply window: the reply can never arrive, so the leader records nothing and re-sends the whole ~300KB batch next cycle. Batches are now chunked, started with bounded parallelism, and the reply timeout scales with chunk size.

The leader re-emitted assignments it had already sent. A leader-side pending-assignment ledger now suppresses duplicate AssignAgent commands for work already in flight, with a TTL so a start that never took still gets re-driven. That also removed the matching telemetry-write flood.

Ejection had no hysteresis. A single stale snapshot read — replica lag, a GC pause, an aggressive StaleNodeTimeout — was enough to delete a live node’s row, its in-flight envelope ownership, and its assignments. The irreversible delete now requires N consecutive stale observations, and a follower may never delete the leader’s row; only a node actually holding the leadership lock can do that.

Shutdown couldn’t finish inside a grace window. The node-shutdown drain stopped every local agent serially, so a node with thousands of shards got SIGKILLed mid-drain, abandoning unflushed daemon progression. It now fans out with bounded parallelism, passes CancellationToken.None deliberately (this is the shutdown path — a cancelled drain leaves agents half-stopped), and contains a wedged agent so it can’t abort its peers’ drain.

Every one of these knobs is on Durability, and the defaults are the ones we’d pick for you:

opts.Durability.AgentStartBatchSize        = 50;   // chunk size for assignment batches
opts.Durability.MaxAgentStartParallelism   = 10;   // bounded fan-out starting a chunk
opts.Durability.MaxAgentStopParallelism    = 10;   // symmetric, on the shutdown drain
opts.Durability.StaleNodeEjectionThreshold = 2;    // consecutive stale reads before ejection

Surfacing a paused shard

CritterWatch will use this new capability to help your systems be more resilient

Wolverine deliberately does not restart a shard the daemon paused on a poison event — restarting would fail on the identical event, so the shard would thrash instead of advance. But “we’re not going to restart it” is only defensible if you know. So Wolverine now surfaces it four ways: the agent’s health check reports the failure category, failing event, and root exception type; a NodeRecordType.AgentPaused record lands in the node-record log; IEventSubscriptionAgent.Failure exposes the ShardFailure directly; and there’s an observer hook:

public class AlertingObserver : IWolverineObserver
{
    public Task AgentPaused(Uri agentUri, ShardFailure? failure)
    {
        // Fires once per transition into the failed state -- not on every health check tick
        _alerts.Raise($"{agentUri} paused: {failure?.Category} on event {failure?.Event}");
        return Task.CompletedTask;
    }
}

Only the Other category — a database outage, a timeout, a transient bug, anything you can’t pin on a single event — is treated as potentially self-healing and auto-restarted by the stall detector. Details and the full category table are in When a Projection Fails; it applies identically to Polecat.

Agent start retries

An agent’s very first assignment can race the subsystems it depends on coming up — a subscription shard evaluated before its store’s high-water detection is running, for instance. Previously the loser of a sub-second startup race idled for a full CheckAssignmentPeriod. Now it retries locally first:

opts.Durability.AgentStartRetryAttempts = 2;                            // default; 0 disables
opts.Durability.AgentStartRetryDelay    = TimeSpan.FromMilliseconds(250); // default, × attempt number

See Agent Start Retries.

And the 6.23.1 follow-ups

Three fixes landed on top, all from running the fixed code against real deployments:

  • A pending assignment is now confirmed on delivery rather than on continued assignment — a 6.23.0 regression where a pause→restart cycle left the ledger entry unconfirmed forever.
  • Advisory-lock session hygiene for Marten’s gap-liveness gate, the Wolverine-side twin of marten#4953.
  • Agent restriction changes are merged and persisted before health detection is kickstarted.

3. Polecat Got Materially Faster

Hey, we’re serious about making Polecat a first class citizen within the greater Critter Stack

Polecat — the SQL Server document database and event store — spent this window on performance, and one of the finds was some serious egg on my face.

A one-word bug that cost 6x on string identities

String identity columns in Polecat (pc_streams.idpc_events.stream_idtenant_id, document ids, progression names, tag values) are varchar(250). String parameters were being bound as nvarchar. SQL Server’s data-type precedence rules then convert the column side, not the parameter — so every single id lookup became CONVERT_IMPLICIT(...) over a full index scan instead of a seek.

The numbers, at 50k streams under a SQL collation: StreamIdentity.AsString appends ran at 53/sec versus 304/sec for AsGuid. Version reads were 7,814µs across 990 reads versus 42µs across 3 reads. That’s not a tuning opportunity, that’s a missing index seek on every string-keyed operation in the store.

Every bespoke site that filters a varchar column now binds through AddVarChar/AddIdParameter helpers with a fixed size for plan-cache stability: version reads, FetchStreamFetchForWriting, document exists/metadata, batched loads, DCB tag queries, natural-key operations, the daemon loader and high-water detector, progression, HiLo, and the rebuild/delete admin paths. If you use string stream keys on SQL Server, upgrade to 5.6.0 or later — this one is free.

Server-side Select() projections

On SQL Server 2025’s native json type, a “simple” Select() projection — an anonymous type or DTO composed only of (optionally nested) scalar member accesses — is now translated to a server-side JSON_OBJECT(...) and streamed with no hydrate/reserialize step at all. Emitted keys honor your serializer’s naming policy and [JsonPropertyName]; numbers stay numbers and strings stay quoted.

Two correctness guards ship regardless of whether the optimization kicks in: a non-translatable Select() falls back to a client-side transform when materialized with ToListAsync() (never a silent drop), and attempting to stream a client-side-fallback projection now throws BadLinqExpressionException instead of silently ignoring the Select and returning raw documents.

Streaming paged JSON in one round trip

Both Marten and Polecat now have the full raw-JSON streaming result family, byte-for-byte compatible with each other so clients are interchangeable:

app.MapGet("/issues/paged/{pageNumber:int}/{pageSize:int}",
    (int pageNumber, int pageSize, IQuerySession session) =>
        new StreamPaged<Issue>(session.Query<Issue>().OrderBy(x => x.Description), pageNumber, pageSize));
{"pageNumber":3,"pageSize":25,"totalItemCount":1207,"pageCount":49,"hasNextPage":true,"hasPreviousPage":true,"items":[...]}

The total row count rides along on every row via count(*) OVER() in the same query that fetches the page — so count and documents both come from a single database round trip — and the documents inside items are the already-persisted JSON, streamed straight through with no deserialize/serialize.

For infinite scroll and export feeds, StreamPagedByCursor<T> does keyset (seek) pagination with an opaque, versioned cursor, at constant cost regardless of depth:

app.MapGet("/issues/feed", (string? cursor, IQuerySession session) =>
    new StreamPagedByCursor<Issue>(
        session.Query<Issue>().OrderBy(x => x.Description).ThenBy(x => x.Id), cursor, pageSize: 25));

The terminal ordering key must be the document identity so the ordering is a total order — that’s enforced, not assumed. See Keyset (Cursor) Pagination and Polecat’s paging docs.

Marten 9.20 rounded the family out with StreamEventState and StreamEvents for streaming a single stream’s metadata and raw events (docs).

Batched event fetches

FetchStreamStatePlan and FetchStreamPlan landed in both Marten and Polecat, and Polecat gained a batched event surface it simply didn’t have — batch.Events, with FetchStreamState and FetchStream in Guid and string overloads. Both new batch items compose their SQL from the same canonical column projections and hydrate through the same readers as the standalone path, so batched and standalone can’t drift apart across a schema migration. See Batched Queries.

Native SQL Server 2025 JSON indexes

One JSON index covers many paths at once and accelerates JSON_VALUE equality, JSON_PATH_EXISTS, and JSON_CONTAINS — with no per-path computed columns:

opts.Schema.For<User>().JsonIndex(x => new { x.UserName, x.Department });
opts.Schema.For<Document>().JsonIndex();                                    // whole-document
opts.Schema.For<Article>().JsonIndex(x => x.Tags, i => i.OptimizeForArraySearch = true);

This is the SQL Server counterpart to Marten’s GinIndexJsonData(), and it requires the native json column type (SQL Server 2025) — Polecat throws a clear error rather than emitting invalid DDL if you configure one against nvarchar(max). Covering indexes landed alongside it: Index(..)/UniqueIndex(..) now carry extra members as non-key INCLUDE columns so a query can be satisfied from the index alone. JSON Indexes docs.

Polecat also picked up per-tenant managed partitioning for documents and streams, AggregateToManyAsync(), the HasTag DCB tag operator in LINQ Where(), and tenant-scoped event/tag explorer reads.


4. Weasel Generates EF Core Migrations Now

I myself strongly prefer the “it just works” style of migrations that Marten and later Wolverine and Polecat do, but hey, a large plurality of the .NET community is probably very used to EF Core migrations, so we’re allowing our users to jump on board that train too!

This is a bigger deal than its version number suggests. Weasel 9.18 can emit standard, compilable EF Core migration files from its own schema model — the reverse of the mapping direction it already had. Instead of Weasel applying schema changes itself via db-patch/db-apply, your team applies them with the tooling it already standardized on: dotnet ef database update, idempotent SQL scripts, migration bundles, and versioned migration files a DBA can review.

dotnet run -- db-ef-migration add AddOrderProjection

That writes migration classes with real Up() and Down() bodies, a stub DbContext per database (with __EFMigrationsHistory relocated into the critter-stack schema so it can’t collide with your application’s own EF context), and a weasel-schema-snapshot.json that the next add diffs against. Everything flows through one door — IDatabase.AllObjects() — so Marten system tables, Wolverine envelope storage, Polecat event storage, and EF-projection tables all generate the same way. Verified end-to-end on both EF 9 and EF 10.

Docs: EF Core Migration Generation and Migration Coexistence.

Weasel 9.19 also gave Oracle a first-class command builder with real statement splitting — which is what unblocked Wolverine’s Oracle durability agent running through the shared batching mechanics. ODP.NET has no DbBatch support and won’t execute ;-separated statements in a single command, so this had to be solved at the Weasel layer.

One consistent finding across the decade plus of Critter Stack development is that database query batching is very frequently advantageous for performance, and we’ve taked that very seriously over the years


5. JasperFx: The Shared Core

Nine JasperFx releases in ten days, because it’s where the shared event-store and daemon abstractions live. The highlights, most of which you’ve already met above through Marten and Polecat:

  • ShardFailure + ShardFailureCategory (jasperfx#565/#567) — the classified reason a shard paused, exposed on ISubscriptionAgent and persisted through extended progression.
  • StopAndDrainTimeout (jasperfx#564) — the configurable per-shard drain bound.
  • HighWaterAgent liveness heartbeat (jasperfx#539) — a staleness surface and a local restart seam, which is what the improved health check reads.
  • Lost-wakeup race closed (jasperfx#572) and the WaitForShardState race against an already-published state (jasperfx#568).
  • Batched extended-progression writes (jasperfx#553/#554) — per-database-flush-interval batching, so the telemetry write path stopped being a per-shard-per-tick insert.
  • Natural key extraction widened to bind IEvent<T> sources, stop fabricating aggregates, and fail loudly rather than silently (jasperfx#569).
  • DCB workIDcbAggregateRegistry for runtime discovery, serializable rich EventTagQuery as a DCB source, and a step-instrumented aggregation fold with MultiAggregateProjectionResult.
  • F# support got more robust tuple/record handling and a DerivedVariable reference-propagation fix.

6. Everything Else

A partial list, because the window was busy:

Wolverine

  • Claim checks got size-threshold auto-offload, per-message/per-endpoint store selection, and honor a DI-registered IClaimCheckStore.
  • GCP Pub/Sub: named-broker support for sharded/partitioned topics, plus ListenToPubsubSubscriptionOnNamedBroker.
  • Conventional routing no longer ignores named brokers.
  • Oracle: the durability agent runs through the shared batching mechanics; the durable inbox binds RAW(16) Guids correctly; the message store URI uses the registered wolverinedb agent scheme.
  • Redis: scheduled retries no longer vanish on an unreadable timestamp, and entries that repeatedly fail to deserialize get dead-lettered instead of looping.
  • NServiceBus interop: the EnclosedMessageTypes header is split before resolution, shared across Azure Service Bus, SNS, SQS, and the database transports.
  • HTTP: a raft of OpenAPI and binding fixes — [FromQuery] on arrays and collections, case-insensitive enum array parsing, 415 instead of 404 when no Content-Type reaches an [AcceptsContentType] route, no duplicate description of route-bound [FromQuery]/[FromHeader] parameters, fail-fast when an endpoint advertises a body its HTTP method can’t carry, and explicitly-routed chains mapped inside the constructor so PublishMessage/SendMessage endpoints get their metadata.
  • IHost.ClearAllWolverineStorageAsync(), and resources setup provisions message storage even under AutoCreate.None.
  • Exclusive listener inboxes are now recovered on the listening node.

Marten

  • TimescaleDB support — projection and document hypertables, folded into core Marten.
  • Natural keys hardened: the previous key row is retired when the key changes, and the foreign key guard is scoped to its own table.
  • Simple LINQ Select() projections translate to jsonb_build_object (the Postgres side of the same optimization Polecat got).
  • ETag / If-None-Match (304) support on StreamOne and StreamAggregate.
  • Tenant-scoped event and tag explorer reads.

Upgrading

In this case, everything in the critter stack moved in lockstep — Wolverine 6.23.1 pins Marten 9.20.0, Polecat 5.7.0, and JasperFx 2.36.1+, so upgrading Wolverine pulls the rest forward for you. If you’re on a Marten-or-Polecat-only application, take Marten 9.20.0 / Polecat 5.7.0 directly.

Nothing here is a breaking change. The agent-assignment work is entirely behavioral and needs no configuration to benefit from; the new Durability knobs exist for tuning, not for opting in. The classified shard-failure columns require extended progression tracking, which is still off by default — turn it on with Events.EnableExtendedProgressionTracking if you want database-visible per-shard health, and note that the per-tenant high-water health check needs it too.

If you’re running the critter stack at any real scale, CritterWatch consumes all of the new failure surfacing described above without any work on your part.


Closing Thoughts

I would dearly appreciate it if the world could slow down a bit in the next couple weeks so that release cadence can come back to Earth. I’d also appreciate it if everybody else could chill out a bit in their OSS activity so GitHub actions can be more performant and responsive for me and the Critter Stack community!

Critter Stack Roadmap for the Rest of 2026?

Just to wind down from a busy week, I thought it would be nice to jot down an update about the Critter Stack and JasperFx roadmap as it looks like right now for the rest of the year.

We’ve had a torrid release cadence this whole year with the big highlight being the “Critter Stack 2026” wave of major releases, then quite a few follow up releases to add more features and improve performance and resilience. The real goal of this year for JasperFx was to finally release…

CritterWatch

CritterWatch has ended up being a much, much larger and more ambitious tool than originally conceived as the advent of AI assisted development really changed everything. CritterWatch is absolutely going to still be the management and observability console for the Critter Watch as it was originally conceived. Now though, it will also serve as the central hub of AI assisted development and support for the Critter Stack.

For timing, I’m calling:

  • 1.0 RC 1 for this Monday, July 27th
  • The official, 1.0 GA is targeted for Monday, August 3rd

And of course, incremental releases with new features throughout the rest of the year and to deal with the inevitable feedback once it’s being used by more people. Here are some ideas currently in our backlog for a “1.1” release:

  • Some kind of recurring cron-based message scheduling with deep integration of Wolverine with Quartz.Net and/or TickerQ. I’m currently thinking that Hangfire is just its own huge thing and not looking to mess with that. It’s quite possible that Wolverine gets first class documentation and integration for Quartz.Net and TickerQ first, then the CritterWatch integration is really just provides management and observability over that.
  • Scheduling projection rebuilds or other management actions for off hours
  • More integration for Event Modeling visualizations and code generation? We’re going to have quite a bit of visualization of the cause and effect of a system right off the bat, but we’ll also be moving more into development time assistance. I don’t have any details about what exactly that’s going to be yet.

Spec Driven Development, AI Assisted Development, and Event Modeling

Every major Event Sourcing tool company or community is working on some sort of approach for AI assisted development, and we’re already well into that ourselves. I think a lot of how people see AI usage in development is almost completely a reflection of where their opinions about software development before AI.

For myself, I was hugely influenced by Extreme Programming and I’ve long been deeply skeptical of Model Driven Development or really any kind of purported “low code” approach to software development. That’s carried over to also being unenthusiastic about any approach for generating event sourced applications by first modeling in some kind of custom XML or YAML format or some kind of external DSL. I’m also admittedly dubious about any kind of user interface tooling to generate code. Moreover, I tend to scoff at a lot of these tools as taking more time to do the intermediate model than it would to just write the code with the Critter Stack and our very low code ceremony model.

Instead, JasperFx will be leaning toward much more code centric approaches:

  • Using what we used to call “Executable Specifications” (BDD) for AI assisted development and building tooling to reduce the effort to do so. Right now we’re pursuing Gherkin based tooling as at least one alternative, but we’re not locked into only doing that.
  • Expanding the already existing tooling in CritterWatch for visualizing event sourcing code through the Event Modeling notation at development time or even live in requirements workshops with domain experts rather than generating code from the intermediate models.

And as always, we put a lot of emphasis on low code ceremony approaches as is.

I do actually admire what KurrentDb is doing so far with their Capacitor tool and I’m interested in building out our tooling, but that might be a “build your own lightsaber” learning experience or something very optimized for JasperFx’s own development on the Critter Stack.

Spaghetti against the Wall?

Alright, now it’s time for farther out ideas that aren’t even remotely fleshed out just to see what other folks would find compelling:

  • Improve Wolverine’s story for long running workflows, meaning tasks that might take hours and can’t be done as a single message. I think a huge chunk of this is just having more documentation and examples for already existing capabilities, but I think there’s also an opportunity to exploit Wolverine’s virtual actor subsystem to expand into Temporal.io type territory
  • Maybe a JasperFx curated Hot Chocolate package. Not taking anything away from ChiliCream, but I know there’s performance fat in the Marten integration and repetitive code for integrating Wolverine into Hot Chocolate mutations. I have zero interest in a full blown GraphQL product, but it’s something I’ve thought about from time to time
  • Integrating caching options into Wolverine, but that’s low hanging fruit

What else folks? What would you like to see improved or added?

Wolverine.HTTP Learns the QUERY Verb

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.

Here’s a complete search endpoint:

using Wolverine.Http;
public record SearchRequest(string Term, int Page);
public record SearchResults(string Term, int Page, string[] Hits);
// QUERY (RFC 10008) is safe and idempotent like GET, but carries a request body — ideal for
// search endpoints whose criteria are too large or structured for the query string. Wolverine
// binds the request body just like it would for POST.
[WolverineQuery("/search")]
public static SearchResults Search(SearchRequest request)
{
var hits = Enumerable.Range(1, request.Page)
.Select(i => $"{request.Term}-{i}")
.ToArray();
return new SearchResults(request.Term, request.Page, hits);
}

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:

using Marten;
using Wolverine.Http;
// Taking an IDocumentSession attracts AutoApplyTransactions on a QUERY endpoint
// exactly as it would on a POST — there is no verb-based exemption.
[WolverineQuery("/search/audited")]
public static SearchResults SearchAudited(SearchRequest request, IDocumentSession session)
{
session.Store(new SearchAudit(Guid.NewGuid(), request.Term));
return new SearchResults(request.Term, request.Page, []);
}
// IQuerySession is Marten's read-only session and does NOT trigger transactional
// middleware — the right dependency for a QUERY endpoint that reads the database.
[WolverineQuery("/search/readonly")]
public static SearchResults SearchReadonly(SearchRequest request, IQuerySession session)
{
return new SearchResults(request.Term, request.Page, []);
}

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:

public class query_verb_support : IntegrationContext
{
public query_verb_support(AppFixture fixture) : base(fixture) { }
[Fact]
public async Task query_endpoint_reads_request_body_and_returns_result()
{
// QUERY carries a request body (unlike GET). Alba's scenario helpers assume standard
// verbs, so drive a genuine QUERY request through the test server's HttpClient.
var client = Host.GetTestServer().CreateClient();
var request = new HttpRequestMessage(new HttpMethod("QUERY"), "/search")
{
Content = JsonContent.Create(new SearchRequest("widget", 3))
};
var response = await client.SendAsync(request);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var results = await response.Content.ReadFromJsonAsync<SearchResults>();
results.ShouldNotBeNull();
results.Term.ShouldBe("widget");
results.Page.ShouldBe(3);
results.Hits.ShouldBe(["widget-1", "widget-2", "widget-3"]);
}

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:

[Fact]
public void query_route_is_registered_with_QUERY_method_metadata()
{
var endpoint = EndpointFor("/search");
var methods = endpoint.Metadata.GetMetaata<HttpMethodMetadata>();
methods.ShouldNotBeNull();
methods.HttpMethods.ShouldContain("QUERY");
}
[Fact]
public void query_endpoint_is_not_wrapped_in_transactional_middleware()
{
// Non-transactional because it takes no persistence dependency — NOT because QUERY is "safe".
var chain = HttpChains.Chains.Single(x => x.RoutePattern!.RawText == "/search");
chain.RequiresOutbox().ShouldBeFalse();
chain.IsTransactional.ShouldBeFalse();
}
[Fact]
public void query_endpoint_with_document_session_is_transactional()
{
// The dependency-based rule cuts both ways: an IDocumentSession dependency
// attracts AutoApplyTransactions on a QUERY endpoint exactly as on a POST.
var chain = HttpChains.Chains.Single(x => x.RoutePattern!.RawText == "/search/audited");
chain.IsTransactional.ShouldBeTrue();
chain.RequiresOutbox().ShouldBeFalse();
}

And, closing the loop on the OpenAPI caveat above, you can pin the “don’t break the document” guarantee:

[Fact]
public void swagger_generation_still_succeeds_with_a_query_endpoint()
{
var generator = Host.Services.GetRequiredService<ISwaggerProvider>();
var doc = generator.GetSwagger("default");
// 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.

Full documentation lives in the HTTP Endpoints guide → The HTTP QUERY Method.

Your AI Agent Just Got a Lot Better at the Critter Stack: AI Skills 1.6.0

JasperFx Software and the greater “Critter Stack” community is advancing our tools pretty rapidly and we have (at least for now) a release cadence that’s far more rapid than our competitors in the .NET space. It’s perfectly possible to be an experienced Critter Stack user and not be aware of the latest, greatest features, fixes, and improvements.

That’s the problem JasperFx AI Skills exists to solve. It’s a curated library of agent skills — now 81 of them — covering Wolverine, Marten, Polecat, and CritterWatch, written and maintained by the people who build these tools. Install them once and your agent stops guessing from stale training data and starts working from documentation that’s verified against the actual source code, current as of this month, and organized the way agents actually consume knowledge: task-shaped, example-heavy, and honest about the sharp edges.

Release 1.6.0 is out today, and it’s a big one. Here’s what’s inside.

Ready for the Critter Stack 2026 release

Marten 9 and Wolverine 6 changed the code-generation and deployment story in ways that make most existing internet advice actively wrong. Marten 9 eliminated runtime code generation entirely — no more codegen write step for Marten, no more GeneratedCodeMode knobs, no more Internal/Generated/ folders. Wolverine 6 kept its codegen but moved the Roslyn compiler into the opt-in WolverineFx.RuntimeCompilation package, which means your production image can now run in Static mode with zero Roslyn on disk — roughly 100 MB lighter and Native-AOT-ready.

Every skill in the library that touches code generation was re-audited for this release. The canonical codegen skill now teaches the full Roslyn-free production shape: pre-generate in your Docker build stage, run Static with AssertAllPreGeneratedTypesExist, keep the runtime compiler out of Release builds. And it’s precise about the mixed-host nuance that trips people up: a host running both Marten 9 and Wolverine 6 still needs codegen write — but only for the Wolverine half. Your agent will now get that distinction right instead of cargo-culting a Dockerfile step “for the Critter Stack.”

A new troubleshooting line — with the real error messages

Two new skills anchor a troubleshooting category: message routing and service location & code generation. The second one is my favorite thing in this release. When your Wolverine 6 upgrade throws InvalidServiceLocationException at startup (and it will, because the ServiceLocationPolicy default flipped), the skill has the exact exception text, every reason string the codegen can emit — “opaque lambda factory,” “concrete type is not public,” “directly using IServiceProvider” — and the specific registration fix for each one. Every error message was verified verbatim against the Wolverine and JasperFx source, because an agent pattern-matching on error text needs the real text, not a paraphrase.

It also documents a CI trick that deserves to be better known: dotnet run -- codegen test generates and compiles every handler and endpoint in memory and fails the build on any codegen error — so the opaque registration someone adds on Tuesday fails Tuesday’s PR, not Friday’s deploy.

.NET Aspire, done properly

A new consolidated Wolverine with .NET Aspire skill covers the one pattern that repeats across every resource — AddXWithReferenceWaitFor → read the injected connection string — plus a per-provider matrix for SQL Server, MySQL, Oracle, PostgreSQL, RavenDB, and Cosmos DB persistence, and the transport-side stories for RabbitMQ, Kafka, NATS, and Azure Service Bus (including which ones have UsingNamedConnection helpers and which ones don’t — we checked the source, there are exactly two). The per-transport skills each gained their own Aspire sections, and the skill is refreshingly blunt about AWS SQS/SNS: there is no first-party Aspire integration, so here’s the LocalStack pattern instead.

CritterWatch operations

The skills aren’t just for writing code — they work with CritterWatch, our monitoring and operations console for the Critter Stack, too. Three new skills cover operating a fleet: service actions (like evicting a stale service registration), the embedded CLI (cw-* read commands), and lifecycle diagnostics — on top of the existing setup and routing-diagnostics skills. If you’re running CritterWatch, your agent can now help you install it, wire it into Aspire, and operate it day to day.

Self-contained by design

A principle we hardened this release: if a skill shows you a helper method, the skill carries the complete source. The TrackedHttpCall helper that makes Alba + Wolverine integration testing so pleasant? It’s never shipped in a NuGet — it’s a pattern from Wolverine’s own test suite, and every testing skill now embeds the full method and says so explicitly. Same for the Azure Service Bus emulator helper. No more agents (or humans) hunting for a package reference that doesn’t exist. Where we found the upstream docs implying otherwise, we filed the issues too.

And the steady sharpening

Beyond the headlines: clarified exactly when Marten’s IncludeType<T>() is needed (only when Marten can’t infer event types — explicit Evolve overrides or base-type Apply methods), covered Wolverine 6.17’s HTTP QUERY verb support, [AsParameters] binding patterns, Polecat’s typed streaming result types for HTTP endpoints, migration-guide fixes driven directly by user feedback, and more. Twenty-four merged PRs since 1.5.0, every open issue in the tracker closed.

Getting it

AI Skills is available for purchase at jasperfx.net/our-products. Once you’re licensed, it ships as the JasperFx.AiSkills package on the JasperFx feed:

agentskills-cli add JasperFx.AiSkills

Browse the full catalog and per-release changelog at the AI Skills documentation site to see exactly what your agent would be working from. And if your agent still gets something wrong — file an issue. Half of this release started life as user feedback, and the loop from “the skill told me something stale” to “fixed, verified against source, released” is exactly the point of maintaining these ourselves.

Things that Have Worked for Our OSS Community

I’m the technical leader and founder of the “Critter Stack” tools (Marten, Polecat, Wolverine, and Weasel) and the greater JasperFx organization on GitHub. After 15+ years of OSS community work of varying degrees of technical and project adoption success, I’ve got a few things to share that I think have helped us be more successful. Just know though, that there was plenty of iteration, friction, pain, and flat out failures in the rear view mirror before we arrived at most of the things I’m sharing as “positives” here — and of course, plenty of people would argue with me that at various times we’ve done things badly for them.

First for some soft, non technical things. If you can possibly get to this, having a community invested in the success of your tools and the community succeeding as well is immeasurably valuable. I think we’ve actually got that for the Critter Stack and it shows from the sheer number of contributions we’ve gotten in the past couple years. I won’t lie and say I know how to create that from scratch. The only concrete things I can recommend is to try to be as responsive as possible as a maintainer and at least acknowledge user requests or issues as they come in. We pride ourselves on being responsive and not letting issues linger, and we also try to aggressively improve our tools based on user feedback. I think this has quite clearly improved once I was able to work full time on the Critter Stack from the founding of JasperFx Software. The rise of AI tools has also made it much easier to stay on top of incoming issues and turn around fixes quickly.

Living Documentation

Just know that we’ve had hundreds of complaints over the years about the documentation, but much, much less over time as we’ve adapted and improved. Or maybe just because many people are only using our LLM friendly version of the docs through an AI agent. I’m still taking credit for the apparent reduction in complaints though!

You do not have an OSS project of much value unless you have a documentation website of some sort that helps people know what your project is and how to effectively use your published tools. As a maintainer, it’s also your first line of defense against people needing more of your time than you can afford to give.

First off, make your documentation be user centric. Try to organize the flow of content in terms of what users are needing to do and their use cases. Try to avoid the temptation to organization your documentation around the technical concepts or APIs in your system because that’s an awfully fast way to create an unusable website. One thing that I think has helped us more recently is investing in something like Martin Fowler’s “Duplex Book” idea from years ago where you have top level, prose style tutorials that link to pages with more specific information on various capabilities. Having tutorials that talk about use cases or sample applications that again link to separate pages with API details are also very helpful, if more time consuming for us the maintainers. And you’re likely to do that wrong anyway, so see my later comments about continuous improvement and adaptation in your documentation.

Just use Markdown for all your content now. GitHub and I assume other shells happily render it for you from web browsing anyway, many developers already know how to use it, tools like Vitepress already expect it for static website creation, and anyway, you pretty well have to know Markdown now for AI prompts anyway. I’m explicitly stating this because I remember trying to write documentation websites in straight up HTML or earlier competitors to Markdown that don’t seem to be common any more.

Colocate your documentation with your code. As a default, try to put your documentation content in the same code repository as the code it’s documenting. Again, not everybody does that, but I’ve found that to be hugely valuable compared to older approaches. If you’re using markdown, GitHub by itself helps render the raw doc content in a reasonably usable way.

Invest in some kind of quick automation to update your documentation website. Babu has us fully automated to build and publish our documentation websites built with Vitepress to Netlify via GitHub actions so we’ve got quick a 1-2 click process to update docs. Unsurprisingly, it turns out that if you make it mechanically cheap to republish your documentation, you’re much more likely to make improvements much more frequently.

Try to be responsive to what your users are being tripped up by and continuously evolve and improve your documentation structure, wording, samples, and explanations based off feedback from your users. And do a better job of staying on top of that than I do sometimes!

From bitter experience, it’s very easy for code samples in technical documentation to drift away from the tool’s public API, especially with a long lived project. To that end, I very strongly recommend using some kind of tool like MarkdownSnippets that can extract code samples from code that you know is compiling and runnable. That enables us to decorate sample code snippets from within either test projects or sample applications in the main .NET solution like this:

And have that code inserted live into our Markdown documentation files and on our documentation site. You can see that code snippet above in action here.

Make it as easy as possible for external contributors to suggest or make improvements to the documentation. Having Markdown files directly in your GitHub repository with enough README explanation to know how to edit those files certainly helps. We embed a footer on all of our pages like this with a direct link to fork and create a pull request for the current page:

And every little pull request improving wording or (sorry) grammatical or spelling errors adds up over time. One defensive thing I started doing that turned out to be very helpful over time is to try to defang people up in arms about your documentation by asking them how they would suggest improving the documentation for whatever it was that wasn’t working for them. Some people just want to blow off steam at you, but often enough that’s led to a new contributor pitching in and contributing improvements to our documentation.

When someone complains about your documentation, ask them what they think should change or what would have helped them find the information they needed or how something should be explained differently. Some people just want to gripe, but I’ve found that just asking for feedback or even asking for pull requests to improve the documentation has actually led to quite a few improvements for us. And sometimes it even gets someone to stop yelling at you online, which is frequently my main goal as an OSS maintainer:)

Actually, let me generalize that to say that simply asking someone complaining about your tools what they think we should do instead has been very helpful to either eliminate some friction with the tools or at least defuse the situation.

One last, very important note about your technical documentation. Try very hard to clearly describe how you think your tools are meant to be used and what the intended idioms are for your OSS tools. At this point, I think most of the problems we deal with from users are coming from folks who try to use the tool non-idiomatically (or are just hitting permutations or scenarios we didn’t anticipate of course). You can theoretically head off some of those issues by describing and providing samples of “this is how you should use our tool.” That advice might be more germane to an application framework than a library that has much more limited usage patterns though.

For the record, we have frequently been told that we have much better documentation than most of our competitors. I will tell people that I think that Marten is the most capable event sourcing tool for .NET developers — but at one point the one tool I think might be in the running with us in capability has never invested enough in their documentation to prove it.

Ruthlessly Eliminate Friction in your Getting Started Story

My good friend, fellow OSS maintainer, and even a groomsman for me Dru Sellers once gut punched me by comparing an older project of mine to Bowser in Super Mario Kart — slow to get going, but really fast once he gets there!

Ouch.

Every since then I’ve put a lot of focus on making any OSS tool I’m a part of as easy to start with as possible. Let’s take Marten as an example. Here’s the absolute easiest way to add Marten to a .NET system that’s ready to roll (assuming that you have a PostgreSQL database, which is conveniently enough very cheap to spin up in a Docker container):

// This is the absolute, simplest way to integrate Marten into your
// .NET application with Marten's default configuration
builder.Services.AddMarten(options =>
{
// Establish the connection string to your Marten database
options.Connection(builder.Configuration.GetConnectionString("Marten")!);
// If you want the Marten controlled PostgreSQL objects
// in a different schema other than "public"
options.DatabaseSchemaName = "other";
// There are of course, plenty of other options...
});

With that minimal bit of documentation, you can literally start persisting and saving documents (entities) with Marten’s services. Let’s say you’ve got this little class you want to be persisted:

public class User
{
public Guid Id { get; set; }
public required string FirstName { get; set; }
public required string LastName { get; set; }
public bool Internal { get; set; }
}

And now, here’s a working Minimal API endpoint that happily persists a new User on the very first usage with our setup from up above with no explicit configuration, no database schema migrations, or scripts, or anything but a working connection to a database:

app.MapPost("/user",
async (CreateUserRequest create,
// Inject a session for querying, loading, and updating documents
[FromServices] IDocumentSession session) =>
{
var user = new User {
FirstName = create.FirstName,
LastName = create.LastName,
Internal = create.Internal
};
session.Store(user);
// Commit all outstanding changes in one
// database transaction
await session.SaveChangesAsync();
});

So, a couple things and then I’ll talk about the concepts underneath the code above:

  • We’ve tried to adopt an attitude of “it should just work” toward our tools. As a prime example of that, Marten in its default mode will happily make sure that the database schema is exactly what the Marten configuration needs it to be at runtime for you. That leads to a much faster getting started story than it is without that. Likewise, Wolverine can configure message brokers for you for the same experience with Rabbit MQ or Azure Service Bus. Please chill out a little bit if you’re thinking that you’ve personally had trouble with Marten and Wolverine because I specifically said the word “try.”
  • I’m going to claim that we judiciously use some Sensible Defaults. Look up above at the User type and notice that it has a property called “Id.” Without any explicit configuration, Marten will happily decide that’s the identity for the User type. That also tells Marten to use sequential Guid values for assigning identity if one isn’t assigned by the user
  • Marten and Polecat both support a pretty efficient “upsert” for documents that’s yet another way to remove friction and repetitive code. We’ve had that so long I’d really kind of forgotten about that, but I always miss that when I’m forced to use EF Core instead:)
  • A little bit of “Convention over Configuration”, but that one works really well for some folks, and not so much for others, so you can’t take that as a globally applicable strategy

My kids love Jack Black after he was Bowser in the Super Mario Brothers movies and the Minecraft movie.

Technical Things

Just a potpourri of things that I think have contributed to whatever success we’ve had as an OSS community:

Semantic Model. Wolverine, Marten, and Polecat all use the Semantic Model approach to framework configuration. This allows us to accommodate a mix of conventions and explicit configuration while providing much more diagnostic information about our tools than anything else out there in .NET land. This strategy is also key to Wolverine’s composable middleware strategy that allows you to control the application and ordering of middleware on a handler by handler basis. I wrote much more about this recently in Wolverine Middleware and Some Random Observations, but see the section on “Wolverine’s Configuration vs Runtime Model”

Compliance Tests. Wolverine has a library of reusable “compliance” test suites for our message durability (think transactional outbox et al), messaging “transports”, and leadership election that try to cover every basic scenario you need to say that an integration to a new technology works correctly with Wolverine. Once we refactored those test suites out and made them reusable, that opened the door to add a lot more capabilities to Wolverine. At this point, Wolverine actually supports more message broker technologies than our much older competitors, and I attribute plenty of that to the compliance tests. Moreover, several of our supported options (GCP Pubsub, Redis, NATS.io) came from community contributors rather than core team members. Likewise, the compliance tests for message persistence enabled us to expand from our earlier PostgreSQL / SQL Server duality to Oracle, MySql, Sqlite, RavenDb, and CosmosDb now.

Orthogonal Code. All this means is that the internal code is relatively well factored and types have well defined responsibilities such that they can be composed in new ways. That’s a lot of gobbledygook, but the very real impact is that Wolverine’s internals allow us to support every possible type of message error handling strategy for every possible messaging technology that Wolverine supports without duplicating much code. As an example, one of our older competitors just added the ability to do delayed message retries when using Kafka. Because of the way Wolverine’s internals are structure, we support that capability for every single transport option and not just Rabbit MQ (but liek every other messaging tool, the Rabbit MQ integration is much more heavily used than everything else). As another example, Wolverine can mix and match its transactional inbox and outbox support for every supported database and every supported messaging transport.

Diagnostics. If you try to build out any kind of application framework like Wolverine or configuration intensive libraries like Marten or Polecat, you better damn well have diagnostics left and right to explain what, how, and why the tools are doing what they’re doing. CritterWatch is well underway, but even without that, we build in command line diagnostics pretty early and we’ve continued that investment. That turned out to be very advantageous for AI usage, but even before that, that helped us quite a bit in user support as is.

Standardizing Test Automation. In Marten we’ve built a couple shared test harness recipes over the years that help (especially me) contributors and yes, AI agents, fall into consistent and optimized patterns for automated tests. Here’s an example of using our OneOffConfigurationContext recipe for testing any kind of non-default Marten configuration:

public class event_statistics : OneOffConfigurationsContext
{
[Fact]
public async Task fetch_from_empty_store()
{
await theStore.Advanced.Clean.DeleteAllEventDataAsync();
var statistics = await theStore.Advanced.FetchEventStoreStatistics();
statistics.EventCount.ShouldBe(0);
statistics.StreamCount.ShouldBe(0);
statistics.EventSequenceNumber.ShouldBe(1);
}
[Fact]
public async Task fetch_from_non_empty_event_store()
{
await theStore.Advanced.Clean.DeleteAllEventDataAsync();
theSession.Events.Append(Guid.NewGuid(), new AEvent(), new BEvent(), new CEvent(), new DEvent());
theSession.Events.Append(Guid.NewGuid(), new AEvent(), new CEvent(), new DEvent());
theSession.Events.Append(Guid.NewGuid(), new AEvent(), new BEvent(), new CEvent(), new DEvent());
theSession.Events.Append(Guid.NewGuid(), new BEvent(), new CEvent(), new DEvent());
theSession.Events.Append(Guid.NewGuid(), new AEvent(), new BEvent(), new CEvent(), new DEvent());
await theSession.SaveChangesAsync();
var statistics = await theStore.Advanced.FetchEventStoreStatistics();
statistics.EventCount.ShouldBe(18);
statistics.StreamCount.ShouldBe(5);
statistics.EventSequenceNumber.ShouldBe(18);
}
}

This base type recipe helps in a couple ways:

  1. It enforces some standardization that makes tests easier to read once you’re experienced with the codebase
  2. Notice the usage of theStore and theSession? The test fixture base class is lazily giving you access to a document store and a document session based on your configuration in a declarative way. I think this helps make tests be more terse and declarative since there’s less junk code for setting up scenarios.
  3. It handles resource clean up for you
  4. It’s quietly helping keep the test harnesses isolated from each other and “parallelizable” by using database schema names based on the actual class type name

Ask for reproduction code for bug reports. This obviously won’t help for every project, but at least for the Critter Stack tools we’ve been hugely successful at simply asking users reporting problems to either build a reproduction project on GitHub that demonstrates the problem or better yet, asking them to submit a pull request with failing tests. Not every issue requires that, but man, that’s been so helpful to myself and other maintainers in addressing issues fast. For whatever reason, our community is just absolutely fantastic about doing that for us.

A Big Week for the Critter Stack

Right before heading into a short vacation, I wanted to blog about some of our recent releases this past week as the entire CritterStack has been busy lately. Between June 22 and June 29, we shipped three Wolverine releases, three Marten releases, and three Polecat releases — a week heavy on database-backed messaging, brand-new interoperability with the rest of the .NET messaging ecosystem, and a steady drumbeat of work to make every part of the stack more observable and more manageable from CritterWatch.

Here’s a tour of what landed.


Release Timeline

DayWolverineMartenPolecat
Jun 229.10.0
Jun 236.14.04.5.2
Jun 254.6.0
Jun 266.15.09.11.0
Jun 296.16.09.12.04.7.0

CritterWatch Beta 1

The big win for the week is (finally) getting out the first CritterWatch 1.0 beta, which I finally managed to present in a live stream yesterday. And while there’s a lot further to go for a true, quality 1.0 release, I think it’s showing a lot of promise and will add quite a bit of value for Critter Stack users at both development and production time.

There’s also several new sample solutions at https://github.com/JasperFx/CritterStackSamples/tree/main/critterwatch that show little fake systems stood up with CritterWatch and Aspire using several permutations of Rabbit MQ, AWS SQS, Azure Service Bus, SQL Server, and PostgreSQL — including the new embedded model.


Wolverine

Three releases this week, but the headline is clear: database-backed messaging got faster, and Wolverine now has more options for interoperability with NServiceBus and MassTransit. Both of these improvements were client requests for JasperFx Software.

🚀 Database queue performance

Both the PostgreSQL and SQL Server transports got performance improvements, with the SQL Server work being quite a bit more important, but with a new opt in option so that existing users won’t be surprised by database migrations.

On SQL Server the new optimization is one fluent call. Clustering the queue and scheduled tables on a monotonic seq identity (instead of the previous random-Guid clustered key) turns FIFO dequeue into a clustered seek with physically contiguous deletes:

opts.UseSqlServerPersistenceAndTransport(connectionString)
.OptimizeQueueThroughput()

The raw-DDL benchmark behind the PR tells the story — same hardware, same workload:

LayoutThroughputp50 latencyp99 latency
baseline (clustered Guid, no index)98/s845 ms1,860 ms
OptimizeQueueThroughput() (clustered seq)34,612/s2.4 ms3.7 ms

If you lean on Wolverine’s database queues — whether as a no-broker option or to keep messaging transactionally consistent with your business data — the indexed dequeue path is a free win on upgrade. OptimizeQueueThroughput() is opt-in specifically because enabling it on an existing database triggers a one-time queue-table rebuild, so it’s a maintenance-window change for existing systems and a no-brainer for new apps.

📖 SQL Server transport docs · 📖 PostgreSQL transport docs

🆕 Interop with MassTransit and NServiceBus over SQL Server and PostgreSQL

Wolverine already has quite a few options for interoperability with pre-canned recipes for both NServiceBus and MassTransit against all the major message brokers, but we had a client request to do the same with NServiceBus and SQL Server, so we just beefed up all the permutations while we had the hood up. Wolverine can now send to and receive from MassTransit and NServiceBus applications using each framework’s own SQL Server or PostgreSQL queueing — reading and writing their native tables directly, no shared broker required.

Landed across 6.14.0 and 6.16.0:

  • NServiceBus over SQL Server (#3198)
  • NServiceBus over PostgreSQL (#3201)
  • MassTransit over PostgreSQL (#3203)
  • Each interop transport is pinned to a dedicated database under multi-tenanted storage (#3271), Seq is indexed on the NServiceBus PostgreSQL queue table (#3205), and a shared DatabaseListener base now backs the polling loop across all of these (#3206).

For NServiceBus, Wolverine reads and writes the NServiceBus queue tables directly — one table per queue with a JSON Headers column and a raw Body column:

using Wolverine.SqlServer.Transport.NServiceBus;
builder.UseWolverine(opts =>
{
// Wolverine's own durable inbox/outbox still lives in SQL Server
opts.PersistMessagesWithSqlServer(connectionString, "wolverine");
opts.UseNServiceBusSqlServerInterop();
// Publish to an NServiceBus endpoint's queue table
opts.PublishMessage<OrderPlaced>().ToNServiceBusSqlServerQueue("nsb");
// Listen to Wolverine's own queue table and use it for replies
opts.ListenToNServiceBusSqlServerQueue("wolverine").UseForReplies();
// Bind NServiceBus interface-typed messages to Wolverine's concrete types
opts.Policies.RegisterInteropMessageAssembly(typeof(IOrderContract).Assembly);
})

PostgreSQL is identical with the UseNServiceBusPostgresqlInterop() / ListenToNServiceBusPostgresqlQueue() / ToNServiceBusPostgresqlQueue() trio. MassTransit is a different shape — its SQL transport is a function-driven, two-table model (transport.message + transport.message_delivery) that MassTransit owns and migrates, so Wolverine calls its stored functions rather than touching a table:

using Wolverine.Postgresql.Transport.MassTransit;

builder.UseWolverine(opts =>
{
opts.PersistMessagesWithPostgresql(connectionString, "wolverine");

opts.UseMassTransitPostgresqlInterop(autoProvision: true);

opts.PublishMessage<OrderPlaced>().ToMassTransitPostgresqlQueue("masstransit");
opts.ListenToMassTransitPostgresqlQueue("wolverine").UseForReplies();

opts.Policies.RegisterInteropMessageAssembly(typeof(IOrderContract).Assembly);
});

These join the existing Amazon SQS interop options (which also picked up two bug fixes this week, #3190) and a fix to map Wolverine’s TenantId from incoming MassTransit messages (#3192). The practical upshot: you can introduce Wolverine into an existing MassTransit or NServiceBus shop incrementally, service by service, over infrastructure both sides already trust.

📖 Interop with NServiceBus over database transports · 📖 Interop with MassTransit over database transports

🔭 Observability & health

Okay, so big parts of this are AI written and you don’t care much about the details. Just take my word for it that all this mumbo jumbo means that CritterWatch can “see” and report back to you much more about how your system is running, what your system actually is, and we’ve added more robustness to monitoring and kick starting external transport listeners.

A large share of the week’s Wolverine work exists to make running systems legible — much of it surfaced directly through CritterWatch:

  • A shared BackgroundReceiveLoop with receive-loop health reporting, now adopted across SQS, Redis, the PostgreSQL queue, the SQL Server queue, and Kafka (#3236).
  • Transport connection state surfaced in EndpointHealthSnapshot, with a new IReportConnectionState implemented for NATS, MQTT, Pulsar, and Redis (#3231), plus a force-restart path for stuck listeners (#3232).
  • sanitized, credential-free broker connection summary on BrokerDescription (#3272) — so the dashboard can show you where a broker points without ever leaking secrets.
  • Richer metrics: every instrument tagged with a source service name (#3221), dimensional inbox/outbox/scheduled gauges, and configurable histogram buckets (#3224).
  • The discovered gRPC endpoint manifest is now exposed via a ServiceCapabilities descriptor source (#3268#3266), and RabbitMQ sending endpoints are now properly named in health snapshots (#3273).

📖 Instrumentation and Metrics · 📖 Diagnostics

🐛 Reliability fixes & Pulsar

We did a big round of improvements for Kafka a couple weeks ago to open up more Kafka idioms to Wolverine users. Later though, we did the exact same thing for Wolverine’s Pulsar support.

6.14.0 also closed out a major Pulsar re-evaluation effort — DLQ/retry precedence, initial subscription position, multi-topic and regex subscriptions, native per-message redelivery, acknowledgment-strategy choice, a Reader interface for bounded replay and non-durable hot-tail, a tiered retry-letter error policy, producer deduplication, and both JSON and Avro schema support with broker-side registration (#3194#3215).

Two of those are worth showing. Pulsar’s defining feature is broker-side schema registration and compatibility checking — now a single fluent call, with the message body still owned by Wolverine’s serialization:

opts.PublishMessage<OrderPlaced>()
    .ToPulsarTopic("persistent://public/default/orders")
    .UseJsonSchema<OrderPlaced>();   // or UseAvroSchema<T>() for Avro on the wire

And the new tiered retry-letter policy — the Pulsar analogue of the Kafka transport’s MoveToKafkaRetryTopic — expresses native redelivery delays as a first-class, discoverable error policy:

// On failure: redeliver after 5s, then 30s, then 2m, then dead-letter.
opts.OnException<TransientException>()
    .MoveToPulsarRetryTopic(5.Seconds(), 30.Seconds(), 2.Minutes());

📖 Pulsar schema support · 📖 Tiered retry-letter policy · 📖 Producer deduplication

Plus targeted reliability fixes: a RabbitMQ agent that could latch Disconnected after a channel-only shutdown (#3187), stable node identity for storeless Solo hosts (#3189), and re-attaching the sender wire tap to recovered envelopes (#3276).


Polecat — Making It More Robust

Polecat is finally getting some serious people using it, and that has inevitably meant that more issues are arising. While the Critter Stack team can certainly not claim to be perfect in our delivery, I’ll swear up and down that we’re the most responsive team of maintainers in .NET and we’ve been turning around Polecat issues fast to get that thing as robust as possible for our early users. Polecat is also moving pretty fast because I’m making a big deal of ensuring that all CritterWatch features for Event Sourcing or the Document Database features are fully supported for Polecat, and that’s generated a lot of recent work in Polecat as well.

Polecat shipped three releases this week (4.5.2, 4.6.0, 4.7.0), and the through-line is hardening: fewer sharp edges, more parity with Marten’s behavior, and a real document-metadata story.

🛡️ Robustness & correctness fixes

  • Repopulate the natural-key lookup table on projection rebuild (#261) — rebuilds no longer leave natural-key lookups stale (mirrored by the same fix in Marten, below).
  • Patch().Set() now honors EnumStorage (#264) and supports DateTime/DateTimeOffset/DateOnly/TimeOnly (#265).
  • Sequential GUIDs for auto-assigned document ids (#245) — far friendlier to index locality than random GUIDs.
  • AsString enum LINQ predicates honor the JsonNamingPolicy (#224), computed-column indexes are usable by the LINQ translator (#225), on-the-fly event-store schema creation and InitialData seeding work on startup (#233), and IEventStore.Identity now varies by StoreName so multiple stores stay distinct (#208).

🆕 Document Metadata

I found this gap during CritterWatch development:(

A genuinely new capability area: opt-in document metadata, end to end — mirroring Marten’s metadata model so the two stores behave alike. Enable the columns you want with a fluent DSL (or attributes) (#251#252):

opts.Schema.For<Order>().Metadata(m =>
{
m.LastModifiedBy.Enabled = true;
m.CorrelationId.Enabled = true;
m.CreatedAt.MapTo(x => x.CreatedDate); // project a column onto your own member
})

Then read just the metadata for a row — no document body deserialization — via the new MetadataForAsync<T> API (#253):

 metadata = await session.MetadataForAsync(order);
// metadata.Version, .LastModified, .LastModifiedBy, .CorrelationId, .CausationId, ...

Rounding it out: an opt-in user_name (LastModifiedBy) event-metadata column (#248), auto-seeding of CorrelationId/CausationId from Activity.Current on session open (#250), and session-level Headers with SetHeader/GetHeader (#249).

🔭 Observability & CritterWatch

  • An opt-in polecat.event.append OpenTelemetry counter (#247) and runtime event-append observations via IEventStoreInstrumentation.AppendObserver (#215).
  • IDocumentStoreDiagnostics with an enriched mapping descriptor (#210), structured partitioning in the DocumentMappingDescriptor (#214), and metadata capabilities + an IEventStore bridge with tenant-scoped document diagnostics (#254) — the same descriptor surface Marten exposes, so CritterWatch sees Polecat stores the same way it sees Marten.

🆕 Range partitioning

This came from CritterWatch integration as well.

Declarative range partitioning for document tables (#257#212), now with a Marten-parity fluent surface — the classic time-series retention pattern:

// Marten manages the boundaries:
opts.Schema.For<MetricsSample>().PartitionOn(x => x.BucketEnd).ByRange(jan, feb, mar);
// Or let a DBA / pg_partman own SPLIT/SWITCH/DROP at runtime for retention:
opts.Schema.For<MetricsSample>().PartitionOn(x => x.BucketEnd).ByExternallyManagedRange(jan, feb)

ByExternallyManagedRange(...) provisions the partitions once and then never reconciles them, so a later schema apply won’t clobber the months your retention job has been splitting and dropping.

📖 Wolverine + Polecat integration guide · 📦 Polecat on GitHub


Marten

Three releases (9.10.0, 9.11.0, 9.12.0), with a mix of concurrency-hardening, new partitioning options, and — again — observability work feeding CritterWatch.

🐛 Concurrency & correctness

  • Close the mt_events_sequence gap on concurrent Quick OCC failures (#4771) — a first contribution from @KMDjkb. Under truly concurrent FetchForWriting + Quick-append writes to the same stream, a losing transaction could burn a sequence value it never rolled back, leaving a permanent gap that stalls the async daemon’s high-water mark. A new opt-in option takes a FOR UPDATE lock in the OCC path so the loser blocks and raises a clean concurrency error before consuming a sequence value — no schema migration required:opts.Events.UseExclusiveLockOnConcurrentAppends = true;
  • Fix a false ConcurrencyException from non-RETURNING event ops in a batched SaveChanges (#4784).
  • Repopulate mt_natural_key on projection rebuild (#4793) — the Marten side of the same natural-key fix that landed in Polecat.

🆕 Partitioning & queries

  • Range-partition a document table by a non-tenant date column (#4780) — the PartitionOn(member, cfg) API already existed; a Weasel 9.3.0 fix makes the date-keyed retention path stable across deployments and time zones (partition bounds are now compared by normalized instant rather than raw SQL literal, so migrations no longer report a spurious destructive rebuild).
  • Metadata-filtered document and event queries (#4792) — the diagnostics surface can now filter documents and events by correlation_id / causation_id / last_modified_by, honored only when the store actually captures that metadata column.

📖 Document storage & date range partitioning · 📖 Document & event metadata

🔭 Observability & CritterWatch

  • IDocumentStoreDiagnostics with an enriched mapping descriptor (#4776) and populated event/document metadata capabilities with tenant-scoped document diagnostics (#4790).
  • Runtime event-append observations via IEventStoreInstrumentation (#4783) and an exact-identity DeleteProjectionProgressByShardNameAsync for surgical projection-progress management (#4786).

The Common Thread

Three themes ran through all nine releases this week:

  1. Database-backed messaging matured — Wolverine’s PostgreSQL and SQL Server queues got faster, and now interoperate directly with MassTransit and NServiceBus over the same databases.
  2. Polecat got tougher — a stack of correctness fixes, sequential GUIDs, a full document-metadata story, and range partitioning.
  3. Everything got more observable — diagnostics descriptors, instrumentation hooks, OpenTelemetry counters, connection-state reporting, and credential-safe broker summaries across Wolverine, Marten, and Polecat — all converging on a single, consistent surface for CritterWatch to manage.

SQL Server as a Document Database — and why you want that!

While Polecat is pretty new, it’s based on over a decade of experience and usage patterns established by the Marten library (and also shares a ton of common infrastructure code as well). Polecat is also backed up by JasperFx Software and we’re available for either consulting or support agreements for Polecat usage.

If you’re a .NET developer, it’s pretty likely your default choice of database at work is SQL Server. If you follow technical content on LinkedIn or reddit from the .NET community, you’re absolutely bombarded with a deluge of content about EF Core with a smattering of Dapper. All of that content assumes that you’re using SQL Server as a relational database paired with an object-relational mapper (ORM) like Entity Framework Core — and all the mapping ceremony, migration scripts, and impedance mismatch that comes along for the ride.

A decade ago some friends and I set out to escape a lot of that friction with Marten, which turns PostgreSQL into a rock-solid document database and event store for .NET. Marten has been running in production systems since the fall of 2016. And while not every system is a great fit for a document database, the Marten community has been able to be far more productive than they would be using an ORM with PostgreSQL where a document database fits. The one persistent question we got from the .NET community the whole time was some variation of: “This is great, but my shop is a SQL Server shop. Can I have this on SQL Server?”

Now you can. Polecat is a new member of the “Critter Stack” that brings the same document database (and event sourcing) developer experience to SQL Server 2025 — taking direct advantage of SQL Server 2025’s brand new native json data type. If you know Marten, you already know Polecat; the public API surface intentionally mirrors Marten so the patterns and muscle memory carry straight over. If you’ve never touched Marten, this post is a gentle introduction to what a document database can do for your productivity.

Show Me the Whole Thing First

Before we break it down, here’s a complete, runnable console application — top-level statements, nothing hidden. Create a new console project, add the Polecat NuGet package, point it at a SQL Server 2025 instance, and run it. There’s no migration step and no mapping file to write first; this is the whole program.

// Program.cs
using Polecat;

const string connectionString =
    "Server=localhost,1433;Database=app;User Id=sa;Password=P@55w0rd;Encrypt=False";

// 1. Spin up the document store. In its default development settings it will
//    create any missing tables on demand the first time it needs them.
await using var store = DocumentStore.For(connectionString);

// 2. Write a couple of documents in a single ACID transaction.
await using (var session = store.LightweightSession())
{
    session.Store(new Customer { Region = "West Coast", Name = "Acme, Inc." });
    session.Store(new Customer { Region = "East Coast", Name = "Initech" });
    await session.SaveChangesAsync();
}

// 3. Query them right back with LINQ — this queries *inside* the JSON column.
await using (var query = store.QuerySession())
{
    var westCoast = await query
        .Query<Customer>()
        .Where(x => x.Region == "West Coast")
        .OrderBy(x => x.Name)
        .ToListAsync();

    foreach (var customer in westCoast)
    {
        Console.WriteLine($"{customer.Name} ({customer.Region})");
    }
}

// A plain POCO. No attributes, no DbContext, no mapping. Just a class.
public class Customer
{
    public Guid Id { get; set; }
    public string Region { get; set; }
    public string Name { get; set; }
}

Run that and you’ll see Acme, Inc. (West Coast) print to the console — and if you peek at the database, you’ll find a pc_doc_customer table that you never asked anyone to create. No mappings, no database migrations, nothing but just getting stuff done!

The rest of this post is just explaining why each of those steps was so short.

A Quick Start

To get going, all you really need is a connection string to a SQL Server 2025 database. SQL Server is very Docker-friendly, which makes it a great choice for local development and disposable test databases.

Here’s the absolute simplest “hello world.” Say I have a plain old C# class for a customer:

public class Customer
{
    public Guid Id { get; set; }
    public string Region { get; set; }
    public string Name { get; set; }
}

Now let’s persist one and read it back:

using Polecat;

await using var store = DocumentStore.For(
    "Server=localhost,1433;Database=app;User Id=sa;Password=...;Encrypt=False");

var customer = new Customer
{
    Region = "West Coast",
    Name = "Acme, Inc."
};

await using var session = store.LightweightSession();
session.Store(customer);
await session.SaveChangesAsync();

// ...and later, load it right back into the same shape
var loaded = await session.LoadAsync<Customer>(customer.Id);

That’s the whole thing. Two facts about that little sample are worth slowing down on, because they’re the entire pitch:

  1. We didn’t do any mapping. There’s no DbContext, no OnModelCreating, no fluent configuration, no attributes, nothing telling Polecat how to flatten Customer into columns. We just wrote a class.
  2. We didn’t create any database structure. We never wrote a CREATE TABLE, never authored a migration, never ran a script. In its default “just get things done” settings, Polecat detects that the table for Customer doesn’t exist yet and quietly builds it out for us the first time we read or write one.

Polecat is using JSON serialization to persist the data. As long as your type can round-trip to and from JSON, Polecat can store it and load it. That’s it — that’s the contract.

What’s a Document Database, and Why Should You Care?

A document database lets you store and retrieve whole object graphs — “documents” — almost always as JSON, where the database lets you marshal objects in your code straight to storage and query them right back into the same structures later.

The payoff is that you get to code much more productively because you just don’t have nearly as much friction as you do with object-relational mapping, whether that’s wrangling an ORM or hand-writing SQL and mapping code. You don’t have to maintain a parallel relational schema that’s a slightly-wrong reflection of your domain model. You don’t have to keep a stack of migration scripts in lockstep with every property you add. You design the class you actually want, and you store the class you actually want.

If you’ve spent any real time with EF Core, the contrast is stark. With EF Core you’re maintaining a mapping layer: configuring keys, owned entities, value conversions, navigation properties, and a migration history table — all so that a relational schema can approximate your object model. With Polecat there is no mapping layer to maintain. The document is the model.

The SQL Server 2025 Native json Type

Here’s where Polecat gets to lean on something genuinely new. Earlier document-on-SQL-Server attempts had to shove JSON into an nvarchar(max) column and hope for the best. SQL Server 2025 introduced a real, first-class json data type, and Polecat uses it by default for document bodies.

When you stored that Customer above, Polecat created a table named pc_doc_customer with the document serialized into a native json column called data:

CREATE TABLE [dbo].[pc_doc_customer] (
    [id]            uniqueidentifier  PRIMARY KEY NOT NULL,
    [data]          json              NOT NULL,   -- native SQL Server 2025 JSON
    [version]       bigint            NOT NULL,
    [last_modified] datetimeoffset    NOT NULL,
    [created_at]    datetimeoffset    NOT NULL,
    [tenant_id]     varchar(250)      NOT NULL,
    [dotnet_type]   varchar(500)      NULL
);

That native type isn’t just cosmetic — it’s stored in an optimized internal representation and lets the SQL Server query engine reach inside the JSON efficiently, which is exactly what makes the LINQ querying and indexing below practical instead of a parlor trick.

If you’re on a SQL Server instance older than 2025, Polecat has your back: flip one switch and it falls back to nvarchar(max) storage transparently.

await using var store = DocumentStore.For(opts =>
{
    opts.ConnectionString = connectionString;
    opts.UseNativeJsonType = false; // store JSON as nvarchar(max) on pre-2025 SQL Server
});

Integrating with Your Application

For a real application you’ll want Polecat wired into your IHost and dependency injection container. At this point in the .NET ecosystem it’s more or less idiomatic to use an Add[Tool]() method to integrate tools with your app, and Polecat follows that convention:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPolecat(opts =>
    {
        opts.ConnectionString = builder.Configuration.GetConnectionString("sqlserver");
    })
    .UseLightweightSessions();

var app = builder.Build();

From there your endpoints and services can inject IDocumentStore to open sessions, or inject an IDocumentSession / IQuerySession directly. A lightweight session is the lean, no-change-tracking workhorse — there’s no dirty checking or identity map overhead unless you ask for it.

You Still Get LINQ

Schemaless storage is great right up until somebody asks you to actually find something. A common worry is that going document-style means giving up real querying. Not here. Because the document body lives in that native json column, Polecat ships a LINQ provider that translates your C# expressions into SQL that queries inside the JSON:

await using var session = store.QuerySession();

var westCoast = await session
    .Query<Customer>()
    .Where(x => x.Region == "West Coast")
    .OrderBy(x => x.Name)
    .Take(25)
    .ToListAsync();

WhereOrderBy / OrderByDescendingSkip / TakeFirstOrDefaultAsyncCountAsyncAnyAsync — the usual LINQ vocabulary works against your documents. There’s even a paged-list helper for the extremely common “page N of these results, and tell me the total count” use case:

using Polecat.Pagination;

var page = await session
    .Query<Customer>()
    .OrderBy(x => x.Name)
    .ToPagedListAsync(pageNumber: 2, pageSize: 20);

// page.TotalItemCount, page.PageCount, page.HasNextPage, ...

…and You Still Get Indexes

It’s not only possible to query within the structured JSON data — you can also add indexes that work inside it, so those queries stay fast as your tables grow. Under the covers Polecat creates a persisted computed column that pulls a value out of the JSON with JSON_VALUE, then builds an ordinary nonclustered index on it. You don’t have to know any of that, though. You just declare the index.

You can do it inline on a property with an attribute:

using Polecat.Attributes;

public class Customer
{
    public Guid Id { get; set; }

    [Index]
    public string Region { get; set; }

    [UniqueIndex]
    public string Email { get; set; }

    public string Name { get; set; }
}

…or in your store configuration with a fluent, strongly-typed API that should feel awfully familiar to Marten users:

await using var store = DocumentStore.For(opts =>
{
    opts.ConnectionString = connectionString;

    opts.Schema.For<Customer>().Index(x => x.Region);
    opts.Schema.For<Customer>().UniqueIndex(x => x.Email);
});

Either way, the computed column and its index are created and kept in sync as part of the same “just works” schema management we’ll talk about next.

“It Just Works” Database Migrations

This is my favorite part, and it’s the thing that genuinely changes how fast you can move day to day.

In its default development settings, Polecat manages your database schema for you. The first time you touch a Customer, Polecat checks the database, sees that pc_doc_customer (and any indexes you declared) are missing, and builds them on demand. There’s no migration step standing between writing a class and running your code. This whole mechanism — schema detection, diffing, and migration — comes from the Critter Stack’s Weasel library that Polecat shares with Marten.

You control how aggressive that is with a single setting:

opts.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate; // the development default
// AutoCreate.All        — drop & recreate (great for tests)
// AutoCreate.None       — never touch the schema (production-locked)

For production you almost certainly don’t want the app altering schema on a hot path at runtime, so Polecat plugs into the Critter Stack’s stateful-resource model. Add the resource setup on startup and Polecat will reconcile the database schema once, up front, as the host boots:

builder.Services.AddPolecat(opts =>
{
    opts.ConnectionString = connectionString;
});

// provision/migrate all Polecat schema objects as the host starts
builder.Services.AddResourceSetupOnStartup();

The same machinery also drives the command-line tooling, so you can export migration scripts for a DBA to review, or apply changes through your deployment pipeline instead of at runtime. The point is that you get to decide — Polecat never makes “should this app change my production schema?” an accident.

Evolving Your Model Without Fear

Now put the JSON storage and the automatic schema management together, and you get the thing that makes document databases so liberating: your model can evolve at the speed of your code.

Say next sprint the Customer needs a phone number and a signup date:

public class Customer
{
    public Guid Id { get; set; }
    public string Region { get; set; }
    public string Name { get; set; }

    // new this sprint — no migration required
    public string PhoneNumber { get; set; }
    public DateOnly SignedUpOn { get; set; }
}

There is no ALTER TABLE. There is no migration script. There is no dotnet ef migrations add. Because the whole document is stored as JSON, new properties simply start showing up in the JSON the next time you save a customer. Documents written before the change deserialize cleanly — the new properties just come back as their defaults until that record gets re-saved. Compare that to the EF Core loop of “edit the entity, add a migration, review the generated SQL, apply it, hope the data backfill is right.” With Polecat you edit the class and keep going.

That’s the productivity story in a nutshell. The friction that normally sits between “I changed my mind about the model” and “the database agrees with me” mostly evaporates.

But Is It Safe? (Yes — It’s ACID)

A fair objection to a lot of document databases is that you trade away transactional integrity for that flexibility, and end up fighting eventual-consistency bugs. Polecat doesn’t make that trade. It’s built directly on SQL Server, which means it’s fully ACID-compliant. Every SaveChangesAsync() is one transaction. You can batch a whole range of inserts, updates, and deletes across multiple document types and commit them atomically, and an immediate query afterward sees exactly what you’d expect — no “give it a few hundred milliseconds and try again” caveats.

And because Polecat is, at bottom, a (rather fancy) library on top of SQL Server — one of the most widely deployed database engines on earth — adopting it doesn’t mean introducing a new piece of exotic infrastructure your ops team has never seen. You keep your existing SQL Server backups, your existing monitoring, your existing hosting and cloud options, and your existing DBA’s hard-won expertise. Polecat just changes how productively you get to use all of it.

Wrapping Up

Polecat brings the document-database developer experience that Marten users have enjoyed for years to the SQL Server world, built squarely on top of SQL Server 2025’s new native json type. You get to:

  • Get started in minutes — a connection string and a POCO, no mapping, no migrations.
  • Skip the ORM ceremony — there’s no mapping layer to maintain like there is with EF Core, because the document is the model.
  • Store documents in a real JSON column — SQL Server 2025’s native json type, not a stringly-typed nvarchar hack.
  • Keep your LINQ and your indexes — query inside the JSON and index inside it too.
  • Let schema migrations just work — automatic in development, controlled and explicit in production.
  • Evolve your model at the speed of your code — add a property, keep moving.
  • Keep ACID transactions and your existing SQL Server investment the entire time.

If your shop runs on SQL Server and you’ve ever envied how fast the document-database crowd seems to move, this is the one for you. Go grab the Polecat NuGet package, point it at a SQL Server 2025 instance, and write a class. That’s genuinely all it takes to get started.

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

Some Reflections on JasperFx’s 3rd Anniversary

I “officially” went solo with JasperFx Software in June of 2023 at the tender age of 49 because hopefully I’m a late bloomer. I’d of course been planning that specific move for quite some time and idly dreaming of being able to found my own company around my OSS passion projects for decades before that. I’ll be writing up something much more official in the JasperFx Software website next year for our 3rd Anniversary as we also officially launch our CritterWatch commercial tool next week, but I felt like jotting down some personal reflections as I wait on a bevy of CI runs to hopefully turn green.

I’ve stumbled around in my career from “real” engineering (petrochemical plants) to “Shadow IT” to kind of doing skunkworks type work in a huge company before fleeing their attempts to do CMMi and off to a high flying Extreme Programming consultancy. Since then its been a mixed bag of small to medium sized companies either doing consulting or product development. I’ve almost always been either a technical lead or architect or even some kind of “Director of Software Architecture,” but almost never felt particularly invested in my job.

I realized early on that I was always much more passionate about my personal work in whatever OSS development tool I was working on at the time. To that end, I’ve long known that I wanted a job building development tooling but it never quite worked out for me to land with a company that did that. My first big attempt at a big OSS tool for other devs that I thought could lead to eventual opportunities (FubuMVC) was a failure so bad that it put me in a multi-year funk. I’ve also been severely limited by being extremely risk averse, so I never had the guts (or wherewithal) to go solo and make the bet on myself and my portfolio of OSS tools.

I will say though that my times where I was actively mentoring other technical leads or architects was very enjoyable. I should say to anybody that I worked with that sees this that I genuinely enjoyed trying to be a mentor at a couple stops when you had to deal with me as the architecture team lead:) But again, that plays into the theme of “wanting to feel respected” that I didn’t realize was a thing for me until the past decade.

But anyway, flash foward a couple years, and Marten was becoming undeniably capable and successful. A few interactions with other people convinced me that there was a genuine professional opportunity there. At the same time, my previous job was clearly going South as we got all new C-level management from the outside. I fortunately had a once in a lifetime personal opportunity to try to do my own company instead. So here I am, three years into having my own company.

Our new CTO at the time told me directly not to come to a big meeting in our Dallas office because I wouldn’t add any value after I had asked to be involved specifically to meet him in person for the first time. Ouch. I might send him a little thank you note after this for helping give me an unintentional shove into what I really wanted to be doing in the first place!

The big takeaways for me are that I’m working harder than I ever have, but I love what I do most of the time. I especially love getting to roll out of bed knowing that I’m working on my tools and my vision every day — even when I’m helping clients. One other thing I very much appreciate is that JasperFx clients have specifically sought out my company because they wanted me to be involved and respect what I bring to the table. After a long career of not always feeling exactly respected and valued by management types, that’s turned out to be a very positive thing for me.

I’m constantly frustrated as hell at how long everything has taken to get rolling and that certain products still aren’t perfectly out, but also occasionally amazed at how much has gotten done and what the company has been able to achieve if you flip to the glass half full view of things.

The downsides are just that it’s a tremendous amount of stress, I get exhausted from the overhead of having a business, always being worried about where the next clients and the next work is going to come from, and never really feeling comfortable. And of course, I’m an American and our health care system is awful, so the health insurance angle is frequently stressful as it has limited my wife’s career options a little bit now that I don’t have company sponsored health insurance.

Result pattern or throwing exceptions? Wolverine says “neither”

Just a warning, unless a topic is personal, I’m going to start mostly writing on the JasperFx Software website to try to drive more traffic there.

Don’t throw exceptions for an entity that isn’t found or a value out of range — fair. But swapping that for Result<T> everywhere just trades one kind of noise for another: wrappers, .Match() calls, discriminated unions in every layer.

With Wolverine, most endpoints skip both:

  • [Entity(Required = true)] → your 404
  • FluentValidation → a 400 ProblemDetails
  • Endpoint stays clean, and the OpenAPI metadata still comes out right — no .Produces() calls, no IResult “mystery meat,” no fake C# discriminator union ugliness

Exceptions still earn their place for real failures — Wolverine turns those into retries, re-queues, and dead-lettering. But error handling shouldn’t be your control flow, and neither should a Result wrapper with idiomatic Wolverine usage.

Less ceremony, less noise. Full write-up with code in the comments 👇

Result Pattern or Exceptions for Errors? Wolverine Lets You Say “Neither”