---
title: Add Chronicle to an ASP.NET Core app
editUrl: https://github.com/Cratis/Chronicle/edit/main/Documentation/get-started/aspnetcore.mdx
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.
---

import { Tabs, TabItem } from '@astrojs/starlight/components';


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/choose-hosting-model/#run-chronicle-locally) 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:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Microsoft.AspNetCore.Builder;

public static class AspNetCoreRegistration
{
    public static void ConfigureApp(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args)
            .AddCratisChronicle(options => options.EventStore = "Quickstart");

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

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/aspnetcore/register.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * The `cratis-chronicle-spring-boot-starter` on the classpath is the entire integration - there
 * is no `AddCratisChronicle`/`UseCratisChronicle` to call. Autoconfiguration connects to the
 * kernel, discovers every event type, read model, reducer, reactor and constraint in this
 * application's packages. After the application is ready, it waits up to
 * `cratis.chronicle.registration-timeout` (30 seconds by default) for the first registration pass,
 * then continues with a warning if needed. The embedded server may already accept requests; the
 * first append waits separately for that pass. The entry point needs only Spring Boot's own `@SpringBootApplication` class
 * and `runApplication<...>(*args)`. Point it at an event store with one setting in
 * application.yml:
 *
 * ```yaml
 * cratis:
 *   chronicle:
 *     event-store: Quickstart
 * ```
 */
object AspNetCoreRegistration
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/aspnetcore/register.md)

</TabItem>
<TabItem label="Java">

```java
/**
 * The {@code cratis-chronicle-spring-boot-starter} on the classpath is the entire integration -
 * there is no {@code AddCratisChronicle}/{@code UseCratisChronicle} to call. Autoconfiguration
 * connects to the kernel, discovers every event type, read model, reducer, reactor and
 * constraint in this application's packages. After the application is ready, it waits up to
 * {@code cratis.chronicle.registration-timeout} (30 seconds by default) for the first registration
 * pass, then continues with a warning if needed. The embedded server may already accept requests;
 * the first append waits separately for that pass. The entry point needs only Spring Boot's own
 * {@code @SpringBootApplication} class and {@code SpringApplication.run(...)}. Point it at an
 * event store with one setting in application.yml:
 *
 * <pre>
 * cratis:
 *   chronicle:
 *     event-store: Quickstart
 * </pre>
 */
class AspNetCoreRegistration {
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/aspnetcore/register.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/aspnetcore/register.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/aspnetcore/register.md)

</TabItem>
</Tabs>

`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 (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:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;

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

[EventType]
public record GetStartedBookBorrowed(string MemberName);

[EventType]
public record GetStartedBookReturned;
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/a-events.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventType

@EventType
data class GetStartedBookAdded(val title: String, val isbn: String)

@EventType
data class GetStartedBookBorrowed(val memberName: String)

@EventType
class GetStartedBookReturned
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/a-events.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventType;

@EventType
record GetStartedBookAdded(String title, String isbn) {}

@EventType
record GetStartedBookBorrowed(String memberName) {}

@EventType
record GetStartedBookReturned() {}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/a-events.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Events.GetStartedBookAdded do
  use Chronicle.Events.EventType, id: "get-started-book-added"

  defstruct [:title, :isbn]
end

defmodule MyApp.Events.GetStartedBookBorrowed do
  use Chronicle.Events.EventType, id: "get-started-book-borrowed"

  defstruct [:member_name]
end

defmodule MyApp.Events.GetStartedBookReturned do
  use Chronicle.Events.EventType, id: "get-started-book-returned"

  defstruct []
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/a-events.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { eventType } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@eventType()
class GetStartedBookAdded {
    @field(String) title: string;
    @field(String) isbn: string;

    constructor(title: string, isbn: string) {
        this.title = title;
        this.isbn = isbn;
    }
}

@eventType()
class GetStartedBookBorrowed {
    @field(String) memberName: string;
    constructor(memberName: string) { this.memberName = memberName; }
}

@eventType()
class GetStartedBookReturned {
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/a-events.md)

</TabItem>
</Tabs>

`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:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;

public class GetStartedBookService(IEventStore eventStore)
{
    public async Task<Guid> AddBook()
    {
        var eventLog = eventStore.EventLog;

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

        return bookId;
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/append.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.IEventStore
import java.util.UUID

class GetStartedBookService(private val eventStore: IEventStore) {
    suspend fun addBook(): String {
        val eventLog = eventStore.eventLog

        val bookId = UUID.randomUUID().toString()
        eventLog.append(bookId, GetStartedBookAdded("The Pragmatic Programmer", "978-0135957059"))

        return bookId
    }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/append.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.IEventStore;
import io.cratis.chronicle.java.BlockingEventStore;
import java.util.UUID;

class GetStartedBookService {
    private final BlockingEventStore eventStore;

    GetStartedBookService(IEventStore eventStore) {
        this.eventStore = new BlockingEventStore(eventStore);
    }

    String addBook() {
        var bookId = UUID.randomUUID().toString();

        eventStore.getEventLog().append(
            bookId,
            new GetStartedBookAdded("The Pragmatic Programmer", "978-0135957059"));

        return bookId;
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/append.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.BookService do
  alias MyApp.Events.GetStartedBookAdded

  def add_book do
    book_id = Base.encode16(:crypto.strong_rand_bytes(8), case: :lower)

    :ok =
      Chronicle.append(book_id, %GetStartedBookAdded{
        title: "The Pragmatic Programmer",
        isbn: "978-0135957059"
      })

    book_id
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/append.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class GetStartedBookService {
    constructor(private readonly store: IEventStore) {}

    async addBook(bookId: string): Promise<void> {
        const eventLog = this.store.eventLog;

        await eventLog.append(bookId, new GetStartedBookAdded('The Pragmatic Programmer', '978-0135957059'));
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/append.md)

</TabItem>
</Tabs>

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 generated id here to keep the quickstart short. In C#, real code would 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. Kotlin, Elixir, and TypeScript event source ids are plain strings; those clients don't currently have an equivalent typed-id wrapper.
:::

Run your app, then open the <a href="https://localhost:35000" target="_blank" rel="noopener">workbench</a>, pick your event store, and select **Sequences** — your `GetStartedBookAdded` is sitting there, permanent and in order. In a fresh event store the first event is at sequence number `0`.

:::note
If you are using the Development or Development-slim image, log in with username **Admin** and password **ChangeMeNow!**. See [Workbench — Development](/chronicle/workbench/development/) for details.
:::

![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.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

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

[FromEvent<GetStartedBookAdded>]
public record GetStartedBook(
    [Key]
    Guid Id,

    string Title,

    string Isbn,

    [SetValue<GetStartedBookAdded>(false)]
    [SetValue<GetStartedBookBorrowed>(true)]
    [SetValue<GetStartedBookReturned>(false)]
    bool OnLoan,

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

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/book-read-model.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/book-read-model.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/book-read-model.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.ReadModels.GetStartedBook do
  use Chronicle.ReadModels.ReadModel

  defstruct id: "", title: "", isbn: "", on_loan: false, borrowed_by: ""

  from MyApp.Events.GetStartedBookAdded,
    set: [id: :event_source_id, title: :title, isbn: :isbn, on_loan: false]

  from MyApp.Events.GetStartedBookBorrowed,
    set: [on_loan: true, borrowed_by: :member_name]

  from MyApp.Events.GetStartedBookReturned,
    set: [on_loan: false]
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/book-read-model.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { fromEvent, Guid, setFrom, setValue } from '@cratis/chronicle';
import { field } from '@cratis/fundamentals';

@fromEvent(GetStartedBookAdded)
export class GetStartedBook {
    id: Guid = Guid.empty;

    @setFrom(GetStartedBookAdded, 'title')
    title = '';

    @setFrom(GetStartedBookAdded, 'isbn')
    isbn = '';

    @setValue(GetStartedBookAdded, false)
    @setValue(GetStartedBookBorrowed, true)
    @setValue(GetStartedBookReturned, false)
    onLoan = false;

    @setFrom(GetStartedBookBorrowed, 'memberName')
    @field(String) borrowedBy: string | null = null;
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/book-read-model.md)

</TabItem>
</Tabs>

Read the attributes as a sentence: a book comes into the view 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.

:::note
The class-level `[FromEvent<GetStartedBookAdded>]` maps event properties onto read model properties **by naming convention** — `Title` and `Isbn` flow across without a single per-property attribute. You only reach for `[SetFrom<T>]` when the names differ, the way `GetStartedBookBorrowed.MemberName` feeds `BorrowedBy` here. Setting a literal value per event type (like `OnLoan` here) isn't currently supported in Kotlin or Java's model-bound projections — only event-property mappings are.
:::

One view rarely answers every question. The librarian's next one is "what's out on loan right now?" — and rather than filtering `GetStartedBook`, you declare a second, purpose-built read model whose very *existence* tracks the loan:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

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

[FromEvent<GetStartedBookBorrowed>]
[RemovedWith<GetStartedBookReturned>]
public record GetStartedBorrowedBook(
    [Key]
    Guid Id,

    string MemberName);
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.projections.FromEvent
import io.cratis.chronicle.projections.RemovedWith
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
@FromEvent(GetStartedBookBorrowed::class)
@RemovedWith(GetStartedBookReturned::class)
data class GetStartedBorrowedBook(
    val id: String = "",
    val memberName: String = ""
)
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.projections.FromEvent;
import io.cratis.chronicle.projections.RemovedWith;
import io.cratis.chronicle.readModels.ReadModel;

@ReadModel
@FromEvent(eventType = GetStartedBookBorrowed.class)
@RemovedWith(eventType = GetStartedBookReturned.class)
class GetStartedBorrowedBook {
    private String id = "";
    private String memberName = "";

    public GetStartedBorrowedBook() {}

    public String getId() { return id; }
    public void setId(String id) { this.id = id; }

    public String getMemberName() { return memberName; }
    public void setMemberName(String memberName) { this.memberName = memberName; }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/borrowed-book-read-model.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.ReadModels.GetStartedBorrowedBook do
  use Chronicle.ReadModels.ReadModel

  defstruct id: "", member_name: ""

  from MyApp.Events.GetStartedBookBorrowed,
    set: [id: :event_source_id, member_name: :member_name]

  removed_with MyApp.Events.GetStartedBookReturned, []
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { fromEvent, Guid, removedWith, setFrom } from '@cratis/chronicle';

@fromEvent(GetStartedBookBorrowed)
@removedWith(GetStartedBookReturned)
export class GetStartedBorrowedBook {
    id: Guid = Guid.empty;

    @setFrom(GetStartedBookBorrowed, 'memberName')
    memberName = '';
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/borrowed-book-read-model.md)

</TabItem>
</Tabs>

The moment a `GetStartedBookBorrowed` lands, a `GetStartedBorrowedBook` instance appears — keyed by the book's id, its `MemberName` mapped by the same naming convention. When the matching `GetStartedBookReturned` arrives, `[RemovedWith<GetStartedBookReturned>]` removes the instance from the view again. The collection always holds exactly the books that are out *right now* — no flag to maintain, no filter to remember, no cleanup job. Kotlin and Java don't currently have an equivalent to `[RemovedWith<T>]`.

## Query the read models

The most direct way to look at a read model is to ask Chronicle itself. `IReadModels` — reached through `eventStore.ReadModels` — hands you every instance of a read model, one call per view:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;

public class GetStartedBookQueryService(IEventStore eventStore)
{
    public async Task<(IEnumerable<GetStartedBook> Books, IEnumerable<GetStartedBorrowedBook> BorrowedBooks)> QueryBooks()
    {
        var books = await eventStore.ReadModels.GetInstances<GetStartedBook>();
        var borrowedBooks = await eventStore.ReadModels.GetInstances<GetStartedBorrowedBook>();

        return (books, borrowedBooks);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/query-read-models.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/query-read-models.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/query-read-models.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.BookQueryService do
  alias MyApp.ReadModels.GetStartedBook
  alias MyApp.ReadModels.GetStartedBorrowedBook

  def query_books do
    {:ok, books} = Chronicle.all(GetStartedBook)
    {:ok, borrowed_books} = Chronicle.all(GetStartedBorrowedBook)

    {books, borrowed_books}
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/query-read-models.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class GetStartedBookQueryService {
    constructor(private readonly store: IEventStore) {}

    async queryBooks(): Promise<{ books: GetStartedBook[]; borrowedBooks: GetStartedBorrowedBook[] }> {
        const books = await this.store.readModels.getInstances(GetStartedBook);
        const borrowedBooks = await this.store.readModels.getInstances(GetStartedBorrowedBook);

        return { books, borrowedBooks };
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/query-read-models.md)

</TabItem>
</Tabs>

These instances come from the **sink**. By default Chronicle **materializes** every projection into the sink storage configured for the event store — MongoDB unless you change it. For the default namespace the database is named after the event store (`Quickstart`), and each collection after its read model, pluralized: `GetStartedBooks` and `GetStartedBorrowedBooks`. That makes a query cheap — a database fetch, whatever the length of the history.

Materialization happens in the background as events arrive, so these reads are **eventually consistent**. An append completes when the event is in the log, not when the read model has caught up, so a query issued straight after an append can miss it. That is most noticeable on the first event against a fresh event store: processing that event also creates the projection pipeline and its storage for the first time, so the read straight after that append is the one most likely to come back empty. Query again a moment later and the book is there.

When a read has to include the event you just appended, you have two tools. Mark the read model **passive**, and Chronicle keeps it out of the sink and computes it from its events on every read. Or keep it materialized and wait for the append's observers to catch up before reading. [Read model consistency](/chronicle/read-models/consistency/) compares the two.

[Getting a collection of instances](/chronicle/read-models/getting-collection-instances/) covers the details, including the event count you can pass to replay a read model from the beginning instead. Kotlin and Java currently only support fetching a single instance by key (`getInstanceByKey`) — fetching every instance of a read model isn't available yet.

`GetInstances` returns every stored instance at once. The `Materialized` API reads the same stored instances back a page at a time. Kotlin and Java don't currently have a materialized/paging query API at all; Elixir's equivalent pages by page number rather than skip/take:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle;

public class GetStartedBookPagingService(IEventStore eventStore)
{
    public async Task<IEnumerable<GetStartedBook>> GetPage() =>
        await eventStore.ReadModels.Materialized.GetInstances<GetStartedBook>(skip: 0, take: 20);
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/materialized-paging.md)

</TabItem>
<TabItem label="Kotlin">

```text
Kotlin does not support this workflow yet.
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/materialized-paging.md)

</TabItem>
<TabItem label="Java">

```text
Java does not support this workflow yet.
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/materialized-paging.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.BookPagingService do
  alias MyApp.ReadModels.GetStartedBook

  def get_page do
    {:ok, result} = Chronicle.ReadModels.query(GetStartedBook, page: 0, page_size: 20)
    result.instances
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/materialized-paging.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { IEventStore } from '@cratis/chronicle';

class GetStartedBookPagingService {
    constructor(private readonly store: IEventStore) {}

    getPage(): Promise<GetStartedBook[]> {
        return this.store.readModels.materialized.getInstances(GetStartedBook, 0, 20);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/materialized-paging.md)

</TabItem>
</Tabs>

`skip` and `take` are plain paging (they default to `0` and `50`), so you can window through a large collection without ever loading all of it — [Materialized read models](/chronicle/read-models/materialized-pagination/) shows the paging and observing patterns.

Neither call filters, though: `GetInstances` returns everything (you'd filter with LINQ in memory), and `Materialized` only pages. When you need to filter efficiently — "which books are on loan?" — query the [sink](/chronicle/sinks/)'s database directly with its native tools. Our sink is MongoDB, so `GetStartedBooks` is an ordinary collection that your language's MongoDB driver can query (the Chronicle client does not bring one in; add it to your project):

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using MongoDB.Driver;

public class GetStartedBooks(IMongoCollection<GetStartedBook> collection)
{
    public IEnumerable<GetStartedBook> OnLoan() => collection.Find(b => b.OnLoan).ToList();
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/mongo-query.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.IEventStore
import io.cratis.chronicle.readModels.ReadModel

@ReadModel
data class GetStartedBook(val title: String = "", val onLoan: Boolean = false)

/**
 * Kotlin has no raw MongoCollection to query directly - reads go through the read model service
 * instead, which returns already-deserialized instances to filter in memory.
 */
class GetStartedBooks(private val store: IEventStore) {
    suspend fun onLoan(): List<GetStartedBook> =
        store.readModels.getInstances(GetStartedBook::class).filter { it.onLoan }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/mongo-query.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.IEventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
import io.cratis.chronicle.readModels.ReadModel;

import java.util.List;
import java.util.stream.Collectors;

@ReadModel
record GetStartedBook(String title, boolean onLoan) {}

// Java has no raw MongoCollection to query directly - reads go through the read model service
// instead, which returns already-deserialized instances to filter in memory.
class GetStartedBooks {
    private final IEventStore store;

    GetStartedBooks(IEventStore store) {
        this.store = store;
    }

    List<GetStartedBook> onLoan() {
        return ReadModelsJavaBridge.getInstances(store.getReadModels(), GetStartedBook.class).stream()
            .filter(GetStartedBook::onLoan)
            .collect(Collectors.toList());
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/mongo-query.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/mongo-query.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/mongo-query.md)

</TabItem>
</Tabs>

Append a `GetStartedBookBorrowed` against the same `bookId`, query again, and `OnLoan` is `true` with `BorrowedBy` set — and a `GetStartedBorrowedBook` now sits in its collection. Append a `GetStartedBookReturned` and both flip back: the flag clears, the `GetStartedBorrowedBook` disappears. You never wrote an `UPDATE`. The trade-offs between the on-demand and materialized paths are laid out in [read model consistency](/chronicle/read-models/consistency/).

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

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

public class GetStartedBookReturnedNotifier : IReactor
{
    public Task Returned(GetStartedBookReturned @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;
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/common/reactor.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.events.EventContext
import io.cratis.chronicle.observation.Reactor

@Reactor
class GetStartedBookReturnedNotifier {
    fun returned(event: GetStartedBookReturned, context: EventContext) {
        // context.eventSourceId is the bookId this happened to
        println("Book ${context.eventSourceId} was returned — notify the next member in line.")
    }
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/common/reactor.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.events.EventContext;
import io.cratis.chronicle.observation.Reactor;

@Reactor
class GetStartedBookReturnedNotifier {
    void returned(GetStartedBookReturned event, EventContext context) {
        // context.getEventSourceId() is the bookId this happened to
        System.out.println("Book " + context.getEventSourceId() + " was returned — notify the next member in line.");
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/common/reactor.md)

</TabItem>
<TabItem label="Elixir">

```elixir
defmodule MyApp.Reactors.GetStartedBookReturnedNotifier do
  use Chronicle.Reactors.Reactor

  alias MyApp.Events.GetStartedBookReturned

  @handles GetStartedBookReturned

  @impl true
  def handle(%GetStartedBookReturned{}, %{event_source_id: book_id}) do
    # book_id is the id this happened to
    IO.puts("Book #{book_id} was returned — notify the next member in line.")
    :ok
  end
end
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/common/reactor.md)

</TabItem>
<TabItem label="TypeScript">

```typescript
import { EventContext, onceOnly, reactor } from '@cratis/chronicle';

@reactor()
class GetStartedBookReturnedNotifier {
    // Method name must be the exact camelCase of the event's class name -
    // Chronicle discovers handlers by name, not by parameter type.
    @onceOnly()
    async getStartedBookReturned(event: GetStartedBookReturned, context: EventContext): Promise<void> {
        // context.eventSourceId is the bookId this happened to
        console.log(`Book ${context.eventSourceId} was returned — notify the next member in line.`);
    }
}
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/common/reactor.md)

</TabItem>
</Tabs>

No registration, no wiring — drop the class in and every `GetStartedBookReturned` flows to it. Reactors must be idempotent, because the same event may be delivered more than once (during a replay or a recovery). [Delivery identity](/chronicle/reactors/delivery-identity/) explains what Chronicle does and does not guarantee there. 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:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;

public static class AspNetCoreBookEndpoint
{
    public static void MapEndpoints(WebApplication app)
    {
        app.MapPost("/api/books/{bookId}/borrow", async (
            [FromServices] IEventLog eventLog,
            [FromRoute] Guid bookId,
            [FromQuery] string memberName) =>
                await eventLog.Append(bookId, new GetStartedBookBorrowed(memberName)));
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/aspnetcore/endpoint.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
import io.cratis.chronicle.IEventStore

/**
 * Kotlin has no ASP.NET Core minimal-API route builder to show here - the handler is a plain
 * suspend function taking the values a web framework would already have extracted from the
 * request (a path variable and a query parameter), and appends the event directly. Spring MVC
 * handlers are synchronous, so bridge with runBlocking there; on WebFlux, or anywhere that
 * already supports suspend functions, call this directly instead.
 */
class AspNetCoreBookEndpoint(private val eventStore: IEventStore) {
    suspend fun borrow(bookId: String, memberName: String) =
        eventStore.eventLog.append(bookId, GetStartedBookBorrowed(memberName))
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/aspnetcore/endpoint.md)

</TabItem>
<TabItem label="Java">

```java
import io.cratis.chronicle.java.BlockingEventStore;

// Java has no ASP.NET Core minimal-API route builder to show here - the handler is a plain
// method taking the values a web framework would already have extracted from the request (a
// path variable and a query parameter), and appends the event directly through the blocking
// event store facade a Spring MVC handler can call without any coroutine bridging.
class AspNetCoreBookEndpoint {
    private final BlockingEventStore eventStore;

    AspNetCoreBookEndpoint(BlockingEventStore eventStore) {
        this.eventStore = eventStore;
    }

    void borrow(String bookId, String memberName) {
        eventStore.getEventLog().append(bookId, new GetStartedBookBorrowed(memberName));
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/aspnetcore/endpoint.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/aspnetcore/endpoint.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/aspnetcore/endpoint.md)

</TabItem>
</Tabs>

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 projections pick it up from there — `GetStartedBook` flips to on loan and a `GetStartedBorrowedBook` instance appears — along with any reactors. The append returns once the event is in the log; the materialized read models catch up a moment later.

## Register your artifacts

Chronicle creates its discovered artifacts — reactors, reducers, projections — through the container. If an artifact is registered as a service, Chronicle uses that registration; otherwise it constructs the artifact itself and resolves its constructor parameters from the container. Registering the artifact itself is therefore optional — do it when you want to control its lifetime:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

public static class AspNetCoreExplicitRegistration
{
    public static void ConfigureServices(WebApplicationBuilder builder)
    {
        builder.Services.AddTransient<GetStartedBookReturnedNotifier>();
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * Automatic discovery registers the `@Reactor`; this factory only supplies its instance.
 * Manual registration is for applications with `cratis.chronicle.auto-discover-and-register=false`. In a Spring Boot application this factory function is exposed as an `@Bean` inside an
 * `@Configuration` class, so the constructed instance is also available for injection elsewhere.
 */
fun registerBookReturnedNotifier(): GetStartedBookReturnedNotifier {
    val notifier = GetStartedBookReturnedNotifier()
    return notifier
}
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md)

</TabItem>
<TabItem label="Java">

```java
// Automatic discovery registers the @Reactor; this factory only supplies its instance.
// Manual registration is for applications with cratis.chronicle.auto-discover-and-register=false. In a Spring Boot application this factory method is exposed as an @Bean inside an
// @Configuration class, so the constructed instance is also available for injection elsewhere.
class AspNetCoreExplicitRegistration {
    GetStartedBookReturnedNotifier bookReturnedNotifier() {
        GetStartedBookReturnedNotifier notifier = new GetStartedBookReturnedNotifier();
        return notifier;
    }
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/aspnetcore/register-artifact.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-artifact.md)

</TabItem>
</Tabs>

What you must register is everything an artifact depends on, such as the notification service a real waitlist reactor would take. As the solution grows, Cratis Fundamentals can register those services by convention:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Cratis.DependencyInjection;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

public static class AspNetCoreConventionRegistration
{
    public static void ConfigureServices(WebApplicationBuilder builder)
    {
        builder.Services
            .AddBindingsByConvention()
            .AddSelfBindings();
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * Spring's own convention is `@ComponentScan`: every class carrying a recognized stereotype in
 * the scanned packages becomes a registered bean automatically - the same mechanism that finds
 * Chronicle artifacts. `@SpringBootApplication` already implies scanning its own package and
 * below, so this is only needed to widen the scan to another package explicitly, by adding
 * `@ComponentScan("io.cratis.chronicle.samples.getstarted")` to an `@Configuration` class.
 */
object AspNetCoreConventionRegistration
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md)

</TabItem>
<TabItem label="Java">

```java
// Spring's own convention is @ComponentScan: every class carrying a recognized stereotype in the
// scanned packages becomes a registered bean automatically - the same mechanism that finds
// Chronicle artifacts. @SpringBootApplication already implies scanning its own package and
// below, so this is only needed to widen the scan to another package explicitly, by adding
// @ComponentScan("io.cratis.chronicle.samples.getstarted") to an @Configuration class.
class AspNetCoreConventionRegistration {
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/aspnetcore/register-by-convention.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/aspnetcore/register-by-convention.md)

</TabItem>
</Tabs>

`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 `GetStartedBooks` query reads documents Chronicle wrote, so the MongoDB driver needs to match how Chronicle stores them — register these conventions once at startup:

## MongoDB

When you read Chronicle's read models with the MongoDB driver, the driver has to expect the documents Chronicle
writes. Three things matter:

- Chronicle adds metadata fields of its own, such as `__subject`, so the driver must ignore elements your type does
  not declare.
- Identifiers such as a `Guid` key are stored as standard UUIDs.
- Element names match your property names exactly as declared, and each collection is named after its read model,
  pluralized. Chronicle only writes camel-case names if you opt into its
  [camel case naming policy](/chronicle/configuration/camel-casing/); match the driver to whichever policy you chose,
  or queries match nothing without reporting an error.

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using MongoDB.Bson;
using MongoDB.Bson.Serialization;
using MongoDB.Bson.Serialization.Conventions;
using MongoDB.Bson.Serialization.Serializers;

public static class GetStartedMongoDbDefaults
{
    public static void Configure()
    {
        BsonSerializer
            .RegisterSerializer(new GuidSerializer(GuidRepresentation.Standard));

        var pack = new ConventionPack
        {
            // Chronicle adds its own metadata fields to every document, such as __subject.
            new IgnoreExtraElementsConvention(true)

            // Element names match your C# property names by default. Add a
            // CamelCaseElementNameConvention here only if you configured Chronicle
            // with the camel case naming policy.
        };
        ConventionRegistry.Register("conventions", pack, t => true);
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/mongodb/conventions.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * Kotlin has nothing to configure here. Chronicle stores event source ids as standard UUIDs and
 * read model fields as camelCase names - which already match Kotlin property names - and reads
 * go through `IEventStore.readModels` rather than a raw MongoDB driver, so there is no
 * `MongoClientSettings`, GUID representation, or "ignore unknown elements" convention to
 * register. Unknown document fields are simply not deserialized into properties the read model
 * class does not declare.
 */
object GetStartedMongoDbDefaults
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/mongodb/conventions.md)

</TabItem>
<TabItem label="Java">

```java
// Java has nothing to configure here. Chronicle stores event source ids as standard UUIDs and
// read model fields as camelCase names - which already match Java property names - and reads go
// through IEventStore.getReadModels() rather than a raw MongoDB driver, so there is no
// MongoClientSettings, GUID representation, or "ignore unknown elements" convention to register.
// Unknown document fields are simply not deserialized into properties the read model class does
// not declare.
class GetStartedMongoDbDefaults {
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/mongodb/conventions.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/mongodb/conventions.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/mongodb/conventions.md)

</TabItem>
</Tabs>

:::note
These conventions match how Chronicle names and shapes the stored document, but the driver still builds the instance
itself — Chronicle's own read behavior does not travel with it. The one that bites is a child collection with no
children, which is stored as an absent field: read through Chronicle and a non-nullable property comes back empty,
read it with the driver and it comes back as whatever the driver does with a missing field. See
[Empty child collections](/chronicle/read-models/empty-child-collections/).

It doesn't travel the other way either: these conventions apply to the reads you make with the driver, never to a
read model Chronicle hands back or injects into a command. See
[What a MongoDB convention pack reaches](/chronicle/sinks/#what-a-mongodb-convention-pack-reaches).
:::

Then register the database and the collections you want to inject, so a type can take an `IMongoCollection<GetStartedBook>` dependency without ever touching `MongoClient`:

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using MongoDB.Driver;

public static class AspNetCoreMongoRegistration
{
    public static void ConfigureServices(WebApplicationBuilder builder)
    {
        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<GetStartedBook>("GetStartedBooks"));
    }
}
```

[View C# snippet source on GitHub](https://github.com/Cratis/Chronicle/blob/main/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md)

</TabItem>
<TabItem label="Kotlin">

```kotlin
/**
 * Kotlin needs no separate MongoDB registration step here. Queries go through
 * `IEventStore.readModels`, which the connected client already exposes - there is no raw
 * `MongoCollection` to construct or register as a bean, in Spring Boot or anywhere else. See
 * `get-started/common/mongo-query` for the equivalent query.
 */
object AspNetCoreMongoRegistration
```

[View Kotlin snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md)

</TabItem>
<TabItem label="Java">

```java
// Java needs no separate MongoDB registration step here. Queries go through
// IEventStore.getReadModels(), which the connected client already exposes - there is no raw
// MongoCollection to construct or register as a bean, in Spring Boot or anywhere else. See
// get-started/common/mongo-query for the equivalent query.
class AspNetCoreMongoRegistration {
}
```

[View Java snippet source on GitHub](https://github.com/Cratis/Chronicle.Kotlin/blob/main/Documentation/client-snippets-java/get-started/aspnetcore/mongo-registration.md)

</TabItem>
<TabItem label="Elixir">

```text
Elixir does not support this workflow yet.
```

[View Elixir snippet source on GitHub](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md)

</TabItem>
<TabItem label="TypeScript">

```text
TypeScript does not support this workflow yet.
```

[View TypeScript snippet source on GitHub](https://github.com/Cratis/Chronicle.TypeScript/blob/main/Documentation/client-snippets/get-started/aspnetcore/mongo-registration.md)

</TabItem>
</Tabs>

Now the `GetStartedBooks` query from the querying 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.
