---
title: Add Chronicle to a worker service
description: Set up Chronicle in a .NET worker service (generic host) — the natural home for the reacting side of an event-sourced system, with the DI container wiring everything up for you.
---


A worker service is the natural home for the *reacting* side of an event-sourced system: no web front end, just a long-running host that processes events, runs scheduled jobs, or keeps derived data up to date. Setup is mostly a matter of letting the generic host's DI container do the wiring — register Chronicle once, then inject what you need wherever you need it.

We'll build the same small library domain as the other host guides. If you're building a web API instead, the [ASP.NET Core guide](/chronicle/get-started/aspnetcore/) covers that host; for the bare-bones, no-container version, see the [console guide](/chronicle/get-started/console/).

## Before you start

Have the Chronicle kernel running locally. [Run Chronicle locally](/chronicle/get-started/running-chronicle/) brings it up with a single `docker run` and lists the prerequisites (.NET 8+, Docker); this guide assumes it's listening on `chronicle://localhost:35000`.

## Set up the project

Create a folder for your project, then a .NET worker service inside it:

```shell
dotnet new worker
```

Add a reference to the [Chronicle client package](https://www.nuget.org/packages/Cratis.Chronicle):

```shell
dotnet add package Cratis.Chronicle
```

:::note
A worker only needs the base `Cratis.Chronicle` package — `Cratis.Chronicle.AspNetCore` is for web applications.
:::

## Register Chronicle on the host

The generic host builds your app through `IHostApplicationBuilder`, which already has a dependency-injection container. One call hooks Chronicle into it and names the event store to use:

```csharp
var builder = Host.CreateApplicationBuilder(args);

builder.AddCratisChronicle(options => options.EventStore = "Quickstart");
builder.Services.AddHostedService<Worker>();

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

Like the [ASP.NET Core](/chronicle/get-started/aspnetcore/) host, `AddCratisChronicle` registers Chronicle's services — `IEventStore`, `IEventLog`, `IReactors`, `IReducers`, `IProjections` — and automatically discovers and registers your artifacts (reactors, reducers, projections) from the loaded assemblies. It reads its connection settings from the `Cratis:Chronicle` section of `appsettings.json`:

```mermaid
flowchart LR
    Host["Host.CreateApplicationBuilder"] -->|AddCratisChronicle| DI["DI container<br/>(auto-discovers artifacts)"]
    DI --> Svcs["IEventStore · IEventLog ·<br/>IReactors · IReducers · IProjections"]
    Svcs --> Worker["your BackgroundService"]
```

```json title="appsettings.json"
{
  "Cratis": {
    "Chronicle": {
      "ConnectionString": "chronicle://localhost:35000",
      "EventStore": "Quickstart"
    }
  }
}
```

## Define some events

Everything in Chronicle starts with a fact. You model facts as `record` types marked with `[EventType]` — records because an event, once it happened, never changes. The attribute is how Chronicle discovers the type; it takes no name, the type name *is* the identity.

Here are the facts of a small library — a book arrives, gets borrowed, and comes back:

```csharp
using Cratis.Chronicle.Events;

[EventType]
public record BookAdded(string Title, string Isbn);

[EventType]
public record BookBorrowed(string MemberName);

[EventType]
public record BookReturned;
```

`BookReturned` carries no data at all — that it *happened*, on a particular book's stream, is the whole story. Not every fact needs a payload.

## Append them

You record a fact by **appending** it to an [event sequence](/chronicle/concepts/event-sequence/). Chronicle gives you one by default — the **event log**, the main sequence you'll use, much like the `main` branch of a Git repository. Reach it through the event store:

```csharp
var eventLog = eventStore.EventLog;

var bookId = Guid.NewGuid();
await eventLog.Append(bookId, new BookAdded("The Pragmatic Programmer", "978-0135957059"));
```

That first argument is the [event source id](/chronicle/concepts/event-source/) — the identity of the thing this fact is about, like a primary key. Every event you append against `bookId` becomes part of *that book's* stream of history.

:::tip
We use a raw `Guid` here to keep the quickstart short. In real code you'd wrap it in a strongly-typed `BookId` so the compiler can't confuse a book's id with a member's — the [tutorial](/chronicle/tutorial/) shows exactly that.
:::

Run your app, then open the [workbench](http://localhost:8080), pick your event store, and select **Sequences** — your `BookAdded` is sitting there at sequence number `0`, permanent and in order.

![Chronicle Workbench showing events](workbench.png)

## Turn events into a read model

Events are the **write** side — the source of truth. To *read* current state you don't query the log directly; you let Chronicle fold the events into a **read model** for you. The declarative way to do that is a [projection](/chronicle/concepts/projection/): you declare the shape you want and which events feed each field, and Chronicle keeps it in sync — no update code, ever.

```csharp
using Cratis.Chronicle.Keys;
using Cratis.Chronicle.Projections.ModelBound;

[ReadModel]
public record Book(
    [Key]
    Guid Id,

    [SetFrom<BookAdded>(nameof(BookAdded.Title))]
    string Title,

    [SetFrom<BookAdded>(nameof(BookAdded.Isbn))]
    string Isbn,

    [SetValue<BookAdded>(false)]
    [SetValue<BookBorrowed>(true)]
    [SetValue<BookReturned>(false)]
    bool OnLoan,

    [SetFrom<BookBorrowed>(nameof(BookBorrowed.MemberName))]
    string? BorrowedBy);
```

Read the attributes as a sentence: a book's `Title` and `Isbn` come from `BookAdded`; `OnLoan` is `false` when it's added, `true` when borrowed, `false` again when returned; `BorrowedBy` is whoever borrowed it. You're *declaring* how facts map onto the view — Chronicle replays the events in order and applies the mapping.

The projection writes `Book` to a store (MongoDB by default), so reading it is an ordinary query:

```csharp
public class Books(IMongoCollection<Book> collection)
{
    public IEnumerable<Book> OnLoan() => collection.Find(b => b.OnLoan).ToList();
}
```

Append a `BookBorrowed` against the same `bookId`, query again, and `OnLoan` is `true` with `BorrowedBy` set; append a `BookReturned` and it flips back. You never wrote an `UPDATE`.

## React when something happens

Projections build *state*. When you need to *do something* the moment a fact lands — notify someone, call another system — you write a **reactor**. `IReactor` is a marker; you just add a method whose first parameter is the event you care about, and Chronicle routes matching events to it:

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;

public class BookReturnedNotifier : IReactor
{
    public Task BookReturned(BookReturned @event, EventContext context)
    {
        // context.EventSourceId is the BookId this happened to
        Console.WriteLine($"Book {context.EventSourceId} was returned — notify the next member in line.");
        return Task.CompletedTask;
    }
}
```

No registration, no wiring — drop the class in and every `BookReturned` flows to it. Reactors must be idempotent, because the same event may be delivered more than once (during a replay or a recovery). In a real app you'd inject a notification service here — the [tutorial](/chronicle/tutorial/) and the [Reactors](/chronicle/reactors/) guide show that, along with how reactors get their dependencies under a host.

That's the whole loop — **append → project → react**. The [tutorial](/chronicle/tutorial/) builds exactly this library one concept at a time and explains each as you go; the [Projections](/chronicle/projections/), [Reducers](/chronicle/reducers/), and [Reactors](/chronicle/reactors/) guides go deeper on each piece.

## Append from the worker

In a worker you append from your `BackgroundService` rather than inline. Inject `IEventStore` (or `IEventLog` directly) and append inside `ExecuteAsync`:

```csharp
public class Worker(IEventStore eventStore) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await eventStore.Connection.Connect();

        var bookId = Guid.NewGuid();
        await eventStore.EventLog.Append(bookId, new BookAdded("The Pragmatic Programmer", "978-0135957059"));

        // Keep running so reactors and projections keep processing.
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}
```

The projection and the `BookReturnedNotifier` reactor pick those events up from the kernel — the worker stays alive to keep processing them.

## Configure the MongoDB client

The `Books` query reads documents Chronicle wrote, so the MongoDB driver needs to match how Chronicle stores them — register these conventions once at startup:

## MongoDB

When leveraging the Reducer and Projection capabilities of Chronicle, your MongoDB Client needs to be configured
to match how it produces documents and naming conventions. By adding the following code, you'll have something that
matches:

```csharp
BsonSerializer
    .RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));

var pack = new ConventionPack
{
    // We want to ignore extra elements that might be in the documents, Chronicle adds some metadata to the documents
    new IgnoreExtraElementsConvention(true),

    // Chronicle uses camelCase for element names, so we need to use this convention
    new CamelCaseElementNameConvention()
};
ConventionRegistry.Register("conventions", pack, t => true);
```

[Snippet source](https://github.com/cratis/samples/blob/main/Chronicle/Quickstart/Common/MongoDBDefaults.cs#L16-L27)

Then register the database and the collection so a type can take an `IMongoCollection<Book>` dependency:

```csharp
builder.Services.AddSingleton<IMongoClient>(new MongoClient("mongodb://localhost:27017"));
builder.Services.AddSingleton(provider => provider.GetRequiredService<IMongoClient>().GetDatabase("Quickstart"));
builder.Services.AddTransient(provider => provider.GetRequiredService<IMongoDatabase>().GetCollection<Book>("book"));
```

## Register your artifacts

Chronicle creates its discovered artifacts through the container, so they need to be registered as services. For a handful, register them explicitly; as the solution grows, let Cratis Fundamentals do it by convention:

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

`AddBindingsByConvention` registers any service that implements an interface of the same name prefixed with `I` (`IFoo` → `Foo`); `AddSelfBindings` registers concrete classes as themselves.

## Going further

- **Multi-tenant namespaces** — provide a custom `IEventStoreNamespaceResolver` to route operations per tenant. See [Namespace resolution](/chronicle/namespaces/dotnet-client/).
- **Structural dependencies** — supply custom identity providers, correlation-id accessors, or namespace resolvers through the `configure` callback on `AddCratisChronicle`. See [Structural dependencies](/chronicle/configuration/structural-dependencies/).

## Recap

You added Chronicle to a worker service with a single `AddCratisChronicle` call, pointed it at an event store through `appsettings.json`, and appended events from a `BackgroundService` — the generic host's DI container discovered your reactors, reducers, and projections and handed you the services to use them. The same library domain runs here unchanged from the other hosts.

## Where to go next

- **[Build the domain step by step](/chronicle/tutorial/)** — the tutorial walks the library model one concept at a time.
- **Reacting to events** — a worker's main job; see [Reactors](/chronicle/reactors/) for the patterns.
- **A different host** — the same artifacts run unchanged behind a web API ([ASP.NET Core](/chronicle/get-started/aspnetcore/)) or with no container at all ([console](/chronicle/get-started/console/)).
