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 eventually consistent reads served from the materialized sink — the default — and strongly consistent reads of a passive read model, computed from the event log on every call.
Get one instance
Section titled “Get one instance”GetInstanceById returns the instance for one key. For a materialized read model — the default — it reads the stored instance from the sink, so it is eventually consistent. For a [Passive] read model it replays the key’s events through the projection or reducer on demand, 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 use Chronicle.ReadModels.ReadModel
defstruct title: "", on_loan: falseend
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';import { field } from '@cratis/fundamentals';
class ScenariosQueryBook { @field(String) readonly title: string; @field(Boolean) readonly onLoan: boolean;
constructor(title: string, onLoan: boolean) { this.title = title; this.onLoan = onLoan; }}
class ScenariosQueryBookService { constructor(private readonly store: IEventStore) {}
async getBook(bookId: string): Promise<ScenariosQueryBook | null> { return this.store.readModels.findInstanceById(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 returns every instance in one call — read from the sink for a materialized read model, replayed from the event log for a passive one. Either way the whole set comes back at once, so filter the result with LINQ and keep it for small sets:
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 use Chronicle.ReadModels.ReadModel
defstruct title: "", on_loan: falseend
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 use Chronicle.ReadModels.ReadModel
defstruct title: "", on_loan: falseend
defmodule MyApp.ScenariosQueryBookPageService do alias MyApp.ReadModels.ScenariosQueryPagedBook
def get_page do {:ok, result} = Chronicle.ReadModels.query(ScenariosQueryPagedBook, page: 0, 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()); }); }}defmodule MyApp.ScenariosQueryBookWatcher do alias MyApp.ReadModels.ScenariosQueryBook
def watch do Chronicle.ReadModels.watch(ScenariosQueryBook) end
def handle_message({:chronicle_read_model_changed, ScenariosQueryBook, changeset}) do if not changeset.removed and changeset.read_model do IO.puts("#{changeset.model_key}: on loan = #{changeset.read_model.on_loan}") end end
# The stream failed and the watch ended; call watch/0 again to resume. def handle_message({:chronicle_read_model_watch_error, ScenariosQueryBook, reason}) do IO.puts("Watch ended: #{inspect(reason)}") end
def stop(watcher), do: Chronicle.ReadModels.unwatch(watcher)endimport { 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 java.util.List;import java.util.concurrent.Flow;
record ScenariosObserveBook(String title, boolean onLoan) {}
class ScenariosQueryLiveBookPage { private final IEventStore store;
ScenariosQueryLiveBookPage(IEventStore store) { this.store = store; }
// Frameworks such as Arc accept Flow.Publisher<List<T>> as an observable query. Flow.Publisher<List<ScenariosObserveBook>> observe() { return ReadModelsJavaBridge.observeMaterializedInstancesPublisher( store.getReadModels(), ScenariosObserveBook.class, 0, 50); }}defmodule MyApp.ScenariosQueryLiveBookPage do alias MyApp.ReadModels.ScenariosQueryBook
# The calling process receives the page now, and again every time it changes. def subscribe do Chronicle.ReadModels.observe(ScenariosQueryBook, page_size: 50) end
def handle_message({:chronicle_read_model_instances, ScenariosQueryBook, result}) do result.instances end
# The stream failed and the observation ended; call subscribe/0 again to resume. def handle_message({:chronicle_read_model_observe_error, ScenariosQueryBook, reason}) do {:error, reason} end
def unsubscribe(observer), do: Chronicle.ReadModels.unobserve(observer)endimport { 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.