Skip to content

Get read models

Goal: your events project into a read model — now you need it back out. One book, the whole catalog, a page for a grid, or a live stream that updates the UI as events arrive. IReadModels (on IEventStore.ReadModels) covers all of these; the choice is between strongly consistent reads computed from the event log and eventually consistent reads served from the materialized sink.

GetInstanceById replays the key’s events through the projection or reducer on demand — strongly consistent, reflecting everything appended up to this moment:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
public record ScenariosQueryBook(string Title, bool OnLoan);
public class ScenariosQueryBookService(IEventStore eventStore)
{
public Task<ScenariosQueryBook> GetBook(EventSourceId bookId) =>
eventStore.ReadModels.GetInstanceById<ScenariosQueryBook>(bookId.Value);
}

ReadModelKey converts implicitly from string, Guid, and EventSourceId, so you can pass the id you already have.

GetInstances rebuilds every instance by replaying the event log — strongly consistent, suited to reads where accuracy beats latency. The cost grows with history; filter the result with LINQ:

using Cratis.Chronicle;
public class ScenariosQueryOnLoanBooks(IEventStore eventStore)
{
public async Task<IEnumerable<ScenariosQueryBook>> GetOnLoan()
{
var books = await eventStore.ReadModels.GetInstances<ScenariosQueryBook>();
return books.Where(b => b.OnLoan);
}
}

For list views and grids, skip the replay: Materialized reads the instances the projection has already persisted to the sink — eventually consistent (typically milliseconds behind), fast regardless of history, and paged:

using Cratis.Chronicle;
public class ScenariosQueryBookPageService(IEventStore eventStore)
{
public Task<IEnumerable<ScenariosQueryBook>> GetPage() =>
eventStore.ReadModels.Materialized.GetInstances<ScenariosQueryBook>(skip: 0, take: 20);
}

Watch<T>() returns an IObservable<ReadModelChangeset<T>> that emits whenever any instance of that read model type changes — the changeset carries the key, the new state, and whether the instance was removed:

using Cratis.Chronicle;
public class ScenariosQueryBookWatcher(IEventStore eventStore)
{
public IDisposable Watch() =>
eventStore.ReadModels.Watch<ScenariosQueryBook>()
.Subscribe(changeset =>
{
if (changeset.Removed || changeset.ReadModel is null)
{
return;
}
Console.WriteLine($"{changeset.ModelKey}: on loan = {changeset.ReadModel.OnLoan}");
});
}

Dispose the subscription when you’re done. If you need explicit lifecycle control — for example awaiting the watcher’s Subscribed before producing events — use GetWatcherFor<Book>() instead.

Materialized.ObserveInstances pushes a fresh page snapshot whenever the stored data changes — a live dashboard or table with no polling:

using Cratis.Chronicle;
public class ScenariosQueryLiveBookPage(IEventStore eventStore)
{
public IDisposable Subscribe(Action<IEnumerable<ScenariosQueryBook>> updateView) =>
eventStore.ReadModels.Materialized
.ObserveInstances<ScenariosQueryBook>(take: 50)
.Subscribe(books => updateView(books));
}

When a command appends BookBorrowed, the projection updates the materialized Book and every subscriber receives the new page. The brief gap between append and push is the normal eventual consistency window — usually imperceptible.

You needUseConsistency
One instance, exact current stateGetInstanceById<T>(key)Strong
All instances, exact current stateGetInstances<T>()Strong
A page for a list or gridMaterialized.GetInstances<T>(skip, take)Eventual
To react to every change of a typeWatch<T>()Pushed as changes apply
A live-updating pageMaterialized.ObserveInstances<T>(skip, take)Eventual, pushed