Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Marten and other JasperFx libraries. Please check our Support Plans for more details.

Modeling documents

In this chapter, we'll define the domain model for our freight and delivery system and store it in PostgreSQL using Marten as a document database.

Learning Goals

  • Design C# document types (Shipment, Driver)
  • Store documents using Marten
  • Query documents using LINQ
  • Understand Marten's identity and schema conventions

Defining Documents

We'll start by modeling two core entities in our domain: Shipment and Driver.

cs
public class Shipment
{
    public Guid Id { get; set; }
    public string Origin { get; set; } = null!;
    public string Destination { get; set; } = null!;
    public DateTime CreatedAt { get; set; }
    public DateTime? DeliveredAt { get; set; }
    public string Status { get; set; } = null!;
    public Guid? AssignedDriverId { get; set; }
}

public class Driver
{
    public Guid Id { get; set; }
    public string Name { get; set; } = null!;
    public string LicenseNumber { get; set; } = null!;
}

Marten uses Id as the primary key by convention. No attributes or base classes are required.

Once defined, Marten will automatically create tables like mt_doc_shipment and mt_doc_driver with a jsonb column to store the data.

Storing Documents

cs
var driver = new Driver
{
    Id = Guid.NewGuid(),
    Name = "Alice Smith",
    LicenseNumber = "A123456"
};

var shipment = new Shipment
{
    Id = Guid.NewGuid(),
    Origin = "New York",
    Destination = "Chicago",
    CreatedAt = DateTime.UtcNow,
    AssignedDriverId = driver.Id,
    Status = "Created"
};

await using var session = store.LightweightSession();
session.Store(driver);
session.Store(shipment);
await session.SaveChangesAsync();

Marten uses PostgreSQL's INSERT ... ON CONFLICT DO UPDATE under the hood to perform upserts.

Querying Documents

Use LINQ queries to fetch or filter data:

cs
await using var querySession = store.QuerySession();

// Load by Id
var existingShipment = await querySession.LoadAsync<Shipment>(shipment.Id);
Console.WriteLine($"Loaded shipment {existingShipment!.Id} with status {existingShipment.Status}");

// Filter by destination
var shipmentsToChicago = await querySession
    .Query<Shipment>()
    .Where(x => x.Destination == "Chicago")
    .ToListAsync();
Console.WriteLine($"Found {shipmentsToChicago.Count} shipments to Chicago");

// Count active shipments per driver
var active = await querySession
    .Query<Shipment>()
    .CountAsync(x => x.AssignedDriverId == driver.Id && x.Status != "Delivered");
Console.WriteLine($"Driver {driver.Name} has {active} active shipments");

You can also project into DTOs or anonymous types for performance if you don’t need the full document.

Indexing Fields for Performance

If you frequently query by certain fields, consider duplicating them as indexed columns:

cs
opts.Schema.For<Shipment>().Duplicate(x => x.Status);
#pragma warning disable CS8603 // Possible null reference return.
opts.Schema.For<Shipment>().Duplicate(x => x.AssignedDriverId);
#pragma warning restore CS8603 // Possible null reference return.

This improves query performance by creating indexes on those columns outside the JSON.

Visual Recap

Store

Store

Query: Destination = Chicago

Shipment Created

mt_doc_shipment (JSONB)

Driver Registered

mt_doc_driver (JSONB)

LINQ Result

6w01kj

Summary

  • Documents are plain C# classes with an Id property
  • Marten stores them in PostgreSQL using jsonb
  • You can query documents using LINQ
  • Index fields you query often for better performance

INFO

You can download the source code zip file freight-shipping-tutorial.zip from this link.

  1. Ensure you have .NET 9.0 installed in your machine.
  2. Unzip the downloaded zip file and run the project using dotnet run, it will show you the list of commands for each tutorial page.
  3. You can set up the Postgres database as outlined in the docker-compose.yml file. Or run your own Postgres instance and update the connection string accordingly in appsettings.json file
  4. As an example, run dotnet run -- getting-started which executes the sample code in Getting Started page. Similarly the other list of commands will correspond to the respective tutorial pages accordingly.

Released under the MIT License.