Skip to content

Event Sourcing on SQL Server with Polecat

Jeremy Miller30th September 2026
PolecatEvent SourcingCQRSSQL ServerWolverineCritterWatchMarten
Polecat

Polecat is fully supported by JasperFx Software and is automatically covered by every existing and future support plan, right alongside Marten and Wolverine. If your team is a SQL Server shop that wants to adopt Event Sourcing with a real safety net, we'd love to talk.

I've had some version of this conversation more times than I can count over the last ten years:

"We really like what Marten does. We'd love to use it. But we're a SQL Server shop, and that's not changing."

And for most of that decade, my only real answer was some variation of "I'm sorry, but SQL Server's JSON support just isn't there, and I don't have the bandwidth to do it anyway." That answer changed this year. Polecat is a port of Marten to SQL Server 2025 and its new native JSON data type. It's been out since March, it's had a lot of releases since then, and the SQL Server flavor of our own CritterWatch product is built on top of it.

This post is for the SQL Server shops. If you've been curious about Event Sourcing, or you've already tried to hand roll it on top of SQL Server and found out the hard way how much of the real work is in projections, concurrency, and the boring background processing, this is the tour I'd give you if you had me in the room. If you're already a Marten user, most of this is going to look awfully familiar. That's on purpose.

Here's the claim I'm going to make and then spend the rest of the post backing up: Polecat is the most full featured event store you can get for SQL Server in the .NET ecosystem, and it benefits from more than a decade of Marten running in production.

What you get in the box ​

Polecat is one library that gives you two things under the same IDocumentStore API that Marten users already know:

  1. A very full fledged event store for SQL Server, and that's the focus of this post
  2. A document database on top of SQL Server with LINQ querying, the patching API, multi-tenancy, soft deletes, optimistic concurrency, and more

Hey, if you're a dyed in the wool EF Core user and think that software development begins and ends with an ORM on top of a relational database, do yourself a favor and check out Polecat's model and see how it lets you get things done with far less code ceremony than EF Core possibly can.

For the event sourcing side, here's what "full featured" actually means, with links so you don't have to take my word for it:

I don't know of anything else targeting SQL Server that gets anywhere close to that list, and I'm including the "we wrote our own" event stores I've seen at client sites over the years. Those always start out as "it's just an append to a table," and they always end up rediscovering the async daemon the hard way.

Ten years of Marten, running on SQL Server ​

The most important thing to understand about Polecat is why a library that went 1.0 in March can credibly ship that feature list. It's not because we're geniuses, it's because we'd already done most of the work.

During the Marten 8.0 release cycle we pulled the event sourcing abstractions, the projection model, and the async daemon out of Marten and into a shared library called JasperFx.Events. At the time that was a miserable, expensive slog that I privately thought might turn out to be a very expensive sunk cost. It's turned out to be the single most important enabler for Polecat. Today, Marten and Polecat share:

  • The projection model itself: the Apply() / Create() / ShouldDelete() conventions, the source generators that emit the projection dispatch at compile time, and every projection base type
  • The async daemon coordinator, high water mark detection, and the hardening we did this summer to guarantee that a projection never silently skips an event
  • Projection rebuilds, dead lettering, subscriptions, upcasting, and the event metadata model
  • Weasel for schema management and database migrations -- with the newer Weasel.Storage library for shared infrastructure between Polecat, Marten, and Fisher, our SQLite backed store
  • A large compliance test project in our shared JasperFx repository
  • The Wolverine integration, the Bobcat testing tools, the AI skills, and CritterWatch

What's actually different between the two is the storage layer: SQL Server 2025's json type instead of PostgreSQL's jsonb, MERGE instead of ON CONFLICT, IDENTITY and sequences instead of bigserial, sp_getapplock instead of advisory locks, datetimeoffset instead of timestamptz. Which is exactly the part you'd want to be database specific.

Here's what that means for you in practice. Every hard won lesson from Marten in production, the subtle READ COMMITTED race in high water detection, the "what happens when a projection throws on one bad event at 3AM" question, the partitioning strategy for very large multi-tenant systems, the resiliency policies around database hiccups, all of that came along for free. When we fix or improve something in the shared guts, Polecat gets it in the next release. The strong consistency post and the scaling levers post I wrote earlier this year were written about Marten, but they apply to Polecat nearly line for line.

I'll also tell you that Polecat was deliberately built with somewhat simpler internals than Marten has accumulated over ten years, partly for faster cold starts and AOT friendliness, and partly because there's a lot of technical baggage in Marten we flat out didn't want to carry over.

A quick tour of the event store ​

You'll need SQL Server 2025 (the native JSON type is non-negotiable), and for local development the easiest thing is Docker:

yaml
services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2025-CU1-ubuntu-24.04
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: "YourStrong!Passw0rd"
    ports:
      - "1433:1433"

Add the Polecat Nuget, then wire it into your Program file:

csharp
builder.Services.AddPolecat(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("SqlServer"));

    // Optional, defaults to "dbo"
    opts.DatabaseSchemaName = "incidents";
});

Polecat will happily build out its own tables at runtime in development mode, so you don't have to fiddle with database migrations just to get going. For production you can have it generate the migration scripts for your DBAs to review, and I know that's a real requirement in a lot of SQL Server shops.

Since you're surely involved in software development and your job has at some point been dictated by an issue tracking tool, let's build an incident tracking system for a help desk. The events are just immutable records:

csharp
public record IncidentLogged(Guid CustomerId, Contact Contact, string Description, Guid LoggedBy);
public record IncidentCategorised(Guid IncidentId, IncidentCategory Category, Guid CategorisedBy);
public record AgentRespondedToIncident(Guid IncidentId, string Response, DateTimeOffset RespondedAt);
public record CustomerRespondedToIncident(Guid IncidentId, string Response, DateTimeOffset RespondedAt);
public record IncidentResolved(Guid IncidentId, ResolutionType Resolution, Guid ResolvedBy);
public record IncidentClosed(Guid ClosedBy);

You start a new event stream for a single logical Incident and append more events to it later:

csharp
await using var session = store.LightweightSession();

// Start a new stream
var incidentId = session.Events.StartStream<Incident>(
    new IncidentLogged(customerId, contact, "It's broken", loggedBy));

await session.SaveChangesAsync();

// ...and later, append to it
session.Events.Append(incidentId,
    new IncidentCategorised(incidentId, IncidentCategory.Software, agentId));

await session.SaveChangesAsync();

That's the raw event capture, and it's a single INSERT per batch of events. Polecat only supports what Marten calls the "Quick Append" mode, which happens to be the fastest option Marten has, and it means fewer round trips and much less contention on busy streams.

Projections: the write model and the read models ​

Events by themselves aren't terribly useful, so sooner or later you need a projection of those events into the current state. Here's the Incident write model that both our command handlers and our UI are going to use:

csharp
// The "partial" is required so that the JasperFx.Events
// source generator can emit the event dispatch at compile time
public partial class Incident
{
    public Guid Id { get; set; }

    // Polecat sets this itself for optimistic concurrency
    public int Version { get; set; }

    public IncidentStatus Status { get; set; } = IncidentStatus.Pending;
    public IncidentCategory? Category { get; set; }
    public bool HasOutstandingResponseToCustomer { get; set; }

    public void Apply(IncidentLogged _) { }
    public void Apply(IncidentCategorised e) => Category = e.Category;
    public void Apply(AgentRespondedToIncident _) => HasOutstandingResponseToCustomer = false;
    public void Apply(CustomerRespondedToIncident _) => HasOutstandingResponseToCustomer = true;
    public void Apply(IncidentResolved _) => Status = IncidentStatus.Resolved;
    public void Apply(IncidentClosed _) => Status = IncidentStatus.Closed;
}

Just like Marten, you have a dial for when that projection gets updated, and it's one line of configuration:

csharp
builder.Services.AddPolecat(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("SqlServer"));

    // Updated in the same SQL Server transaction as the events.
    // Genuinely strongly consistent.
    opts.Projections.Snapshot<Incident>(SnapshotLifecycle.Inline);

    // Or updated in the background by the async daemon
    // opts.Projections.Snapshot<Incident>(SnapshotLifecycle.Async);

    // Or don't persist it at all and aggregate on demand with
    // session.Events.AggregateStreamAsync<Incident>(id)
});

I want to linger on that first option for a second, because it's where SQL Server shops especially should perk up. Most event sourcing tools put your events in one specialized database and your read models in a different database, with a background process shuffling data between them, so eventual consistency is a mandatory tax you pay on day one whether you need it or not. Polecat keeps the events and the projected documents in the same SQL Server database, so an inline projection is updated in the very same transaction as the event append. Your read model is never stale, and when you outgrow that, you flip the lifecycle to async, and I'll show you in the next section why your command handlers don't have to change when you do. I wrote a whole post about this dial in Event Sourcing Without the Eventual Consistency Tax.

And if you do go async, the daemon is the same battle tested one from Marten. It runs across your application cluster with leader election, it tracks its progress with a high water mark so it never skips an event, you can rebuild any projection from scratch, and it can dead letter a poison event and keep going instead of stopping the world.

For the EF Core shops, you can also project events straight into EF Core entities through your existing DbContext with the EF Core projection support. You don't have to throw away the relational model your reporting tools already understand to get the benefits of event sourcing on the write side.

FetchForWriting(), the killer API only the Critter Stack has ​

If you take one API away from this post, make it this one. Think about what a command handler in an event sourced system actually has to do:

  1. Load the current state of the thing being changed (our Incident)
  2. Decide, based on that state, what new events should be recorded
  3. Protect against somebody else changing the same Incident at the same time
  4. Commit the new events, and update the projection if it's inline

Steps 1, 3, and 4 are a real pain to get right by hand, and they're where hand rolled event stores tend to fall over. FetchForWriting() does all three for you:

csharp
public static async Task Handle(CategoriseIncident command, IDocumentSession session)
{
    // Loads the current Incident state *and* sets up
    // an optimistic concurrency check on the stream
    var stream = await session.Events.FetchForWriting<Incident>(command.IncidentId);

    var incident = stream.Aggregate;
    if (incident.Status == IncidentStatus.Closed)
    {
        throw new InvalidOperationException("Incident is already closed");
    }

    // "Decide" what happened
    stream.AppendOne(new IncidentCategorised(incident.Id, command.Category, command.CategorisedBy));

    // If anybody else appended to this stream since we fetched it,
    // this throws and nothing is committed
    await session.SaveChangesAsync();
}

Here's the part people miss. FetchForWriting() is smart about the projection lifecycle:

  • If Incident is an inline snapshot, it just loads the persisted document, and after you append it forwards the same in-memory object through the new events and writes it back, all in one transaction
  • If Incident is an async projection, it loads the last persisted snapshot and then applies any newer events the daemon hasn't gotten to yet, so your command handler is always deciding against the truly current state even when the background projection is lagging
  • If Incident is live, it aggregates the stream on the fly

Your handler code is identical in every case. That's what makes the projection lifecycle a configuration detail you can change next year instead of an architectural commitment you're stuck with on day one. Start strong and inline, relax deliberately to async when the write throughput tells you to, and don't touch the handlers.

There are a couple of other tools in the same family worth knowing about:

  • FetchForExclusiveWriting() takes a pessimistic lock on the stream (UPDLOCK, HOLDLOCK under the covers) for the rare cases where you really do need to serialize writers
  • stream.AlwaysEnforceConsistency = true makes Polecat verify the stream version even when your handler doesn't append anything, for those "read, validate, and maybe do nothing" workflows where a race still matters
  • FetchLatest<T>() is the read side companion, giving you the current projected state of a stream even when the async daemon is behind

Now, the code above is still a little repetitive. Loading a session, calling FetchForWriting(), remembering to call SaveChangesAsync(), deciding what to do about concurrency exceptions. So let's add Wolverine and make all of that disappear.

CQRS with Polecat and Wolverine ​

The canonical Critter Stack architecture is Wolverine on the front edge handling HTTP requests and messages, Polecat on the storage edge, and the two wired together so that your command handlers can be small, explicit, and frequently pure functions. This is the same aggregate handler workflow that Marten users have had for years, and there's a complete CQRS with Polecat tutorial in the Wolverine documentation with a working sample in the Wolverine codebase.

Start from dotnet new webapi, add the WolverineFx.Http.Polecat Nuget to get Polecat, Wolverine, and the HTTP integration in one shot, and the bootstrapping looks like this:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPolecat(opts =>
    {
        opts.Connection(builder.Configuration.GetConnectionString("SqlServer"));
        opts.DatabaseSchemaName = "incidents";

        opts.Projections.Snapshot<Incident>(SnapshotLifecycle.Inline);
    })
    // Adds the transactional outbox, the aggregate handler
    // workflow, and lets Wolverine spread async projections
    // and subscriptions across the whole cluster
    .IntegrateWithWolverine(x => x.UseWolverineManagedEventSubscriptionDistribution = true);

builder.Host.UseWolverine(opts =>
{
    // Every handler that touches Polecat gets a transaction
    opts.Policies.AutoApplyTransactions();
});

builder.Services.AddWolverineHttp();

var app = builder.Build();

app.MapWolverineEndpoints();

// Extended command line support from the JasperFx / Critter Stack ecosystem
return await app.RunJasperFxCommands(args);

Logging a new incident is an HTTP endpoint that starts a new event stream. Notice that there's no IDocumentSession anywhere in sight:

csharp
public record LogIncident(Guid CustomerId, Contact Contact, string Description, Guid LoggedBy);

public static class LogIncidentEndpoint
{
    [WolverinePost("/api/incidents")]
    public static (CreationResponse<Guid>, IStartStream) Post(LogIncident command)
    {
        var (customerId, contact, description, loggedBy) = command;

        var logged = new IncidentLogged(customerId, contact, description, loggedBy);
        var start = PolecatOps.StartStream<Incident>(logged);

        var response = new CreationResponse<Guid>("/api/incidents/" + start.StreamId, start.StreamId);

        return (response, start);
    }
}

IStartStream is a Polecat specific side effect, and Wolverine takes care of actually executing it against the session and committing the transaction. Because that endpoint is a pure function, the unit test is trivial and doesn't need a database at all:

csharp
[Fact]
public void unit_test()
{
    var contact = new Contact(ContactChannel.Email);
    var command = new LogIncident(Guid.NewGuid(), contact, "It's broken", Guid.NewGuid());

    // Pure function FTW!
    var (response, startStream) = LogIncidentEndpoint.Post(command);

    startStream.Events.ShouldBe([
        new IncidentLogged(command.CustomerId, command.Contact, command.Description, command.LoggedBy)
    ]);
}

Now the good part. Remember that list of responsibilities a command handler has to take care of? Here's the CategoriseIncident endpoint using the aggregate handler workflow, which is FetchForWriting() wrapped up in Wolverine middleware:

csharp
public record CategoriseIncident(IncidentCategory Category, Guid CategorisedBy, int Version);

public static class CategoriseIncidentEndpoint
{
    // Wolverine runs this first and stops with a 400 if there's a problem
    public static ProblemDetails Validate(Incident incident)
    {
        return incident.Status == IncidentStatus.Closed
            ? new ProblemDetails { Detail = "Incident is already closed" }
            : WolverineContinue.NoProblems;
    }

    [EmptyResponse]
    [WolverinePost("/api/incidents/{incidentId:guid}/category")]
    public static IncidentCategorised Post(
        CategoriseIncident command,
        [WriteAggregate] Incident incident)
    {
        // "Decide" what event gets recorded
        return new IncidentCategorised(incident.Id, command.Category, command.CategorisedBy);
    }
}

Behind the scenes, Wolverine is generating code at build time to:

  1. Call FetchForWriting<Incident>() for the incidentId route argument, which also sets up the optimistic concurrency check
  2. Run your Validate() method against the current state, and stop with a ProblemDetails response if it says so
  3. Append whatever events your method returns to the stream
  4. Commit the Polecat transaction, including any outgoing messages through the transactional outbox
  5. Track the correlation id of the HTTP request on the events for observability

The business logic is a pure function from the current state and the command to a new event. You can unit test it with nothing but new Incident() and a command. And if you ever flip Incident from an inline snapshot to an async projection, this endpoint doesn't change at all, because FetchForWriting() is doing the right thing underneath.

Command handlers frequently need to kick off other work too, and that's where the transactional outbox earns its keep. Closing an incident records an event and schedules a follow up message to archive the stream a few days later:

csharp
public record CloseIncident(Guid ClosedBy, int Version);

public static class CloseIncidentEndpoint
{
    [WolverinePost("/api/incidents/close/{id}")]
    public static (UpdatedAggregate, Events, OutgoingMessages) Handle(
        CloseIncident command,
        [WriteAggregate] Incident incident)
    {
        if (incident.Status == IncidentStatus.Closed)
        {
            return (new UpdatedAggregate(), [], []);
        }

        return (
            // Respond with the updated Incident state
            new UpdatedAggregate(),

            // Events to append to the stream
            [new IncidentClosed(command.ClosedBy)],

            // Messages to publish *only if* the transaction succeeds
            [new ArchiveIncident(incident.Id).DelayedFor(3.Days())]);
    }
}

The IncidentClosed event and the scheduled ArchiveIncident message go into SQL Server in the same transaction. Either everything commits or nothing does. There's no "we wrote the event but the message got lost" failure mode, and no "we published the message but the transaction rolled back" failure mode either. That's the transactional outbox, and it's built into the Wolverine + Polecat integration.

Reacting to events ​

Once you've got events, you're going to want to do things with them. Wolverine gives you two main options with Polecat. Event forwarding publishes events as messages right at the time they're captured, through the outbox, with no ordering guarantees. Event subscriptions run through Polecat's async daemon and process the events in strict order, either by invoking Wolverine handlers on each event or by publishing them as messages:

csharp
builder.Services.AddPolecat(opts => { /* ... */ })
    .IntegrateWithWolverine(x => x.UseWolverineManagedEventSubscriptionDistribution = true)

    // Process Incident events in strict order with Wolverine handlers
    .ProcessEventsWithWolverineHandlersInStrictOrder("Incidents", o =>
    {
        o.IncludeType<IncidentResolved>();
        o.IncludeType<IncidentClosed>();
        o.Options.SubscribeFromPresent();
    });

And that UseWolverineManagedEventSubscriptionDistribution flag you've seen a couple times now means Wolverine will spread every async projection and subscription evenly across your running cluster, and move them to a healthy node if one goes down. That was a real scalability ceiling in early Marten and it isn't one anymore in either tool.

There's more in the Wolverine + Polecat integration than I can fit here: sagas persisted as Polecat documents, the declarative persistence attributes, and the store agnostic [WriteModel] / [ReadModel] spelling of everything above if you want code that compiles against both Marten and Polecat.

Running it in production: CritterWatch supports Polecat ​

Something I've become a lot more attuned to over the past couple years of JasperFx client work is that the sales pitch for event sourcing is the easy part. The part that decides whether the team still likes the decision a year later is operating it. Why is that projection falling behind? Which events did it dead letter, and why? What does the projected state look like right now for this stream, step by step?

That's exactly what CritterWatch is for, and CritterWatch supports Polecat as a first class citizen right alongside Marten. Add the CritterWatch.SqlServer package and you get:

  • A projections view of every projection and subscription in every service, per shard, with lag, status, and the ability to pause, resume, or rebuild without a redeploy
  • The projection stepper that replays a stream through a projection one event at a time so you can see exactly where the state went sideways
  • Dead letter triage and replay for both Wolverine messages and failed projection events
  • Alerts on projection lag, dead letter rates, and the rest
  • An MCP server so your AI agent can interrogate the running system with the same RBAC gated control surface you use

CritterWatch can run as a standalone console watching a whole fleet of services, or in embedded mode inside a single application. Both flavors are backed by either PostgreSQL and Marten or SQL Server 2025 and Polecat, so a SQL Server shop never has to bring in a second database engine just to monitor the first one. CritterWatch's SQL Server flavor is built on Polecat, which I like to think counts for something.

Polecat also ships its own built in MCP endpoints for agent driven exploration of the event store, and the Critter Stack AI skills cover Polecat setup, projections, and the Wolverine integration, so your coding agent already knows how this stuff is supposed to be used.

How Polecat is different from Marten ​

I promised you honesty, so here's where the two diverge beyond the database engine:

  • SQL Server 2025 is required. The native json type is the whole point. There's an nvarchar(max) fallback for older versions but you lose the JSON functions Polecat leans on, and I wouldn't run it that way in production.
  • System.Text.Json only. Marten supports Newtonsoft.Json for historical reasons. Polecat skipped that fork.
  • Quick Append only. Which is the faster of the two append modes in Marten anyway, so I don't think anybody is going to miss the other one.
  • Lightweight sessions by default, no automatic dirty checking. You explicitly Store() what you want saved. Marten users will recognize this as the configuration we recommend for Marten anyway.
  • Source generators instead of runtime code generation for projection dispatch, hence the partial on the Incident class earlier. That's better for cold start time and AOT, but it's a difference.
  • The community is younger. Marten has ten years of conference talks, blog posts, and Stack Overflow answers. Polecat has six months. The code is mature because the patterns are mature, but you'll be leaning on the docs, the Discord, and us a little more.

If you want the whole list, the Polecat introduction has a side by side comparison table.

How JasperFx supports you ​

I'll be blunt about the business angle. For most of the last decade, "we're a SQL Server shop" was the single biggest reason .NET teams told me they couldn't adopt Marten, and by extension, why they couldn't work with JasperFx. Polecat exists so that's no longer the case, and we've gone all in on it: the same team builds, tests, documents, and supports Marten, Polecat, and Wolverine, and Polecat has shipped alongside Marten on every shared improvement since March.

Concretely:

  • Support plans cover Polecat exactly the same as Marten and Wolverine, with a direct line to the people who wrote it, guaranteed response times, and prioritized bug fixes. If you already have a plan, Polecat is already in it.
  • Consulting, whether that's an architecture review before you start, a second set of eyes on the projection and consistency decisions in your design, or help getting a hand rolled event store onto something supported.
  • Workshops on Event Sourcing and CQRS with the Critter Stack. Every example we teach with Marten works on Polecat, and we'll happily run the whole thing against SQL Server for your team.
  • And the Critter Stack Discord, where we're around most days and where I'd genuinely like to hear how it goes.

If you're a SQL Server shop that's been watching event sourcing from across the PostgreSQL fence, this is your invitation. Start with the Polecat quick start, then the CQRS with Polecat tutorial, and come find us when you've got questions.

Further reading ​

RSS Feed · All Rights Reserved.