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.

Announcing Polecat: Event Sourcing with SQL Server

Polecat is now completely supported by JasperFx Software and automatically part of any existing and future support agreements through our existing plans.

Polecat was released as 1.0 this past week (with 1.1 & now 1.2 coming soon). Let’s call it what it is, Polecat is a port of (most of) Marten to target SQL Server 2025 and SQL Server’s new JSON data type. For folks not familiar with Marten, Polecat is in one library:

  1. A very full fledged Event Store library for SQL Server that includes event projection and subscriptions, Dynamic Consistency Boundary support, a large amount of functionality for Event Sourcing basics, rich event metadata tracking capabilities, and even rich multi-tenancy support.
  2. A feature rich set of Document Database capabilities backed by SQL Server including LINQ querying support

And while Polecat is brand spanking new, it comes out of the gate with the decade old Marten pedigree and its own Wolverine integration for CQRS usage. I’m confident in saying Polecat is now the best technical option for using Event Sourcing with SQL Server in the .NET ecosystem.

And of course, if you’re a shop with deep existing roots into EF Core usage, Polecat also comes with projection support to EF Core, so Polecat can happily coexist with EF Core in the same systems.

Alright, let’s just into a quick start. First, let’s say you’ve started a brand new .NET project through dotnet run webapi and you’ve added a reference to Polecat through Nuget (and you have a running SQL Server 2025 instance handy too of course!). Next, let’s start with the inevitable AddPolecat() usage in your Program file:

builder.Services.AddPolecat(options =>
{
// Connection string to your SQL Server 2025 database
options.Connection("Server=localhost;Database=myapp;User Id=sa;Password=YourStrong!Password;TrustServerCertificate=True");
// Optionally change the default schema (default is "dbo")
options.DatabaseSchemaName = "myschema";
});

Polecat can be used without IHost or IServiceCollection registrations by just directly building a DocumentStore object.

Next, let’s say you’ve got this simplistic document type (entity in Polecat parlance):

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

And now, let’s use Polecat within some Minimal API endpoints to capture and query User documents:

// Store a document
app.MapPost("/user", async (CreateUserRequest create, IDocumentSession session) =>
{
var user = new User
{
FirstName = create.FirstName,
LastName = create.LastName,
Internal = create.Internal
};
session.Store(user);
await session.SaveChangesAsync();
});
// Query with LINQ
app.MapGet("/users", async (bool internalOnly, IDocumentSession session, CancellationToken ct) =>
{
return await session.Query<User>()
.Where(x => x.Internal == internalOnly)
.ToListAsync(ct);
});
// Load by ID
app.MapGet("/user/{id:guid}", async (Guid id, IQuerySession session, CancellationToken ct) =>
{
return await session.LoadAsync<User>(id, ct);
});

For folks used to EF Core, I should point out that Polecat has its own “it just works” database migration subsystem that in the default development mode will happily make sure that all necessary database tables, views, and functions are exactly as they should be at runtime so you don’t have to fiddle with database migrations when all you want to do is just get things done.

While I initially thought that we’d mainly focus on the event sourcing support, we were also able to recreate the mass majority of Marten’s document database capabilities (including the “partial update” model, LINQ support, soft deletes, multi-tenancy, and batch updates for starters) as well if you’d only be interested in that feature set by itself.

Moving over to event sourcing instead, let’s say you’re into fantasy books like I am and you want to build a system to model the journeys and adventures of a quest in your favorite fantasy series. You might model some of the events in that system like:

public record QuestStarted(string Name);
public record MembersJoined(string Location, string[] Members);
public record MembersDeparted(string Location, string[] Members);
public record QuestEnded(string Name);

And you model the current state of the quest party like this:

public class QuestParty
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public List<string> Members { get; set; } = new();
public void Apply(QuestStarted started)
{
Name = started.Name;
}
public void Apply(MembersJoined joined)
{
Members.AddRange(joined.Members);
}
public void Apply(MembersDeparted departed)
{
foreach (var member in departed.Members)
Members.Remove(member);
}
}

The step above isn’t strictly necessary for event sourcing, but you usually need a projection of some sort sooner or later.

And finally, we can add events by starting a new event stream:

var store = DocumentStore.For(opts =>
{
opts.Connection("Server=localhost,1433;Database=myapp;User Id=sa;Password=YourStrong!Password;TrustServerCertificate=True");
});
await using var session = store.LightweightSession();
// Start a new stream with initial events
var questId = session.Events.StartStream<QuestParty>(
new QuestStarted("Destroy the Ring"),
new MembersJoined("Rivendell", ["Frodo", "Sam", "Aragorn", "Gandalf"])
);
await session.SaveChangesAsync();

And even append some new ones to the same stream later:

await using var session = store.LightweightSession();
session.Events.Append(questId,
new MembersJoined("Moria", ["Gimli", "Legolas"]),
new MembersDeparted("Moria", ["Gandalf"])
);
await session.SaveChangesAsync();

And derive the current state of our quest:

var party = await session.Events.AggregateStreamAsync<QuestParty>(questId);
// party.Name == "Destroy the Ring"
// party.Members == ["Frodo", "Sam", "Aragorn", "Gimli", "Legolas"]

And there’s much, much more of course, including everything you’d need to build real systems based on our 10 years and counting supporting Marten with PostgreSQL.

How is Polecat Different than Marten?

There are of course some differences besides just the database engine:

  • Polecat is using source generators instead of the runtime code generation that Marten does today
  • Polecat will only support System.Text.Json for now as a serialization engine
  • Polecat only supports the “Quick Append” option from Marten
  • There is no automatic dirty checking
  • No “duplicate fields” support so far, we’re going to reevaluate that though
  • Plenty of other technical baggage features I flat out didn’t want to support in Marten didn’t make the cut, but I can’t imagine anyone will miss any of that!

Summary

For over a decade people have been telling me that Marten would be more successful and adopted by more .NET shops if it only supported SQL Server in addition to or instead of PostgreSQL. While I’ve never really disagreed with that idea — and it’s impossible to really prove the counter factual anyway — there have always been real blockers in both SQL Server’s JSON support lagging far behind PostgreSQL and frankly the time commitment on my part to be able to attempt that work in the first place.

So what changed to enable this?

  1. SQL Server 2025 added much better JSON support rivaling PostgreSQL’s JSONB type
  2. We had already invested in pulling the basic event abstractions and projection support out of Marten and into a common library called JasperFx.Events as part of the Marten 8.0 release cycle and that work was always meant to be an enabler for what is now Polecat
  3. Claude & Opus 4.5/4.6 turned out to be very, very good at grunt work

That second item had to this point been a near disaster in my mind because of how much work and time that took compared to the benefits and was the single most time consuming part of Polecat development. Let’s just say that I’m very relieved that that effort didn’t turn out to be a very expensive sunk cost for JasperFx!

I have no earthly idea how much traction Polecat will really get, but we’ve already had some interest from folks who have wanted to use Marten, but couldn’t get their .NET shop to adopt PostgreSQL. I’m hopeful!

Critter Stack Roadmap for March 2026

It’s only a month since I’ve written an update on the Critter Stack roadmap, but it’s maybe worth some time on my part to update what I think the roadmap is now. The biggest change is the utter dominance of AI in the software development discourse and the fact that Claude usage has allowed us to chew through a shocking amount of backlog in the past 6 weeks. That’s probably also changed my own thinking about what should be next throughout this year.

First, some updates on what’s been added to the Critter Stack in just the last month:

By the time you read this, we may very well have Polecat 1.0 out as well.

Short Term

The short term priority for myself and JasperFx Software is to deliver the CritterWatch MVP in a usable form by the end of March.

Marten, Wolverine, and even Polecat have no major new features planned for the short term and I think they will only get tactical releases for bug fixes and JasperFx client requests for a little while. And let me tell you, it feels *weird* to say that, but we’ve blown through a tremendous amount of the backlog so far in 2026.

Medium Term

  • Enhance CritterWatch until it’s the best in class monitoring tool for asynchronous messaging and event sourcing. Part of that will probably be adding quite a bit more functionality for development time as well.
  • For a JasperFx Software client, we’re doing PoC work on scaling Marten to be able to handle having several hundred billion events in a single system. I’m going to assume that this PoC will probably lead to enhancements in both Marten and Wolverine!
  • We’ll finally add some direct support to Marten for the PostGIS PostgreSQL extension
  • I’m a little curious to try to use the hstore extension with Marten as a possible way to optimize our new DCB support
  • Play with Pgvector and TimescaleDb in combination with Marten as some kind of vague “how can we say that Marten is even more awesome for AI?”
  • There’s going to be a new wave of releases later this year for Marten 9.0, Wolverine 6.0, and Polecat 2.0 that will mostly about performance optimizations and especially finding ways to optimize the cold start time of applications using these tools.
  • Babu and I (really all Babu so far) are going to be building a set of AI skills for using the Critter Stack tools that will be curated in a GitHub repository and available to JasperFx Software clients. I do not know what the full impact of AI tools are really going to be on software development, but I personally want to plan for the worst case that AI tools plus LLM-friendly documentation drastically reduces the demand for consulting and try to belatedly pivot JasperFx Software to being at least partially a product company.
  • Build tooling for spec driven development using the Critter Stack. I don’t have any details beyond “hey, wouldn’t that be cool?”. My initial thought is to play with Gherkin specifications that generates “best practices” Critter Stack code with the accompanying automated tests to boot.
  • One way or another, we’ll be building MCP support into the Critter Stack, but again, I don’t know anything more than “hey, wouldn’t that be cool?”

Long Term

Profit?

I’m playing with the idea of completely rebooting Storyteller as a new spec driven development tool. I have the Nuget rights to the “Storyteller” name and graphics from Khalid (a necessary requirement for any successful effort on my part), and I’ve always wanted to go back to it some day.

Natural Keys in the Critter Stack

Just to level set everyone, there are two general categories of identifiers we use in software:

  • “Surrogate” keys are data elements like Guid values, database auto numbering or sequences, or snowflake generated identifiers that have no real business meaning and just try to be unique values.
  • “Natural” keys have some kind of business meaning and usually utilize some piece of existing information like email addresses or phone numbers. A natural key could also be an external supplied identifier from your clients. In fact, it’s quite common to have your own tracking identifier (usually a surrogate key) while also having to track a client or user’s own identification for the same business entity.

That very last sentence is where this post takes off. You see Marten can happily track event streams with either Guid identifiers (surrogate key) or string identifiers — or strong typed identifiers that wrap an inner Guid or string, but in this case that’s really the same thing, just with more style I guess. Likewise, in combination with Wolverine for our recommended “aggregate handler workflow” approach to building command handlers, we’ve only supported the stream id or key. Until now!

With the Marten 8.23 and Wolverine 5.18 releases last week (we’ve been very busy and there are newer releases now), you are now able to “tag” Marten (or Polecat!) event streams with a natural key in addition to its surrogate stream id and use that natural key in conjunction with Wolverine’s aggregate handler workflow.

Of course, if you use strings as the stream identifier you could already use natural keys, but let’s just focus on the case of Guid identified streams that are also tagged with some kind of natural key that will be supplied by users in the commands sent to the system.

First, to tag streams with natural keys in Marten, you have to have a strong typed identifier type for the natural key. Next, there’s a little bit of attribute decoration in the targeted document type of a single stream projection, i.e., the “write model” for an event stream. Here’s an example from the Marten documentation:

public record OrderNumber(string Value);
public record InvoiceNumber(string Value);
public class OrderAggregate
{
public Guid Id { get; set; }
[NaturalKey]
public OrderNumber OrderNum { get; set; }
public decimal TotalAmount { get; set; }
public string CustomerName { get; set; }
public bool IsComplete { get; set; }
[NaturalKeySource]
public void Apply(OrderCreated e)
{
OrderNum = e.OrderNumber;
CustomerName = e.CustomerName;
}
public void Apply(OrderItemAdded e)
{
TotalAmount += e.Price;
}
[NaturalKeySource]
public void Apply(OrderNumberChanged e)
{
OrderNum = e.NewOrderNumber;
}
public void Apply(OrderCompleted e)
{
IsComplete = true;
}
}

In particular, see the usage of [NaturalKey] which should be self-explanatory. Also see the [NaturalKeySource] attribute that we’re using to mark when a natural key value might change. Marten is starting to use source generators for some projection internals (in place of some nasty, not entirely as efficient as it should have been, Expression-compiled-to-Lambda functions).

And that’s that, really. You’re now able to use the designated natural keys as the input to an “aggregate handler workflow” command handler with Wolverine. See Natural Keys from the Wolverine documentation for more information.

For a little more information:

  • The natural keys are stored in a separate table, and when using FetchForWriting(), Marten is doing an inner join from the tag table for that natural key type to the mt_streams table in the Marten database
  • You can change the natural key against the surrogate key
  • We expect this to be most useful when you want to use the Guid surrogate keys for uniqueness in your own system, but you frequently receive a natural key from API users of your system — or at least this has been encountered by a couple different JasperFx Software customers.
  • The natural key storage does have a unique value constraint on the “natural key” part of the storage
  • Really only a curiosity, but this was done in the same wave of development as Marten’s new DCB support

New Improvements to Marten’s LINQ Support

I hold these two facts to be simultaneously true:

  1. Language Integrated Query (LINQ) is the singular best feature in .NET that developers would miss out working in other development platforms
  2. Developing and supporting a LINQ provider is a nastily hard and laborious task for an OSS author and probably my least favorite area of Marten to work in

Alright, on that note, let’s talk about a couple potentially important recent improvements to Marten’s LINQ support. First, we’ve received the message loud and clear that Marten was sometimes hard when what you really need is to fetch data from more than one document type at a time. We’d also got some feedback about the difficultly overall in making denormalized views projected from events with a mix of different streams and potentially different reference documents.

For projections in the event sourcing space, we added the Composite Projection capability. For straight up document database work, Marten 8.23 introduced support for the LINQ GroupJoin operator as shown in this test from the Marten codebase:

public class JoinCustomer
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public string City { get; set; } = "";
}
public class JoinOrder
{
public Guid Id { get; set; }
public Guid CustomerId { get; set; }
public string Status { get; set; } = "";
public decimal Amount { get; set; }
}
[Fact]
public async Task GroupJoin_select_outer_entity_properties()
{
// It's just setting up some Customer and Order data in the database
await SetupData();
var results = await _session.Query<JoinCustomer>()
.GroupJoin(
_session.Query<JoinOrder>(),
c => c.Id,
o => o.CustomerId,
(c, orders) => new { c, orders })
.SelectMany(
x => x.orders,
(x, o) => new { x.c.Name, x.c.City })
.ToListAsync();
results.Count.ShouldBe(3);
results.Count(r => r.City == "Seattle").ShouldBe(2); // Alice's 2 orders
results.Count(r => r.City == "Portland").ShouldBe(1); // Bob's 1 order
}

This is of course brand new, which means there are probably “unknown unknown” bugs in there, but just give us a reproduction in a GitHub issue and we’ll address whatever it is.

Select/Where Hoisting

Without getting into too many details, the giant “hey, let’s rewrite our LINQ support almost from scratch!” effort in Marten V7 a couple years ago made some massive strides in our LINQ provider, but unintentionally “broke” our support for chaining Where clauses *after* Select transforms. To be honest, that’s nothing I even realized you could or would do with LINQ, so I was caught off guard when we got a couple bug reports about that later. No worries now, because you can now do that with Marten as this new test shows:

    [Fact]
    public async Task select_before_where_with_different_type()
    {
        var doc1 = new DocWithInner { Id = Guid.NewGuid(), Name = "one", Inner = new InnerDoc { Value = 10, Text = "low" } };
        var doc2 = new DocWithInner { Id = Guid.NewGuid(), Name = "two", Inner = new InnerDoc { Value = 50, Text = "mid" } };
        var doc3 = new DocWithInner { Id = Guid.NewGuid(), Name = "three", Inner = new InnerDoc { Value = 90, Text = "high" } };

        theSession.Store(doc1, doc2, doc3);
        await theSession.SaveChangesAsync();

        // Select().Where() - the problematic ordering from GH-3009
        var results = await theSession.Query<DocWithInner>()
            .Select(x => x.Inner)
            .Where(x => x.Value > 40)
            .ToListAsync();

        results.Count.ShouldBe(2);
        results.ShouldContain(x => x.Value == 50);
        results.ShouldContain(x => x.Value == 90);
    }

Summary

So you might ask, how did we suddenly get to a point where there are literally no open GitHub issues related to LINQ in the Marten codebase? It turns out that the LINQ provider support is an absolutely perfect place to just let Claude go fix it — but know that that is backed up by about a 1,000 regression tests for LINQ to chew through at the same time.

My limited experience suggests that the AI assisted development really works when you have very well defined and executable acceptance requirements and better yet tests for your AI agent to develop to. Duh.

Fun Five Year Retrospective on Marten Adoption

In the midst of some let’s call it “market” research to better understand how Marten stacks up to a newer competitor, I stumbled back over a GitHub discussion I initiated in 2021 called “What would it take for you to adopt Marten?” long before I was able to found JasperFx Software. I seeded this original discussion with my thoughts about the then forthcoming giant Marten 4.0 release that was meant to permanently put Marten on a solid technical foundation for all time and address all known significant technical shortcomings of Marten.

Narrators’s voice: the V4 release was indeed a major step forward and still shapes a great deal of Marten’s internals, but it was not even remotely the end all, be all of technical releases and V5 came out less than 6 months later to address shortcomings of V4 and to add first class multi-tenancy through separate databases. And arguably, V7 just three years later was nearly as big a change to Marten’s internals.

So now that it’s five years later and Marten’s usage numbers are vastly greater than that moment in time in 2021, let me run through the things we thought needed to change to garner more usage, whether or not and how those ideas took fruit, and whether or not I think those ideas made any difference in the end.

Enterprise Level Support

People frequently told me that they could not seriously consider Marten or later Wolverine without there being commercial support for those tools or at least a company behind them. As of now, JasperFx Software (my company) provides support agreements for any tool under the JasperFx GitHub organization. I would say though that the JasperFx support agreement ends up being more like an ongoing consulting engagement rather than the “here’s an email for support, we’ll response within 72 hours” licensing agreement that you’d be getting from other Event Driven Architecture tools and companies in the .NET space.

Read more about that in Little Diary of How JasperFx Helps Our Clients.

And no, we’re not a big company at all, but we’re getting there and at least “we” isn’t just the royal “we” now:)

I’m hoping that JasperFx is able to expand when we are able to start selling the CritterWatch commercial add on soon.

More Professional Documentation

Long story short, a good, a modern looking website for your project is an absolute must. Today, all of the Critter Stack / JasperFx projects use VitePress and MarkdownSnippets for our documentation websites. Plus we have real project logo images that I really like myself created by Khalid Abuhakmeh. Babu Annamalai did a fantastic job on setting up our documentation infrastructure.

People do still complain about the documentation from time to time, but after I was mercilessly flogged online for the StructureMap documentation being so far behind in the late 00’s and FubuMVC never really having had any, I’ve been paranoid about OSS documentation ever since and we as a community try really hard to curate and expand our documentation. Anne Erdtsieck especially has added quite a bit of explanatory detail to the Marten documentation in the last six months.

It’s only anecdotal evidence, but the availability of the LLMS-friendly docs plus the most recent advances in AI LLM tools seem to have dramatically reduced the amount of questions we’re fielding in our Discord chat rooms while our usage numbers are still accelerating.

Oh, and I cannot emphasize more how important and valuable it is to be able to both quickly publish documentation updates and to enable users to quickly make suggestions to the documentation through pull requests.

Moar YouTube Videos

I dragged my feet on this one for a long time and probably still don’t do well enough, but we have the JasperFx Software Channel now with some videos and plenty of live streams. I’ve had mostly positive feedback on the live streams, so it’s just up to me to get back in a groove on this one.

SQL Server or CosmosDb Support in Addition to PostgreSQL

The most common complaint or concern about Marten in its first 5-7 years was that it only supported PostgreSQL as a backing data store. The most common advice we got from the outside was that we absolutely had to have SQL Server support in order to be viable inside the .NET ecosystem where shops do tend to be conservative in technology adoption and also tend to favor Microsoft offerings.

While I’ve always seen the obvious wisdom in supporting SQL Server, I never believed that it was practical to replicate Marten’s functionality with SQL Server. Partially because SQL Server lagged far behind PostgreSQL in its JSON capabilities for a long time and partially just out of sheer bandwidth limitations. I think it’s telling that nobody built a truly robust and widely used event store on top of SQL Server in the mean time.

But it’s 2026 and the math feels very different in many ways:

  1. PostgreSQL has grown in stature and at least in my experience, far more .NET shops are happy to take the plunge into PostgreSQL. It absolutely helps that the PostgreSQL ecosystem has absolutely exploded with innovation and that PostgreSQL has first class managed hosting or even serverless support on every cloud provider of any stature.
  2. SQL Server 2025 introduced a new native JSON type that brought SQL Server at least into the same neighborhood as PostgreSQL’s JSONB type. Using that, the JasperFx Software is getting close to releasing a full fledged port of most of Marten (you won’t miss the parts that were left out, I know I won’t!) called “Polecat” that will be backed by SQL Server 2025. We’ll see how much traction that tool gets, but early feedback has been positive.
  3. While we’re not there yet on Event Sourcing, at least Wolverine does have CosmosDb backed transactional inbox and outbox support as well as other integration into Wolverine handlers. I don’t have any immediate plans for Event Sourcing with CosmosDb other than “wouldn’t that be nice?” kind of thoughts. I don’t hear that many requests for this. I get even less feedback about DynamoDb, but I’ve always assumed we’d get around to that some day too.

Better Support for Cross Document Views or Queries

So, yeah. Document database approaches are awesome when your problem domain is well described by self-contained entities, but maybe not so much if you really need to model a lot of relationships between different first class entities in your system. Marten already had the Include() operator in our LINQ support, but folks aren’t super enthusiastic about it all the time. As Marten really became mostly about Event Sourcing over time, some of this issue went away for folks who could focus on using projections to just write documents out exactly as your use cases needed — which can sometimes happily eliminate the need for fancy JOIN queries and AutoMapper type translation in memory. However, I’ve worked with several JasperFx clients and other users in the past couple years who had real struggles with creating denormalized views with Marten projections, so that needed work too.

While that complaint was made in 2021, we now have or are just about to get in the next wave of releases:

  • The new “composite projection” model that was designed for easier creation of denormalized event projection views that has already met with some early success (and actionable feedback). This feature was also designed with some performance and scalability tuning in mind as well.
  • The next big release of Marten (8.23) will include support for the GroupJoin LINQ provider. Finally.
  • And let’s face it, EF Core will always have better LINQ support than Marten over all and a straight up relational table is probably always going to be more appropriate for reporting. To that end, Marten 8.23 will also have an extension library that adds first class event projections that write to EF Core.

Polecat 1.0 will include all of these new Marten features as well.

Improving LINQ Support

LINQ support was somewhat improved for that V4 release I was already selling in 2021, but much more so for V7 in early 2024 that moved us toward using much more PostgreSQL specific optimizations in JSONB searching as we were able to utilize JSONPath searching or back to the PostgreSQL containment operator.

At this point, it has turned out that recent versions of Claude are very effective at enhancing or fixing issues in the LINQ provider and at this point we have zero open issues related to LINQ for the first time since Marten’s founding back in 2015!

There’s one issue open as I write this that has an existing fix that hasn’t been committed to master yet, so if you go check up on me, I’m not technically lying:)

Open Telemetry Support and other Improved Metadata

Open Telemetry support is table stakes for .NET application framework tools and especially for any kind of Event Driven Architecture or Event Sourcing tool like Marten. We’ve had all that since Marten V7, with occasional enhancements or adjustments since in reaction to JasperFx client needs.

More Sample Applications

Yeah, we could still do a lot better on this front. Sigh.

One thing I want to try doing soon is developing some Claude skills for the Critter Stack in general, and a particular focus on creating instructions for best practices converting codebases to the Critter Stack. As part of that, I’ve identified about a dozen open source sample applications out there that would be good targets for this work. It’s a lazy way to create new samples applications while building an AI offering for JasperFx, but I’m all about being lazy sometimes.

We’ll see how this goes.

Scalability Improvements including Sharding

We’ve done a lot here since that 2021 discussion. Some of the Event Sourcing scalability options are explained here. This isn’t an exhaustive list, but since 2021 we have:

  • Much better load distribution of asynchronous projection and subscription work within clusters
  • Support for PostgreSQL read replicas
  • First class support for managing PostgreSQL native partitioning with Marten
  • A ton of internal improvements including work to utilize the latest, greatest low level support in Npgsql for query batching

And for that matter, I’ve finishing up a proposal this weekend for a new JasperFx client looking to scale a single Marten system to the neighborhood of 200-300 billion events, so there’s still more work ahead.

Event Streaming Support or Transactional Outbox Integration

This was frequently called out as a big missing feature in Marten in 2021, but with the integration into the full Critter Stack with Wolverine, we have first class event streaming support that was introduced in 2024 for every messaging technology that Wolverine supports today, which is just about everything you could possibly think to use!

Management User Interface

Sigh. Still in flight, but now very heavily in flight with a CritterWatch MVP promised to a JasperFx client by the end of this month. Learn more about that here:

Cloud Hosting Models and Recipes

People have brought this up a bit over the years, but we don’t have much other than some best practices with using Marten inside of Docker containers. I think it helps us that PostgreSQL is almost ubiquitous now and that otherwise a Marten application is just a .NET application.