Storing Documents
The unit of work
Nothing is written until SaveChangesAsync():
await using var session = store.LightweightSession();
session.Store(user);
session.Store(order);
session.Delete<Invoice>(invoiceId);
await session.SaveChangesAsync(); // one transactionDocuments, events, patches, raw SQL commands, inline projection writes and other tenants' rows all commit together.
TIP
The unit of work is strictly sequential, where Marten runs its operations in parallel and aggregates the failures. One writer per file means there is nothing to gain from parallelism, and each queued operation is executed as its own command with its own reader, sharing only the transaction.
Store, Insert, Update
session.Store(document); // upsert — insert or update
session.Insert(document); // insert only; a duplicate id fails
session.Update(document); // update only; a missing row fails
session.Store(doc1, doc2, doc3);Store is the one you want almost always. The upsert is a single INSERT … ON CONFLICT … DO UPDATE … RETURNING statement, as on Marten — SQLite has had upsert syntax since 3.24.
TIP
The document is serialized when the batch runs, not when Store is called, so mutating it in between still takes effect. That matches Marten.
What happens at commit
In order:
- Session listeners'
BeforeSaveChanges/ dirty-tracking change detection - Inline projections are applied — which queues further operations, which is why it happens before the batch is taken
- Document tables that do not exist yet are created
BEGIN IMMEDIATE, and every queued operation runs- DCB boundary checks, event appends, tag rows, natural key rows
ITransactionParticipant.BeforeCommitAsyncand the outbox's — the last things inside- Commit
AfterCommitAsynchooks, the append observer, listeners — all outside the resilience pipeline
Deleting
See Deleting Documents.
Storing with a revision
If a type uses numeric revisions:
session.Store(doc, revision: 4);
session.UpdateRevision(doc, 4);
session.TryUpdateRevision(doc, 4); // no exception if it losesCross-tenant writes
session.ForTenant("globex").Store(order);Bulk loading
For a lot of documents at once, Store in a loop is not the tool — see Bulk Insert.
Initial data
To seed reference data at startup, see Initial Baseline Data.
Storing without registering
You do not have to register a document type. The first Store of an unregistered type creates its table.
A read provisions the table too, so Query<T>() or LoadAsync<T> against a type nothing has written yet answers empty rather than failing.
WARNING
The exception is an enlisted session, where a missing table throws by name rather than being created, on reads as on writes — running a migration on a second connection from inside your transaction would deadlock against your own write lock. Apply the schema before enlisting.

JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our