
While Polecat is pretty new, it’s based on over a decade of experience and usage patterns established by the Marten library (and also shares a ton of common infrastructure code as well). Polecat is also backed up by JasperFx Software and we’re available for either consulting or support agreements for Polecat usage.
If you’re a .NET developer, it’s pretty likely your default choice of database at work is SQL Server. If you follow technical content on LinkedIn or reddit from the .NET community, you’re absolutely bombarded with a deluge of content about EF Core with a smattering of Dapper. All of that content assumes that you’re using SQL Server as a relational database paired with an object-relational mapper (ORM) like Entity Framework Core — and all the mapping ceremony, migration scripts, and impedance mismatch that comes along for the ride.
A decade ago some friends and I set out to escape a lot of that friction with Marten, which turns PostgreSQL into a rock-solid document database and event store for .NET. Marten has been running in production systems since the fall of 2016. And while not every system is a great fit for a document database, the Marten community has been able to be far more productive than they would be using an ORM with PostgreSQL where a document database fits. The one persistent question we got from the .NET community the whole time was some variation of: “This is great, but my shop is a SQL Server shop. Can I have this on SQL Server?”
Now you can. Polecat is a new member of the “Critter Stack” that brings the same document database (and event sourcing) developer experience to SQL Server 2025 — taking direct advantage of SQL Server 2025’s brand new native json data type. If you know Marten, you already know Polecat; the public API surface intentionally mirrors Marten so the patterns and muscle memory carry straight over. If you’ve never touched Marten, this post is a gentle introduction to what a document database can do for your productivity.
Show Me the Whole Thing First
Before we break it down, here’s a complete, runnable console application — top-level statements, nothing hidden. Create a new console project, add the Polecat NuGet package, point it at a SQL Server 2025 instance, and run it. There’s no migration step and no mapping file to write first; this is the whole program.
// Program.cs using Polecat; const string connectionString = "Server=localhost,1433;Database=app;User Id=sa;Password=P@55w0rd;Encrypt=False"; // 1. Spin up the document store. In its default development settings it will // create any missing tables on demand the first time it needs them. await using var store = DocumentStore.For(connectionString); // 2. Write a couple of documents in a single ACID transaction. await using (var session = store.LightweightSession()) { session.Store(new Customer { Region = "West Coast", Name = "Acme, Inc." }); session.Store(new Customer { Region = "East Coast", Name = "Initech" }); await session.SaveChangesAsync(); } // 3. Query them right back with LINQ — this queries *inside* the JSON column. await using (var query = store.QuerySession()) { var westCoast = await query .Query<Customer>() .Where(x => x.Region == "West Coast") .OrderBy(x => x.Name) .ToListAsync(); foreach (var customer in westCoast) { Console.WriteLine($"{customer.Name} ({customer.Region})"); } } // A plain POCO. No attributes, no DbContext, no mapping. Just a class. public class Customer { public Guid Id { get; set; } public string Region { get; set; } public string Name { get; set; } }
Run that and you’ll see Acme, Inc. (West Coast) print to the console — and if you peek at the database, you’ll find a pc_doc_customer table that you never asked anyone to create. No mappings, no database migrations, nothing but just getting stuff done!
The rest of this post is just explaining why each of those steps was so short.
A Quick Start
To get going, all you really need is a connection string to a SQL Server 2025 database. SQL Server is very Docker-friendly, which makes it a great choice for local development and disposable test databases.
Here’s the absolute simplest “hello world.” Say I have a plain old C# class for a customer:
public class Customer { public Guid Id { get; set; } public string Region { get; set; } public string Name { get; set; } }
Now let’s persist one and read it back:
using Polecat; await using var store = DocumentStore.For( "Server=localhost,1433;Database=app;User Id=sa;Password=...;Encrypt=False"); var customer = new Customer { Region = "West Coast", Name = "Acme, Inc." }; await using var session = store.LightweightSession(); session.Store(customer); await session.SaveChangesAsync(); // ...and later, load it right back into the same shape var loaded = await session.LoadAsync<Customer>(customer.Id);
That’s the whole thing. Two facts about that little sample are worth slowing down on, because they’re the entire pitch:
- We didn’t do any mapping. There’s no
DbContext, noOnModelCreating, no fluent configuration, no attributes, nothing telling Polecat how to flattenCustomerinto columns. We just wrote a class. - We didn’t create any database structure. We never wrote a
CREATE TABLE, never authored a migration, never ran a script. In its default “just get things done” settings, Polecat detects that the table forCustomerdoesn’t exist yet and quietly builds it out for us the first time we read or write one.
Polecat is using JSON serialization to persist the data. As long as your type can round-trip to and from JSON, Polecat can store it and load it. That’s it — that’s the contract.
What’s a Document Database, and Why Should You Care?
A document database lets you store and retrieve whole object graphs — “documents” — almost always as JSON, where the database lets you marshal objects in your code straight to storage and query them right back into the same structures later.
The payoff is that you get to code much more productively because you just don’t have nearly as much friction as you do with object-relational mapping, whether that’s wrangling an ORM or hand-writing SQL and mapping code. You don’t have to maintain a parallel relational schema that’s a slightly-wrong reflection of your domain model. You don’t have to keep a stack of migration scripts in lockstep with every property you add. You design the class you actually want, and you store the class you actually want.
If you’ve spent any real time with EF Core, the contrast is stark. With EF Core you’re maintaining a mapping layer: configuring keys, owned entities, value conversions, navigation properties, and a migration history table — all so that a relational schema can approximate your object model. With Polecat there is no mapping layer to maintain. The document is the model.
The SQL Server 2025 Native json Type
Here’s where Polecat gets to lean on something genuinely new. Earlier document-on-SQL-Server attempts had to shove JSON into an nvarchar(max) column and hope for the best. SQL Server 2025 introduced a real, first-class json data type, and Polecat uses it by default for document bodies.
When you stored that Customer above, Polecat created a table named pc_doc_customer with the document serialized into a native json column called data:
CREATE TABLE [dbo].[pc_doc_customer] ( [id] uniqueidentifier PRIMARY KEY NOT NULL, [data] json NOT NULL, -- native SQL Server 2025 JSON [version] bigint NOT NULL, [last_modified] datetimeoffset NOT NULL, [created_at] datetimeoffset NOT NULL, [tenant_id] varchar(250) NOT NULL, [dotnet_type] varchar(500) NULL );
That native type isn’t just cosmetic — it’s stored in an optimized internal representation and lets the SQL Server query engine reach inside the JSON efficiently, which is exactly what makes the LINQ querying and indexing below practical instead of a parlor trick.
If you’re on a SQL Server instance older than 2025, Polecat has your back: flip one switch and it falls back to nvarchar(max) storage transparently.
await using var store = DocumentStore.For(opts => { opts.ConnectionString = connectionString; opts.UseNativeJsonType = false; // store JSON as nvarchar(max) on pre-2025 SQL Server });
Integrating with Your Application
For a real application you’ll want Polecat wired into your IHost and dependency injection container. At this point in the .NET ecosystem it’s more or less idiomatic to use an Add[Tool]() method to integrate tools with your app, and Polecat follows that convention:
var builder = WebApplication.CreateBuilder(args); builder.Services.AddPolecat(opts => { opts.ConnectionString = builder.Configuration.GetConnectionString("sqlserver"); }) .UseLightweightSessions(); var app = builder.Build();
From there your endpoints and services can inject IDocumentStore to open sessions, or inject an IDocumentSession / IQuerySession directly. A lightweight session is the lean, no-change-tracking workhorse — there’s no dirty checking or identity map overhead unless you ask for it.
You Still Get LINQ
Schemaless storage is great right up until somebody asks you to actually find something. A common worry is that going document-style means giving up real querying. Not here. Because the document body lives in that native json column, Polecat ships a LINQ provider that translates your C# expressions into SQL that queries inside the JSON:
await using var session = store.QuerySession(); var westCoast = await session .Query<Customer>() .Where(x => x.Region == "West Coast") .OrderBy(x => x.Name) .Take(25) .ToListAsync();
Where, OrderBy / OrderByDescending, Skip / Take, FirstOrDefaultAsync, CountAsync, AnyAsync — the usual LINQ vocabulary works against your documents. There’s even a paged-list helper for the extremely common “page N of these results, and tell me the total count” use case:
using Polecat.Pagination; var page = await session .Query<Customer>() .OrderBy(x => x.Name) .ToPagedListAsync(pageNumber: 2, pageSize: 20); // page.TotalItemCount, page.PageCount, page.HasNextPage, ...
…and You Still Get Indexes
It’s not only possible to query within the structured JSON data — you can also add indexes that work inside it, so those queries stay fast as your tables grow. Under the covers Polecat creates a persisted computed column that pulls a value out of the JSON with JSON_VALUE, then builds an ordinary nonclustered index on it. You don’t have to know any of that, though. You just declare the index.
You can do it inline on a property with an attribute:
using Polecat.Attributes; public class Customer { public Guid Id { get; set; } [Index] public string Region { get; set; } [UniqueIndex] public string Email { get; set; } public string Name { get; set; } }
…or in your store configuration with a fluent, strongly-typed API that should feel awfully familiar to Marten users:
await using var store = DocumentStore.For(opts => { opts.ConnectionString = connectionString; opts.Schema.For<Customer>().Index(x => x.Region); opts.Schema.For<Customer>().UniqueIndex(x => x.Email); });
Either way, the computed column and its index are created and kept in sync as part of the same “just works” schema management we’ll talk about next.
“It Just Works” Database Migrations
This is my favorite part, and it’s the thing that genuinely changes how fast you can move day to day.
In its default development settings, Polecat manages your database schema for you. The first time you touch a Customer, Polecat checks the database, sees that pc_doc_customer (and any indexes you declared) are missing, and builds them on demand. There’s no migration step standing between writing a class and running your code. This whole mechanism — schema detection, diffing, and migration — comes from the Critter Stack’s Weasel library that Polecat shares with Marten.
You control how aggressive that is with a single setting:
opts.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate; // the development default // AutoCreate.All — drop & recreate (great for tests) // AutoCreate.None — never touch the schema (production-locked)
For production you almost certainly don’t want the app altering schema on a hot path at runtime, so Polecat plugs into the Critter Stack’s stateful-resource model. Add the resource setup on startup and Polecat will reconcile the database schema once, up front, as the host boots:
builder.Services.AddPolecat(opts => { opts.ConnectionString = connectionString; }); // provision/migrate all Polecat schema objects as the host starts builder.Services.AddResourceSetupOnStartup();
The same machinery also drives the command-line tooling, so you can export migration scripts for a DBA to review, or apply changes through your deployment pipeline instead of at runtime. The point is that you get to decide — Polecat never makes “should this app change my production schema?” an accident.
Evolving Your Model Without Fear
Now put the JSON storage and the automatic schema management together, and you get the thing that makes document databases so liberating: your model can evolve at the speed of your code.
Say next sprint the Customer needs a phone number and a signup date:
public class Customer { public Guid Id { get; set; } public string Region { get; set; } public string Name { get; set; } // new this sprint — no migration required public string PhoneNumber { get; set; } public DateOnly SignedUpOn { get; set; } }
There is no ALTER TABLE. There is no migration script. There is no dotnet ef migrations add. Because the whole document is stored as JSON, new properties simply start showing up in the JSON the next time you save a customer. Documents written before the change deserialize cleanly — the new properties just come back as their defaults until that record gets re-saved. Compare that to the EF Core loop of “edit the entity, add a migration, review the generated SQL, apply it, hope the data backfill is right.” With Polecat you edit the class and keep going.
That’s the productivity story in a nutshell. The friction that normally sits between “I changed my mind about the model” and “the database agrees with me” mostly evaporates.
But Is It Safe? (Yes — It’s ACID)
A fair objection to a lot of document databases is that you trade away transactional integrity for that flexibility, and end up fighting eventual-consistency bugs. Polecat doesn’t make that trade. It’s built directly on SQL Server, which means it’s fully ACID-compliant. Every SaveChangesAsync() is one transaction. You can batch a whole range of inserts, updates, and deletes across multiple document types and commit them atomically, and an immediate query afterward sees exactly what you’d expect — no “give it a few hundred milliseconds and try again” caveats.
And because Polecat is, at bottom, a (rather fancy) library on top of SQL Server — one of the most widely deployed database engines on earth — adopting it doesn’t mean introducing a new piece of exotic infrastructure your ops team has never seen. You keep your existing SQL Server backups, your existing monitoring, your existing hosting and cloud options, and your existing DBA’s hard-won expertise. Polecat just changes how productively you get to use all of it.
Wrapping Up
Polecat brings the document-database developer experience that Marten users have enjoyed for years to the SQL Server world, built squarely on top of SQL Server 2025’s new native json type. You get to:
- Get started in minutes — a connection string and a POCO, no mapping, no migrations.
- Skip the ORM ceremony — there’s no mapping layer to maintain like there is with EF Core, because the document is the model.
- Store documents in a real JSON column — SQL Server 2025’s native
jsontype, not a stringly-typednvarcharhack. - Keep your LINQ and your indexes — query inside the JSON and index inside it too.
- Let schema migrations just work — automatic in development, controlled and explicit in production.
- Evolve your model at the speed of your code — add a property, keep moving.
- Keep ACID transactions and your existing SQL Server investment the entire time.
If your shop runs on SQL Server and you’ve ever envied how fast the document-database crowd seems to move, this is the one for you. Go grab the Polecat NuGet package, point it at a SQL Server 2025 instance, and write a class. That’s genuinely all it takes to get started.












