Materialized Read Models
When a projection or reducer processes an event, Chronicle persists the resulting read model instance in a sink — a database-backed store that holds the materialized state. The Materialized API gives you direct, paginated access to that stored state without replaying the event log.
What Materialized Access Covers
Section titled “What Materialized Access Covers”The Materialized API is intentionally narrow. It answers one question well: give me a page of instances I already know are stored in a sink. This keeps it:
- Database-agnostic — the same call works regardless of whether the sink is MongoDB, SQL, or any future backend
- Simple to use — two methods, optional skip/take, and sensible defaults
- Safe for large datasets — only the requested page is loaded, never the full collection
What it does not cover:
- Filtering by field value
- Sorting by any property
- Aggregation or count queries
- Full-text or range searches
- Complex joins or projections across collections
For those needs, inject the sink’s native client directly. If your sink is MongoDB, inject IMongoCollection<TReadModel>. If it is SQL, inject your DbContext. Those tools are purpose-built for complex queries and Chronicle does not try to replace them.
Accessing the API
Section titled “Accessing the API”IMaterializedReadModels is exposed through IReadModels.Materialized:
using Cratis.Chronicle;
public record MaterializedPaginationOrder(string CustomerName, decimal Total);
public class MaterializedPaginationAccessingApi(IEventStore eventStore){ public async Task<IEnumerable<MaterializedPaginationOrder>> GetOrders() { // Inject IEventStore, then reach through to the Materialized API var instances = await eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>(); return instances; }}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.ReadModels.MaterializedPaginationOrder do defstruct [:customer_name, :total]end
defmodule MyApp.MaterializedPaginationAccessingApi do alias MyApp.ReadModels.MaterializedPaginationOrder
def get_orders do # Query the materialized read-model container directly, rather than replaying events Chronicle.ReadModels.query(MaterializedPaginationOrder) endendimport { IEventStore } from '@cratis/chronicle';
class MaterializedPaginationOrder { constructor( readonly customerName: string, readonly total: number ) {}}
class MaterializedPaginationAccessingApi { constructor(private readonly store: IEventStore) {}
// Inject IEventStore, then reach through to the materialized API async getOrders(): Promise<MaterializedPaginationOrder[]> { return this.store.readModels.materialized.getInstances(MaterializedPaginationOrder); }}Getting Instances
Section titled “Getting Instances”Basic Usage
Section titled “Basic Usage”Retrieve the first page of stored instances using the defaults (skip: 0, take: 50):
using Cratis.Chronicle;
public class MaterializedPaginationBasicUsage(IEventStore eventStore){ public Task<IEnumerable<MaterializedPaginationOrder>> GetOrders() => eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>();}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.MaterializedPaginationBasicUsage do alias MyApp.ReadModels.MaterializedPaginationOrder
def get_orders do Chronicle.ReadModels.query(MaterializedPaginationOrder) endendimport { IEventStore } from '@cratis/chronicle';
class MaterializedPaginationBasicUsage { constructor(private readonly store: IEventStore) {}
async getOrders(): Promise<MaterializedPaginationOrder[]> { return this.store.readModels.materialized.getInstances(MaterializedPaginationOrder); }}Pagination
Section titled “Pagination”Both skip and take are optional with sensible defaults. Use them for page-based or offset-based navigation:
using System.Linq;using Cratis.Chronicle;
public class MaterializedPaginationPagination(IEventStore eventStore){ public async Task GetPages() { // First page of 20 var page1 = await eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>(take: 20); Console.WriteLine($"Page 1: {page1.Count()} orders");
// Second page of 20 var page2 = await eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>(skip: 20, take: 20); Console.WriteLine($"Page 2: {page2.Count()} orders");
// Third page of 20 var page3 = await eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>(skip: 40, take: 20); Console.WriteLine($"Page 3: {page3.Count()} orders"); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.defmodule MyApp.MaterializedPaginationPagination do alias MyApp.ReadModels.MaterializedPaginationOrder
def get_pages do # Page 1 of 20 {:ok, page1} = Chronicle.ReadModels.query(MaterializedPaginationOrder, page: 1, page_size: 20) IO.puts("Page 1: #{length(page1.instances)} orders")
# Page 2 of 20 {:ok, page2} = Chronicle.ReadModels.query(MaterializedPaginationOrder, page: 2, page_size: 20) IO.puts("Page 2: #{length(page2.instances)} orders")
# Page 3 of 20 {:ok, page3} = Chronicle.ReadModels.query(MaterializedPaginationOrder, page: 3, page_size: 20) IO.puts("Page 3: #{length(page3.instances)} orders") endendimport { IEventStore } from '@cratis/chronicle';
class MaterializedPaginationPagination { constructor(private readonly store: IEventStore) {}
async getPages(): Promise<void> { // First page of 20 const page1 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 0, 20); console.log(`Page 1: ${page1.length} orders`);
// Second page of 20 const page2 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 20, 20); console.log(`Page 2: ${page2.length} orders`);
// Third page of 20 const page3 = await this.store.readModels.materialized.getInstances(MaterializedPaginationOrder, 40, 20); console.log(`Page 3: ${page3.length} orders`); }}Pagination Parameters
Section titled “Pagination Parameters”The parameters use strongly-typed concepts that convert implicitly from int:
| Parameter | Type | Default | Named Constants |
|---|---|---|---|
skip | InstanceCountToSkip? | 0 | InstanceCountToSkip.Zero |
take | InstanceCount? | 50 | InstanceCount.Default, InstanceCount.Unlimited |
using Cratis.Chronicle;using Cratis.Chronicle.ReadModels;
public class MaterializedPaginationNamedConstants(IEventStore eventStore){ public Task<IEnumerable<MaterializedPaginationOrder>> GetOrders() => // Using named constants eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>( skip: InstanceCountToSkip.Zero, take: InstanceCount.Default);}Building a Paged API Endpoint
Section titled “Building a Paged API Endpoint”using Cratis.Chronicle;using Microsoft.AspNetCore.Mvc;
[ApiController][Route("orders")]public class MaterializedPaginationOrdersController(IEventStore eventStore) : ControllerBase{ [HttpGet] public async Task<IEnumerable<MaterializedPaginationOrder>> GetOrders( [FromQuery] int page = 0, [FromQuery] int pageSize = 20) { return await eventStore.ReadModels.Materialized.GetInstances<MaterializedPaginationOrder>( skip: page * pageSize, take: pageSize); }}Observing Changes
Section titled “Observing Changes”ObserveInstances returns an IObservable<IEnumerable<TReadModel>> that emits a new page snapshot whenever the underlying stored data changes. This is useful for live-updating UIs, dashboards, and monitoring tools.
using System.Linq;using Cratis.Chronicle;
public record MaterializedPaginationProduct(string Name, decimal Price);
public class MaterializedPaginationObserving(IEventStore eventStore){ public void Run() { var subscription = eventStore.ReadModels.Materialized .ObserveInstances<MaterializedPaginationProduct>(take: 50) .Subscribe(products => { // Called whenever the stored instances change Console.WriteLine($"Products updated: {products.Count()} in view"); });
// Dispose when done to release the change stream subscription.Dispose(); }}Kotlin does not support this workflow yet.Java does not support this workflow yet.Elixir does not support this workflow yet.import { IEventStore } from '@cratis/chronicle';
class MaterializedPaginationProduct { constructor( readonly name: string, readonly price: number ) {}}
class MaterializedPaginationObserving { constructor(private readonly store: IEventStore) {}
async run(): Promise<void> { // Called whenever the stored instances change for await (const products of this.store.readModels.materialized.observeInstances(MaterializedPaginationProduct, 0, 50)) { console.log(`Products updated: ${products.length} in view`); } }}Observation relies on the sink’s change stream mechanism:
- MongoDB — uses native MongoDB change streams
- SQL — uses polling-based change detection via
DbContext
Observing in a Service
Section titled “Observing in a Service”using Cratis.Chronicle;
public class MaterializedPaginationProductDashboard : IDisposable{ readonly IDisposable _subscription;
public MaterializedPaginationProductDashboard(IEventStore eventStore) { _subscription = eventStore.ReadModels.Materialized .ObserveInstances<MaterializedPaginationProduct>(take: 100) .Subscribe(UpdateView); }
void UpdateView(IEnumerable<MaterializedPaginationProduct> products) { /* ... */ }
public void Dispose() => _subscription.Dispose();}When to Use the Materialized API
Section titled “When to Use the Materialized API”Use Materialized.GetInstances and ObserveInstances when:
- You need a page of stored read model instances for a list view, data grid, or infinite scroll UI
- You want real-time updates pushed to a connected UI without polling
- The dataset is large and loading everything into memory via event replay would be too slow or too expensive
Do not use the Materialized API when:
- You need to filter by a specific field — query the sink directly
- You need instances sorted by a property — query the sink directly
- You need a count of matching records — query the sink directly
- You need to run an aggregation — query the sink directly
Comparison with On-Demand GetInstances
Section titled “Comparison with On-Demand GetInstances”ReadModels.GetInstances<T>() | ReadModels.Materialized.GetInstances<T>() | |
|---|---|---|
| Data source | Event log replay | Materialized sink (database) |
| Consistency | Strong — always current | Eventual — milliseconds behind |
| Performance | Proportional to event history | O(1) — direct database lookup |
| Pagination | No | Yes — skip/take |
| Large datasets | Slow — replays all events | Fast — loads only the requested page |
| Filtering/sorting | Post-fetch with LINQ | Not supported — query the sink directly |
Related Topics
Section titled “Related Topics”- Consistency Models — Understanding strong vs. eventual consistency
- Getting a Single Instance — On-demand computation for a single instance
- Getting a Collection of Instances — On-demand collection retrieval via event replay
- Watching Read Models — Observe event-log-sourced read model changesets
- Projections — How read models are produced from events
- Reducers — Imperative state-building from events
- MongoDB Sink — How read model instances are stored in MongoDB
- SQL Sink — How read model instances are stored in a SQL database