We’ve had an undocumented until now API in Marten for years called EventProjectionScenario for declarative testing of Marten projections — somewhat based on the Scenario usage in our Alba library for ASP.Net Core testing. As part of some cleanup this week, I finally added some documentation and lifted that to where Polecat (Event Sourcing with SQL Server) can use it as well.
I’m more curious than anything to get some feedback here if anyone things this would be useful. After CritterWatch 1.0 lands, my attention is going to turn to the Critter Stack’s story for “Spec Driven Development,” and maybe this feature will be part of that.
Scripted Scenarios with EventProjectionScenario
For a more declarative way to test a projection end to end, Marten has a built-in scenario runner on IDocumentStore.Advanced that scripts a sequence of event appends and document assertions, then executes the whole sequence for you:
The scenario works with any projection lifecycle. If the store has any asynchronous projections registered, the scenario quietly spins up a projection daemon, waits for it to catch up after each batch of appended events, and shuts it down afterward — your test code looks identical either way.
A few things to know about how a scenario executes:
The Append() / StartStream() / AppendEvents() calls queue work — nothing touches the database until the scenario executes. The StartStream() overloads that generate their own stream id return that Guid so you can capture it for later assertions.
Consecutive appends are batched into a single commit; the pending work is saved whenever the next step is an assertion, and once more at the end of the scenario.
Assertion steps run against a query session after the projected data is up to date. DocumentShouldExist<T>() and DocumentShouldNotExist<T>() cover the common cases, and AssertAgainstProjectedData() is the general purpose hook for anything else.
If an action step fails, the scenario stops immediately — the remaining steps would be running against a state nobody intended. Failed assertions accumulate instead, and everything is reported at the end in a single ProjectionScenarioException that lists each step and what went wrong. Assertion failures inside the aggregate are typed as ProjectionScenarioAssertionException so tooling can tell them apart from infrastructure failures.
WARNING
By default the scenario deletes all event data plus the storage for every registered projection before it runs, so each scenario starts from a clean slate. Only use this feature against a test database! To run a scenario on top of existing data instead, set scenario.DeleteExistingData = false.
// Keep any existing event/projection data (the default is to wipe it)
scenario.DeleteExistingData=false;
// Apply the whole scenario to one tenant when using multi-tenancy
scenario.TenantId="tenant1";
// Maximum time to wait for async projections to catch up
// after each batch of events. The default is 30 seconds
scenario.Timeout=5.Seconds();
// ... queue up appends and assertions
});
We’ll also have the “Projection Stepper” feature in CritterWatch that will allow you to step through a series of events to see how a projection creates and modifies its view event by event. That functionality is part of the CritterWatch user interface, but also exposed via an MCP endpoint on CritterWatch for easy access for AI agents building and troubleshooting Critter Stack applications using Event Sourcing.
This is a potentially big performance optimization you can opt into starting with Marten 9.0. Not coincidentally, we’re using this for CritterWatch to help optimize the responsiveness and database size for a JasperFx client this week.
Marten can serialize individual event types to a binary wire format (MemoryPack, MessagePack, or anything else implementing IEventBinarySerializer) instead of the default JSON, trading a few of JSON’s ergonomic wins for a meaningful throughput and storage-size improvement on hot streams. See #4515 for the design discussion.
The opt-in is per event type — binary-serialized and JSON-serialized events coexist in the same mt_events table, so the feature can be rolled out on an existing store with no migration of existing data.
How it works
A second column, bdata bytea NULL, sits alongside the existing data jsonb NOT NULL on mt_events. The row-level discriminator is bdata IS NULL:
When
data
bdata
Event uses the JSON serializer
full JSON payload
NULL
Event uses an IEventBinarySerializer
the placeholder '{}'::jsonb
the serialized bytes
On read, Marten inspects bdata:
NULL → existing JSON deserialization path. Pre-feature rows continue to work without conversion.
Because the discriminator is on the row and the serializer is resolved per event type, the same stream can carry rows of either format with no special handling at the call site.
Quick start with Marten.MemoryPack
The companion Marten.MemoryPack NuGet package ships a ready-to-use IEventBinarySerializer over MemoryPack:
dotnet add package Marten.MemoryPack
Mark event types you want to serialize as binary with both [BinaryEvent] (so Marten picks them up) and [MemoryPackable] (so MemoryPack can serialize them):
If a type carries [BinaryEvent] but no per-type serializer was wired AND DefaultBinarySerializer is null, Marten throws at the first append with a remediation message naming both registration entry points.
Bring your own serializer
IEventBinarySerializer is small enough to implement directly against any binary format — MessagePack, protobuf, etc.:
publicinterfaceIEventBinarySerializer
{
byte[] Serialize(Typetype, objectdata);
objectDeserialize(Typetype, byte[] data);
}
The serializer is a singleton — keep its state thread-safe.
On-disk shape
For binary events, data holds the literal {} placeholder so the existing data jsonb NOT NULL constraint stays intact (no schema relaxation):
Purely additive: the only schema change is bdata bytea NULL on mt_events. Existing rows have bdata = NULL (the column’s default for prior data) and read through the JSON path. Marten’s standard schema migration creates the column for existing installations — no event data conversion required.
Append modes
Binary event serialization works with everyEventAppendMode Marten ships — Rich, Quick, and QuickWithServerTimestamps. The Quick modes route appends through the mt_quick_append_events PostgreSQL function, which carries a bdatas bytea[] parameter that’s inserted into mt_events.bdata in parallel with the existing bodies jsonb[]. BulkEventAppender (the COPY-based bulk loader) also supports binary events — its COPY column list includes bdata, and each event row writes either the binary payload or NULL.
You don’t have to think about the append mode: binary opt-in is per event type and works identically across all of them.
Schema evolution — use versioned event types
Marten’s existing event upcasters operate on the JSON wire form and don’t generalize to a byte[] payload, so they don’t apply to binary events. The recommended pattern for evolving a binary event’s shape is introduce a new event type for each version rather than upcasting in place:
The coexistence design lets old rows (written as TripStarted) and new rows (written as TripStartedV2) live on the same stream without migration.
Why not in-place backward-compatible schema changes?
You can lean on MemoryPack’s backward-compatible field evolution ([MemoryPackOrder], nullable fields, the VersionTolerant mode) for additive-only changes to a single event type. That works as long as the serializer itself can deserialize old payloads into the new shape — but the moment a change goes beyond the serializer’s tolerance rules (renaming, type changes, splitting a field), there’s no JSON-style upcaster path to fall back on. Versioning the event type works for every shape of change and stays explicit about which version each row was written with.
Mixing binary + JSON
If you have an existing JSON-serialized event and want a future version to go binary, the same pattern applies: define a new [BinaryEvent]-marked type for the new version, leave the old (JSON) type and its upcasters alone, and have the aggregate handle both. The per-row dispatch already copes with mixed formats on the same stream.
In many .NET systems, writing a web service that returns query results means some combination of:
Query data from EF Core — which is going to do who knows what to build up SQL, execute that, then spend some time materializing the raw database results into .NET objects
Since we’ve all been taught for years that it’s harmful to expose our internal entity shapes to the outside world, maybe you’re running the results through some kind of object to object mapping to a different DTO shape
Finally, after all the database querying and object mapping, you’ll finally use a JSON serializer to write results to the HTTP response stream
Whew. That’s a non-trivial amount of your time (or AI tokens) and a significant amount of runtime overhead with all the transformations and thrashing your memory with all the object allocations involved.
Now let’s talk about some capabilities in Marten and Polecat to sidestep the mass majority of that overhead in some cases — but first, I do need to say that if you’re using Event Sourcing, the persisted data in a Marten or Polecat database for query models is purpose built for clients as it is. No extra mapping necessary. In a way, the “AutoMapper” activity happens directly in projections for a system using Event Sourcing.
If you are building HTTP services on top of Marten or Polecat, both of these tools have a “JSON Streaming” feature that can be used to build very fast web services by writing the raw JSON stored in PostgreSQL or SQL Server directly to the HTTP response for the most efficient possible HTTP web services in the read side of a CQRS architecture.
Core team member Anne Erdtsieck just made some a bunch of extensions to Marten and Polecat‘s ability to stream the raw, persisted JSON data stored in the database straight to HTTP responses, and that makes now a good time to show off what we have.
For Minimal API endpoints (and for frameworks like Wolverine.Http that dispatch any IResult return value), Marten.AspNetCore (Polecat.AspNetCore has similar support) ships seven typed result wrappers that carry the streaming behavior above as endpoint return values while also contributing correct OpenAPI metadata:
Type
Source
Response shape
404 on miss?
StreamOne<T>
IQueryable<T> — regular Marten document query
Single T
yes
StreamMany<T>
IQueryable<T> — regular Marten document query
JSON array T[]
no (empty array = 200)
StreamAggregate<T>
IDocumentSession + stream id — event-sourced
Single T
yes
StreamPaged<T>
IQueryable<T> — regular Marten document query
Paged JSON envelope
no (empty page = 200)
StreamPagedByCursor<T>
IQueryable<T> (with OrderBy/ThenBy)
no (empty array = 200)
StreamEventState
IQuerySession + stream id — event stream
Single StreamStateResponse
yes
StreamEvents
IQuerySession + stream id — event stream
JSON array EventResponse[]
yes (configurable)
Each type implements both IResult (so ASP.NET Minimal API dispatches it via ExecuteAsync) and IEndpointMetadataProvider (so Swashbuckle, NSwag, and the built-in OpenAPI generator see the right response shape), while delegating the actual body write to WriteSingle/WriteArray/WriteLatest/WriteStreamState/WriteEvents. Returning one from an endpoint is a concise, typed alternative to writing the HTTP handshake manually.
Returns 200 application/json with the document JSON on a hit, 404 on a miss. Content-Length and Content-Type are set automatically, matching the behavior of WriteSingle<T>.
pageNumber is 1-based. totalItemCount and pageCount are computed from a count(*) OVER() window function added to the same SQL query that fetches the page, so the whole response — count and documents both — comes from a single database round trip. Documents inside items are streamed as raw, already-persisted JSON, without a deserialize/serialize step. An empty page still returns 200 with totalItemCount: 0, pageCount: 0, and an empty items array — never a 404.
Internally, StreamPaged<T> delegates to the IQueryable<T>.StreamPagedJsonArray() extension method described in the Paging docs, which can also be used directly (e.g. from an MVC controller action) instead of through the IResult wrapper.
Returns 200 application/json with the JSON of the latest projected aggregate state, or 404 if no stream exists. A constructor overload accepts string ids for stores configured with string-keyed streams.
StreamEventState — event stream metadata
Writes the high level metadata of a single event stream — Marten’s StreamState — as JSON, or 404 when the stream does not exist:
app.MapGet("/minimal/order/{id:guid}/state",
(Guidid, IQuerySessionsession)
=>newStreamEventState(session, id));
A constructor overload accepts a string stream key for stores configured with string-keyed streams.
The response body is a StreamStateResponse, not StreamState itself. StreamState.AggregateType is a System.Type, and System.Text.Json refuses to serialize those outright (Serialization and deserialization of 'System.Type' instances is not supported), so the aggregate type is projected down to its simple name in AggregateTypeName:
{
"id":"0198e1b4-5b1c-7a1e-9a3f-2f2f5b6c7d8e",
"key":null,
"version":2,
"aggregateTypeName":"Order",
"lastTimestamp":"2026-07-26T09:41:02.113Z",
"created":"2026-07-26T09:41:02.098Z",
"isArchived":false
}
StreamEvents — raw events of a stream 9.20
Writes the raw events of a single event stream as a JSON array:
app.MapGet("/minimal/order/{id:guid}/events",
(Guidid, IQuerySessionsession)
=>newStreamEvents(session, id));
StreamEvents carries the same optional version, timestamp, and fromVersion filters as FetchStreamAsync(), and there is a string stream key overload as well.
Elements are EventResponse, not IEvent itself — IEvent.EventType is a System.Type and hits the same System.Text.Json wall as above. Use eventTypeName, Marten’s stable event type alias, to discriminate event types on the client. The assembly qualified .NET type name (DotNetTypeName) is deliberately left off the wire:
FetchStream yields an empty list both for a stream that does not exist and for a filter that excludes every event, and the two cannot be told apart. StreamEvents therefore exposes an OnEmptyStatus that defaults to 404, matching the other single-resource results. Set it to 200 when running off the end of a stream is expected rather than exceptional — paging forward with fromVersion, for example:
// Paging forward through a stream: running off the end is expected, not a 404
I’m taking a little bit of time today to start writing up a “strategery” document on the JasperFx / Critter Stack approach to AI in the near future. One of the arguments I’m wanting to lean into quite heavily to call Wolverine AI-friendly is how it compresses code in its approach to “Vertical Slice Architecture” and how its focus on the “A-Frame Architecture” to testability has also made Wolverine AI friendly as well.
Formal layered architecture approaches like the Clean Architecture or other Hexagonal Architecture flavors have dominated the .NET landscape as the accepted “best practices” approach for years. It was a well intentioned strategy to improve maintainability through enforcing some level of loose coupling. Today though, there’s a new factor that’s annoyingly dominating seemingly all software development discourse: the AI coding agent.
An agent doesn’t get tired, but it does have a finite context window, and it pays — in tokens, in latency, and in accuracy — for every irrelevant file it has to load to understand one feature. The structure of your codebase is now, effectively, part of the prompt. And it turns out that the architecture that’s easiest for an agent to reason about is a vertical slice approach, which is conveniently enough, what I’d prefer to do anyway and something that Wolverine is very good at.
I want to make the case that the Critter Stack — and Wolverine specifically — is the best foundation and approach in .NET for vertical slice architecture in the age of coding agents, precisely because of how aggressively it compresses the code you have to write and read for each feature as well as the structural consistency you’ll get by following our recommended idioms.
Why layered architectures fight the agent
Picture the canonical “clean” layered solution: controllers in one project, application services in another, a pile of IRequest/IRequestHandler types, repository interfaces and their implementations, DTOs, mapping profiles, and a domain project underneath it all. To change one behavior — say, how a shipment gets created — an agent has to go find the controller, the request, the handler, the validator, the repository interface, the repository implementation, and probably a mapping profile or two. They live in six or seven directories, scattered among dozens of unrelated features that share those same folders.
Every one of those files has to be pulled into the agent’s context before it can safely make a change. Most of what it loads is irrelevant to the task. The signal-to-noise ratio in the context window collapses, and that’s exactly the condition under which agents start guessing — inventing abstractions you didn’t ask for, “fixing” error cases that can’t happen, and drifting away from the intent of the change. The architecture that was supposed to manage complexity ends up manufacturing context pollution.
This isn’t a hypothetical. It’s become the dominant theme in writing about AI-ready codebases over the last year, usually under the banner of locality of reference: keep everything a feature needs in one place, and the agent loads only what’s relevant. The conclusion practitioners keep arriving at is that feature-organized, vertical-slice code is simply easier — and cheaper — for an agent to work in than layer-organized code.
Vertical slices, and the ceremony problem
Vertical slice architecture, as Jimmy Bogard originally framed it, organizes code around features instead of technical layers. A slice owns its whole pathway: take the input, do the work, produce the output, all in one place. It’s no accident that the .NET community tends to conflate VSA with MediatR — they share an originator, and MediatR’s request-per-handler model nudges you naturally toward one self-contained unit per use case.
For the record, FubuMVC (Wolverine’s predecessor) also promoted what we now call “vertical slices,” but as Jimmy told me, does it matter if nobody used the tool? (ouch).
But here’s the thing, the “just use MediatR” approach still carries a surprising amount of ceremony. For one feature you typically write a request record, the IRequest<T> marker, a handler class implementing IRequestHandler<,>, constructor injection for every dependency, an explicit SaveChangesAsync, a separate call through IMediator/IPublisher to raise follow-on events, a Program.cs registration, and a pipeline behavior to wire up validation. The slice is co-located, which is good. But it’s not small. There’s a lot of structural code surrounding the two or three lines that actually express the business decision — and the agent has to read all of it.
If fewer files and tighter context are what make vertical slices good for AI, then the natural question is: how few artifacts can a slice actually be reduced to?
Wolverine takes the slice to its logical conclusion
This is where Wolverine earns its place in the conversation. Wolverine is, in effect, vertical slice architecture compressed about as far as the language allows. Consider a typical “create a shipment” slice the MediatR way:
Look at what disappeared. No marker interfaces — Wolverine discovers handlers by convention, so the type carries no IRequest/IRequestHandler noise. No constructor and no fields — dependencies arrive by method injection, declared right where they’re used. No manual transaction management — [Transactional] (or a global auto-transaction policy) lets Wolverine and Marten manage the unit of work and use the document session as a transactional outbox. No separate publish call — returning a value is publishing it, as a cascading message. The validator is still a plain FluentValidation validator, but it’s discovered and run by Wolverine’s middleware; there’s no pipeline behavior to hand-wire.
And also, the Wolverine version also integrates a transactional outbox capability for durable execution. Now that there is so much interest in modular monoliths right now as well, I think it’s going to be important for folks to consider asynchronous workflows between modules within the same system. Conveniently enough, Wolverine has very strong support for that through its in process queueing, transactional outbox for durability, and built in Open Telemetry tracing for visibility into the asynchronous workflows. MediatR and all the tools it has inspired typically do not have any of that.
What’s left is almost entirely the business decision — and that’s the whole point!
It compresses even further on the HTTP edge. With Wolverine.Http, the endpoint is the handler — there’s no controller calling a mediator calling a handler:
And if you’re doing event sourcing with Marten, the aggregate handler workflow collapses the load-decide-append-save dance into a single method that receives the current aggregate state and returns the resulting events:
Wolverine fetches and rehydrates the Order from its event stream, hands it to you, appends whatever events you return, and commits — transactionally — without you writing any of that plumbing. The slice is the decision and nothing else.
Mediator tools were valuable when you were using them within ASP.Net Core MVC architectures where MVC controllers organized around domain entities (think InvoiceController) had a tendency to get very bloated. MediatR absolutely provided value in those days for teams to mitigate and control the accidental complexity from that type of MVC controller approach. In my opinion though, using a “mediator” should be completely unnecessary with Wolverine.
Wolverine can of course be used as just a “mediator” too, but I tend to recommend against that in most cases.
Why compression is the feature for AI
Tightly co-located slices are good for agents; small slices are better. Three reasons it compounds:
The whole slice fits in context. When a feature is one record, one validator, and one short static handler, the agent can load the entire unit of work and still have headroom for the task. It never has to reconstruct a flow from fragments strewn across layers, which is precisely the situation where agents hallucinate.
There’s far less surface to get wrong. Boilerplate isn’t free for an agent — it’s more code to generate correctly, more interfaces to implement consistently, more registration to remember. Every artifact Wolverine removes is an artifact the agent can’t fumble. Returning a cascading message can’t drift the way a hand-written IPublisher.Publish call can be forgotten or mis-ordered.
It’s cheaper to operate. This one is easy to overlook. Fewer tokens loaded per task is a direct, recurring cost reduction every single time an agent touches the code — whether that’s a developer’s Copilot session, a Claude Code run, or an automated diagnostic agent. In a world where you may be paying per token to run agents against your system, the most compressed codebase is also the cheapest one to keep an agent working in.
And by the way, our new curated AI Skills for the Critter Stack will help you settle into idiomatic vertical slice usage with Wolverine and Marten that will fit well into AI usage. And it so happens that you can purchase access to those AI Skills from the JasperFx website🙂
Compression alone isn’t the whole story
I want to be honest about where “just write tiny handlers” stops being sufficient, because the failure mode is real. When every part is small and stateless, the burden of knowing how the parts wire together — what conventions discover a handler, what a cascading return actually does, what middleware runs around your method — doesn’t vanish. It shifts somewhere. If it shifts into the agent’s context as guesswork, you’ve traded one problem for another.
The answer is conventions plus documented context. Wolverine’s behavior is convention-driven, which means it’s learnable and, more importantly, encodable. This is exactly why we ship AI skill files for the Critter Stack: they give the agent the macrostructure that the compressed slice deliberately leaves implicit — handler discovery rules, the cascading-message model, when and how the transactional middleware applies, the idiomatic shape we actually want. The skills are the constitution; the slices are the code. Together they give an agent a codebase that is both minimal to read and unambiguous to extend. Compressed code without the conventions documented is just terse code. Compressed code with the conventions encoded is an architecture an agent can work in confidently.
Two practical notes in the same honest spirit. Wolverine generates the glue code around your handlers, and that generated code is a benefit for AI — the agent writes the small handler; the framework produces the plumbing it would otherwise have to read and reproduce — but your conventions should tell the agent plainly that generated code is not to be hand-edited, and explain the model so it doesn’t fight the generator. And the classic VSA critique about duplication at scale still applies: resist the urge to grow a sprawling shared “services” layer the moment two slices rhyme. Wolverine’s middleware and compound-handler patterns are the right place to absorb genuinely shared concerns without rebuilding the layered architecture you just escaped.
It’s still in flight, but we’ve put work into our forthcoming CritterWatch tool to give you visualizations of how messages flow between systems or even between handlers within the same system. I’ll be recording a video on that some time next week.
The takeaway
The industry is converging on vertical slices as the AI-friendly way to organize code, and it’s converging there for sound reasons: locality, focus, and a clean context window. Wolverine is the most thorough expression of that idea in .NET. It strips a slice down to the business decision, removes the ceremony that an agent would otherwise have to read and reproduce, and — paired with skill files that encode the conventions — gives a coding agent a codebase that is small, coherent, and cheap to reason about.
If your architecture is now part of the prompt, the move is to make that prompt as short and as clear as it can be. That’s been the Wolverine philosophy from the start. It just happens to be exactly what the agents want too.
Polecat 4.0 shares many more internals with Marten and I’m hopeful that it’s also much better for F# developers as well.
Wolverine and more so Marten do already have F# users, but we just made the deployment story a lot better in both tools for F# developers. One of the key components of Wolverine especially has been our usage of runtime code generation and compilation using Roslyn, which is how Wolverine is able to adapt to your application code instead of forcing you to write adapters to our specific interfaces or abstractions like basically every other application framework in .NET.
That’s the special sauce in Wolverine that allows your application code to be far simpler than it would be with other application frameworks, but it comes at the cost of Roslyn being a beast for memory consumption (sometimes, but not always), the size of the binaries shipped, and cold start times (again, sometimes). We’ve long had the ability in both Marten and Wolverine to pre-generate the Wolverine or Marten adapter code ahead of time and let it be compiled into the application itself to side step the Roslyn runtime issues. But in a story I’m sure is aggravatingly familiar for F# folks, that was only useful for C# projects as we could only generate C# code (and Roslyn only compiles C# code at runtime as far as I know, but feel free to correct me on that one).
I’m hopeful that these changes make Marten and Wolverine better for folks building and deploying systems with F#.
As we’ve been able to burn down so much of our backlog and other issues, I’ve had time to turn my attention to making our tools better for people who don’t code the exact same way I do. For example, we’ve invested a lot in the last year for the EF Core integration with Wolverine. Just this week we’ve made some progress toward making Wolverine better when folks insist on using more runtime IoC trickery that we would recommend. Along those lines, this post talks about how we hopefully got better for F# developers.
Just so I don’t have to have this conversation yet again, yes, we’re aware of Source Generators in .NET, and no, we don’t believe that it’s remotely possible to replace our usage of Roslyn in Wolverine with Source Generators without Wolverine becoming a much lesser tool because of how much runtime information we use to do the code generation. We have started using far more Source Generators in other elements of the Critter Stack though.
There’s and important Wolverine 6.1.0 enhancement release this week that follows up on the big Critter Stack 2026 wave with even more improvements to our “cold start” performance. Specifically today, I’d actually like to talk about how we improved and extended the command line diagnostics tools in the Critter Stack to make it faster and more useful — especially in our new world order of AI assisted software development.
If you’ll pay attention to a coding agent at work, you’ll notice that it’s doing a lot of brute force work through successive command line calls on your system. It turns out that exposing diagnostic information about your system or database or really any system state through command line tools that generate easily parseable information to stdout turns out to be a great way to enable AI agents. You could even say (with a cringe) that:
Now though, the AI utilization of command line tools brings us to some new needs:
It would be awfully nice to optimize the command line tools for faster cycles now that it’s a machine trying to utilize the output rather than a human who probably won’t hardly notice some minor delays due to command discovery and application bootstrapping
The command line output in some cases now needs to be optimized for terse, easily parsed data that will be read by the AI agent
Alright, back to the Critter Stack. Buried all the way at the bottom of our stack is a command line parser that originated with FubuMVC about 15 years ago. The value of our particular CLI tooling is that it allows you to utilize custom commands discovered in assemblies within your system. We’ve depended on that pretty heavily over the years to introduce quite a few built in utilities for managing dependencies, environment checks, projection rebuilds, and database management.
This seems to be a good point to give a shoutout to Spectre Console that we use internally to make our output a lot prettier than it would be otherwise.
Here’s a little visualization of the various commands hanging out across the Critter Stack:
And of course, many folks will utilize our command line discovery to create their own CLI commands for any number of their own custom diagnostics, batch job runners, or data loading tasks that can be baked directly into their application.
All good so far? It’s been a very useful subsystem for us, but the auto-magic wiring of commands from all these assemblies in your system was enabled by assembly scanning to discover concrete command types upfront.
For the recent “Critter Stack 2026” wave of releases, we switched the command discovery to relying on a source generator (JasperFx.SourceGenerator) to build in the discovery and even more of the parsing code up front to reduce the time it takes a JasperFx CLI enabled application to spin up and be ready to work. This was done quite purposely to optimize the cycle time for AI agent usage, especially if you’re running from pre-compiled code. We also made some optimizations to the command parsing itself too.
First off, if you have either Marten, Polecat, or Wolverine active in your codebase, you can add this line at the very bottom of your Program file (or Program.Main() method, same difference) that opts into the JasperFx command line runner:
// Opt into JasperFx for command line parsing to unlock the built in
// diagnostics and utility tools within your Wolverine application
// And this *exact* signature is important so that the exit code is
// correctly handled to denote failures!
returnawaitapp.RunJasperFxCommands(args);
You can verify that the JasperFx is enabled by:
dotnet run help
And also, dotnet run help [command name] to see the specific arguments and flag usage of any specific command.
Wolverine and Marten are a configuration-heavy with plenty of options that impact behavior. Conventions, policies, middleware, transports, and explicit routing all layer together, which is powerful — but it can leave you asking “what is my app actually doing?” For that, we have this command that’s writes out a textual report
dotnet run describe
describe prints a series of tabular reports about the running configuration:
Wolverine Options — the basics, including which assembly Wolverine thinks is your application assembly and which extensions loaded
Listeners — every configured listening endpoint and local queue, with how each is configured
Message Routing — where known, published message types are routed
Sending Endpoints — configured endpoints that send messages externally
Error Handling — a preview of the active message-failure policies
HTTP Endpoints — all Wolverine HTTP endpoints (only when WolverineFx.Http is in use)
Marten or Polecat configuration
This is also the report that I will most often ask you to paste when you need help online.
Step 2: See the Code Wolverine Generates
Wolverine generates the adapter code around your handlers and HTTP endpoints at startup. When you want to see it — what middleware ran, how dependencies resolve, where transactions wrap — write it out or preview it:
# Write all generated code to /Internal/Generated
dotnet run -- codegen write
# Or just dump it to the terminal
dotnet run -- codegen preview
codegen covers the whole app, which can be noisy. When you want to understand a single entry point, reach for codegen-preview under the wolverine-diagnostics parent command :
# A message handler (fully-qualified, short, or handler class name — fuzzy matched)
dotnet run -- wolverine-diagnostics codegen-preview --handler CreateOrder
# An HTTP endpoint (requires Wolverine.Http; format "METHOD /path")
dotnet run -- wolverine-diagnostics codegen-preview --route "POST /api/orders"
# A proto-first gRPC service (requires Wolverine.Grpc)
dotnet run -- wolverine-diagnostics codegen-preview --grpc Greeter
The output is identical to codegen preview, but scoped to one handler, so the signal-to-noise ratio is far higher. This command was 100% meant for AI tool usage, but it’s hopefully helpful for human usage. We have been investing in JasperFx’s curated AI skills to “know” how to use these tools to troubleshoot Critter Stack application building inside of AI coding agent work.
We’re going to add an interactive mode to the new wolverine-diagnostics tool soon for easier human usage.
Step 3: Understand Where and Why Messages Route
describe shows you the routing table. When you need to focus on one message type — or understand why it routes the way it does — use describe-routing :
# One message type (full name, short name, or fuzzy match)
dotnet run -- wolverine-diagnostics describe-routing CreateOrder
# The complete routing topology
dotnet run -- wolverine-diagnostics describe-routing --all
For a single type you get the local handler, a routes table (destination, local vs. external, Buffered/Durable/Inline mode, outbox enrollment, serializer, and how each route was resolved), and any message-level attributes such as [DeliverWithin].
The most useful flag is --explain , which walks Wolverine’s route-source chain in order and shows what each source produced and which terminating source short-circuited the rest:
dotnet run -- wolverine-diagnostics describe-routing CreateOrder --explain
# Same explanation as structured JSON, for tooling or AI agents
dotnet run -- wolverine-diagnostics describe-routing CreateOrder --json
This is the command-line surface over the IWolverineRuntime.ExplainRoutingFor(Type) API. The text output is deliberately stable and labeled so it reads well for humans and parses cleanly for automated tooling. See Troubleshooting Message Routing for the programmatic side.
Step 4: Troubleshoot Handler Discovery
If you expected Wolverine to find a handler but it isn’t running, ask Wolverine to explain its discovery decision for that type:
# By handler class name (also accepts a fully-qualified name or a fuzzy partial match)
dotnet run -- wolverine-diagnostics describe-handlers CreateOrderHandler
The argument is matched against the types in your application, and if it matches more than one type you get a report for each. Every report tells you whether the type’s assembly is being scanned, which type-level include/exclude rules HIT or MISS, and — per method — whether it satisfies the handler naming and signature conventions. It’s the command-line surface over WolverineOptions.DescribeHandlerMatch(Type), so you don’t have to drop a temporary Console.WriteLine(...) into your bootstrapping code (see Troubleshooting Handler Discovery).
Step 5: Check Your Infrastructure
Wolverine’s transports and the durable inbox/outbox register self-diagnosing environment checks — can I reach the database? the broker? are the IoC registrations valid?
dotnet run -- check-env
To create, inspect, or tear down the stateful infrastructure Wolverine needs (queues, tables, topics):
resources setup is a great way to provision a clean environment before a test run.
Step 6: Inspect and Recover Message Storage
For applications using the durable inbox/outbox, the storage command administers the message store:
dotnet run -- storage counts # incoming / outgoing / scheduled / dead-letter / handled
dotnet run -- storage clear
dotnet run -- storage rebuild # --file to emit the schema script
dotnet run -- storage release --exception-type Some.Exception # replay dead-lettered messages
storage counts is the quick “is anything backing up?” check, and release re-queues dead-lettered envelopes (optionally filtered to a single exception type). To purge inbox rows already marked Handled:
dotnet run -- clear-handled
Step 7: Export a Full Snapshot with capabilities
dotnet run -- capabilities wolverine.json
Writes a complete JSON description of the application — settings, message types, message store, messaging endpoints, even configured event stores. It’s useful for support, for feeding external tooling, and for detecting unintended configuration drift between deployments.
Bonus: Generate OpenAPI Offline (Wolverine.Http)
If you use WolverineFx.Http, you can generate the OpenAPI document without starting the host — no database or broker required, which makes it CI-friendly:
dotnet run -- openapi --list # list document names from AddOpenApi()
dotnet run -- openapi -d v1 -o swagger.json # generate a document to a file
dotnet run -- openapi --route "GET /orders/{id}"
But wait, what if you’re using Aspire as a de facto replacement for docker compose locally such that your applications can’t really run without Aspire being started up first? How is that going to work?
Um, I need to have a better answer for that myself. Let me get back to you on that one!
So, what’s JasperFx Software’s game plan for AI?
We’re going to throw all the spaghetti up against the wall!
More seriously, we’re going to bet on the command line usage described here + AI Skills combination as our first step. We’re also building quite a bit of MCP support into the CritterWatch commercial tooling that will hopefully be generally available in the next couple weeks. The last bit of planned spaghetti is some spec driven development experimentation we’re doing off in the background that’s going down a Behavior Driven Development strategy rather than trying for any kind of user interface modelling approach.
To try to explain “Dynamic Consistency Boundary” usage in Event Sourcing, I’d contrast it to “traditional” Event Sourcing where events are only organized into a stream of related events. For example, all the events related to a single invoice in an invoicing system are an example of an event stream. DCB came about because Axon IQ has weak consistency and couldn’t support transactions across multiple streams the way that Marten or Polecat can it’s often impossible to model a system where every operation only involves a single event stream. To that end, folks created the idea of “Dynamic Consistency Boundary” Event Sourcing where events are more organized by tags and the event stores that support DCB are able to enforce transactional boundaries based on an event tag query (think: all the events of these types that are related to either this class id, student id, or instructor id) so that systems can be much more flexible over time.
Marten has had support for the Dynamic Consistency Boundary approach (DCB) to Event Sourcing for a little while. The Marten 9.0 release last week added a new, potentially more performant option for DCB using the PostgreSQL HSTORE extension — which is supported by all the major cloud providers plus specialized cloud providers for managed PostgreSQL like Neon and Supabase.
Unfortunately, we don’t yet have a way to retroactively switch from “classic” DCB to the HSTORE style DCB in an existing application, but let’s say that you’re starting:
A greenfield application
A problem domain where the event stream boundaries either aren’t clear upfront or you think will never cleanly line up neatly in terms of event streams
You might want to adopt DCB style Event Sourcing from the get go, then use the HSTORE flavor of Marten DCB to be more performant. To get started, just opt into that style of DCB storage like this:
// This is all you need to do, but this does assume
// that the HSTORE extension is available
opts.Events.DcbStorageMode=DcbStorageMode.HStore;
});
The HSTORE style of DCB is a big performance improvement if you are querying events by two or more tag values at a time, which I’d probably argue is the only time DCB is worthwhile to use from a logical structure perspective anyway:)
I was the technical lead of a very successful software project for supply chain management in the early 2000’s. The most popular feature within that early web application was a last minute throw in report that I did as a favor for our business contact that wasn’t even part of our original specification. Once the system was live and folks found out about that report, we actually had to add new servers to the application cluster to keep up with the unexpected load just because that one single report was so popular with supply chain analysts.
My only point there is that I’m not always sure what features will actually resonate with users. Sometimes you know based on reported friction that a new feature will eliminate a pain point, but with the DCB support in Marten and Wolverine, I flat out don’t know. DCB is very popular in the Event Sourcing community outside of the Critter Stack, and it was clear we had to have that feature set just to be competitive from an adoption perspective, but I’m not seeing a lot of interest in DCB from our existing community as Marten is much more able to handle more flexible transactional consistency across event streams than specialist Event Store databases seem to be able to do.
But, if DCB is something you’re interested in or just works much easier for your mental model of how the domain should be modeled in your system, Marten has you covered!
This is in lieu of the “official” release post I’ll make early next week after the Memorial Day holiday when I have enough rest and energy to do something much better by hand. Until then, here’s a peek at the new releases we’ll be announcing early next week — but with some published updates to our AI Skills tomorrow morning that will help you do the migrations!
The whole stack just shipped to NuGet — a coordinated major release across every Critter Stack project, built on a shared JasperFx 2.0 foundation and targeting .NET 9 + .NET 10. Headline themes: AOT-friendly, faster cold-start, and Marten ↔ Polecat dedupe onto shared infrastructure.
(Quick notes for now — a full writeup written by an actual human being with benchmarks lands next week.)
JasperFx 2.0 (foundation)
The shared base for the whole stack — also ships JasperFx.Events 2.0 + JasperFx.RuntimeCompiler 5.0.
AOT-compatible core; runtime Roslyn split into JasperFx.RuntimeCompiler so Static-mode apps drop it and the trimmer removes Roslyn.
Source generators for projection dispatch (FEC-free), CLI command discovery, and options descriptions — less runtime reflection.
Database-primitive foundation on JasperFx 2.0; the consolidation home for IStorageOperation, OperationRole, BulkInsertMode, and the SQL Server advisory lock (dedupe pillar).
Postgres / SQL Server / Sqlite / MySql / Oracle / EF Core providers, all 9.0.0.
No more runtime code generation — Roslyn is gone. Closed-shape document/event storage + Marten.SourceGenerator compiled queries. No codegen write step for Marten; AOT-publishable in Static mode.
Best-perf defaults flipped on: Quick append w/ server timestamps, advanced async tracking, bigint events, lightweight sessions, System.Text.Json. One-line revert with opts.RestoreV8Defaults().
IRevisioned.Version stays int (V8-compatible); new ILongVersioned for long. DCB gains optional HSTORE tag storage + identity-less boundary aggregates.
SQL Server / EF Core-rooted event sourcing on the shared foundation — source-generator-first and AOT-clean end to end.
Adopted the lifted JasperFx.Events async-daemon abstractions; folded SqlServerAppLock into Weasel.SqlServer.AdvisoryLock; SingleTenant lock-ids now align with Marten.
⚠️ Runtime codegen decoupled from core. Apps in the default Dynamic mode must add WolverineFx.RuntimeCompilation (dev/test), or pre-generate via codegen write + Static mode (prod). This is the #1 upgrade gotcha — see the migration guide.
⚠️ ServiceLocationPolicy.NotAllowed is the new default — restructure registrations or call opts.RestoreV5Defaults() to revert.
AOT-clean; Tier-1 cold-start static handler registry. Newtonsoft extracted to WolverineFx.Newtonsoft; IForwardsTo<T> discovery is now explicit.
Upgrading? Bump the whole stack in lockstep — JasperFx 2.0 / Weasel 9.0 / Marten 9.0 / Polecat 4.0 / Wolverine 6.0. Each migration guide above has an at-a-glance table of breaking changes. The big perf/benchmark writeup is coming next week. 🚀
I’ve been piddling this weekend with testing out JasperFx Software‘s soon to be officially curated AI Skills. To test and refine those new skills, I’ve been using my buddies Chris Woodruff and Joseph Guadagno‘s MoreSpeakers application as a sample application to port to Wolverine and Marten (and a half dozen others too so far).
I’m sure that you’ll be positively shocked to know that it’s taken quite a bit of minor corrections and “oh, yeah” enhancements to the guidance in the skills to get to exactly where I’d want the translated code to get to. It’s not exactly this bad, but what it’s most reminded me of was my experience coaching youth basketball teams of very young kids when I constantly kick myself after the first game for all the very basic basketball rules and strategies I’d forgotten to tell them about.
Anyway, on to the Marten and Wolverine part of this. Consider this HTTP endpoint in the translated system:
Pretty common request to run a query against the database, then stream the results down to the HTTP response. I’ll write a follow up post later to discuss the greater set of changes, but let’s take that endpoint code above and make it a whole lot more efficient by utilizing Marten.AspNetCore‘s ability to just stream JSON write out of the database like this:
publicstaticclassGetExpertiseCategoriesEndpoint
{
[WolverineGet("/api/expertise")]
// It's an imperfect world. I've never been able to come up with a syntax
// option that would eliminate the need for this attribute that isn't as ugly
The version above is 100% functionally equivalent to the first version, but it’s a lot more efficient at runtime because what it’s doing is writing the JSON directly from the database (Marten is already storing state using PostgreSQL’s JSONB type) right to the HTTP response byte by byte.
And just to be silly and be even more serious about the optimization, let’s introduce Marten’s compiled query feature that completely eliminates the runtime work of having to interpret the LINQ expression into an executable plan for executing the query:
// Compiled query — Marten pre-compiles the SQL and query plan once,
// then reuses it for every execution. Combined with WriteArray(),
// the result streams raw JSON from PostgreSQL with zero C# allocation.
That’s a little bit uglier code that we had to go out of our way to write compared to the simpler, original mechanism, but that’s basically how performance optimization generally goes!
At no point is it ever trying to deserialize the actual ExpertiseCategory objects in memory. There are of course some limitations or gotchas:
There’s no anti-corruption layer of any kind, and this can only send down exactly what is persisted in the Marten database. I’ll tackle this in more detail in a follow up post about the conversion, but I’m going to say I don’t really think this is a big deal at all, and we can introduce some kind of mapping later if we want to change what’s actually stored or how the JSON is served up to the client.
You may have to be careful to make Marten’s JSON storage configuration match what HTTP clients want — which is probably just using camel casing and maybe opting into Enum values being serialized as strings.
But now, let’s compare the code above to what the original version using EF pCore had to do. Let’s say it’s about a wash in how long it takes Marten and EF Core to translate the
EF Core has to parse the LINQ expression and turn that into both SQL and some internal execution plan about how to turn the raw results into C# objects
EF Core executes the SQL statement, and if this happens to be a .NET type that has nested objects or collections, this could easily be an ugly SQL statement with multiple JOINs — which Marten doesn’t have to do all
EF Core has to loop around the database results and create .NET objects that map to the raw database results
The original version used AutoMapper in some places to map the internal entities to the DTO types that were going to be delivered over HTTP. That’s a very common .NET architecture, but that’s more runtime overhead and Garbage Collection thrashing than the Marten version
My buddies used an idiomatic Clean/Onion Architecture approach, so there’s a couple extra layers of indirection in their endpoints that require a DI container to build more objects on each request, so there’s even more GC thrasing. It’s not obvious at all, but in the Wolverine versions of the endpoint, there’s absolutely zero usage of the DI container at runtime (that’s not true for every possible endpoint of course).
ASP.Net Core feeds those newly created objects into a JSON serializer and writes the results down to the HTTP response. The AspNetCore team has optimized the heck out of that process, but it’s still overhead.
The whole point of that exhaustive list is just to illustrate how much more efficient the Marten version potentially is than the typical .NET approach with EF Core and Clean Architecture approaches.
I’ll come back later this week with a bigger post on the differences in structure between the original version and the Critter Stack result. It’s actually turning out to be a great exercise for me because the problem domain and domain model mapping of MoreSpeakers actually lends itself to a good example of using DCB to model Event Sourcing. Stay tuned later for that one!
For non basketball fans, the NCAA Tournament championship game broadcasts end each year with a highlight montage to a cheesy song called “One Shining Moment” that’s one of my favorite things to watch each year.
The Critter Stack community is pretty much always busy, but we were able to make some releases to Marten, Polecat, and Wolverine yesterday and today that dropped our open issue counts on GitHub to the lowest number in a decade. That’s bug fixes, some long overdue structural improvements, quite a few additions to the documentation, new features, and some quiet enablement of near term improvements in CritterWatch and our AI development strategy.
Wolverine 5.28.0 Released
We’re happy to announce Wolverine 5.28.0, a feature-packed release that significantly strengthens both the messaging and HTTP sides of the framework. This release includes major new infrastructure for transport observability, powerful new Wolverine.HTTP capabilities bringing closer parity with ASP.NET Core’s feature set, and several excellent community contributions.
Last week I took some time to do a “gap analysis” of Wolverine.HTTP against Minimal API and MVC Core for missing features and did a similar exercise of Wolverine’s asynchronous messaging support against other offerings in the .NET and Java world. This release actually plugs most of those gaps — albeit with just documentation in many cases.
Highlights
🔍 Transport Health Checks
This has been one of our most requested features. Wolverine now provides built-in health check infrastructure for all message transports — RabbitMQ, Kafka, Azure Service Bus, Amazon SQS, NATS, Redis, and MQTT. The new WolverineTransportHealthCheck base class reports point-in-time health status including connection state and, where supported, broker queue depth — critical for detecting the “silent failure” scenario where messages are piling up on the broker but aren’t being consumed (a situation we’ve seen in production with RabbitMQ).
Health checks integrate with ASP.NET Core’s standard IHealthCheck interface, so they plug directly into your existing health monitoring infrastructure.
This was built specifically for CritterWatch integration. I should also point out that CritterWatch is now able to kickstart the “silent failure” issues where Marten/Polecat projections claim to be running, but not advancing and messaging listeners who appear to be active but also aren’t actually receiving messages.
🔌 Wire Tap (Message Auditing)
Implementing the classic Enterprise Integration Patterns Wire Tap, this feature lets you record a copy of every message flowing through configured endpoints — without affecting the primary processing pipeline. It’s ideal for compliance logging, analytics, or debugging.
opts.ListenToRabbitQueue("orders")
.UseWireTap();
Implement the IWireTap interface with RecordSuccessAsync() and RecordFailureAsync() methods, and Wolverine handles the rest. Supports keyed services for different implementations per endpoint.
This feature is meant to be a new type of “declarative invariant” that will enable Critter Stack systems to be more efficient. If this is used with other declarative persistence helpers in the same HTTP endpoint or message handler, Wolverine is able to opt into Marten’s batch querying for more efficient code.
New [DocumentExists<T>] and [DocumentDoesNotExist<T>] attributes let you declaratively guard handlers with Marten document existence checks. Wolverine generates optimized middleware at compile time — no manual boilerplate needed:
A community contribution that adds first-class support for Confluent Schema Registry serialization with Kafka topics. Both JSON Schema and Avro (for ISpecificRecord types) serializers are included, with automatic schema ID caching and the standard wire format (magic byte + 4-byte schema ID + payload).
This release brings a wave of HTTP features that close the gap with vanilla ASP.NET Core while maintaining Wolverine’s simpler programming model:
Response Content Negotiation
New ConnegMode configuration with Loose (default, falls back to JSON) and Strict (returns 406 Not Acceptable) modes. Use the [Writes] attribute to declare supported content types and [StrictConneg] to enforce strict matching per endpoint.
This is orthogonal to Wolverine’s error handling policies.
Handler and middleware methods named OnException or OnExceptionAsync are now automatically wired as exception handlers, ordered by specificity. Return ProblemDetails, IResult, or HandlerContinuation to control the response:
public static ProblemDetails OnException(OrderNotFoundException ex)
{
return new ProblemDetails { Status = 404, Detail = ex.Message };
Direct integration with ASP.NET Core’s output caching middleware via the [OutputCache] attribute on endpoints, supporting policy names, VaryByQuery, VaryByHeader, and tag-based invalidation.
Form endpoints automatically require antiforgery validation. Use [ValidateAntiforgery] to opt in non-form endpoints or [DisableAntiforgery] to opt out. Global configuration available via opts.RequireAntiforgeryOnAll().
Documentation and examples for Server-Sent Events and streaming responses using ASP.NET Core’s Results.Stream(), fully integrated with Wolverine’s service injection.
Marten 8.29.0 Release — Performance, Extensibility, and Bug Fixes
Marten 8.29.0 shipped yesterday with a packed release: a new LINQ operator, event enrichment for EventProjection, major async daemon performance improvements, the removal of the FSharp.Core dependency, and several important bug fixes for partitioned tables.
New Features
OrderByNgramRank — Sort Search Results by Relevance
You can now sort NGram search results by relevance using the new OrderByNgramRank() LINQ operator:
varresults=awaitsession
.Query<Product>()
.Where(x=>x.Name.NgramSearch("blue shoes"))
.OrderByNgramRank(x=>x.Name, "blue shoes")
.ToListAsync();
This generates ORDER BY ts_rank(mt_grams_vector(...), mt_grams_query(...)) DESC under the hood — no raw SQL needed.
EnrichEventsAsync for EventProjection
The EnrichEventsAsync hook that was previously only available on aggregation projections (SingleStreamProjection, MultiStreamProjection) is now available on EventProjection too. This lets you batch-load reference data before individual events are processed, avoiding N+1 query problems:
ConfigureNpgsqlDataSourceBuilder — Plugin Registration for All Data Sources
A new ConfigureNpgsqlDataSourceBuilder API on StoreOptions ensures Npgsql plugins like UseVector(), UseNetTopologySuite(), and UseNodaTime() are applied to everyNpgsqlDataSource Marten creates — including tenant databases in multi-tenancy scenarios:
This is the foundation for external PostgreSQL extension packages (PgVector, PostGIS, etc.) to work correctly across all tenancy modes.
And by the way, JasperFx will be releasing formal Marten support for pgvector and PostGIS in commercial add ons very soon.
Performance Improvements
Opt-in Event Type Index for Faster Projection Rebuilds
If your projections filter on a small subset of event types and your event store has millions of events, rebuilds can time out scanning through non-matching events. A new opt-in composite index solves this:
opts.Events.EnableEventTypeIndex=true;
This creates a (type, seq_id) B-tree index on mt_events, letting PostgreSQL jump directly to matching event types instead of sequential scanning.
And as always, remember that adding more indexes can slow down inserts, so use this judiciously.
Adaptive EventLoader
TL;DR: this helps make the Async Daemon be more reliable in the face of unexpected usage and more adaptive to get over unusual errors in production usage.
Even without the index, the async daemon now automatically adapts when event loading times out. It falls back through progressively simpler strategies — skip-ahead (find the next matching event via MIN(seq_id)), then window-step (advance in 10K fixed windows) — and resets when events flow normally. No configuration needed.
See the expanded tuning documentation for guidance on when to enable the index and how to diagnose slow rebuilds.
FSharp.Core Dependency Removed
Marten no longer has a compile-time dependency on FSharp.Core. F# support still works — if your project references FSharp.Core (as any F# project does), Marten detects it at runtime via reflection. This unblocks .NET 8 users who were stuck on older Marten versions due to the FSharp.Core 9.0.100 requirement.
If you use F# types with Marten (FSharpOption, discriminated union IDs, F# records), everything continues to work unchanged. The dependency just moved from Marten’s package to your project.
Bug Fixes
Partitioned Table Composite PK in Update Functions (#4223)
The generated mt_update_* PostgreSQL function now correctly uses all composite primary key columns in its WHERE clause. Previously, for partitioned tables with a PK like (id, date), the update only matched on id, causing duplicate key violations when multiple rows shared the same ID with different partition keys.
Long Identifier Names (#4224)
Auto-discovered tag types with long names (e.g., BootstrapTokenResourceName) no longer cause PostgresqlIdentifierTooLongException at startup. Generated FK, PK, and index names that exceed PostgreSQL’s 63-character limit are now deterministically shortened with a hash suffix.
This has been longstanding problem in Marten, and we probably should have dealt with this years ago:-(
EF Core 10 Compatibility (#4225)
Updated Weasel to 8.12.0 which fixes MissingMethodException when using Weasel.EntityFrameworkCore with EF Core 10 on .NET 10.
Some time in the last couple weeks I wrote a blog post about my experiences so far with Claude assisted developement where I tried to say that you absolutely have to carefully review what your AI tools are doing because they can take short cuts. So, yeah, I should do that even more closely.
Polecat 2.0.1 is using the SQL Server 2025 native JSON type correctly now, and the database migrations are now all done with the underlying Weasel library that enables Polecat to play nicely with all of the Critter Stack command line support for migrations.