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

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

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

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

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

Why layered architectures fight the agent

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

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

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

Vertical slices, and the ceremony problem

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

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

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

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

Wolverine takes the slice to its logical conclusion

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

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

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

Here’s the same slice in Wolverine:

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

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

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

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

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

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

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

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

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

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

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

Why compression is the feature for AI

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

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

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

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

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

Compression alone isn’t the whole story

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

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

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

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

The takeaway

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

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

Wolverine and Marten Just Got Better for F# Folks

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

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

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

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

dotnet run -- codegen write --language fsharp

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

Last Thoughts

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

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

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

CLI Improvements in the Critter Stack and AI Stuff

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

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

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

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

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

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

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

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

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

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

Step 1: Get the Big Picture with describe

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

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

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

You can verify that the JasperFx is enabled by:

dotnet run help

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

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

dotnet run describe

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

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

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

Step 2: See the Code Wolverine Generates

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

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

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

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

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

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

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

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

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

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

Step 3: Understand Where and Why Messages Route

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

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

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

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

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

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

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

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

Step 4: Troubleshoot Handler Discovery

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

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

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

Step 5: Check Your Infrastructure

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

dotnet run -- check-env

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

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

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

Step 6: Inspect and Recover Message Storage

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

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

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

dotnet run -- clear-handled

Step 7: Export a Full Snapshot with capabilities

dotnet run -- capabilities wolverine.json

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

Bonus: Generate OpenAPI Offline (Wolverine.Http)

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

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

Cheat Sheet

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

Where to Go Next

Wait, how does this work with Aspire?

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

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

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

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

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

Higher Performance Dynamic Consistency Boundary Development with Marten 9.0

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

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

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

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

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

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

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

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

Summary

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

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

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

Sneak Peek at Critter Stack 2026

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

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

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


JasperFx 2.0 (foundation)

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

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

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

Weasel 9.0 (database layer)

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

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

Marten 9.0

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

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

Polecat 4.0

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

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

Wolverine 6.0

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

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


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

The Fastest Possible HTTP Queries with Marten

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:

public static class GetExpertiseCategoriesEndpoint
{
[WolverineGet("/api/expertise")]
public static Task<IReadOnlyList<ExpertiseCategory>> Get(IQuerySession session, CancellationToken ct)
=> session.Query<ExpertiseCategory>()
.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name)
.ToListAsync(ct);
}

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:

public static class GetExpertiseCategoriesEndpoint
{
[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
// as using the attribute, so ¯\_(ツ)_/¯
[ProducesResponseType<ExpertiseCategory[]>(200, "application/json")]
public static Task Get(IQuerySession session, HttpContext context)
=> session.Query<ExpertiseCategory>()
.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name)
.WriteArray(context);
}

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.
public class ActiveExpertiseCategoriesQuery : ICompiledListQuery<ExpertiseCategory>
{
public Expression<Func<IMartenQueryable<ExpertiseCategory>, IEnumerable<ExpertiseCategory>>> QueryIs()
=> q => q.Where(c => c.IsActive)
.OrderBy(c => c.Sector)
.ThenBy(c => c.Name);
}
public static class GetExpertiseCategoriesEndpoint
{
[WolverineGet("/api/expertise")]
[ProducesResponseType<ExpertiseCategory[]>(200, "application/json")]
public static Task Get(IQuerySession session, HttpContext context)
=> session.WriteArray(new ActiveExpertiseCategoriesQuery(), context);
}

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

  1. 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
  2. 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
  3. EF Core has to loop around the database results and create .NET objects that map to the raw database results
  4. 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
  5. 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).
  6. 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!

Marten, Polecat, and Wolverine Releases — One Shining Moment Edition

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.

Transport health check documentation →

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.

Wire Tap documentation →

📋 Declarative Marten Data Requirements

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:

[DocumentExists<Customer>]
public static OrderConfirmation Handle(PlaceOrder command)
{
// Customer is guaranteed to exist here
}

Throws RequiredDataMissingException if the precondition fails.

Marten integration documentation →

🎯 Confluent Schema Registry Serializers for Kafka

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).

opts.UseKafka("localhost:9092")
.ConfigureSchemaRegistry(config =>
{
config.Url = "http://localhost:8081";
})
.UseSchemaRegistryJsonSerializer();

Kafka Schema Registry documentation →

Wolverine.HTTP Improvements

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.

Content negotiation documentation →

OnException Convention

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 ProblemDetailsIResult, or HandlerContinuation to control the response:

public static ProblemDetails OnException(OrderNotFoundException ex)
{
return new ProblemDetails { Status = 404, Detail = ex.Message };
}

Exception handling documentation →

Output Caching

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.

Output caching documentation →

Rate Limiting

Apply ASP.NET Core’s rate limiting policies to Wolverine endpoints with [EnableRateLimiting("policyName")] — supporting fixed window, sliding window, token bucket, and concurrency algorithms.

Rate limiting documentation →

Antiforgery / CSRF Protection

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().

Antiforgery documentation →

Route Prefix Groups

Organize endpoints with class-level [RoutePrefix("api/v1")] or namespace-based prefixes for cleaner API versioning:

opts.RoutePrefix("api/orders", forEndpointsInNamespace: "MyApp.Features.Orders");

Routing documentation →

SSE / Streaming Responses

Documentation and examples for Server-Sent Events and streaming responses using ASP.NET Core’s Results.Stream(), fully integrated with Wolverine’s service injection.

Streaming documentation →

Community Contributions

Thank you to our community contributors for this release:

  • @LodewijkSioen — Structured ValidationResult support for FluentValidation (#2332)
  • @dmytro-pryvedeniuk — AutoStartHost enabled by default (#2411)
  • @outofrange-consulting — Bidirectional MassTransit header mapping (#2439)
  • @Sonic198 — PartitionId on Envelope for Kafka partition tracking (#2440)
  • Confluent Schema Registry serializers for Kafka (#2443)

Bug Fixes

  • Fixed exchange naming when using FromHandlerType conventional routing (#2397)
  • Fixed flaky GloballyLatchedListenerTests caused by async disposal race condition in TCP SocketListener
  • Added handler.type OpenTelemetry tag for better tracing of message handlers and HTTP endpoints

New Documentation

We’ve also added several new tutorials and guides:

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:

var results = await session
.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:

public class TaskProjection : EventProjection
{
public override async Task EnrichEventsAsync(
IQuerySession querySession, IReadOnlyList<IEvent> events,
CancellationToken cancellation)
{
// Batch-load users for all TaskAssigned events in one query
var userIds = events.OfType<IEvent<TaskAssigned>>()
.Select(e => e.Data.UserId).Distinct().ToArray();
var users = await querySession.LoadManyAsync<User>(cancellation, userIds);
// ... set enriched data on events
}
}

ConfigureNpgsqlDataSourceBuilder — Plugin Registration for All Data Sources

A new ConfigureNpgsqlDataSourceBuilder API on StoreOptions ensures Npgsql plugins like UseVector()UseNetTopologySuite(), and UseNodaTime() are applied to every NpgsqlDataSource Marten creates — including tenant databases in multi-tenancy scenarios:

opts.ConfigureNpgsqlDataSourceBuilder(b => b.UseVector());

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.

Upgrading

dotnet add package Marten --version 8.29.0

The full changelog is on GitHub.

Polecat 2.0.1

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.

Multi-Tenancy in the Critter Stack

We put on another Critter Stack live stream today to give a highlight tour of the multi-tenancy features and support across the entire stack. Long story short, I think we have by far and away the most comprehensive feature set for multi-tenancy in the .NET ecosystem, but I’ll let you judge that for yourself:

The Critter Stack provides comprehensive multi-tenancy support across all three tools — Marten, Wolverine, and Polecat — with tenant context flowing seamlessly from HTTP requests through message handling to data persistence. Here’s some links to various bits of documentation and some older blog posts at the bottom as well.

Marten (PostgreSQL)

Marten offers three tenancy strategies for both the document database and event store:

  • Conjoined Tenancy — All tenants share tables with automatic tenant_id discrimination, cross-tenant querying via TenantIsOneOf() and AnyTenant(), and PostgreSQL LIST/HASH partitioning on tenant_id (Document Multi-TenancyEvent Store Multi-Tenancy)
  • Database per Tenant — Four strategies ranging from static mapping to single-server auto-provisioning, master table lookup, and runtime tenant registration (Database-per-Tenant Configuration)
  • Sharded Multi-Tenancy with Database Pooling — Distributes tenants across a pool of databases using hash, smallest-database, or explicit assignment strategies, combining conjoined tenancy with database sharding for extreme scale (Database-per-Tenant Configuration)
  • Global Streams & Projections — Mix globally-scoped and tenant-specific event streams within a conjoined tenancy model (Event Store Multi-Tenancy)

Wolverine (Messaging, Mediator, and HTTP)

Wolverine propagates tenant context automatically through the entire message processing pipeline:

  • Handler Multi-Tenancy — Tenant IDs tracked as message metadata, automatically propagated to cascaded messages, with InvokeForTenantAsync() for explicit tenant targeting (Handler Multi-Tenancy)
  • HTTP Tenant Detection — Built-in strategies for detecting tenant from request headers, claims, query strings, route arguments, or subdomains (HTTP Multi-Tenancy)
  • Marten Integration — Database-per-tenant or conjoined tenancy with automatic IDocumentSession scoping and transactional inbox/outbox per tenant database (Marten Multi-Tenancy)
  • Polecat Integration — Same database-per-tenant and conjoined patterns for SQL Server (Polecat Multi-Tenancy)
  • EF Core Integration — Multi-tenant transactional inbox/outbox with separate databases and automatic migrations (EF Core Multi-Tenancy)
  • RabbitMQ per Tenant — Map tenants to separate virtual hosts or entirely different brokers (RabbitMQ Multi-Tenancy)
  • Azure Service Bus per Tenant — Map tenants to separate namespaces or connection strings (Azure Service Bus Multi-Tenancy)

Polecat (SQL Server)

Polecat mirrors Marten’s tenancy model for SQL Server:

Related Blog Posts

DatePost
Feb 2024Dynamic Tenant Databases in Marten
Mar 2024Recent Critter Stack Multi-Tenancy Improvements
May 2024Multi-Tenancy: What is it and why do you care?
May 2024Multi-Tenancy: Marten’s “Conjoined” Model
Jun 2024Multi-Tenancy: Database per Tenant with Marten
Sep 2024Multi-Tenancy in Wolverine Messaging
Dec 2024Message Broker per Tenant with Wolverine
Feb 2025Critter Stack Roadmap Update for February
May 2025Wolverine 4 is Bringing Multi-Tenancy to EF Core
Oct 2025Wolverine 5 and Modular Monoliths
Mar 2026Announcing Polecat: Event Sourcing with SQL Server
Mar 2026Critter Stack Wide Releases — March Madness Edition

Critter Stack Wide Releases — March Madness Edition

As anybody knows who follows the Critter Stack on our Discord server, I’m uncomfortable with the rapid pace of releases that we’ve sustained in the past couple quarters and I think I would like the release cadence to slow down. However, open issues and pull requests feel like money burning a hole in my pocket, and I don’t letting things linger very long. Our rapid cadence is somewhat driven by JasperFx Software client requests, some by our community being quite aggressive in contributing changes, and our users finding new issues that need to be addressed. While I’ve been known to be very unhappy with feedback saying that our frequent release cadence must be a sign of poor quality, I think our community seems to mostly appreciate that we move relatively fast. I believe that we are definitely innovating much faster and more aggressively than any of the other asynchronous messaging tools in the .NET space, so there’s that. Anyway, enough of that, here’s a rundown of the new releases today.

It’s been a busy week across the Critter Stack! We shipped coordinated releases today across all five projects: Marten 8.27, Wolverine 5.25, Polecat 1.5, Weasel 8.11.1, and JasperFx 1.21.1. Here’s a rundown of what’s new.


Marten 8.27.0

Sharded Multi-Tenancy with Database Pooling

For teams operating at extreme scale — we’re talking hundreds of billions of events — Marten now supports a sharded multi-tenancy model that distributes tenants across a pool of databases. Each tenant gets its own native PostgreSQL LIST partition within a shard database, giving you the isolation benefits of per-tenant databases with the operational simplicity of a managed pool.

Configuration is straightforward:

opts.MultiTenantedWithShardedDatabases(x =>
{
    // Connection to the master database that holds the pool registry
    x.ConnectionString = masterConnectionString;

    // Schema for the registry tables in the master database
    x.SchemaName = "tenants";

    // Seed the database pool on startup
    x.AddDatabase("shard_01", shard1ConnectionString);
    x.AddDatabase("shard_02", shard2ConnectionString);
    x.AddDatabase("shard_03", shard3ConnectionString);
    x.AddDatabase("shard_04", shard4ConnectionString);

    // Choose a tenant assignment strategy (see below)
    x.UseHashAssignment(); // this is the default
});

Calling MultiTenantedWithShardedDatabases() automatically enables conjoined tenancy for both documents and events, with native PG list partitions created per tenant.

Three tenant assignment strategies are built-in:

  • Hash Assignment (default) — deterministic FNV-1a hash of the tenant ID. Fast, predictable, no database queries needed. Best when tenants are roughly equal in size.
  • Smallest Database — assigns new tenants to the database with the fewest existing tenants. Accepts a custom IDatabaseSizingStrategy for balancing by row count, disk usage, or any other metric.
  • Explicit Assignment — you control exactly which database hosts each tenant via the admin API.

The admin API lets you manage the pool at runtime: AddTenantToShardAsyncAddDatabaseToPoolAsyncMarkDatabaseFullAsync — all with advisory-locked concurrent safety.

See the multi-tenancy documentation for the full details.

Bulk COPY Event Append for High-Throughput Seeding

For data migrations, test fixture setup, load testing, or importing events from external systems, Marten now supports a bulk COPY-based event append that uses PostgreSQL’s COPY ... FROM STDIN BINARY for maximum throughput:

// Build up a list of stream actions with events
var streams = new List<StreamAction>();

for (int i = 0; i < 1000; i++)
{
    var streamId = Guid.NewGuid();
    var events = new object[]
    {
        new OrderPlaced(streamId, "Widget", 5),
        new OrderShipped(streamId, $"TRACK-{i}"),
        new OrderDelivered(streamId, DateTimeOffset.UtcNow)
    };

    streams.Add(StreamAction.Start(store.Events, streamId, events));
}

// Bulk insert all events using PostgreSQL COPY for maximum throughput
await store.BulkInsertEventsAsync(streams);

This supports all combinations of Guid/string identity, single/conjoined tenancy, archived stream partitioning, and metadata columns. When using conjoined tenancy, a tenant-specific overload is available:

await store.BulkInsertEventsAsync("tenant-abc", streams);

See the event appending documentation for more.

Other Fixes

  • FetchForWriting now auto-discovers natural keys without requiring an explicit projection registration, and works correctly with strongly typed IDs combined with UseIdentityMapForAggregates
  • Compiled queries using IsOneOf with array parameters now generate correct SQL
  • EF Core OwnsOne().ToJson() support (via Weasel 8.11.1) — schema diffing now correctly handles JSON column mapping when Marten and EF Core share a database
  • Thanks to @erdtsieck for fixing duplicate codegen when using secondary document stores!

Wolverine 5.25.0

This is a big release with 12 PRs merged — a mix of bug fixes, new features, and community contributions.

MassTransit and NServiceBus Interop for Azure Service Bus Topics

Previously, MassTransit and NServiceBus interoperability was only available on Azure Service Bus queues. With 5.25, you can now interoperate on ASB topics and subscriptions too — making it much easier to migrate incrementally or coexist with other .NET messaging frameworks:

// Publish to a topic with NServiceBus interop
opts.PublishAllMessages().ToAzureServiceBusTopic("nsb-topic")
    .UseNServiceBusInterop();

// Listen on a subscription with MassTransit interop
opts.ListenToAzureServiceBusSubscription("wolverine-sub")
    .FromTopic("wolverine-topic")
    .UseMassTransitInterop(mt => { })
    .DefaultIncomingMessage<ResponseMessage>().UseForReplies();

Both UseMassTransitInterop() and UseNServiceBusInterop() are available on AzureServiceBusTopic (for publishing) and AzureServiceBusSubscription (for listening). This is ideal for brownfield scenarios where you’re migrating services one at a time and need different messaging frameworks to talk to each other through shared ASB topics.

Other New Features

  • Handler Type Naming for Conventional Routing — NamingSource.FromHandlerType names listener queues after the handler type instead of the message type, useful for modular monolith scenarios with multiple handlers per message
  • Enhanced WolverineParameterAttribute — new FromHeaderFromClaim, and FromMethod value sources for binding handler parameters to HTTP headers, claims, or static method return values
  • Full Tracing for InvokeAsync — opt-in InvokeTracingMode.Full emits the same structured log messages as transport-received messages, with zero overhead in the default path
  • Configurable SQL transport polling interval — thanks to new contributor @xwipeoutx!

Bug Fixes


Polecat 1.5.0

Polecat — the Critter Stack’s newer, lighter-weight event store option — had a big jump from 1.2 to 1.5:

  • net9.0 support and CI workflow
  • SingleStreamProjection<TDoc, TId> with strongly-typed ID support
  • Auto-discover natural keys for FetchForWriting
  • Conjoined tenancy support for DCB tags and natural keys
  • Fix for FetchForWriting with UseIdentityMapForAggregates and strongly typed IDs

Weasel 8.11.1

  • EF Core OwnsOne().ToJson() support — Weasel’s schema diffing now correctly handles EF Core’s JSON column mapping, preventing spurious migration diffs when Marten and EF Core share a database

JasperFx 1.21.1 / JasperFx.Events 1.24.1

  • Skip unknown flags when AutoStartHost is true — fixes an issue where unrecognized CLI flags would cause errors during host auto-start
  • Retrofit IEventSlicer tests

Upgrading

All packages are available on NuGet now. The Marten and Wolverine releases are fully coordinated — if you’re using the Critter Stack together, upgrade both at the same time for the best experience.

As always, please report any issues on the respective GitHub repositories and join us on the Critter Stack Discord if you have questions!

New Option for Simple Projections in Marten or Polecat

JasperFx Software is around and ready to assist you with getting the best possible results using the Critter Stack.

The projections model in Marten and now Polecat has evolved quite a bit over the past decade. Consider this simple aggregated projection of data for our QuestParty in our tests:

public class QuestParty
{
public List<string> Members { get; set; } = new();
public IList<string> Slayed { get; } = new List<string>();
public string Key { get; set; }
public string Name { get; set; }
// In this particular case, this is also the stream id for the quest events
public Guid Id { get; set; }
// These methods take in events and update the QuestParty
public void Apply(MembersJoined joined) => Members.Fill(joined.Members);
public void Apply(MembersDeparted departed) => Members.RemoveAll(x => departed.Members.Contains(x));
public void Apply(QuestStarted started) => Name = started.Name;
public override string ToString()
{
return $"Quest party '{Name}' is {Members.Join(", ")}";
}
}

That type is mutable, but the projection library underneath Marten and Polecat happily supports projecting to immutable types as well.

Some people actually like the conventional method approach up above with the Apply, Create, and ShouldDelete methods. From the perspective of Marten’s or Polecat’s internals, it’s always been helpful because the projection subsystem “knows” in this case that the QuestParty is only applicable to the specific event types referenced in those methods, and when you call this code:

var party = await query
.Events
.AggregateStreamAsync<QuestParty>(streamId);

Marten and Polecat are able to quietly use extra SQL filters to limit the events fetched from the database to only the types utilized by the projected QuestParty aggregate.

Great, right? Except that some folks don’t like the naming conventions, just prefer explicit code, or do some clever things with subclasses on events that can confuse Marten or Polecat about the precedence of the event type handlers. To that end, Marten 8.0 introduced more options for explicit code. We can rewrite the projection part of the QuestParty above to a completely different class where you can add explicit code:

public class QuestPartyProjection: SingleStreamProjection<QuestParty, Guid>
{
public QuestPartyProjection()
{
// This is *no longer necessary* in
// the very most recent versions of Marten,
// but used to be just to limit Marten's
// querying of event types when doing live
// or async projections
IncludeType<MembersJoined>();
IncludeType<MembersDeparted>();
IncludeType<QuestStarted>();
}
public override QuestParty Evolve(QuestParty snapshot, Guid id, IEvent e)
{
snapshot ??= new QuestParty{ Id = id };
switch (e.Data)
{
case MembersJoined j:
// Small helper in JasperFx that prevents
// double values
snapshot.Members.Fill(j.Members);
break;
case MembersDeparted departed:
snapshot.Members.RemoveAll(x => departed.Members.Contains(x));
break;
}
return snapshot;
}
}

There are several more items in that SingleStreamProjection base type like versioning or fine grained control over asynchronous projection behavior that might be valuable later, but for now, let’s look at a new feature in Marten and Polecat that let’s you use explicit code right in the single aggregate type:

public class QuestParty
{
public List<string> Members { get; set; } = new();
public IList<string> Slayed { get; } = new List<string>();
public string Key { get; set; }
public string Name { get; set; }
// In this particular case, this is also the stream id for the quest events
public Guid Id { get; set; }
public void Evolve(IEvent e)
{
switch (e.Data)
{
case QuestStarted _:
// Little goofy, but this let's Marten know that
// the projection cares about that event type
break;
case MembersJoined j:
// Small helper in JasperFx that prevents
// double values
Members.Fill(j.Members);
break;
case MembersDeparted departed:
Members.RemoveAll(x => departed.Members.Contains(x));
break;
}
}
public override string ToString()
{
return $"Quest party '{Name}' is {Members.Join(", ")}";
}
}

This is admittedly yet another convention method in terms of the method name and the possible arguments, but hopefully the switch statement approach is much more explicit for folks who prefer that. As an additional bonus, Marten is able to automatically register the event types via a source generator that the version of QuestParty just above is using automatically so that we get all the benefits of the event filtering without making users do extra explicit configuration.

Projecting to Immutable Views

Just for completeness, let’s look at alternative versions of QuestParty just to see what it looks like if you make the aggregate an immutable type. First up is the conventional method approach:

public sealed record QuestParty(Guid Id, List<string> Members)
{
// These methods take in events and update the QuestParty
public static QuestParty Create(QuestStarted started) => new(started.QuestId, []);
public static QuestParty Apply(MembersJoined joined, QuestParty party) =>
party with
{
Members = party.Members.Union(joined.Members).ToList()
};
public static QuestParty Apply(MembersDeparted departed, QuestParty party) =>
party with
{
Members = party.Members.Where(x => !departed.Members.Contains(x)).ToList()
};
public static QuestParty Apply(MembersEscaped escaped, QuestParty party) =>
party with
{
Members = party.Members.Where(x => !escaped.Members.Contains(x)).ToList()
};
}

And with the Evolve approach:

public sealed record QuestParty(Guid Id, List<string> Members)
{
public static QuestParty Evolve(QuestParty? party, IEvent e)
{
switch (e.Data)
{
case QuestStarted s:
return new(s.QuestId, []);
case MembersJoined joined:
return party with {
Members = party.Members.Union(joined.Members).ToList()
};
case MembersDeparted departed:
return party with
{
Members = party.Members.Where(x => !departed.Members.Contains(x)).ToList()
};
case MembersEscaped escaped:
return party with
{
Members = party.Members.Where(x => !escaped.Members.Contains(x)).ToList()
};
}
return party;
}

Summary

What do I recommend? Honestly, just whatever you prefer. This is a case where I’d like everyone to be happy with one of the available options. And yes, it’s not always good that there is more than one way to do the same thing in a framework, but I think we’re going to just keep all these options in the long run. It wasn’t shown here at all, but I think we’ll kill off the early options to define projections through a ton of inline Lambda functions within a fluent interface. That stuff can just die.

In the medium and longer term, we’re going to be utilizing more source generators across the entire Critter Stack as a way of both eliminating some explicit configuration requirements and to optimize our cold start times. I’m looking forward to getting much more into that work.