Skip to content

Add Cratis Chronicle event sourcing to an Arc slice

Add Cratis Chronicle to a working Arc slice when you need a history of its changes. This guide moves the library tutorial’s write side from a database insert to a returned event. The query signatures, generated proxies, and React screens can stay in place if you preserve the read model’s storage contract.

This outline follows the tutorial’s MongoDB branch: Chronicle’s projection writes the read model into the collection the existing query observes. The EF Core branch queries LibraryDbContext instead. Keeping that query unchanged would require materializing the read model into the same relational table, which this page does not cover. Choose the projection’s storage before changing the command.

Start with the working library slice from the Arc tutorial. The fragments below show its type changes; they assume you have completed this host and storage setup:

  • Add the Cratis package to the ASP.NET Core host and follow its basic setup: builder.AddCratis() before building, then app.UseCratis(). This supplies both Arc and Chronicle’s ASP.NET Core integration. If you bring your own authentication, retain the Cratis package reference and follow its split-registration example instead. Cratis.Arc.Chronicle alone does not establish the ASP.NET Core extension surface used by that example.
  • Run a compatible Chronicle server and configure its connection, event store, namespace, and read-model sink. The client registration does not start the server.
  • For the MongoDB query below, separately install/configure Cratis.Arc.MongoDB. Align its database, collection naming, key mapping, serialization, and tenant mapping with Chronicle’s sink. Sharing an application host does not establish that alignment.
  • Decide how existing rows become history: import explicit initial facts, backfill, or cut over to a new model. Adding a projection does not turn existing rows into events. Plan replay and prevent old direct writes from racing the new projection.

After the change, register one author and check that its event source id, projected key, and query result agree. The projected state becomes visible after projection processing, not necessarily when the command response arrives.

In the slice’s event model, the screen, command, AuthorRegistered fact, read model, and query keep their roles. The fact is now stored as an event. Frame 06 adds a reactor: a handler that performs follow-up work when Chronicle processes that event.

UI/A: AuthorsUI/A: AuthorsUI/A: AuthorsC/RM: AuthorsC/RM: AuthorsStream: Authors
AddAuthor
RegisterAuthor

id: uuid, name: string
AuthorRegistered

name: string
Author
Authors
WelcomeNewAuthors

Frames 01–05 retain the tutorial’s flow. Frame 06 depends on the stored event at 03; it can send a welcome notification without putting that work in the registration command.

Change the command first. Instead of inserting a document, Handle() returns the fact that an author was registered:

[Command]
public record RegisterAuthor(AuthorId Id, AuthorName Name)
{
public AuthorRegistered Handle() => new(Name); // returns the fact, doesn't write
}
[EventType]
public record AuthorRegistered(AuthorName Name);

[EventType] has no name argument here, so Chronicle uses AuthorRegistered as the event-type id: the kind of fact recorded. The event source id answers a different question: whose history receives it? Derive AuthorId from EventSourceId<Guid> in Cratis.Chronicle.Events so the command’s Id property selects the author’s stream:

public record AuthorId(Guid Value) : EventSourceId<Guid>(Value)
{
public static readonly AuthorId NotSet = new(Guid.Empty);
public static AuthorId New() => new(Guid.NewGuid());
public static implicit operator AuthorId(Guid value) => new(value);
}

Next, let a projection fill the read model. [FromEvent<AuthorRegistered>] maps matching event properties onto Author, including Name, and uses the event source id as the model key:

[ReadModel]
[FromEvent<AuthorRegistered>]
public record Author(AuthorId Id, AuthorName Name)
{
// The query is UNCHANGED from chapter 1.
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();
}

Those are the slice’s type changes. With bootstrap, existing-data treatment, and sink alignment in place, the intended contract is:

Over a database With Chronicle
What Handle() does writes a document returns an event
What fills the read model the command, directly a projection over the event
What you can read current state current state and full history
The query method AllAuthors AllAuthors — identical
The generated proxies & React as built identical

With the storage alignment above, AllAuthors still observes the same collection and returns the same type. Its generated proxy and the screen’s subscription stay unchanged; a projection now updates the documents:

// the read model is now event-sourced — the query signature is unchanged
public static ISubject<IEnumerable<Author>> AllAuthors(IMongoCollection<Author> collection) =>
collection.Observe();

The event history lets you build another view of author registrations later by replaying the recorded facts into a new read model. Decide retention and causation privacy explicitly; Chronicle does not automatically audit writes made outside it.

You can also automate work after a registration. A reactor implements IReactor, and its handler’s first parameter identifies the event it consumes:

public class WelcomeNewAuthors(INotificationService notifications) : IReactor
{
[OnceOnly]
public Task AuthorRegistered(AuthorRegistered @event, EventContext context) =>
notifications.Notify($"Welcome aboard, {@event.Name}!");
}

[OnceOnly] skips this notification handler during replay; it does not deduplicate retries. The notification service must tolerate duplicate delivery. These fragments assume the Chronicle event, projection, and reactor namespaces and your existing domain types.

Keep direct database writes for features that only need current state. Choose event sourcing when the recorded history will help you explain changes, rebuild views, or drive follow-up work. Why Event Sourcing explains that choice, and When to use event sourcing covers where it does not fit. Arc supports both approaches in the same application.

Continue with React to an event to develop the notification handler, or follow the Chronicle tutorial to build the library’s event-sourced model step by step. Adopting Cratis covers moving other slices incrementally. For the broader application, the Cratis Stack tour and full-stack capstone connect Arc, Chronicle, and Components.