Marten
.NET Transactional Document DB and Event Store on PostgreSQL
The Marten library provides .NET developers with the ability to use the proven PostgreSQL database engine and its fantastic JSON support as a fully fledged document database. The Marten team believes that a document database has far reaching benefits for developer productivity over relational databases with or without an ORM tool.
Marten also provides .NET developers with an ACID-compliant event store with user-defined projections against event streams.
Access docs here. For any of your queries including the whole of Critter stack, join our Discord channel and it is the best way to reach us quickly. You can also raise questions/queries via GitHub Discussions as well.
Support Plans
While Marten is open source, JasperFx Software offers paid support and consulting contracts for Marten.
Help us keep working on this project 💚
Become a Sponsor on GitHub by sponsoring monthly or one time.
Past Sponsors
Working with the Code
Prerequisites
- .NET SDK 9.0 and 10.0 — the libraries and test projects
multi-target
net9.0;net10.0 - Docker (recommended) or your own PostgreSQL 13+ server
- Node.js 22+ — only needed to work on the documentation website
The build is driven by Nuke (build/build.cs). build.sh, build.ps1 and
build.cmd are thin wrappers, so any target below can be run as ./build.sh <target> on Linux/macOS,
.\build.ps1 <target> in PowerShell, or build.cmd <target> on Windows.
./build.sh compile # restore and build src/Marten.slnx (the default target)
PostgreSQL
Marten supports PostgreSQL 13 or later; the async daemon relies on pg_current_snapshot(),
which first shipped in 13.
The quickest way to get a test database is the Docker Compose file at the repository root:
docker compose up -d # or: ./build.sh init-db (starts it and waits until it is ready)
docker-compose.yml builds its image from docker/postgres/Dockerfile, which layers PostGIS 3 and
pgvector onto the official postgres:17 image, so the Marten.PostGIS and Marten.PgVector test
projects have the extensions they need. Both packages are multi-arch, so this builds natively on
Apple-silicon machines. It listens on port 5432 with the postgres/postgres login and a
marten_testing database. ./build.sh rebuild-db tears the container down and starts it again.
A few suites need something other than the standard database:
| Suite | Database |
|---|---|
| Marten.TimescaleDB | docker compose -f docker-compose.timescaledb.yml up -d (TimescaleDB on port 5433 — point marten_testing_database at it) |
| MultiHostTests | docker compose -f src/MultiHostTests/docker-compose.yaml up -d (primary/standby pair on ports 5440/5441) |
PLV8 is no longer part of developing Marten — the patching API it once backed has been replaced by
native partial updates, and neither the
compose image nor CI installs the extension. Older applications that still depend on it can use the
separate Marten.PLv8 package.
Test configuration
By default the tests connect to:
Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres
To use a different server or PostgreSQL version, set environment variables rather than editing the compose file:
| Variable | Purpose |
|---|---|
marten_testing_database |
Connection string for the test database (the login needs the postgres role) |
DEFAULT_SERIALIZER |
Newtonsoft (default) or SystemTextJson |
DISABLE_TEST_PARALLELIZATION |
true to run test collections serially, as CI does |
Running the tests
The test suites use xUnit.net v3 and Shouldly,
and are split across many test projects under src/. There is one build target per test project:
| Target | Project |
|---|---|
test-base-lib |
Marten.Testing (shared harness) |
test-core |
CoreTests — schema management, retries, core services |
test-document-db |
DocumentDbTests — document storage features |
test-event-sourcing |
EventSourcingTests — events and projections |
test-daemon |
DaemonTests — async projection daemon |
test-linq |
LinqTests — LINQ-to-SQL translation |
test-patching |
PatchingTests — partial document updates |
test-multi-tenancy |
MultiTenancyTests |
test-tenant-partitioned-events |
TenantPartitionedEventsTests |
test-value-types |
ValueTypeTests — strong-typed identifiers |
test-modular-config |
ModularConfigTests |
test-container-scoped-projections |
ContainerScopedProjectionTests |
test-compiled-queries |
CompiledQueryTests |
test-source-generator |
Marten.SourceGenerator.Tests (no database) |
test-aot-runtime |
Native AOT smoke test |
test-stress |
StressTests (not run in CI) |
test-multi-host |
MultiHostTests (needs its own compose file) |
test-noda-time, test-aspnetcore, test-postgis, test-pgvector, test-timescaledb, test-entity-framework-core, test-memory-pack |
The extension packages |
Aggregate targets:
./build.sh test # every core suite against the standard database
./build.sh test-extensions # NodaTime, AspNetCore, PostGIS, PgVector, TimescaleDB, EF Core, MemoryPack
Useful options for any test target:
./build.sh test-event-sourcing --framework net10.0 # one TFM instead of every built TFM
./build.sh test-core --configuration Release
./build.sh test-core --disable-test-retry # see a suite's real stability
Test targets don't shell out to dotnet test. They run each project through the
Bobcat test supervisor (build/SupervisedTests.cs): a failing
test is retried in a fresh process, and a test that only passes on a retry is reported as
flaky — in the console, in the GitHub job summary and in a JSON ledger under
artifacts/test-ledger/ — rather than being counted as a clean pass. For a genuinely racy test,
[Trait("Retry", "3")] raises its attempt limit and [Trait("Isolated", "true")] runs it in its own
process.
While you're iterating, you can also run a project or a single test directly from your IDE's test runner or with the dotnet CLI:
dotnet test src/DocumentDbTests/DocumentDbTests.csproj --framework net10.0
Every test project must import src/Tests.props, which sets up xUnit v3 and the Microsoft Testing
Platform host the supervisor relies on.
Integration test harness
src/Marten.Testing/Harness has base classes that make integration tests against PostgreSQL efficient
and parallel-friendly:
IntegrationContext— a shared, default-configuredDocumentStore. Data is not cleared between tests, so if your assertions depend on a document type only holding what your test wrote, overrideClearedBeforeEachTestto list those types.DestructiveIntegrationContext— likeIntegrationContext, but wipes thepublicschema between tests. Use it sparingly.OneOffConfigurationsContext— for tests that configure their own store throughStoreOptions(...). Each fixture gets an isolated schema named after the test class.BugIntegrationContext— aOneOffConfigurationsContextthat puts every bug-reproduction test in the sharedbugsschema.StoreFixture/StoreContext<T>— share one custom-configuredDocumentStoreacross a class through xUnit'sIClassFixture<T>.
Continuous integration
CI runs on GitHub Actions only (.github/workflows/tests.yml). Each
test project is a separate job on its own runner and database, and the core suites run against both
supported combinations:
| .NET | PostgreSQL | Serializer |
|---|---|---|
| 9 | postgres:15-alpine |
Newtonsoft |
| 10 | postgres:latest |
System.Text.Json |
The extension suites use purpose-built images — postgis/postgis:17-3.5, pgvector/pgvector:pg17 and
timescale/timescaledb-ha:pg17 — and there are extra jobs for the Native AOT smoke test and for the
two-node replication pair used by MultiHostTests. A final flakiness job rolls up every job's retry
ledger. Tests run in Release with parallelization disabled.
Adding a test project takes two changes: a target in build/build.cs and a matrix entry in
tests.yml. A project with no CI entry isn't being run.
See CONTRIBUTING.md for the contribution workflow and PR guidelines.
Other build targets
| Target | Description |
|---|---|
compile |
Restore and build the solution (default) |
init-db / rebuild-db |
Start (or restart) the Docker Compose PostgreSQL database |
docs |
Run the documentation website locally (see below) |
docs-build |
Build the static documentation site |
clear-inline-samples |
Strip generated snippet bodies out of the docs Markdown |
benchmarks |
Run the BenchmarkDotNet suite in MartenBenchmarks |
pack |
Build the NuGet packages |
Documentation
The documentation at martendb.io is written in Markdown under /docs,
built with VitePress, and hosted on Netlify. Code samples are pulled into the
Markdown from compiling, tested source code by MarkdownSnippets,
and search is provided by Algolia DocSearch.
Running the docs locally
dotnet tool restore # installs the pinned mdsnippets tool from .config/dotnet-tools.json
npm install
npm run docs
npm run docs first runs mdsnippets to refresh every code snippet in the Markdown, then starts the
VitePress dev server at http://localhost:5050 with hot reload. ./build.sh docs does the same thing
and installs the npm packages and the mdsnippets tool for you.
Other scripts:
| Command | Description |
|---|---|
npm run mdsnippets |
Refresh the code snippets in the Markdown only |
npm run docs-build |
Refresh snippets, then build the static site |
npm run vitepress-dev |
Start VitePress without refreshing snippets |
To add a new page, create the Markdown file under docs/ and add it to the sidebar in
docs/.vitepress/config.mts.
Code samples with MarkdownSnippets
Don't paste C# into the Markdown. Instead, mark the code in the source — usually in a test, so the
sample is compiled and exercised by the build — with a named region whose name starts with sample_:
#region sample_my_snippet
var user = new User { FirstName = "Han" };
session.Store(user);
await session.SaveChangesAsync();
#endregion
Then reference it from a docs page with an empty snippet block:
<!-- snippet: sample_my_snippet --> <!-- endSnippet -->
When mdsnippets runs (as part of npm run docs), it fills the block in place with the code and a
link back to the source file on GitHub (see mdsnippets.json). Search the
repository for sample_ or snippet: to find plenty of examples.
A few rules:
- Edit the source, not the Markdown. The generated code between the snippet markers is overwritten
on every run, so change the
#regionin the C# file. - Commit the regenerated Markdown with your change, so reviewers can see the actual docs content in the PR.
- Snippet names are unique across the repository and a missing snippet fails the run
(
TreatMissingAsWarningisfalse). - Some directories, including the core
src/Martenlibrary, are excluded as snippet sources inmdsnippets.json— put samples in test or sample projects.
Linting
Pull requests that touch docs/ run markdownlint and cspell
(.github/workflows/docs-prs.yml). Run them locally first:
npx --yes markdownlint-cli@latest --disable MD009 -- "docs/**/*.md"
npx --yes cspell --config ./docs/cSpell.json "docs/**/*.md"
Add legitimate technical terms to the words list in docs/cSpell.json.
Publishing
The docs are deployed to Netlify by the manually triggered
Docs build and deploy workflow (./build.sh publish-docs, or
publish-docs-preview for a preview deploy).
The Marten 3.x documentation lived in the
/documentationfolder and used a different tool; it is maintained only on the 3.14 branch.
License
Copyright © Jeremy D. Miller, Babu Annamalai, Oskar Dudycz, Joona-Pekka Kokko and contributors.
Marten is provided as-is under the MIT license. For more information see LICENSE.
Code of Conduct
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community.