Skip to content

Our Busiest Week Ever: The Critter Stack from September 28th to October 5th

Jeremy Miller5th October 2026
Critter StackMartenWolverinePolecatFisherWeaselNative AOTCommunity
The Critter Stack

This is a round up of everything that happened across the JasperFx GitHub organization from September 28th through October 5th. If you only read one section, read the one about the people who sent us pull requests, because this was one of our busiest weeks for community activity ever.

This past week was bonkers for GitHub activity and was definitely not the old XP ideal of a "sustainable pace":

Sept 28 to Oct 5
Releases shipped across Marten, Wolverine, Polecat, Fisher, Weasel, and the shared JasperFx libraries40
Pull requests merged across the public repositories267
Pull requests from people outside JasperFx38, from 11 contributors
Issues opened by people outside JasperFx39, from 22 different reporters
Commits to main across the public repositoriesroughly 500

I've been doing open source for a very long time now, and I honestly can't remember a single week where this many different people showed up with pull requests, detailed reproductions, and bug reports across this many repositories at once. Some of that is just the Critter Stack having more users than it used to, but a lot of it is the quality of the people who've decided to invest in these tools. More on them in a second.

Beyond the raw activity, four themes ran through the week:

  1. Native AOT problem reports led to us investing in a new CI run to prove out our AOT compliance on every push, by publishing real native binaries and running them
  2. Weasel got a lot deeper on MySQL, Oracle, and SQLite, and gained a brand new Firebird provider
  3. Wolverine's global partitioning got hardened by collaboration with a JasperFx client running it in a real cluster and finding the corners where the "only one node handles this group" promise apparently leaked
  4. Polecat and Fisher kept pace with Marten through every bit of it, with Marten issues frequently leading to similar issues we addressed in the newer stores

Thank you ​

Eleven people outside JasperFx had pull requests merged this week. In rough order of volume:

Marko LahmaFifteen pull requests. Thirteen to Weasel, including a complete Weasel.Firebird provider and a sweep of MySQL, Oracle, and SQLite migration fixes found through Quartz.NET's Weasel integration. Plus Marten no longer retries a command timeout and Wolverine's inline retry discards the failed attempt's outgoing messages.
Laurence GillianSix pull requests to Wolverine, headlined by [DeduplicatedWithResponse], the Stripe style idempotency key that replays the first response. Also user name relay into Marten, Polecat, and Fisher sessions from HTTP endpoints and two more deduplication edge cases.
Jaedyn (Hawxy)A long time Marten core team member. Reverted the HotCold leadership lock to session scope by default, fixed two LINQ regressions, fixed inline to async projection handling on an empty database, and fixed composite projection batch disposal in the shared libraries.
Ali YuksekkayaFour pull requests to Wolverine. The big one fixed a set of EF Core bugs around [Entity] on step methods, query plans, and multi-tenant DbContext resolution, several of which caused silent data loss. Also two OpenAPI accuracy fixes and a [FromClaim] code generation fix for URI claim types.
Anne Erdtsieck-WijnenStreaming large multipart uploads through a MultipartReader endpoint parameter in Wolverine.HTTP, and [Deduplicated] claims with a database per tenant. Also one of the best bug reports this project has ever received, reconstructing an agent control queue stall on a 512 database, 2,200 tenant cluster.
Aleš DvořákFound and fixed an EF Core mark-as-handled bug where one handler's commit retired every destination's copy of a fanned out message, then followed up so a handled inbox row gives up its owner on SQL Server.
Steve ZieglerBatched FetchForExclusiveWriting now holds the row lock when starting the transaction has to open a connection, and fixed a connection leak while there.
Chris CanalKept simple typed constructors out of the source generated EventTypes, and reported that async aggregate fetches could fold events in heap order.
Tom LonghurstFound and fixed Azure Service Bus dead letter moves silently never reaching the dead letter queue. Bisected it across 27 releases.
Robert (thechucklingatom)Each application can keep its CosmosDB store in its own container, so applications sharing a database are no longer forced into one cluster.
Yanyifan LiaoAn empty MassTransit response address is treated as no reply address in the interop path.

And then there are the people who didn't send code but sent something nearly as valuable, which is a reproduction good enough to fix from:

A meaningful share of what follows started as one of those reports. Thank you, all of you.

Native AOT: from "it should work" to "CI proves it works" ​

Native AOT has been one of our pillars for the Critter Stack in 2026. This was the week we stopped trusting the analyzers and started publishing real native binaries in CI.

Here's the uncomfortable thing we learned about Native AOT support in a code generating framework: every static check we had was blind to the failures that actually ship. Marten, Wolverine, and Polecat all had "AOT smoke" projects that compiled with IsAotCompatible=true and promoted every trimming and AOT analyzer warning to an error. Those lanes were green. And users were still hitting MissingMethodException and missing native code or metadata on the first document read or the first dispatched message of a natively published application.

The reason is that an analyzer answers "is this annotated correctly?" and a native image answers "does this generic instantiation exist?" Those are different questions. A MakeGenericMethod or CloseAndBuildAs over a value type like Guid works perfectly under CoreCLR, where the JIT can make the instantiation on demand, and fails only in a native image where it was never compiled. An [UnconditionalSuppressMessage] on the method hides it from the analyzer completely. So the only test that counts is to publish natively, check the publish log actually says Generating native code, and run the binary against a real database.

That is what every store has in CI now:

  • Marten has a dedicated aot-runtime job that publishes a native binary with ILC and executes it against PostgreSQL. This week it grew to cover strong-typed document ids and decimal comparisons, both of which it caught failing first.
  • Polecat gained an aot-runtime-smoke lane alongside the build-only aot-smoke lane. On its very first run it found that Polecat did not work under Native AOT at all, throwing from the DocumentStore constructor before a single document was written. By Polecat 5.36.0 the next day, seven of the eight execution shapes pass natively against SQL Server.
  • Fisher's CI now publishes its AOT consumer natively, asserts the native compile happened, and runs it. That smoke went from a single document write to covering strong-typed ids, document hierarchies, event appends, snapshots, the async daemon, subscriptions, side effects, raw SQL, and a second store over the course of six Fisher releases in one week.
  • Wolverine's CIAotSmoke target already published and ran a native image for plain messaging. This week it gained an HTTP lane that boots Wolverine.HTTP in a native image and serves a request, and a store backed saga lane, because until then no native image in CI had ever executed a chain that a persistence package composed. That gap is exactly why the saga, EF Core, and HTTP crashes reached us from users' production publishes rather than from CI.

Introducing those new CI builds led to finding and addressing a lot more AOT compliance issues that had slipped by us in the big "Critter Stack 2026" release cycle -- and honestly, I expect that to continue, but hopefully not too many more!

Two concrete results worth knowing if you're publishing natively today:

  • Marten strong-typed document ids work, with one registration per document type that does nothing under a JIT and roots the chain of generics under AOT:

    csharp
    opts.Schema.For<Invoice>();
    opts.RegisterValueTypeId<Invoice, InvoiceId, Guid>();
  • Wolverine sagas work, and so does grouping or deduplicating by a Guid property. Both of those closed a generic over a value type at startup and crashed the host before this week.

And one honest piece of bookkeeping. We audited all 284 trim and AOT suppressions in Wolverine and its relational stores. Of the 89 that justify themselves with a claim that the member never runs in a native image, roughly 62 are wrong. Most are safe anyway for reasons unrelated to what they claim, but the stated reason being wrong is the signature of a defect class regenerating faster than review catches it, and it's the argument for the runtime gates above rather than another cleanup pass.

The guides are at Marten, Wolverine, Polecat, and Fisher, and each one says what is measured rather than what we hope.

Weasel: deeper on every database, and a new one ​

We introduced Weasel properly back in August as the least famous member of the Critter Stack. It had a very good week.

Weasel is the schema migration and SQL generation engine under Marten, Polecat, Fisher, and every one of Wolverine's relational message stores. It shipped six releases this week, 9.36.0 through 9.41.0, and thirteen of the pull requests in them came from Marko Lahma, who maintains Quartz.NET and found several of these through Quartz.NET's own Weasel integration. That is the kind of external validation you can't buy.

Weasel.Firebird is the headline: a complete provider for Firebird 3, 4, and 5 at parity with Weasel.MySql, with tables, foreign keys including deferred cycles, indexes, identity and computed columns, sequences, views, PSQL functions, stored procedures, triggers, EF Core support, and CI on all three majors. Firebird joins PostgreSQL, SQL Server, MySQL, Oracle, and SQLite. It also shaped the core in small ways, because Firebird runs one statement per command, has no schemas, and has no IF NOT EXISTS, so every create is a guarded EXECUTE BLOCK and every statement gets its own transaction. Twenty-one thousand lines in one pull request.

The rest of Marko's work was a single shape of defect found over and over on the less used providers: a model declares something in a spelling the server rewrites when it stores it, so the column or index never matches itself, and every migration re-issues DDL that changes nothing. MySQL type synonyms, Oracle ANSI type names, index key direction on both, Oracle index tablespaces, and Oracle LONG fetch sizes all did this, and 9.36.0 fixed every one of them in both directions so a genuine difference is still reported as drift. On SQLite, an add-only table rebuild now keeps the columns and rows the model doesn't declare, which matters for any EF Core model since MapToTable opts into add-only migrations.

From our side, the week added:

  • SQL Server 2025 JSON indexes are now modeled as schema objects and read correctly. Before 9.37.0, the column query filtered them out, so every apply silently emitted a DROP INDEX for them. If you're on SQL Server 2025 with a JSON index, upgrade before your next migration.
  • IAdvisoryLock.FindHolderAsync can finally say who holds a distribution lock, which is the question an operator chasing a projection double runner actually has. Measured against live servers, which mattered, because pg_locks stores a negative lock id in a way the obvious query reports as unheld.
  • Advisory lock shutdown is bounded. A host stop could wait 60 to 120 seconds with nothing logged. 9.40.0 releases locks concurrently under a five second timeout.
  • Creation scripts are written in dependency order and re-run cleanly on all providers.
  • SQL Server batches end after a column change, so adding a column and a filtered index over it in one migration no longer fails with Invalid column name. Reported through Wolverine.
  • A failed migration releases the global lock, instead of locking every other replica out of migrating.

If you've ever wondered why we didn't just use EF Core migrations or a third party migration tool, this week is the answer. We need schema management that works identically across six database engines, settles to zero drift on every one of them, and can be driven from Marten, Polecat, Fisher, and Wolverine without any of them knowing which engine is underneath.

Wolverine's global partitioning, hardened ​

Global partitioning is the feature that lets Wolverine give you strictly ordered processing by group id across a whole cluster while still scaling out, with failover built in. As far as I know, nothing else in the .NET ecosystem does this. That also means nothing else in the .NET ecosystem has had its edge cases found by a user running it hard.

Alexandre Fresnais filed four issues this week that all come down to the same invariant being violated in different places: a node that does not own a partition slot executed a message under that slot's group id, concurrently with the real owner. That is precisely the thing partitioning exists to prevent, and each case leaked through a different door:

  • A scheduled message (GH-4673, fixed in 6.42.0). Slot ownership was evaluated at send time, but a scheduled message executes at due time, hours later and across a rebalance or deploy. The send-time shortcut parked the envelope at the companion local queue's address, which exists on every node by design, so whichever node's poller promoted it ran it.
  • A scheduled retry (GH-4700, fixed in 6.44.0). Same mechanism, but the first attempt was an immediate send so the send-time fix couldn't help. Promotion now asks "does this node own the slot?" when the retry comes due, and forwards it to the owner if not.
  • A replayed dead letter, or a gracefully stopped node's backlog (GH-4776, fixed in 6.46.0). The durability agent that recovers orphaned inbox rows is assigned per database and routinely lands on a node that doesn't own the slot. Now only the slot's owner recovers its companion queue, through a second recovery loop that starts and stops with slot ownership.
  • A slot moving to a joining node (GH-4777, fixed in 6.46.0). The leader already waited for a confirmed stop before starting the slot elsewhere. The problem was what that stop was worth: for a global partition slot, the latch and drain were both no-ops, so the agent reported a clean stop while the companion queue carried on executing beside the new owner. The companion queue is now latched and drained, with in-flight handlers finished or released, before the stop is confirmed.

Also related: the inferred grouping and deduplication rules both closed a generic over a value type at startup, so a Guid group id crashed a native image. Fixed in 6.46.0 as part of the AOT work above.

The feature is documented at Partitioned Message Processing.

The rest of Wolverine ​

Six Wolverine releases, 6.41.0 through 6.46.0. Beyond partitioning and AOT:

[DeduplicatedWithResponse] (6.45.0) is the one I'd point most people at. [Deduplicated] could stop a POST running twice, but it couldn't tell the second caller what the first one created, which is the wrong answer when the client is the one that lost its connection mid-request and has no idea what id it just made. Laurence Gillian's pull request stores the first response and replays it, status code, body, and Location header, with a 422 for a repeat that doesn't match the original bytes and a 409 for a repeat while the first is still running. It arrived with seventeen tests already passing.

csharp
opts.Durability.EnableDeduplicatedResponses = true;

[DeduplicatedWithResponse(DeduplicationScope.User | DeduplicationScope.Endpoint)]
[WolverinePost("/orders")]
public static OrderCreated Post(CreateOrder command, IDocumentSession session) { ... }

Streaming large uploads (6.42.0). Every existing form parameter in Wolverine.HTTP buffers the whole body before the endpoint runs. Anne Erdtsieck-Wijnen's pull request lets an endpoint take a MultipartReader directly, with the same limits as Request.Form, proper 415 and 400 responses, and startup failures if you try to combine it with anything else that reads the body.

One tenant's outage stops costing every tenant (6.42.0). Five fixes from Felipe Ramos's reports: a tenant-only inbox failure no longer pauses the listener for everyone, a deferred tenant failure no longer spins the listener, an envelope whose completion couldn't be written no longer stays owned by the live node, and the PostgreSQL envelope tables' timestamp default is a true UTC instant. Plus the one to read first: a durable publish outside a transaction that cannot be persisted now fails the caller instead of returning silently with the message gone.

The agent control queue stall (6.44.0). Anne's report of 5 to 7 MB agent commands being rejected on a 512 database, 37,000 agent cluster, with assignment stalling for 30 to 60 minutes after every node change. Stops were the one agent command that was never chunked, and one bad control row could kill a node's control queue. Both fixed, with optional gzip for agent command payloads.

A regression you should skip past. 6.44.0 moved the partitioned mark-as-handled command into shared code, and in doing so handed Marten's QueueSqlCommand a two statement string it rejects. With EnableInboxPartitioning = true and a handler committing through a Marten session, every durable inbox message was dead lettered. Anne caught it with 140,000 dead letters in twenty minutes and 6.45.0 fixed it the next day. If you're on 6.44.0 with inbox partitioning, go to 6.45.0 or later.

Also worth a line: AutoApplyTransactions() now fails the chain build when two persistence providers could own a handler instead of silently applying neither (6.41.0, and it is breaking on purpose). TypeLoadMode.Static honors DisableConventionalDiscovery(). SQLite's durable dequeue actually writes the inbox row before committing. MQTT no longer drops a persistent session's backlog before listeners exist. And Envelope.TenantId is normalized through TenantIdStyle the same way the message context always was.

Marten ​

Six Marten releases, 9.41.0 through 9.46.0.

If you use IsolationLevel.Serializable, upgrade. 9.41.0 fixed a silent lost update that had been present since the write retry pipeline shipped. PostgreSQL correctly detected a serialization conflict and Marten discarded the detection by retrying the already computed operations, so the second session's write landed on top of the first with no error anywhere. 40001 is never replayed now and surfaces as ConcurrentUpdateException, which is what you should have been handling all along.

The HotCold leadership lock is session scoped again (9.41.0, from Hawxy). The transaction scoped default meant the leader held an open transaction for as long as it held leadership, with everything that implies for vacuum on a long lived daemon. A follow on from Sven Claesson's report: the stop stall that change exposed is fixed in 9.46.0 by bounding the lock release.

Marten now surfaces failures it used to absorb (9.46.0). A command timeout is no longer retried on reads, because retrying it three times held the caller for four timeouts instead of one (Marko Lahma's fix, and it covers a server side statement_timeout too). Sticky, ambient, and external session lifetimes fail loudly through a new SessionTransactionUnusableException instead of silently reopening onto a different backend. The async serializable session builders actually produce a serializable session. And the async daemon spends one retry budget instead of running a handled failure up to sixteen times.

Fixes from the community: Steve Ziegler's batched FetchForExclusiveWriting fix, which also closed a leaked physical connection per batch. Hawxy's two LINQ regressions around nullable HasValue and literals in Select. Sven's DCB tag bug, where quick append could send stale ArrayPool<string> contents as tag values for untagged events. Chris Canal's report that async aggregate fetches could fold events in heap order rather than stream order. And a LINQ query against nested child collections generating the wrong locator.

Native AOT: strong-typed document ids work with RegisterValueTypeId, and the decimal comparison limitation is documented with a tested workaround. It's the BCL's expression tree factory rather than Marten, but it only shows up from a native binary and decimal is everywhere in the line of business apps most likely to try AOT.

Polecat and Fisher kept pace ​

I keep saying that the point of Marten, Polecat, and Fisher sharing contracts is that an improvement to one lands in all three. This week is a decent proof.

Polecat shipped six releases, 5.32.0 through 5.36.0, and Fisher shipped six, 1.14.0 through 1.19.0. Every shared contract change in the JasperFx libraries this week landed in all three stores within a day: tenant-aware stream compaction, store-wide document diagnostics, an explicit all-tenants query scope, store-agnostic batch reads with LoadManyAsync and FetchManyForWriting, and sub-classes in DocumentTypesAsync.

Polecat highlights beyond AOT:

  • Async projections on a tenant-partitioned store actually run (5.34.0). On a store using UseTenantPartitionedEvents, every async projection silently never advanced, and triage found four independent defects, any one of which was enough on its own. SQL Server reporting current_value = 1 for an untouched sequence was my favorite.
  • DeleteAllTenantDataAsync deletes every row belonging to one tenant on any conjoined store, with table discovery from sys.columns and delete order from the live foreign key graph. Marten and Fisher parity.
  • JSON indexes and full-text indexes are schema objects now, which means the last piece of raw document DDL in Polecat is gone and Weasel can migrate, compare, and assert on them like anything else.
  • A large IN list travels as one JSON array parameter instead of one parameter per value, with a real ceiling on the threshold.
  • Six scoped behavior changes in 5.36.0, listed in a table in the release notes. The one most likely to matter: a search over a hierarchy root now resolves each row to its concrete type instead of materializing everything as the root.

Fisher highlights beyond AOT:

  • FetchManyForWriting in two statements, however many streams you ask for.
  • Fetch by a strong-typed stream id, and live aggregation stamps a strong-typed aggregate id.
  • A read for an unknown tenant no longer creates it, and a tenant id's casing reaches the configured tenant all the way down.
  • Full-text search creates its index on first use rather than only by migration, and a word of only punctuation no longer empties a search.
  • AddFisherStore<T, TImplementation>() for a second store under Native AOT, since the one-argument overload builds its marker interface as a DispatchProxy that a native image can't create.

Fisher's shared compliance suite now stands at 678 tests across 58 suites, all green on net9.0 and net10.0, and Fisher remains covered by JasperFx support plans right alongside Marten, Wolverine, and Polecat.

And the rest ​

Bobcat had 54 commits in the week, mostly around rendering projected specifications and carrying a full, Storyteller grade step report over the wire from the test supervisor. CritterWatch kept pace with every one of the dependency bumps above the same day they shipped, which remains our best proxy for whether a given set of versions plays well together.

Upgrading ​

The dependency line moves as one: Marten 9.46.0, Wolverine 6.46.0, Polecat 5.36.0, Fisher 1.19.0, and Weasel 9.41.0, all on JasperFx 2.80.2. Take them together. Several items above are one shared contract change landing in four repositories, and a couple of the intermediate versions have cross-package floors. If you're on Wolverine 6.44.0 with inbox partitioning, don't linger there.

Three upgrade notes to read first:

  • Marten 9.41.0 changes what a Serializable session does on a conflict, and reverts the HotCold lock to session scope. Both are the correct behavior and both are visible.
  • Weasel 9.36.0 and up may emit DDL your next migration never emitted before, because several drift defects were fixed and a computed column change now drops and recreates its dependants. Read the Weasel release notes before you apply.
  • Wolverine 6.41.0 refuses to build a handler that two persistence providers could own. If that's you, you've been losing one of those writes all along. Add [Transactional(typeof(X))] and pick.

As always, if something here bit you, or should have and didn't, tell us in Discord or on the relevant GitHub repository. This week, more than any other I can remember, the fixes started as somebody else's carefully traced bug report. It shows, and we're grateful for it.

RSS Feed · All Rights Reserved.