---
title: Add Chronicle to a console app
description: Wire Chronicle into a plain .NET console app by hand — no DI container, no web host — and watch a small library domain take shape through events, a projection, and a reactor.
---


A console app is the smallest place to run Chronicle: no web host, no dependency-injection container, nothing between you and the client. That's exactly why it's the clearest place to start — every moving part is something you write explicitly, so nothing is hidden by convention. (The [Worker Service](/chronicle/get-started/worker/) and [ASP.NET Core](/chronicle/get-started/aspnetcore/) guides let the host's DI container wire the same pieces up for you; start here if you want to see what that wiring actually does.)

We'll build a small, familiar domain — a library — and by the end you'll have appended events and projected them into read models you can query in MongoDB.

## 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`.

You can also find the [complete Console quickstart sample](https://github.com/Cratis/Samples/tree/main/Chronicle/Quickstart/Console) on GitHub.

## Set up the project

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

```shell
dotnet new console
```

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

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

## Connect the client

Everything in Chronicle is reached through a `ChronicleClient`. From a client you ask for the **event store** you want to work with — here, one named `Quickstart`. Because there's no DI container to do it for you, you create the client yourself:

```csharp
using Cratis.Chronicle;
using Cratis.Chronicle.Connections;

// ChronicleConnectionString.Development points at the local dev kernel on chronicle://localhost:35000
using var client = new ChronicleClient(ChronicleConnectionString.Development);
var eventStore = await client.GetEventStore("Quickstart");
```

`ChronicleConnectionString.Development` is the built-in connection string for the local development kernel — the same one `new ChronicleClient()` uses when you call it with no arguments. Spelling it out keeps the console version explicit; point it elsewhere with `ChronicleConnectionString.Default` (no credentials) or your own `new ChronicleConnectionString("chronicle://…")`.

That single `eventStore` is your handle to everything that follows:

```mermaid
flowchart LR
    Code["Your console code"] --> Client["ChronicleClient"]
    Client --> ES["event store 'Quickstart'"]
    ES --> Kernel[("Chronicle kernel<br/>(Docker)")]
```

## 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.

## Configure the MongoDB client

The `Books` query above reads documents Chronicle wrote. For your `IMongoCollection<Book>` to deserialize them, the MongoDB driver needs to match how Chronicle stores documents — 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)

With the conventions registered, the `Books` query reads the projection's documents exactly as written.

## Recap

You wired Chronicle into a bare console app by hand: created a `ChronicleClient`, opened the `Quickstart` event store, appended events for a small library domain, projected them into a `Book` read model with model-bound attributes, and reacted to one with a reactor. Because there was no DI container, every connection was explicit and in plain sight.

## Where to go next

- **[Build the same domain step by step](/chronicle/tutorial/)** — the tutorial walks the library model one concept at a time, explaining each as you go.
- **Let a host wire it up** — move the same code into a [Worker Service](/chronicle/get-started/worker/) or an [ASP.NET Core](/chronicle/get-started/aspnetcore/) app and let its DI container register the artifacts for you.
- **Understand the pieces** — the [Concepts](/chronicle/concepts/) section defines events, projections, reducers, and reactors in depth.
