---
title: Filter reducers by appended event metadata
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

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.

## Filter attributes

| Attribute | Filters by | Matches when |
|---|---|---|
| `[FilterEventsByTag("...")]` | Appended or static event tag | Any filter tag matches any tag on the appended event |
| `[EventSourceType("...")]` | Event source type set at append time | The event source type matches exactly |
| `[EventStreamType("...")]` | Event stream type set at append time | The event stream type matches exactly |

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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");
}
```

</TabItem>
</Tabs>

:::note[Client coverage]
Kotlin and Java attach the same filters with `@FilterEventsByTag`, `@EventSourceType`, and `@EventStreamType` on the reducer class. Elixir's `use Chronicle.Reducers.Reducer` and TypeScript's `reducer()` register a reducer without a way to attach these filters.
:::

## Filter by tag

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

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

### Multiple filter tags

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

## Filter by event source type

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

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

## Filter by event stream type

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

## Combine multiple filters

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

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

## Tagging the reducer vs filtering events

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

<Tabs syncKey="chronicle-client">
<TabItem label="C#">

```csharp
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);
}
```

</TabItem>
</Tabs>

## Detailed guides

- [Filter by tag](/chronicle/events/filtering/by-tag/)
- [Filter by event source type](/chronicle/events/filtering/by-event-source-type/)
- [Filter by event stream type](/chronicle/events/filtering/by-event-stream-type/)
