---
title: Add Chronicle to an ASP.NET Core app
description: Wire Chronicle into an ASP.NET Core web application — the DI container registers and discovers everything, and a minimal API endpoint appends events directly.
---


When your app is a web API, Chronicle fits the way you already build: it plugs into the `WebApplicationBuilder`, registers itself in the dependency-injection container, and lets your endpoints append events by taking `IEventLog` as a dependency. There's almost no glue — a couple of calls in `Program.cs` and your routes can start recording facts.

We'll build a small library domain and expose an endpoint that borrows a book. If you're not building a web app — a background processor or scheduled host — the [Worker Service guide](/chronicle/get-started/worker/) covers that host instead, and the [console guide](/chronicle/get-started/console/) shows the bare-bones version with no container at all.

## 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 ASP.NET Core quickstart sample](https://github.com/Cratis/Samples/tree/main/Chronicle/Quickstart/AspNetCore) on GitHub.

## Set up the project

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

```shell
dotnet new web
```

Add a reference to the [Chronicle ASP.NET Core package](https://www.nuget.org/packages/Cratis.Chronicle.AspNetCore) (it brings in the base [Chronicle package](https://www.nuget.org/packages/Cratis.Chronicle) for you):

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

## Register Chronicle on the host

ASP.NET Core builds your app through the `WebApplicationBuilder`, which already has a dependency-injection container. Chronicle hooks straight into it — two calls in `Program.cs` are the entire integration:

```csharp
var builder = WebApplication.CreateBuilder(args)
    .AddCratisChronicle(options => options.EventStore = "Quickstart");

var app = builder.Build();
app.UseCratisChronicle();
```

`AddCratisChronicle` registers Chronicle's services and names the event store to use; `UseCratisChronicle` hooks it into the request pipeline. Unlike the bare-bones [console](/chronicle/get-started/console/) version, all discovery and registration of your artifacts happens automatically — the container finds your reactors, reducers, and projections for you.

```mermaid
flowchart LR
    Builder["WebApplicationBuilder"] -->|AddCratisChronicle| DI["DI container<br/>(auto-discovers artifacts)"]
    App["app"] -->|UseCratisChronicle| Pipeline["request pipeline"]
    DI --> Endpoint["minimal API / controller"]
    Endpoint -->|IEventLog.Append| ES["event store"]
```

## 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 an endpoint

In a web app you usually append events from a route handler rather than inline. Take `IEventLog` as a dependency and append — the container injects it:

```csharp
app.MapPost("/api/books/{bookId}/borrow", async (
    [FromServices] IEventLog eventLog,
    [FromRoute] Guid bookId,
    [FromQuery] string memberName) =>
        await eventLog.Append(bookId, new BookBorrowed(memberName)));
```

The `bookId` from the route is the [event source](/chronicle/concepts/event-source/) — the book this fact is about — and `memberName` is the event's payload. That one `Append` is all it takes; the projection and any reactors pick it up from there.

## Register your artifacts

Chronicle creates its discovered artifacts — reactors, reducers, projections — through the container, so they need to be registered as services. For a handful, register them explicitly:

```csharp
builder.Services.AddTransient<BookReturnedNotifier>();
```

As the solution grows this gets tedious, so Cratis Fundamentals can 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, so you can depend on them directly without registering each one.

## 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 collections you want to inject, so a type can take an `IMongoCollection<Book>` dependency without ever touching `MongoClient`:

```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"));
```

Now the `Books` query from the read-model section above resolves its collection straight from the container.

## Recap

You added Chronicle to an ASP.NET Core app with two lines in `Program.cs` — `AddCratisChronicle` to register and discover everything, `UseCratisChronicle` to hook into the pipeline — then appended events straight from a minimal API endpoint and read them back through MongoDB collections injected by the container. Because you're in a DI world, your reactors, projections, and collections are all just registered services.

## Where to go next

- **Put a typed UI on top** — [Arc](/arc/) adds commands, queries, and generated TypeScript proxies so React stays in lockstep with your C#. See [Build a full-stack feature](/build-a-full-app/).
- **Build the domain step by step** — the [tutorial](/chronicle/tutorial/) walks the library model one concept at a time.
- **A different host** — the same artifacts run unchanged in a [Worker Service](/chronicle/get-started/worker/) or a bare [console](/chronicle/get-started/console/) app.
