Table of Contents

Quickstart Worker Service

Pre-requisites

Objective

In this quickstart, you will set up Chronicle in a .NET worker service (generic host) application — without ASP.NET Core. Worker services are ideal for background processing: reacting to events, running scheduled jobs, or maintaining derived data in message-processing pipelines.

Docker

Chronicle is available as a Docker image.

Image Tags for Development

Use one of these development tags:

Tag Includes MongoDB Typical Use
cratis/chronicle:latest-development Yes Fast local setup with no external database container
cratis/chronicle:latest-development-slim No Local setup with your own database container (MongoDB, PostgreSQL, SQL Server, or SQLite)

Quick Start: Embedded MongoDB (Development Image)

The development image bundles MongoDB, so no separate database setup is required.

docker run -d -p 27017:27017 -p 8080:8080 -p 35000:35000 cratis/chronicle:latest-development

Quick Start: External Database (Development Slim Image)

Use latest-development-slim and configure Chronicle storage through Chronicle options environment variables:

  • Cratis__Chronicle__Storage__Type
  • Cratis__Chronicle__Storage__ConnectionDetails

MongoDB with Docker Compose

Chronicle uses MongoDB transactions (and change streams), so MongoDB must run as a replica set or a sharded cluster. The following example initializes a single-node replica set for local development.

services:
  chronicle:
    image: cratis/chronicle:latest-development-slim
    depends_on:
      - mongodb
      - mongodb-init
    environment:
      - Cratis__Chronicle__Storage__Type=MongoDB
      - Cratis__Chronicle__Storage__ConnectionDetails=mongodb://mongodb:27017/?directConnection=true
    ports:
      - 8080:8080
      - 35000:35000

  mongodb:
    image: mongo:8
    command: ["mongod", "--replSet", "rs0", "--bind_ip_all"]
    ports:
      - 27017:27017

  mongodb-init:
    image: mongo:8
    depends_on:
      - mongodb
    restart: "no"
    command:
      - /bin/bash
      - -lc
      - |
        until mongosh --host mongodb --quiet --eval "db.adminCommand('ping')" >/dev/null 2>&1; do
          sleep 1
        done
        mongosh --host mongodb --quiet --eval "
        try {
          rs.status();
        } catch (e) {
          rs.initiate({
            _id: 'rs0',
            members: [{ _id: 0, host: 'localhost:27017' }]
          });
        }"

Why this setup:

  • host: 'localhost:27017' makes the replica set topology usable from host tools (for example mongosh and Compass) when they connect to mongodb://localhost:27017/?replicaSet=rs0.
  • Chronicle still reaches MongoDB over the Docker network (mongodb:27017) and uses directConnection=true to avoid following the advertised host back to localhost inside the Chronicle container.
  • directConnection=true does not disable transactions; transactions still work because MongoDB is running as a replica set.
  • If your existing data volume was initialized with a different replica-set host, run docker compose down -v (or wipe the MongoDB data volume) before starting again so rs.initiate() can apply the new host.

PostgreSQL with Docker Compose

services:
  chronicle:
    image: cratis/chronicle:latest-development-slim
    depends_on:
      - postgres
    environment:
      - Cratis__Chronicle__Storage__Type=PostgreSql
      - Cratis__Chronicle__Storage__ConnectionDetails=Host=postgres;Port=5432;Database=chronicle;Username=postgres;Password=postgres
    ports:
      - 8080:8080
      - 35000:35000

  postgres:
    image: postgres:16
    environment:
      - POSTGRES_DB=chronicle
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
    ports:
      - 5432:5432

SQL Server with Docker Compose

Warning

The SQL Server credentials in this example are for local development only. For production, use secure credentials and manage secrets through Docker secrets, environment files, or an external secret manager.

services:
  chronicle:
    image: cratis/chronicle:latest-development-slim
    depends_on:
      - sqlserver
    environment:
      - Cratis__Chronicle__Storage__Type=MsSql
      - Cratis__Chronicle__Storage__ConnectionDetails=Server=sqlserver,1433;Database=Chronicle;User Id=sa;Password=Your_strong_password123!;TrustServerCertificate=True;Encrypt=False
    ports:
      - 8080:8080
      - 35000:35000

  sqlserver:
    image: mcr.microsoft.com/mssql/server:2022-latest
    environment:
      - ACCEPT_EULA=Y
      - MSSQL_SA_PASSWORD=Your_strong_password123!
    ports:
      - 1433:1433

SQLite with Docker Compose

services:
  chronicle:
    image: cratis/chronicle:latest-development-slim
    environment:
      - Cratis__Chronicle__Storage__Type=Sqlite
      - Cratis__Chronicle__Storage__ConnectionDetails=Data Source=/data/chronicle.db
    volumes:
      - chronicle-data:/data
    ports:
      - 8080:8080
      - 35000:35000

volumes:
  chronicle-data:

Optional: Add Aspire Dashboard for Logs, Traces, and Metrics

If you want local observability while developing, run Chronicle with the Aspire Dashboard and set OTEL_EXPORTER_OTLP_ENDPOINT on the Chronicle container. Use port 18888 for the dashboard UI in your browser, and port 18889 as the OTLP receiver that Chronicle exports telemetry to inside the Docker network:

services:
  chronicle:
    image: cratis/chronicle:latest-development
    environment:
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://aspire-dashboard:18889
    ports:
      - 27017:27017
      - 8080:8080
      - 11111:11111
      - 30000:30000
      - 35000:35000

  aspire-dashboard:
    image: mcr.microsoft.com/dotnet/aspire-dashboard:latest
    environment:
      - DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true
      - ALLOW_UNSECURED_TRANSPORT=true
      - DOTNET_ENVIRONMENT=Development
    ports:
      - 18888:18888

Setup project

Start by creating a folder for your project and then create a .NET worker service project inside this folder:

dotnet new worker

Add a reference to the Chronicle client package:

dotnet add package Cratis.Chronicle

Note: For worker services you only need the base Cratis.Chronicle package — the Cratis.Chronicle.AspNetCore package is for web applications only.

Host setup

Open your Program.cs and configure Chronicle using AddCratisChronicle on the IHostApplicationBuilder:

var builder = Host.CreateApplicationBuilder(args);

builder.AddCratisChronicle(options =>
{
    options.EventStore = "MyWorkerApp";
});

builder.Services.AddHostedService<Worker>();

var host = builder.Build();
await host.RunAsync();

The AddCratisChronicle call:

  • Registers IChronicleClient, IEventStore, and all the event store components (IEventLog, IReactors, IReducers, IProjections, IReadModels) in the DI container.
  • Automatically discovers and registers all artifacts (Reactors, Reducers, Projections) from the loaded assemblies.
  • Reads configuration from the Cratis:Chronicle section of appsettings.json (connection string, timeouts, etc.).

Configuration

Chronicle reads its connection settings from appsettings.json. Add the following to yours:

{
  "Cratis": {
    "Chronicle": {
      "ConnectionString": "chronicle://localhost:35000",
      "EventStore": "MyWorkerApp"
    }
  }
}

You can also configure the event store name inline (as shown above) and keep the connection string in configuration.

Worker implementation

Inject IEventStore or any of the event store sub-services into your hosted service:

public class Worker(IEventStore eventStore) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Connect to Chronicle and start processing
        await eventStore.Connection.Connect();

        // Keep running until the host shuts down
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}

Structural dependencies

For custom identity providers, correlation ID accessors, or namespace resolvers, use the configure callback:

builder.AddCratisChronicle(
    configureOptions: options => options.EventStore = "MyWorkerApp",
    configure: b => b
        .WithIdentityProvider(new MyServiceIdentityProvider())
        .WithNamespaceResolver(new MyTenantResolver()));

See Structural Dependencies for a full list of configurable dependencies.

Namespace resolution

By default the worker uses the default namespace for all operations. To support multi-tenant scenarios, provide a custom IEventStoreNamespaceResolver via ChronicleClientOptions:

builder.AddCratisChronicle(options =>
{
    options.EventStore = "MyWorkerApp";
    options.EventStoreNamespaceResolverType = typeof(MyTenantNamespaceResolver);
});

Or pass a resolver instance directly through the builder:

builder.AddCratisChronicle(
    configureOptions: options => options.EventStore = "MyWorkerApp",
    configure: b => b.WithNamespaceResolver(new MyTenantNamespaceResolver(config)));

See Namespace resolution for details on built-in resolvers.

Services

Chronicle uses the DI container to create instances of Reactors, Reducers, and Projections it discovers. Register them as services in Program.cs:

builder.Services.AddTransient<MyReactor>();
builder.Services.AddTransient<MyReducer>();

For larger solutions, the Cratis Fundamentals convention-based registration helpers keep this manageable:

builder.Services
    .AddBindingsByConvention()
    .AddSelfBindings();