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
.
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
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:
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:
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
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.
- Ensure you have .NET 9.0 installed in your machine.
- Unzip the downloaded zip file and run the project using
dotnet run
, it will show you the list of commands for each tutorial page. - 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 inappsettings.json
file - 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.