Fast Document Database In .NET With Marten

Most .NET teams reach for Cosmos DB or MongoDB the moment they need document storage. But if you already run PostgreSQL, you may not need either. Marten is a .NET library that turns PostgreSQL into a proper document database and an event store, using the JSONB support that has shipped in PostgreSQL since version 9.4. You get schema-less storage, LINQ queries, and full SQL access on infrastructure you already operate and pay for.

I have used Marten on a couple of projects where the team wanted document-database ergonomics but did not want to introduce a second database engine into their stack. It removes an entire category of operational overhead: no separate service to provision, monitor, back up, and secure. This article walks through installing Marten, storing and querying documents, and the trade-offs you should weigh before putting it in production.

Installing and configuring Marten

You need a running PostgreSQL instance and the Marten NuGet package. Nothing else. Add the package to your project with the dotnet CLI.

dotnet add package Marten

That single command pulls in everything Marten needs to talk to PostgreSQL as a document store. Register it with dependency injection using the AddMarten extension method, typically in Program.cs.

builder.Services.AddMarten(options =>
{
    options.Connection(builder.Configuration.GetConnectionString("Marten"));
});

This one call registers three services you will use throughout your application: IDocumentStore, which creates sessions, generates schema migrations, and runs bulk inserts; IDocumentSession, used for both read and write operations; and IQuerySession, a lighter, read-only session for query-heavy code paths where you do not need to persist changes.

Marten can generate the required schema and tables automatically the first time it needs them. This is convenient in development and I would use it there without hesitation. For a production environment, do not rely on this. Generate migration scripts ahead of time and apply them through your normal deployment pipeline, the same way you would with EF Core migrations. Automatic schema creation on a production database is a common way to get surprised by a locked table during a deploy.

Storing documents with Marten

Storing a document is close to what you would expect from any ORM. You open a session from the document store and call Store, passing in your object.

var store = DocumentStore.For("Connection string to PostgreSQL");
 
using var session = store.OpenSession();
 
var product = new Product
{
    Name = "C# 11 and .NET 7 - Modern Cross-Platform Fundamentals",
    Price = 46.87
};
 
session.Store(product);
 
await session.SaveChangesAsync();

OpenSession gives you a session against PostgreSQL, Store queues the Product instance for persistence, and SaveChangesAsync commits it. Marten populates Product.Id for you at this point if you have not set it, and it supports Guid, int, long, and other identifier types. For numeric keys it uses the HiLo algorithm, which reserves blocks of ids in memory ahead of time rather than issuing one at a time. Under this scheme you will see gaps in your ids whenever the application restarts, because the reserved block is discarded. That is expected behavior, not data loss, and it is the same trade-off you already accept with identity columns in most relational databases.

One thing that catches people out: the IDocumentSession you get from OpenSession does not track changes on your entities automatically. If you load a document, mutate a property on it, and call SaveChangesAsync expecting Marten to notice the change, nothing gets persisted. You need a session with dirty checking switched on, created by calling DirtyTrackedSession on the document store instead of OpenSession. Decide which style you want per use case, since dirty tracking carries a small overhead from snapshotting the document state for comparison.

Querying documents with Marten

Marten gives you two ways to query: LINQ, which will feel immediately familiar if you have used EF Core, and raw SQL, since underneath it all you are still querying a PostgreSQL table. Here is a LINQ query that filters products above a given price.

var store = DocumentStore.For("Connection string to PostgreSQL");
 
using var session = store.QuerySession();
 
var products = session.Query<Product>().Where(p => p.Price > 9.99).ToList();

QuerySession opens a read-only session, which is a good default for query paths since it skips the bookkeeping a full IDocumentSession carries for writes. The Query<Product>() call gives you an IQueryable that Marten translates into a query against the underlying JSONB column, so standard LINQ operators like Where, OrderBy, and Select work the way you expect. The result here is a filtered list of products with a price above 9.99.

Beyond basic filtering, Marten also supports including related documents in a single round trip, batched queries so you can fetch several independent result sets in one call, paging, and full text search over your JSON documents. These cover most of what you would otherwise reach for a document database’s native query language to do.

Transactions, indexing, and event sourcing

Marten sessions are transactional by default. When you call SaveChangesAsync, either every document you stored in that session gets persisted, or none of them do. You do not need to wrap operations in an explicit transaction scope for this guarantee, which is a meaningful advantage over some document databases where cross-document transactions are limited or unavailable.

You can also configure indexes on document properties for faster queries, exactly as you would on a regular PostgreSQL table, because that is what it is under the hood. This is worth doing deliberately rather than as an afterthought. A document store makes it easy to query on any property, but PostgreSQL still has to scan JSONB without an index, and that catches up with you once your document counts grow past a few hundred thousand rows.

Marten is not only a document database wrapper. It has full support for event sourcing and projections, which makes it a genuinely good fit if you are implementing CQRS. You can store your domain events, replay them into projections, and query the projected read models, all in the same PostgreSQL database that holds your regular documents. That is a separate and much larger topic on its own, but it is worth knowing the capability exists before you reach for a dedicated event store.

Marten versus Cosmos DB and MongoDB

The honest comparison depends on what you are optimizing for. Cosmos DB and MongoDB Atlas give you managed, horizontally distributed storage with built-in multi-region replication, which Marten on a single PostgreSQL instance does not give you out of the box. If you need global low-latency writes across continents, Marten is not the right tool without a lot of extra work on the PostgreSQL side.

On the other hand, PostgreSQL is considerably cheaper than most managed document databases at moderate scale, and if your team already knows SQL and already operates PostgreSQL, you are not adding a new mental model or a new bill. You also keep the option of falling back to plain relational tables and joins for the parts of your data model that do not fit the document shape well, something you cannot do at all inside Cosmos DB or MongoDB. For a mid-size application that does not need multi-region distribution, Marten on PostgreSQL is a reasonable default, and one that is easy to underrate simply because it is less fashionable than a dedicated NoSQL product.

Closing thoughts

Marten is worth serious evaluation if you are choosing a data store for a new .NET service and document-shaped storage fits your domain. Before committing to it for production, work through schema migrations properly rather than relying on automatic generation, understand your relationships and foreign key needs since document databases handle references differently from relational joins, and read through the advanced configuration options for indexing and session behavior. None of these are blockers, but they are the areas where teams new to Marten tend to get surprised.

If you are already running PostgreSQL and evaluating a document database purely to avoid schema rigidity, try Marten before you provision a new managed service. You get most of the ergonomics you are after without adding a new piece of infrastructure to operate.

Leave a Reply

Discover more from Behind the Stack

Subscribe now to keep reading and get access to the full archive.

Continue reading