Appended event metadata and projections
Projections build read models by mapping events to fields. They observe all events of the types declared in their definition — they do not use [FilterEventsByTag], [EventSourceType], or [EventStreamType] to filter incoming events.
How projections select their input
Section titled “How projections select their input”A projection declares the event types it observes through its event mappings:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;
[EventType]public record FilteringOrderPlaced(string CustomerId, decimal TotalAmount);
[EventType]public record FilteringOrderShipped(DateTimeOffset ShippedAt);
[FromEvent<FilteringOrderPlaced>][FromEvent<FilteringOrderShipped>]public record FilteringOrderSummary( [Key] string CustomerId, decimal TotalAmount, DateTimeOffset? ShippedAt);import io.cratis.chronicle.events.EventTypeimport io.cratis.chronicle.projections.FromEventimport io.cratis.chronicle.readModels.ReadModelimport java.math.BigDecimalimport java.time.OffsetDateTime
@EventTypedata class FilteringOrderPlaced(val customerId: String, val totalAmount: BigDecimal)
@EventTypedata class FilteringOrderShipped(val shippedAt: OffsetDateTime)
@ReadModel@FromEvent(FilteringOrderPlaced::class)@FromEvent(FilteringOrderShipped::class)data class FilteringOrderSummary( val customerId: String = "", val totalAmount: BigDecimal = BigDecimal.ZERO, val shippedAt: OffsetDateTime? = null)import io.cratis.chronicle.events.EventType;import io.cratis.chronicle.projections.FromEvent;import io.cratis.chronicle.readModels.ReadModel;import java.math.BigDecimal;import java.time.OffsetDateTime;
@EventTyperecord FilteringOrderPlaced(String customerId, BigDecimal totalAmount) {}
@EventTyperecord FilteringOrderShipped(OffsetDateTime shippedAt) {}
@ReadModel@FromEvent(eventType = FilteringOrderPlaced.class)@FromEvent(eventType = FilteringOrderShipped.class)class FilteringOrderSummary { public String customerId = ""; public BigDecimal totalAmount = BigDecimal.ZERO; public OffsetDateTime shippedAt = null;}defmodule MyApp.Events.FilteringOrderPlaced do use Chronicle.Events.EventType, id: "filtering-order-placed"
defstruct [:customer_id, :total_amount]end
defmodule MyApp.Events.FilteringOrderShipped do use Chronicle.Events.EventType, id: "filtering-order-shipped"
defstruct [:shipped_at]end
defmodule MyApp.ReadModels.FilteringOrderSummary do use Chronicle.ReadModels.ReadModel
defstruct customer_id: nil, total_amount: nil, shipped_at: nil
from MyApp.Events.FilteringOrderPlaced, set: [customer_id: :customer_id, total_amount: :total_amount]
from MyApp.Events.FilteringOrderShipped, set: [shipped_at: :shipped_at]endimport { eventType, fromEvent, readModel } from '@cratis/chronicle';
@eventType()class FilteringOrderPlaced { customerId = ''; totalAmount = 0;}
@eventType()class FilteringOrderShipped { shippedAt: Date | null = null;}
@readModel()@fromEvent(FilteringOrderPlaced)@fromEvent(FilteringOrderShipped)class FilteringOrderSummary { customerId = ''; totalAmount = 0; shippedAt: Date | null = null;}This projection receives every OrderPlaced and OrderShipped event regardless of any metadata attached during append. Metadata such as tags or stream type does not affect which events flow into a projection.
Tagging projections
Section titled “Tagging projections”[Tag] and [Tags] on a projection label the projection definition for organizational purposes. They do not filter incoming events:
using Cratis.Chronicle;using Cratis.Chronicle.Projections;
public record FilteringOrderReport(string CustomerId);
// Labels the projection for discoverability — does not affect which events are received[Tag("reporting")]public class FilteringOrderReportingProjection : IProjectionFor<FilteringOrderReport>{ public void Define(IProjectionBuilderFor<FilteringOrderReport> builder) => builder.From<FilteringOrderPlaced>(b => b.UsingKey(e => e.CustomerId));}Combining a projection with metadata-based filtering
Section titled “Combining a projection with metadata-based filtering”When you need a side effect or secondary read model that reacts only to a subset of events based on appended metadata, pair the projection with a reactor or reducer that carries the appropriate filter attributes.
The following example shows an OrderSummaryProjection that builds the full read model for all orders, alongside a PremiumOrderNotifier reactor that fires only when an order is appended with the premium tag:
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.Keys;using Cratis.Chronicle.Projections.ModelBound;using Cratis.Chronicle.Reactors;
[EventType]public record FilteringWithReactorOrderPlaced(string CustomerId, decimal TotalAmount);
// --- Append call ---// Carries the "premium" tag for orders that qualify// eventLog.Append(orderId, new FilteringWithReactorOrderPlaced(customerId, total), tags: ["premium"]);
// --- Projection: receives every OrderPlaced ---[FromEvent<FilteringWithReactorOrderPlaced>]public record FilteringWithReactorOrderSummary( [Key] string CustomerId, decimal TotalAmount);
// --- Reactor: receives only premium-tagged OrderPlaced ---[FilterEventsByTag("premium")]public class FilteringWithReactorPremiumOrderNotifier : IReactor{ public Task Placed(FilteringWithReactorOrderPlaced @event, EventContext context) => Task.CompletedTask;}The same pattern works with a reducer instead of a reactor:
using Cratis.Chronicle;using Cratis.Chronicle.Events;using Cratis.Chronicle.Reducers;
public record FilteringPremiumOrderTotals(int Count, decimal Total);
[FilterEventsByTag("premium")]public class FilteringPremiumOrderTotalsReducer : IReducerFor<FilteringPremiumOrderTotals>{ public FilteringPremiumOrderTotals Placed(FilteringWithReactorOrderPlaced @event, FilteringPremiumOrderTotals? current, EventContext context) => new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);}Use this pattern whenever you need both a projection-based read model (covering all events) and a metadata-filtered view or side effect (covering a subset).
Appending with metadata
Section titled “Appending with metadata”The metadata you provide at append time drives the filters on any accompanying reducers or reactors:
using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;
public class FilteringAppendService(IEventLog eventLog){ public async Task AppendOrders(string customerId) { // Appends to all observers — no extra metadata await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 42m));
// Appends to all observers; additionally dispatched to observers filtering on "premium" await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 299m), tags: ["premium"]);
// Appends with stream type; dispatched to observers filtering on "wholesale" stream type await eventLog.Append(EventSourceId.New(), new FilteringWithReactorOrderPlaced(customerId, 1500m), eventStreamType: "wholesale"); }}Kotlin does not support this workflow yet.`AppendOptions` only carries a `correlationId` — there is no way to attach tagsor a custom event stream type when appending from Kotlin. Track the client SDKissue before relying on metadata-filtered observers from Kotlin.Java does not support this workflow yet.`AppendOptions` only carries a `correlationId` — there is no way to attach tagsor a custom event stream type when appending from Java. Track the client SDKissue before relying on metadata-filtered observers from Java.defmodule MyApp.Events.FilteringMetadataOrderPlaced do use Chronicle.Events.EventType, id: "filtering-metadata-order-placed"
defstruct [:customer_id, :total_amount]end
defmodule MyApp.FilteringAppendService do alias MyApp.Events.FilteringMetadataOrderPlaced
def append_orders(order_id, customer_id) do # Appends to all observers — no extra metadata Chronicle.append(order_id, %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 42})
# Appends to all observers; additionally dispatched to observers filtering on "premium" Chronicle.append( order_id, %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 299}, tags: ["premium"] )
# Appends with stream type; dispatched to observers filtering on "wholesale" stream type Chronicle.append( order_id, %FilteringMetadataOrderPlaced{customer_id: customer_id, total_amount: 1500}, event_stream_type: "wholesale" ) endendTypeScript does not support this workflow yet.`AppendOptions` only carries `correlationId`, `eventSourceId`, and `concurrencyScope` —there is no way to attach tags or a custom event stream type when appending fromTypeScript. Track the client SDK issue before relying on metadata-filteredobservers from TypeScript.