Skip to content

Passive projection

A passive projection is a projection that is not actively materialized to a persistent store but can be queried on-demand for in-memory lookups. This is useful for scenarios where you need to construct read models from events without the overhead of maintaining persistent state.

Use the .Passive() method to mark a projection as passive:

using Cratis.Chronicle.Projections;
public class DecPassiveUserSummaryProjection : IProjectionFor<DecPassiveUserSummary>
{
public void Define(IProjectionBuilderFor<DecPassiveUserSummary> builder) => builder
.Passive()
.From<DecPassiveUserCreated>()
.From<DecPassiveUserUpdated>();
}

View C# snippet source on GitHub

This projection:

  • Will not be actively maintained in persistent storage
  • Can be read on demand through ReadModels.GetInstanceById
  • Reconstructs the read model from events when requested

A read model declaring [EventStream<TSource>(stream)] is also passive, even without .Passive(). It stays keyed by event source ID but requires a stream-aware instance read; a key-only read is refused rather than mixing stream IDs. See Read models scoped to one event stream.

Passive projections are accessed through the event store’s ReadModels using the GetInstanceById method:

using Cratis.Chronicle;
public class DecPassiveUserService(IEventStore eventStore)
{
public Task<DecPassiveUserSummary> GetUserSummary(string userId) =>
eventStore.ReadModels.GetInstanceById<DecPassiveUserSummary>(userId);
}

View C# snippet source on GitHub

The read model is defined the same way as for regular projections:

public record DecPassiveUserSummary(
string Name,
string Email,
int LoginCount,
DateTimeOffset LastLoginAt);

View C# snippet source on GitHub

Events should match the read model structure or use explicit mapping:

using Cratis.Chronicle.Events;
[EventType]
public record DecPassiveUserCreated(string Name, string Email);
[EventType]
public record DecPassiveUserUpdated(string Name, string Email);
[EventType]
public record DecPassiveUserLoggedIn(DateTimeOffset LoginTime);

View C# snippet source on GitHub

When you call GetInstanceById on a passive projection:

  1. Chronicle folds the relevant events for the specified event source ID in memory, starting from the first event.
  2. The result is returned. Nothing is kept between reads: there is no persistent read-model sink and no cached state.
  3. Every later read folds the history again, so it reflects everything appended up to that read, including redactions, revisions and erased personal data.

By-ID reads select one event source. A cross-source join or custom-key aggregation cannot generally reconstruct the same answer as its materialized counterpart this way. See consistency models before making an existing projection passive.

Passive projections are ideal for:

  • Infrequent queries: When read models are accessed rarely or sporadically
  • Current source state: When you need an on-demand fold instead of waiting for materialization
  • Storage-sensitive scenarios: When you want to avoid a persistent read-model sink
  • Temporary calculations: For read models that are computed and discarded
  • Testing and debugging: When you need to inspect event-driven state without persistence
  • Every read folds the source’s history from the start, so read cost grows with the number of relevant events for that source.
  • Passive projections trade persistent storage for read-time processing on every read.
  • A reactor or command that reads a passive model on every event over a long stream repeats that fold each time; keep such histories short, or use a materialized projection when eventual consistency is acceptable.
  • Use bounded, source-local histories when read latency matters.