Skip to content

Declarative Projections

Declarative projections let you define read models with a fluent, code-first projection builder. They are ideal when you need explicit mapping, joins, or hierarchical projections that go beyond simple attribute mapping.

Declarative projections implement IProjectionFor<TReadModel> and use an IProjectionBuilderFor<TReadModel> to define how events map to read model properties. The builder gives you control over mapping, relationships, and event context while keeping the projection definition separate from the read model type.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Projections;
[EventType]
public record DecIndexUserRegistered(string Name, string Email, DateTimeOffset RegisteredAt);
public record DecIndexUserProfile(string Name, string Email, DateTimeOffset RegisteredAt);
public class DecIndexUserProfileProjection : IProjectionFor<DecIndexUserProfile>
{
public void Define(IProjectionBuilderFor<DecIndexUserProfile> builder) => builder
.From<DecIndexUserRegistered>();
}

View C# snippet source on GitHub

Auto-mapping is enabled by default at the top level, so matching properties are mapped automatically. When you need explicit mappings, you can use .Set(), .Add(), .Subtract(), and other builder operations.

Projection types are discovered by implementing IProjectionFor<TReadModel>. The events must be registered in the owning client’s event-type registry, normally through [EventType]; .NET also supports explicit event nominations.

In .NET, a projection class must target only one read model. Implementing two closed IProjectionFor<TReadModel> interfaces, including through inheritance, throws TypeImplementsMultipleProjectionInterfaces during default artifact discovery. Split the class into one projection per read model. CHR0057 reports this before runtime.

  • Auto mapping by default with the option to turn it off or override per event
  • Explicit property mappings for custom transformations
  • Hierarchies with child collections and nested projections
  • Joins across streams for richer read models
  • FromEvery for applying mappings to all events
  • Event context access for timestamps, sequence numbers, and IDs
  • Event sequence selection for sourcing from non-default sequences
  • Passive and not rewindable projections for specialized observation behavior
  • Variants for mutually exclusive read model shapes of the same logical entity

Start with model-bound attributes; use fluent mappings when keys, joins, hierarchy, or keeping the model attribute-free makes a separate definition clearer. Use reducer code when the logic reads better as state transitions, with a self-reducing model as the default for new reducers. Follow Choose how to build a read model for scenarios and limits.

Define emits a kernel-executed definition. Dependencies can help build it, but the builder lambdas are not arbitrary per-event handlers with dependency injection.

RecipeDescription
Simple projectionDeclare events and use the default AutoMap behavior
AutoMapAutomatic property mapping at different levels
PassiveIn-memory projections for on-demand lookups
Set propertiesExplicit property mapping and transformations
ChildrenHierarchical data models with child collections
JoinsCross-stream projections using joins
FunctionsArithmetic and other functions
Composite keysMulti-property key identification
Event contextUsing event metadata in projections
FromEverySetting properties for all events in a projection
FromAllSubscribe to all event types without filtering
Initial valuesDefault values for read model properties
RemoveWithJoinCross-stream child removal
FromEventSequenceSourcing events from specific event sequences
NotRewindableForward-only projections that cannot be replayed
VariantsMutually exclusive read model shapes for the same logical entity

Once you’ve defined a declarative projection, you can retrieve and observe the resulting read models using the IReadModels API:

  • Auto-mapping: Automatically maps properties with matching names between events and read models
    • Enabled by default in fluent projections; call .AutoMap() only to re-enable it in a scope that disabled it
    • Use [FromEvent<TEvent>] in model-bound projections for the same functionality
  • Explicit mapping: Gives you full control over property transformations and mappings
    • Use .Set(), .Add(), etc. in fluent projections
    • Use [SetFrom<TEvent>], [AddFrom<TEvent>], etc. in model-bound projections
  • Projections can handle multiple event types
  • Each event type can have its own property mappings
  • Properties are updated incrementally as events are processed
  • EventSourceId is used as the default key for both read models and parent identification in child collections
  • Child identifiers: Use .IdentifiedBy() to specify how child items are uniquely identified within collections
  • Parent key resolution:
    • By default, the EventSourceId is used to identify the parent when adding children
    • Use .UsingParentKey(e => e.Property) when the parent identifier is in the event content
    • Use .UsingParentKeyFromContext(ctx => ctx.EventSourceId) to explicitly document default behavior
  • Child key specification: Use .UsingKey(e => e.Property) to extract the child identifier from event content
  • Joins: Use keys to link data from different event streams using .On(m => m.Property)

By default the read model is stored in a container (collection, table) named by the client’s naming policy. In C#, call ContainerName("name") on the projection builder to choose a different one. This sets only the container name; the read model keeps its identifier, so the projection still resolves its read model. Every projection of the same read model must use the same container name, otherwise the client reports a conflict. Changing the container name of a read model that already has data points it at a new, empty container, so replay the projection afterwards.

  • Projections are automatically maintained as events are appended
  • The system handles indexing and query optimization
  • Consider join complexity and update frequency when designing projections