Fast Document Database in .NET with Marten
Learn how Marten turns PostgreSQL into a fast document database for .NET. Save C# objects as JSON, query with LINQ, and keep full ACID safety.
Fast Document Database in .NET with Marten
Think about a big steel almirah (cupboard) at home where your family keeps important papers. Some people keep papers in a strange way. They cut every document into small pieces and put each piece in a different drawer. The Aadhaar number in one drawer, the address in another, the photo in a third drawer. When you need the full document back, you must open many drawers and join the pieces together. That is slow and tiring.
Now imagine a better cupboard. You take the full paper, fold it once, put it inside one clean envelope, write a name on the envelope, and drop it in the cupboard. When you need it, you pick the right envelope and the whole paper is there in one go. No joining. No running around.
That second cupboard is how Marten works. Your C# object is the full paper. Marten folds it into JSON, puts it in one envelope, and stores it inside PostgreSQL. When you ask for it, the whole object comes back together. You do not split it into many tables. You do not join rows by hand.
This article shows you how Marten gives you a fast document database in .NET, while still keeping the strong safety of a real SQL database. We will go slow, with short sentences and real examples.
What problem does Marten solve?
When you use a tool like EF Core, you map your .NET classes to flat tables. One class often becomes many tables. A Customer with a list of Orders and each order with a list of Items can spread across three or four tables. To read it back, the database joins those tables. This works well, but for some apps it feels heavy.
Marten takes a different road. It says: "Just give me the object. I will save it as one JSON document." PostgreSQL has a special column type called JSONB. It stores JSON in a smart binary form that can be searched and indexed quickly. Marten uses this JSONB power as its engine.
So you get the best of two worlds:
- The easy feel of a document database (like MongoDB), where you save and load whole objects.
- The strong safety of a SQL database (like PostgreSQL), with real transactions and consistency.
A quick word on the two storage styles
Let us compare the two ways of thinking before we write code.
| Idea | Relational style (tables) | Document style (Marten) |
|---|---|---|
| How data is shaped | Many flat tables | One JSON document per object |
| Reading a full object | Join several tables | Load one envelope |
| Changing the shape | Often needs a migration | Just change the C# class |
| Best for | Highly shared, normalized data | Self-contained objects (carts, profiles) |
| Underlying engine | PostgreSQL or SQL Server | PostgreSQL JSONB |
Neither style is "the winner." They solve different jobs. Marten shines when each object mostly stands on its own, like a shopping cart, a user profile, or a support ticket.
The big picture of Marten
Before code, see the flow. You hand Marten an object. Marten turns it into JSON. PostgreSQL keeps that JSON safely. When you ask, the JSON comes back and Marten rebuilds your object.
The nice part is that you barely see the JSON. You work with normal C# objects. Marten does the folding and unfolding for you, quietly, in the background.
Step 1: Install Marten
Marten lives in a NuGet package. You add it to your project with one command.
dotnet add package MartenYou also need a running PostgreSQL database. For learning, the easiest way is a small Docker container. One command gives you a database on your own machine.
docker run --name marten-pg -e POSTGRES_PASSWORD=secret -p 5432:5432 -d postgresNow you have PostgreSQL listening on port 5432. That is all the setup you need to start.
Step 2: Register Marten in your app
Most modern .NET apps use the built-in service container. Marten gives you a simple helper called AddMarten. You tell it the connection string, and you are done.
using Marten;
var builder = WebApplication.CreateBuilder(args);
// Register Marten with one connection string.
builder.Services.AddMarten(options =>
{
var connectionString =
"Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=secret";
options.Connection(connectionString);
});
var app = builder.Build();
app.Run();That is the whole setup. Marten is now part of your app. You can ask the container for an IDocumentStore, an IDocumentSession, or an IQuerySession whenever you need them.
Here is the chain of objects Marten gives you, from biggest to smallest.
The DocumentStore is the big factory. You make it once and keep it for the whole app. It holds the connection string, the serialization rules, and the schema knowledge.
The session is small and short-lived. It is your unit of work. You open one, do some reads and writes, save, and throw it away.
Step 3: Make a document class
A document is just a plain C# class. The only rule is that Marten needs a way to know the identity of each document. The easiest way is a property named Id.
public class Customer
{
public Guid Id { get; set; }
public string Name { get; set; } = "";
public string City { get; set; } = "";
public List<string> FavouriteProducts { get; set; } = new();
}Look closely. The FavouriteProducts list lives inside the same class. In the table style, that list would become a separate table. In Marten, it stays inside the one JSON envelope. The whole customer travels together.
Step 4: Save a document
To save, you open a session, call Store, and then call SaveChanges. Nothing is written to PostgreSQL until you call SaveChanges. That is important. It means all your changes go in together, as one safe transaction.
public async Task AddCustomer(IDocumentStore store)
{
// Open a lightweight session: best speed for normal work.
await using var session = store.LightweightSession();
var customer = new Customer
{
Id = Guid.NewGuid(),
Name = "Aarav Sharma",
City = "Pune",
FavouriteProducts = { "Tea", "Biscuits" }
};
session.Store(customer);
// Now everything is written together as one transaction.
await session.SaveChangesAsync();
}If your app crashes after Store but before SaveChangesAsync, nothing is saved. The database stays clean. This is the ACID safety that you get for free, because PostgreSQL sits underneath.
Understanding the save flow
Let us slow down and watch each step of a save. This helps you see why nothing is half-saved.
Saving a document with Marten
Steps
Open session
Get a unit of work
Store object
Queue the change in memory
SaveChanges
Send one transaction
Committed
All or nothing
The key word is queue. When you call Store, Marten only remembers the change in memory. It does not touch the database yet. Only SaveChanges sends everything in one shot. So either all your changes land, or none of them do.
Step 5: Load a document by its Id
Loading one document by Id is the fastest read in Marten. You pass the Id and Marten hands back the whole object.
public async Task<Customer?> GetCustomer(IDocumentStore store, Guid id)
{
// A query session is read-only and very light.
await using var session = store.QuerySession();
var customer = await session.LoadAsync<Customer>(id);
return customer;
}No joins. No stitching. The full customer, with the favourite products list inside, comes back in one trip.
Step 6: Query with LINQ
This is where many .NET developers smile. If you know EF Core, you already know how to query Marten. You call Query<T>() and then write normal LINQ.
public async Task<List<Customer>> CustomersInPune(IDocumentStore store)
{
await using var session = store.QuerySession();
var puneCustomers = await session
.Query<Customer>()
.Where(c => c.City == "Pune")
.OrderBy(c => c.Name)
.ToListAsync();
return puneCustomers;
}Behind the scenes, Marten turns your LINQ into a real SQL query that searches inside the JSONB column. You write friendly C#. PostgreSQL does the fast work. You never write the JSON-searching SQL by hand.
You can even search inside the nested list. This query finds every customer who likes "Tea":
var teaLovers = await session
.Query<Customer>()
.Where(c => c.FavouriteProducts.Contains("Tea"))
.ToListAsync();That nested search would need an extra table and a join in the relational style. In Marten it is one simple line.
Choosing the right session
Marten gives you a few session types. Picking the right one keeps your app fast. Here is a simple guide.
| Session type | What it does | When to use it |
|---|---|---|
QuerySession | Read only, no tracking | Pure reads, reports, lookups |
LightweightSession | Read and write, no tracking | Most normal work (recommended) |
IdentitySession | Tracks loaded documents | When you load the same doc many times |
DirtyTrackedSession | Auto-detects changes | When you want EF-style auto save |
A quick rule for beginners: use LightweightSession for writing and QuerySession for reading. These two cover almost everything and give the best speed. Reach for the tracking sessions only when you have a clear reason.
Indexes make searches fast
When your data grows, a plain search must scan many documents. That gets slow. The fix is an index, just like the index at the back of a textbook helps you jump to the right page.
Marten lets you add indexes on the fields you search often. You set them up once when you register Marten.
builder.Services.AddMarten(options =>
{
options.Connection(connectionString);
// Make searches on City fast by indexing it.
options.Schema.For<Customer>()
.Index(c => c.City);
});Now a query that filters by City does not scan every customer. PostgreSQL jumps straight to the matching rows. The more data you have, the bigger this saving becomes.
How Marten fits a typical web request
Let us connect everything into a real web flow. A user asks for customers in a city. Your controller opens a session, runs a LINQ query, and returns the result.
A read request through Marten
Steps
HTTP request
User asks for data
Open QuerySession
Light read session
LINQ query
Where and OrderBy
JSONB search
PostgreSQL works
Return JSON
Send back objects
Each request gets its own short session. The big DocumentStore stays alive the whole time. This pattern keeps connections healthy and your app quick.
Marten is also an event store
There is one more gift inside Marten. Besides being a document database, it is also a full event store. Event sourcing means you save every change as an event, instead of only saving the final state. Think of a bank passbook. It does not only show your balance. It shows every deposit and every withdrawal that led to that balance.
Marten can store these events with the same ACID safety, and it can build "projections" that turn the stream of events into a normal readable view. You do not need this for every app. But it is good to know that the same tool can grow with you when you need it.
For now, focus on the document side. Once you are comfortable, the event store is there waiting for you.
When should you reach for Marten?
Marten is a great fit when:
- Your objects mostly stand on their own, like carts, profiles, drafts, or tickets.
- You change the shape of your data often and do not want a migration every time.
- You already use PostgreSQL and want to avoid adding a separate NoSQL server.
- You want JSON flexibility but refuse to lose ACID safety.
Marten may not be the best fit when:
- Your data is highly shared and deeply relational, with many cross-links.
- You need heavy SQL reporting with complex joins across many entities.
- Your team is committed to another database engine that is not PostgreSQL.
There is no shame in mixing tools. Many teams use EF Core for the relational core and Marten for the document-shaped parts. The two can live in the same app.
A note on licensing and the wider ecosystem
Marten is part of the JasperFx family of open-source .NET tools, and the core document-database features are free and open source. This is worth saying because some popular .NET libraries have recently moved to a commercial license. For example, MediatR and MassTransit now require a paid license for many uses. Marten itself remains an open-source project, though its maintainers do offer paid support and some advanced add-ons. Always check the current license page before you ship a product, because terms can change over time.
Putting it all together
Here is a tiny end-to-end picture in your head. You install Marten. You point it at PostgreSQL. You write a plain class. You store it, load it, and query it with LINQ. PostgreSQL keeps everything safe with real transactions. You never wrote a single line of JSON-handling SQL.
That is the whole promise of Marten: the comfort of a document database, with the trust of a SQL database, all in friendly C#.
References and further reading
- Marten Official Documentation - the home page with full guides.
- Marten Getting Started Guide - the quickest path to your first document.
- Querying Documents with LINQ - all the LINQ you can use.
- Opening Sessions - the different session types explained.
- Marten on GitHub - source code, issues, and releases.
- Fast Document Database in .NET with Marten - Milan Jovanovic - a clear community walkthrough.
Quick recap
- Marten turns PostgreSQL into a fast document database for .NET.
- Your C# object is saved as one JSONB document, not split into many tables.
- The DocumentStore is made once; a session is your short-lived unit of work.
- Use LightweightSession to write and QuerySession to read for the best speed.
- Nothing is saved until you call SaveChanges, so writes are all-or-nothing (ACID).
- You query with normal LINQ, including searches inside nested lists.
- Add an index on fields you search often to keep big datasets fast.
- Marten is also an event store, so it can grow with your app later.
- The core of Marten is open source, unlike some libraries that recently went commercial.
Related Posts
Best Practices When Working With MongoDB in .NET
Learn simple, proven MongoDB best practices in .NET: singleton client, connection pooling, indexes, projections, and safe writes explained for beginners.
Building a Multitenant Cloud Application With Azure Functions and Neon Postgres
A beginner-friendly guide to building a multitenant cloud app with Azure Functions and Neon serverless Postgres, using a database-per-tenant design in .NET.
Getting Started with Event Sourcing in .NET with Marten and PostgreSQL
Learn event sourcing in .NET using Marten and PostgreSQL. Store events, build aggregates and projections, and read state the easy, beginner-friendly way.
Getting Started With MongoDB in EF Core: A Beginner's Guide
A friendly beginner guide to using MongoDB with EF Core in .NET. Learn setup, DbContext, UseMongoDB, CRUD, mapping, and the limits you must know.
Getting Started With pgvector in .NET for Simple Vector Search
Learn pgvector with .NET, Npgsql and EF Core to store embeddings and run simple vector search with cosine distance and HNSW indexes, step by step.
Using Stored Procedures and Functions With EF Core and PostgreSQL
A friendly, beginner guide to calling PostgreSQL stored procedures and functions from EF Core using FromSql, ExecuteSql, and keyless entities.