Skip to content

Filter reducers by appended event metadata

A reducer observes all events that match its handler method signatures by default. You can restrict which events a reducer receives by placing filter attributes on the reducer class. Chronicle evaluates these filters before dispatching an event — events that do not match are dropped before they ever reach the reducer.

AttributeFilters byMatches when
[FilterEventsByTag("...")]Appended or static event tagAny filter tag matches any tag on the appended event
[EventSourceType("...")]Event source type set at append timeThe event source type matches exactly
[EventStreamType("...")]Event stream type set at append timeThe event stream type matches exactly

These attributes correlate directly to the metadata you provide when appending an event:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
[EventType]
public record ReducersFilteringOrderPlaced(decimal TotalAmount);
public class ReducersFilteringMetadataExampleService(IEventLog eventLog)
{
public Task PlaceOrder(decimal totalAmount) =>
eventLog.Append(
EventSourceId.New(),
new ReducersFilteringOrderPlaced(totalAmount),
tags: ["priority"],
eventSourceType: "order",
eventStreamType: "fulfillment");
}

Append an event with a tag and place [FilterEventsByTag] on the reducer to accumulate state only from tagged events.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringByTagOrderPlaced(decimal TotalAmount);
public record ReducersFilteringPriorityOrderTotals(int Count, decimal Total);
public class ReducersFilteringByTagOrderService(IEventLog eventLog)
{
public Task PlacePriorityOrder(decimal totalAmount) =>
eventLog.Append(
EventSourceId.New(),
new ReducersFilteringByTagOrderPlaced(totalAmount),
tags: ["priority"]);
}
[FilterEventsByTag("priority")]
public class ReducersFilteringPriorityOrderTotalsReducer : IReducerFor<ReducersFilteringPriorityOrderTotals>
{
public ReducersFilteringPriorityOrderTotals Placed(ReducersFilteringByTagOrderPlaced @event, ReducersFilteringPriorityOrderTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);
}

PriorityOrderTotalsReducer updates only when the appended event carries the priority tag. Orders without that tag do not affect this read model.

Multiple [FilterEventsByTag] attributes widen the match. The reducer receives the event if the appended event has any of the configured tags:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringMultiTagOrderPlaced(decimal TotalAmount);
public record ReducersFilteringFastTrackOrderTotals(int Count);
[FilterEventsByTag("priority")]
[FilterEventsByTag("express")]
public class ReducersFilteringFastTrackOrderTotalsReducer : IReducerFor<ReducersFilteringFastTrackOrderTotals>
{
public ReducersFilteringFastTrackOrderTotals Placed(ReducersFilteringMultiTagOrderPlaced @event, ReducersFilteringFastTrackOrderTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1);
}

Place [EventSourceType] on the reducer to accumulate state only from events appended with a matching eventSourceType:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringInvoiceIssued(decimal Amount);
public record ReducersFilteringCustomerInvoiceTotal(decimal Amount);
public class ReducersFilteringInvoicingService(IEventLog eventLog)
{
public Task IssueCustomerInvoice(decimal amount) =>
eventLog.Append(
EventSourceId.New(),
new ReducersFilteringInvoiceIssued(amount),
eventSourceType: "customer");
}
[EventSourceType("customer")]
public class ReducersFilteringCustomerInvoiceTotalReducer : IReducerFor<ReducersFilteringCustomerInvoiceTotal>
{
public ReducersFilteringCustomerInvoiceTotal Issued(ReducersFilteringInvoiceIssued @event, ReducersFilteringCustomerInvoiceTotal? current, EventContext context) =>
new((current?.Amount ?? 0m) + @event.Amount);
}

If the same event is appended with eventSourceType: "supplier", this reducer does not receive it.

Place [EventStreamType] on the reducer to accumulate state only from events appended to a matching stream type:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringShipmentSent(decimal ShippingCost);
public record ReducersFilteringShippingTotals(int Count, decimal TotalCost);
public class ReducersFilteringShippingService(IEventLog eventLog)
{
public Task Send(decimal shippingCost) =>
eventLog.Append(
EventSourceId.New(),
new ReducersFilteringShipmentSent(shippingCost),
eventStreamType: "shipping");
}
[EventStreamType("shipping")]
public class ReducersFilteringShippingTotalsReducer : IReducerFor<ReducersFilteringShippingTotals>
{
public ReducersFilteringShippingTotals Sent(ReducersFilteringShipmentSent @event, ReducersFilteringShippingTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1, (current?.TotalCost ?? 0m) + @event.ShippingCost);
}

You can combine [FilterEventsByTag], [EventSourceType], and [EventStreamType] on the same reducer. Chronicle requires all filter categories to match:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringCombineOrderPlaced(decimal TotalAmount);
public record ReducersFilteringPremiumFulfillmentTotals(int Count, decimal Total);
public class ReducersFilteringCombineOrderService(IEventLog eventLog)
{
public Task PlacePremiumOrder(decimal totalAmount) =>
eventLog.Append(
EventSourceId.New(),
new ReducersFilteringCombineOrderPlaced(totalAmount),
tags: ["premium"],
eventSourceType: "order",
eventStreamType: "fulfillment");
}
[FilterEventsByTag("premium")]
[EventSourceType("order")]
[EventStreamType("fulfillment")]
public class ReducersFilteringPremiumFulfillmentTotalsReducer : IReducerFor<ReducersFilteringPremiumFulfillmentTotals>
{
public ReducersFilteringPremiumFulfillmentTotals Placed(ReducersFilteringCombineOrderPlaced @event, ReducersFilteringPremiumFulfillmentTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);
}

The reducer only receives events that match all three: the premium tag, the order event source type, and the fulfillment stream type.

[Tag] and [Tags] on a reducer class label the reducer itself for organization and discoverability. They do not filter incoming events:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reducers;
[EventType]
public record ReducersFilteringTagVsFilterOrderPlaced(decimal TotalAmount);
public record ReducersFilteringTagVsFilterTotals(int Count, decimal Total);
// These labels appear on the reducer definition — they do not affect dispatch
[Tag("reporting")]
[Tag("premium")]
public class ReducersFilteringLabeledFulfillmentTotalsReducer : IReducerFor<ReducersFilteringTagVsFilterTotals>
{
public ReducersFilteringTagVsFilterTotals Placed(ReducersFilteringTagVsFilterOrderPlaced @event, ReducersFilteringTagVsFilterTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);
}
// These filter which events are dispatched to the reducer
[FilterEventsByTag("premium")]
[EventSourceType("order")]
public class ReducersFilteringFilteredFulfillmentTotalsReducer : IReducerFor<ReducersFilteringTagVsFilterTotals>
{
public ReducersFilteringTagVsFilterTotals Placed(ReducersFilteringTagVsFilterOrderPlaced @event, ReducersFilteringTagVsFilterTotals? current, EventContext context) =>
new((current?.Count ?? 0) + 1, (current?.Total ?? 0m) + @event.TotalAmount);
}