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.
Get one instance
Section titled “Get one instance”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);}import io.cratis.chronicle.IEventStore
data class ScenariosQueryBook(val title: String, val onLoan: Boolean)
class ScenariosQueryBookService(private val store: IEventStore) { suspend fun getBook(bookId: String): ScenariosQueryBook? = store.readModels.getInstanceByKey(ScenariosQueryBook::class, bookId)}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.java.BlockingEventStore;
record ScenariosQueryBook(String title, boolean onLoan) {}
class ScenariosQueryBookService { private final BlockingEventStore store;
ScenariosQueryBookService(IEventStore store) { this.store = new BlockingEventStore(store); }
ScenariosQueryBook getBook(String bookId) { return store.getReadModels().getInstanceByKey(ScenariosQueryBook.class, bookId); }}defmodule MyApp.ReadModels.ScenariosQueryBook do defstruct [:title, :on_loan]end
defmodule MyApp.ScenariosQueryBookService do alias MyApp.ReadModels.ScenariosQueryBook
def get_book(book_id) do Chronicle.read_model(ScenariosQueryBook, book_id) endendimport { IEventStore } from '@cratis/chronicle';
class ScenariosQueryBook { constructor( readonly title: string, readonly onLoan: boolean ) {}}
class ScenariosQueryBookService { constructor(private readonly store: IEventStore) {}
async getBook(bookId: string): Promise<ScenariosQueryBook> { return this.store.readModels.getInstanceById(ScenariosQueryBook, bookId); }}ReadModelKey converts implicitly from string, Guid, and EventSourceId, so you can pass the id you already have.
Get all instances
Section titled “Get all instances”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); }}import io.cratis.chronicle.IEventStore
data class ScenariosQueryAllBook(val title: String, val onLoan: Boolean)
class ScenariosQueryOnLoanBooks(private val store: IEventStore) { suspend fun getOnLoan(): List<ScenariosQueryAllBook> = store.readModels.getInstances(ScenariosQueryAllBook::class).filter { it.onLoan }}import io.cratis.chronicle.EventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
import java.util.List;
record ScenariosQueryAllBook(String title, boolean onLoan) {}
class ScenariosQueryOnLoanBooks { private final EventStore store;
ScenariosQueryOnLoanBooks(EventStore store) { this.store = store; }
List<ScenariosQueryAllBook> getOnLoan() { return ReadModelsJavaBridge.getInstances(store.getReadModels(), ScenariosQueryAllBook.class) .stream() .filter(ScenariosQueryAllBook::onLoan) .toList(); }}defmodule MyApp.ReadModels.ScenariosQueryGetAllBook do defstruct [:title, :on_loan]end
defmodule MyApp.ScenariosQueryOnLoanBooksService do alias MyApp.ReadModels.ScenariosQueryGetAllBook
def get_on_loan do {:ok, books} = Chronicle.all(ScenariosQueryGetAllBook) Enum.filter(books, & &1.on_loan) endendimport { IEventStore } from '@cratis/chronicle';
class ScenariosQueryOnLoanBooks { constructor(private readonly store: IEventStore) {}
async getOnLoan(): Promise<ScenariosQueryBook[]> { const books = await this.store.readModels.getInstances(ScenariosQueryBook); return books.filter(book => book.onLoan); }}Page through materialized state
Section titled “Page through materialized state”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);}import io.cratis.chronicle.IEventStore
data class ScenariosMaterializedBook(val title: String, val onLoan: Boolean)
class ScenariosQueryBookPage(private val store: IEventStore) { suspend fun getPage(): List<ScenariosMaterializedBook> = store.readModels.materialized.getInstances(ScenariosMaterializedBook::class, skip = 0, take = 20)}import io.cratis.chronicle.EventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
import java.util.List;
record ScenariosMaterializedBook(String title, boolean onLoan) {}
class ScenariosQueryBookPage { private final EventStore store;
ScenariosQueryBookPage(EventStore store) { this.store = store; }
List<ScenariosMaterializedBook> getPage() { return ReadModelsJavaBridge.getMaterializedInstances(store.getReadModels(), ScenariosMaterializedBook.class, 0, 20); }}defmodule MyApp.ReadModels.ScenariosQueryPagedBook do defstruct [:title, :on_loan]end
defmodule MyApp.ScenariosQueryBookPageService do alias MyApp.ReadModels.ScenariosQueryPagedBook
def get_page do {:ok, result} = Chronicle.ReadModels.query(ScenariosQueryPagedBook, page: 1, page_size: 20) result.instances endendimport { IEventStore } from '@cratis/chronicle';
class ScenariosQueryBookPageService { constructor(private readonly store: IEventStore) {}
getPage(): Promise<ScenariosQueryBook[]> { return this.store.readModels.materialized.getInstances(ScenariosQueryBook, 0, 20); }}Watch every change
Section titled “Watch every change”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}"); });}import io.cratis.chronicle.IEventStoreimport kotlinx.coroutines.flow.collect
data class ScenariosWatchBook(val title: String, val onLoan: Boolean)
class ScenariosQueryBookWatcher(private val store: IEventStore) { suspend fun watch() { store.readModels.watch(ScenariosWatchBook::class).collect { changeset -> if (changeset.removed || changeset.readModel == null) return@collect println("${changeset.modelKey}: on loan = ${changeset.readModel!!.onLoan}") } }}import io.cratis.chronicle.EventStore;
import io.cratis.chronicle.java.ReadModelsJavaBridge;
record ScenariosWatchBook(String title, boolean onLoan) {}
class ScenariosQueryBookWatcher { private final EventStore store;
ScenariosQueryBookWatcher(EventStore store) { this.store = store; }
void watch() { ReadModelsJavaBridge.watch(store.getReadModels(), ScenariosWatchBook.class, changeset -> { if (changeset.getRemoved() || changeset.getReadModel() == null) return; System.out.println(changeset.getModelKey() + ": on loan = " + changeset.getReadModel().onLoan()); }); }}Elixir does not support this workflow yet.import { IEventStore } from '@cratis/chronicle';
class ScenariosQueryBookWatcher { constructor(private readonly store: IEventStore) {}
async watch(): Promise<void> { for await (const changeset of this.store.readModels.watch(ScenariosQueryBook)) { if (changeset.removed) { continue; }
console.log(`${changeset.key}: 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.
Observe a live page
Section titled “Observe a live page”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));}import io.cratis.chronicle.IEventStoreimport kotlinx.coroutines.flow.Flow
data class ScenariosObserveBook(val title: String, val onLoan: Boolean)
class ScenariosQueryLiveBookPage(private val store: IEventStore) { fun subscribe(): Flow<List<ScenariosObserveBook>> = store.readModels.materialized.observeInstances(ScenariosObserveBook::class, skip = 0, take = 50)}import io.cratis.chronicle.IEventStore;import io.cratis.chronicle.java.ReadModelsJavaBridge;
import kotlinx.coroutines.Job;
import java.util.List;import java.util.function.Consumer;
record ScenariosObserveBook(String title, boolean onLoan) {}
class ScenariosQueryLiveBookPage { private final IEventStore store;
ScenariosQueryLiveBookPage(IEventStore store) { this.store = store; }
/** Hands every new page to the subscriber, and returns the job to cancel when done. */ Job subscribe(Consumer<List<ScenariosObserveBook>> onPage) { return ReadModelsJavaBridge.observeMaterializedInstances( store.getReadModels(), ScenariosObserveBook.class, 0, 50, onPage); }}Elixir does not support this workflow yet.import { IEventStore } from '@cratis/chronicle';
class ScenariosQueryLiveBookPage { constructor(private readonly store: IEventStore) {}
async subscribe(updateView: (books: ScenariosQueryBook[]) => void): Promise<void> { for await (const page of this.store.readModels.materialized.observeInstances(ScenariosQueryBook, 0, 50)) { updateView(page); } }}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.
Pick by consistency
Section titled “Pick by consistency”| You need | Use | Consistency |
|---|---|---|
| One instance, exact current state | GetInstanceById<T>(key) | Strong |
| All instances, exact current state | GetInstances<T>() | Strong |
| A page for a list or grid | Materialized.GetInstances<T>(skip, take) | Eventual |
| To react to every change of a type | Watch<T>() | Pushed as changes apply |
| A live-updating page | Materialized.ObserveInstances<T>(skip, take) | Eventual, pushed |
See also
Section titled “See also”- Consistency models — the strong-vs-eventual trade-off in depth.
- Getting a single instance and Getting a collection of instances — caching, performance, and the non-generic forms.
- Materialized read models — pagination details and when to query the sink directly.
- Watching read models — changesets, filtering, and error handling.