Aspire Integration
Chronicle provides first-class support for Microsoft Aspire, making it straightforward to run Chronicle as part of an Aspire distributed application. The Cratis.Chronicle.Aspire package adds a Chronicle resource to your Aspire AppHost, handling container lifecycle, endpoint wiring, and MongoDB configuration automatically.
Installation
Section titled “Installation”Add the Cratis.Chronicle.Aspire package to your AppHost project:
dotnet add package Cratis.Chronicle.AspireDevelopment Mode
Section titled “Development Mode”For local development, call AddCratisChronicle() without arguments. This uses the Chronicle development image (cratis/chronicle:latest-development), which bundles MongoDB, so no external database is required.
using Aspire.Hosting;
public static class HostingAspireDevMode{ public static void ConfigureAppHost(string[] args) { var builder = DistributedApplication.CreateBuilder(args);
var chronicle = builder.AddCratisChronicle();
builder.AddContainer("api", "my-org/my-api") .WithReference(chronicle);
builder.Build().Run(); }}The development image is ideal for:
- Getting started quickly without additional infrastructure
- Running in CI pipelines where a full stack is needed
Production Mode
Section titled “Production Mode”For production or staging environments, use the configure callback. This switches to the slim image (cratis/chronicle:latest-development-slim) — no embedded MongoDB — and lets you wire up an external database resource.
MongoDB
Section titled “MongoDB”using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireMongo{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) { var mongo = builder.AddConnectionString("chronicle-mongo");
return builder.AddCratisChronicle("chronicle", c => c.WithMongoDB(mongo)); }}The WithMongoDB method sets the Cratis__Chronicle__Storage__Type and Cratis__Chronicle__Storage__ConnectionDetails environment variables on the Chronicle container using the connection string from the provided MongoDB resource.
mongo can be any IResourceBuilder<IResourceWithConnectionString> that resolves to a replica set, including:
- A MongoDB Atlas connection string (
builder.AddConnectionString("...")) — Atlas clusters are always replica sets. - Any self-hosted or cloud MongoDB cluster reached through a connection string.
For local development against the slim image, AddCratisChronicleMongoDB() provisions this for you — a MongoDB container that initializes itself as a single-node replica set, handed back as a connection string carrying ?directConnection=true:
using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireMongoReplicaSetHelper{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) { var mongo = builder.AddCratisChronicleMongoDB();
return builder.AddCratisChronicle(configure: chronicle => chronicle.WithMongoDB(mongo)); }}If you would rather define the container yourself, the same recipe written out in full is:
using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireMongoReplicaSet{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) { // Chronicle uses MongoDB transactions and change streams, so the database must run as a // replica set. This command starts a single-node replica set that initializes itself on // first run, keeping mongod as PID 1 for correct signal handling and privilege drop. const string replicaSetCommand = "( until mongosh --quiet --eval 'db.adminCommand({ ping: 1 })' >/dev/null 2>&1; do sleep 0.3; done; " + "mongosh --quiet --eval 'try { rs.status() } catch (error) { rs.initiate({ _id: \"rs0\", members: [{ _id: 0, host: \"localhost:27017\" }] }) }' ) & " + "exec docker-entrypoint.sh mongod --replSet rs0 --bind_ip_all";
var mongo = builder.AddContainer("mongo", "mongo", "8.0") .WithEndpoint(targetPort: 27017, name: "tcp") .WithEntrypoint("/bin/sh") .WithArgs("-c", replicaSetCommand);
var mongoEndpoint = mongo.GetEndpoint("tcp");
// directConnection=true stops the driver from following the advertised replica-set member // host (localhost:27017, only reachable inside the container) back out and hanging. var mongoConnection = builder.AddConnectionString( "chronicle-mongo", ReferenceExpression.Create( $"mongodb://{mongoEndpoint.Property(EndpointProperty.Host)}:{mongoEndpoint.Property(EndpointProperty.Port)}/?directConnection=true"));
return builder.AddCratisChronicle("chronicle", c => c.WithMongoDB(mongoConnection)); }}directConnection=true keeps the driver from following the advertised replica-set member host — localhost:27017, only reachable inside the container — back out and hanging. This is the same single-node-replica-set recipe Chronicle’s own integration tests use. The embedded development image (AddCratisChronicle() with no configure callback) bundles a ready replica set, which is why the development path just works; the requirement only surfaces once you move to the slim image with an external database.
PostgreSQL
Section titled “PostgreSQL”using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspirePostgres{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) { var postgres = builder.AddConnectionString("chronicle-postgres");
return builder.AddCratisChronicle("chronicle", c => c.WithPostgreSql(postgres)); }}postgres can be any IResourceBuilder<IResourceWithConnectionString>, including:
- A connection string (
builder.AddConnectionString("...")) - A PostgreSQL container:
builder.AddPostgres("postgres").AddDatabase("chronicle")
Microsoft SQL Server
Section titled “Microsoft SQL Server”using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireSqlServer{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) { var sql = builder.AddConnectionString("chronicle-sql");
return builder.AddCratisChronicle("chronicle", c => c.WithMsSql(sql)); }}sql can be any IResourceBuilder<IResourceWithConnectionString>, including:
- A connection string (
builder.AddConnectionString("...")) - A SQL Server container:
builder.AddSqlServer("sql").AddDatabase("chronicle")
SQLite
Section titled “SQLite”For SQLite, provide the connection string directly (SQLite is file-based and does not require a network resource):
using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireSqlite{ public static IResourceBuilder<ChronicleResource> ConfigureAppHost(IDistributedApplicationBuilder builder) => builder.AddCratisChronicle("chronicle", c => c.WithSqlite("Data Source=/data/chronicle.db"));}Connecting a .NET Client
Section titled “Connecting a .NET Client”Pass the Chronicle resource as a connection string reference to your application projects so they receive the correct endpoint at runtime:
using Aspire.Hosting;using Aspire.Hosting.ApplicationModel;using Cratis.Chronicle.Aspire;
public static class HostingAspireConnectingClient{ public static void ConfigureAppHost(IDistributedApplicationBuilder builder, IResourceBuilder<ChronicleResource> chronicle) { // Reference any of your application's projects the same way - // builder.AddProject<Projects.MyApi>("api") when Projects.MyApi is generated from your solution builder.AddContainer("api", "my-org/my-api") .WithReference(chronicle); }}The connection string exposed by ChronicleResource uses the chronicle:// scheme and points to the gRPC port (35000 by default), matching the format expected by ChronicleOptions.FromConnectionString().
Chronicle exposes the following ports:
| Port | Endpoint Name | Description |
|---|---|---|
| 35000 | grpc | Primary Chronicle service — gRPC (HTTP/2) and Workbench/API/OAuth/health (HTTP/1.1) |
The grpc endpoint is registered automatically when you call AddCratisChronicle(). ChronicleResource no longer exposes a separate management endpoint — everything is served on the single grpc endpoint.
Complete Example
Section titled “Complete Example”A typical Aspire AppHost Program.cs for a production setup with MongoDB:
using Aspire.Hosting;
public static class HostingAspireCompleteExample{ public static void ConfigureAppHost(string[] args) { var builder = DistributedApplication.CreateBuilder(args);
var mongo = builder.AddConnectionString("chronicle-mongo");
var chronicle = builder.AddCratisChronicle("chronicle", c => c.WithMongoDB(mongo));
builder.AddContainer("api", "my-org/my-api") .WithReference(chronicle);
builder.Build().Run(); }}For development, simplify further by dropping the MongoDB wiring:
using Aspire.Hosting;
public static class HostingAspireCompleteExampleDev{ public static void ConfigureAppHost(string[] args) { var builder = DistributedApplication.CreateBuilder(args);
var chronicle = builder.AddCratisChronicle();
builder.AddContainer("api", "my-org/my-api") .WithReference(chronicle);
builder.Build().Run(); }}Docker Images
Section titled “Docker Images”| Image tag | Description |
|---|---|
cratis/chronicle:latest | Production image — no embedded MongoDB |
cratis/chronicle:latest-development | Development image — includes embedded MongoDB |
cratis/chronicle:latest-development-slim | Development slim image — no embedded MongoDB |
See Production Hosting for guidance on running Chronicle in production environments.