Skip to content

Sinks

A sink is the storage backend where Chronicle writes read model instances produced by projections and reducers. Chronicle ships with two built-in sink types.

The default sink. Read model instances are stored as MongoDB documents, one document per instance. The container name defaults to the read model type name (camelCase), controlled by the INamingPolicy in use.

This requires no additional setup — MongoDB is the default when you configure Chronicle.

Because read model instances land in MongoDB as documents, it’s natural to assume that configuring the MongoDB driver configures how Chronicle hands read models back. It doesn’t, and the gap is worth knowing before you spend an afternoon on it.

A convention pack — whether you provide one through Arc’s ICanProvideMongoDBConventionPacks or call ConventionRegistry.Register yourself — is registered in the MongoDB driver’s global convention registry. That registry configures the driver’s class maps. So a convention pack reaches exactly as far as the driver does, and no further:

Path a read model takesDoes the convention pack apply
Your own IMongoCollection<T> queryYes — the driver builds the instance from a class map
Chronicle’s read model APIs — by key, materialized, snapshots, watchingNo
A read model injected into a command’s validator, Provide(), or Handle()No
The document the sink writesNo

Chronicle returns a read model as JSON and materializes it with System.Text.Json, so no BsonClassMap is consulted anywhere on that path. Command-side read model injection is the one that surprises people: it resolves through the same read model API, so a rename, a custom serializer, or an IgnoreExtraElements convention that is visibly working in your query code has no effect at all on the instance your command receives. And the sink writes the document from the projected state rather than from a class map of your type, so a convention pack doesn’t change what gets stored either.

That is what the convention registration in Get started is for. It isn’t configuring Chronicle — it’s telling your driver how to map your type onto element names Chronicle has already chosen.

The SQL sink treats the database as a document store. Each read model type gets its own table with two columns:

ColumnTypeDescription
Idnvarchar / varcharThe read model key
Documentnvarchar(max) / textThe serialized JSON of the instance

Tables are created automatically on first use via IReadModelMigrator. You do not need to write migrations by hand.

This allows teams using SQL databases (SQL Server, PostgreSQL, SQLite) to take advantage of Chronicle projections without maintaining a MongoDB instance.

Set DefaultSinkTypeId in ChronicleOptions:

using Microsoft.Extensions.Hosting;
using Cratis.Chronicle.Sinks;
public static class SinksSqlRegistration
{
public static void Configure(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.AddCratisChronicle(configureOptions: options =>
{
options.DefaultSinkTypeId = WellKnownSinkTypes.SQL;
});
}
}

Or in appsettings.json:

{
"Cratis": {
"Chronicle": {
"DefaultSinkTypeId": "MongoDB"
}
}
}

The SQL sink is provided by the Cratis.Chronicle.Storage.Sql NuGet package. Add it to your Kernel host project and register it in the IChronicleBuilder:

siloBuilder.AddChronicleToSilo(chronicle =>
{
chronicle.WithSql(options);
});

Refer to the hosting documentation for full server setup details.

MongoDBSQL
Setup complexityRequires MongoDBRequires SQL database
Schema managementSchemalessTables auto-created on first use
Query flexibilityRich aggregation pipelineStandard SQL queries
Best forEvent-driven microservices, flexible schemasExisting SQL infrastructure, relational tooling

The sink type applies globally to all projections and reducers registered by the client. It cannot be set per-read-model from client configuration today — all read models share the same sink backend.