Skip to content

Projection Architecture

Chronicle supports three distinct approaches for defining projections, each designed for different use cases and developer preferences. This page explains the architecture and how these approaches work together.

Execution

Core Processing

Projection Declaration Approaches

Compiler

Reflection

Builder

Projection Declaration Language

(Declaration)

Model-Bound

(C# Attributes)

Declarative

(C# Fluent API)

ProjectionDefinition

Projection Pipeline

Event Stream

Projector

Read Model Storage

The Projection Declaration Language provides a concise, indentation-based syntax optimized for readability and quick authoring. Typically available from the tooling like Workbench.

Example:

projection MyModel
from MyEvent
set name = $.eventProperty
set count = $count + 1

Benefits:

  • Minimal syntax and ceremony
  • Easy to read and understand
  • Fast iteration with live preview
  • Context-sensitive auto-completion in editors

Use when:

  • Building new projections
  • Prototyping and experimentation
  • Working with the Workbench UI
  • Team prefers concise, declarative syntax

Model-bound projections use C# attributes on your read model classes, keeping the projection logic close to the data structure.

Example:

[Projection("MyModel")]
public record MyModel
{
[SetFrom<MyEvent>(nameof(MyEvent.Name))]
public string Name { get; init; }
[Count<MyEvent>]
public int Count { get; init; }
}

Benefits:

  • Type-safe at compile time
  • Co-located with read model definition
  • Leverages C# tooling and IntelliSense
  • Natural for C# developers

Use when:

  • Strong type safety is required
  • Using C#-first development workflow
  • Read model and projection logic should be together
  • Refactoring tools are important

Declarative projections use a fluent C# API to define projections programmatically with maximum flexibility.

Example:

public class MyModelProjection : IProjectionFor<MyModel>
{
public void Define(IProjectionBuilderFor<MyModel> builder)
{
builder
.From<MyEvent>(e => e
.Set(m => m.Name).To(e => e.EventProperty)
.Count(m => m.Count));
}
}

Benefits:

  • Full programmatic control
  • Complex conditional logic
  • Dynamic projection generation
  • Maximum flexibility

Use when:

  • Complex business rules require code
  • Generating projections dynamically
  • Advanced scenarios beyond declaration language capabilities
  • Need for custom extensions

Runtime Layer

Core Layer

Compilation Layer

Frontend Layer

Declaration Text

Workbench UI

Monaco Editor

+ Language Service

Tokenizer

Parser

Validator

Definition Builder

ProjectionDefinition

Projection Registry

Projection Engine

Event Stream

Projector

Storage Sink

Workbench UI: Web-based interface for creating, editing, and testing projections.

Monaco Editor with Language Service: Provides rich editing experience with:

  • Syntax highlighting for all declaration keywords
  • Context-sensitive auto-completion
  • Real-time validation with schema awareness
  • Hover documentation
  • Error markers

Tokenizer: Converts declaration text into tokens, handling:

  • Indentation-based structure
  • Keywords and identifiers
  • String literals and expressions
  • Line and column tracking (1-based)

Parser: Builds an Abstract Syntax Tree (AST) from tokens:

  • Validates structure and grammar
  • Detects syntax errors with precise locations
  • Handles nested blocks (children, joins, composite keys)
  • Supports expression parsing

Validator: Performs semantic validation:

  • Checks event types exist
  • Validates property names against schemas
  • Ensures composite key types are objects
  • Verifies expression syntax

Definition Builder: Constructs the final ProjectionDefinition object that all three approaches produce.

ProjectionDefinition: Unified representation of projection logic, regardless of source (Projection Declaration, Model-Bound, or Declarative).

Projection Registry: Manages all registered projections in the system.

Projection Engine: Coordinates projection execution, handles event replay, and manages state.

Event Stream: Source of events to be projected.

Projector: Executes projection logic for each event:

  • Applies transformations
  • Manages read model instances
  • Handles keys and lookups
  • Performs joins and child operations

Storage Sink: Persists read models (typically MongoDB or another database).

StorageEvent StreamProjection EngineCompilerMonaco EditorUserStorageEvent StreamProjection EngineCompilerMonaco EditorUserWrite DeclarationSyntax HighlightCompile on ChangeReturn Errors/WarningsShow MarkersSave & ActivateRegister ProjectionNew EventMatch Event TypeExecute ProjectionUpdate Read ModelQuery Results
CriterionDeclarationModel-BoundDeclarative
Ease of Learning⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Type Safety⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Conciseness⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Flexibility⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Refactoring Support⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Prototyping Speed⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
Complex Logic⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

Recommendation: Start with the Projection Declaration Language for rapid development and prototyping. Move to Model-Bound when type safety becomes critical. Use Declarative for advanced scenarios requiring programmatic control.

Regardless of which approach you choose, all projections share these concepts:

  • Event Types: Define which events trigger the projection
  • Keys: Identify read model instances
  • Property Mappings: Transform event data to read model properties
  • Joins: Link to other read models
  • Children: Model parent-child relationships
  • Filters: Conditionally process events
  • Operations: Set, increment, decrement, count, add, subtract

All three approaches compile down to the same ProjectionDefinition format, ensuring consistent behavior and performance.