Skip to main content
SEMastery
Data Accessbeginner

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.

13 min readUpdated September 24, 2025

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.

IdeaRelational style (tables)Document style (Marten)
How data is shapedMany flat tablesOne JSON document per object
Reading a full objectJoin several tablesLoad one envelope
Changing the shapeOften needs a migrationJust change the C# class
Best forHighly shared, normalized dataSelf-contained objects (carts, profiles)
Underlying enginePostgreSQL or SQL ServerPostgreSQL 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.

How your C# object travels into PostgreSQL and back through Marten

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 Marten

You 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 postgres

Now 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 Marten object family: from the long-lived store down to a short-lived session

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

Open session
Store object
SaveChanges
Committed

Steps

1

Open session

Get a unit of work

2

Store object

Queue the change in memory

3

SaveChanges

Send one transaction

4

Committed

All or nothing

Each write waits until SaveChanges, so the database is never left half-done

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 typeWhat it doesWhen to use it
QuerySessionRead only, no trackingPure reads, reports, lookups
LightweightSessionRead and write, no trackingMost normal work (recommended)
IdentitySessionTracks loaded documentsWhen you load the same doc many times
DirtyTrackedSessionAuto-detects changesWhen 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.

A simple decision path for picking a Marten session

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

HTTP request
Open QuerySession
LINQ query
JSONB search
Return JSON

Steps

1

HTTP request

User asks for data

2

Open QuerySession

Light read session

3

LINQ query

Where and OrderBy

4

JSONB search

PostgreSQL works

5

Return JSON

Send back objects

The request opens a light session, queries JSONB, and returns 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.

Document mode and event mode both live in the same Marten store

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

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