---
title: Route a command through an event source definition
editUrl: https://github.com/Cratis/Arc/edit/main/Documentation/backend/csharp/chronicle/commands/event-source-definitions.md
description: Declare the event source and stream a command appends to with [EventSource<TSource>], choose the stream id, and know which concurrency boundary the definition gives the append.
---


An event source definition is a class that names a kind of event source and the streams it owns. A command that declares `[EventSource<TSource>(stream)]` appends its events through that definition instead of through loose strings. The definition supplies the event source type, the stream type and the concurrency dimensions, and Arc rejects a stream the definition does not declare.

Use this guide when a command should append to a named stream and you want its routing and its concurrency boundary declared once, in one typed place, instead of as strings on each command. A definition gives the append a **boundary**; it does not make a decision hold. [What the definition guards](#what-the-definition-guards) says exactly what you get, and [choosing how a command's rule holds](/arc/backend/csharp/chronicle/consistency/) tells you what to add when the rule depends on state.

The definition is a Chronicle concept; see [Chronicle event sources](/chronicle/event-sources/) for how Chronicle registers and stores it. This page covers what Arc does with it.

## Declare the definition and the command

The definition is a class implementing `IEventSource`, marked `[EventSource]` and one `[EventStream]` per stream. Without an explicit name, the event source name is the class name with a trailing `EventSource` removed.

```csharp
using Cratis.Arc.Chronicle.Commands;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSources;

[EventSource("Account")]
[EventStream("Transactions", Concurrency = ConcurrencyDimensions.EventSourceId | ConcurrencyDimensions.EventStreamType)]
public class AccountEventSource : IEventSource;

[EventType]
public record TransactionRecorded(decimal Amount);

[Command]
[EventSource<AccountEventSource>("Transactions")]
public record RecordTransaction(EventSourceId AccountId, decimal Amount)
{
    public TransactionRecorded Handle() => new(Amount);
}
```

The event is appended with:

- event source type `Account`, from the definition name;
- event stream type `Transactions`, from the stream you named;
- the `AccountId` as event source id, resolved by the usual [event source id conventions](/arc/backend/csharp/chronicle/commands/events/#event-source-id-resolution);
- the default stream id, because the command declares none.

The command's namespace needs `Cratis.Arc.Chronicle.Commands` for `[EventSource<TSource>]` and `Cratis.Chronicle.EventSources` for the definition types. The definition must be discovered by the Chronicle client like any other artifact; an unregistered definition type fails when the command runs.

A command declares at most one `[EventSource<TSource>]`. The stream argument is optional: without it the command routes through the definition's source type with the default stream type.

## Choose the stream

A stream is the part of an event source's history that one kind of decision shares. Pick the stream by asking which events must be checked against each other when two commands run at once.

**Recommended:** one definition per kind of entity, one `[EventStream]` per history that is decided separately, and the event source id as the entity's id. Name the stream on the command with `[EventSource<TSource>(stream)]`. The reason: the definition is the single place that says which streams exist and which dimensions guard them, and Arc rejects a typo. Alternatives, in order of preference: the command key and default stream with no definition, when one history per entity is enough; string attributes (`[EventSourceType]` with `[EventStreamType]`), which ARCCHR0013 flags once a matching definition exists; explicit scopes, when the decision must be bound to a revision it read.

| You want | Declare | Stream id |
| --- | --- | --- |
| One history per entity, everything for it together | One stream, or none | Leave unset; the event source id identifies the entity |
| Independent histories of one entity, such as `Onboarding` and `Transactions` | One `[EventStream]` per history | Leave unset |
| One history per entity and period, owner or process | One stream | A template or `ICanProvideEventStreamId`, see below |

`EventStream` on the attribute must name a stream the definition declares. A typo or an undeclared name fails when the command's context is built, with `EventRoutingContradictsEventSource`, instead of appending to a stream nobody reads. [Scenarios](/arc/backend/csharp/chronicle/commands/event-source-definition-scenarios/) shows each shape as a complete command.

## Choose the stream id

The definition names the stream. The stream id is separate: it picks one concrete stream within the stream type, and the definition does not supply it. Declare it on the command with the existing stream id mechanisms:

- `[EventStreamId("constant")]` for a fixed id.
- `ICanProvideEventStreamId` when code computes it.
- A template in `[EventStreamId("{Owner}:{Period}")]` when it is composed from command properties.

```csharp
using Cratis.Arc.Chronicle.Commands;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSources;

[EventSource("Report")]
[EventStream("Approval", Concurrency = ConcurrencyDimensions.EventSourceId | ConcurrencyDimensions.EventStreamType | ConcurrencyDimensions.EventStreamId)]
public class ReportEventSource : IEventSource;

[EventType]
public record ReportApproved;

[Command]
[EventSource<ReportEventSource>("Approval")]
[EventStreamId("{Owner}:{Period}")]
public record ApproveReport(EventSourceId ReportId, string Owner, DateOnly Period)
{
    public ReportApproved Handle() => new();
}
```

For `Owner = "acme"` and `Period = 2026-10-01`, the stream id is `acme:2026-10-01`. A template part is a public instance property. Concepts use their underlying value, `DateOnly` uses `yyyy-MM-dd`, and `{{` and `}}` are literal braces; the [full format table](/arc/backend/csharp/chronicle/commands/concurrency/#template-event-stream-ids) lists the rest. Arc refuses unknown properties, blank parts, malformed braces and an id that resolves to the default, and it resolves the template after validation, so a validator can reject a missing part first.

Use the same function when you read or seed the stream: `EventStreamIdTemplate.ResolveFor(command)` returns the id exactly as the pipeline resolves it.

Combining a non-empty `[EventStreamId]` with `ICanProvideEventStreamId` is ambiguous and throws `AmbiguousEventStreamId`.

## What the definition guards

Chronicle's Dynamic Consistency Boundary (DCB) selects the events an append must not have missed with *tags*. In Chronicle the stream metadata plays that role: event source id, event source type, stream type and stream id. An event source definition is the typed way to choose which of those dimensions guard an append. The concurrency dimensions on the definition decide which events a returned event is checked against. `[EventSource]` can declare dimensions for every stream, and each `[EventStream]` can declare its own; a stream that declares dimensions replaces the source's for that stream, and a stream that declares none inherits the source's. The dimensions are a combination of:

| Dimension | The append is checked against events with the same |
| --- | --- |
| `EventSourceId` | Event source id. Leave it out only when the guard should span every event source that writes the stream, and only on a definition-routed `EventForEventSourceId`; see below. |
| `EventSourceType` | Event source type, the definition name |
| `EventStreamType` | Stream type, the stream name |
| `EventStreamId` | Stream id, from `[EventStreamId]` |

With no dimensions declared, the definition adds no guard of its own and the event sequence applies its configured strategy as it does for any append.

Which dimensions the scope carries depends on how the event is returned. A definition-routed `EventForEventSourceId`, single or in a returned collection of wrappers (`EventSource` set, or built with `IEventRoutes`), is guarded by the scope Chronicle derives from the definition, which carries exactly the declared dimensions (when the command's definition declares any), so a definition without `EventSourceId` is checked against all sources' events with the same stream type and stream id. For every other returned event, that is a plain event, a collection of plain events, or a legacy-string wrapper, Arc builds the scope itself and always includes the target event source id, then adds the declared stream type, stream id and source type. Omitting `EventSourceId` therefore gives no cross-source guard there. Do not rely on a cross-source boundary from a plain returned event; include `EventSourceId` on the definition so both paths agree, or return definition-routed events. [How routing works](/arc/backend/csharp/chronicle/commands/event-source-definition-routing/) lists each return path.

Four limits matter:

- **It is the default append check, not a decision guard.** Arc resolves the expected tail while it processes the returned events, after `Handle()` has run. A concurrent append between your read and that moment is part of the new expected tail and does not conflict. The first append into an empty scope is checked only when the client's concurrency options opt in. To bind a decision to what it read, use a [stream decision](/arc/backend/csharp/chronicle/commands/stream-decisions/), a [protected decision](/arc/backend/csharp/chronicle/read-models/injecting-into-commands/#decision-reads-for-event-dependent-commands) or [exact scopes](/arc/backend/csharp/chronicle/commands/events/#events-with-exact-concurrency-scopes).
- **An explicit choice wins.** `concurrency: true` on `[EventStreamId]`, `[EventStreamType]` or `[EventSourceType]` selects the dimensions from those attributes, and a returned `EventsWithConcurrencyScopes` replaces every derived scope. The definition stays the policy for every command that does not override it.
- **It guards the whole source or stream, whatever the event type.** The scope narrows by event source id, source type, stream type and stream id, never by event type. In DCB terms that is a coarse, aggregate-like boundary: any new event in the guarded stream conflicts, related or not. That is the right shape when the events in the stream are all relevant to the decisions made against it, for example a short history per entity or per period. When unrelated event types share the stream, give the decision its own stream, or use a [protected decision](/arc/backend/csharp/chronicle/read-models/injecting-into-commands/#decision-reads-for-event-dependent-commands), whose scope narrows to the event types its model folds. A [stream decision](/arc/backend/csharp/chronicle/commands/stream-decisions/) has the same whole-stream boundary and also reads the whole stream.
- **One event source per event.** The boundary is an event source id plus the dimensions above. Chronicle does not tag one event with several entities, so a fact about two entities is two events, one per event source, appended in one atomic batch. See [Chronicle's Dynamic Consistency Boundary](/chronicle/dynamic-consistency-boundary/) for how a concurrency scope maps to the boundary and [when to use which mechanism](/chronicle/dynamic-consistency-boundary/choosing/).

[How routing works](/arc/backend/csharp/chronicle/commands/event-source-definition-routing/) follows a single append from the attribute to the scope Chronicle receives.

## Reading and completing the routed stream

The route the command declares is the same route its reads and completions use:

- [`IStreamReads.ForCommand()`](/arc/backend/csharp/chronicle/commands/stream-decisions/) reads the command's stream at a captured tail and returns a `StreamDecision` whose `Append(...)` carries an exact scope.
- [`CompleteStream`](/arc/backend/csharp/chronicle/commands/completing-streams/) closes the command's stream after its events commit.
- `IEventRoutes` resolves a route for any definition outside a command; see [per-event routing](/arc/backend/csharp/chronicle/commands/events/#per-event-routing).

## Migrate from string routing

Commands without a definition keep working. To move one over:

1. Declare an `IEventSource` with `[EventSource("Name")]` and an `[EventStream]` for each stream type the command used. Use the strings you already store as the names so existing events and observers match.
2. Replace `[EventSourceType("Name")]` and `[EventStreamType("Stream")]` on the command with `[EventSource<TSource>("Stream")]`. Keep `[EventStreamId]` and `ICanProvideEventStreamId`; the definition does not replace them.
3. Move `concurrency: true` to dimensions on the definition. While a command still carries the flag, the flag chooses the dimensions and the definition's are ignored for it.

[ARCCHR0013](/arc/backend/csharp/chronicle/code-analysis/#arcchr0013-use-the-event-source-definition) (Info) finds the commands to migrate: when `[EventSourceType("Name")]` on a command or aggregate root names a source that a definition in the compilation declares, it suggests `[EventSource<TSource>]`, or `[EventSource<TSource>("stream")]` when an `[EventStreamType]` names a stream the definition declares. It stays silent when the stream is not declared, and for a type that already has `[EventSource<TSource>]`. It has no code fix, because the attributes can carry `concurrency: true` flags that the definition's dimensions do not reproduce.

Do not keep a string attribute that contradicts the definition. `[EventSourceType]` or `[EventStreamType]` with a value other than the definition's source or stream fails with `EventRoutingContradictsEventSource` rather than append events whose strings disagree with the definition.

### Wrapper-named streams

A returned `EventForEventSourceId` can name its route with `EventSourceType` and `EventStreamType` strings. That still works, but the event is appended by those strings: it loses `EventContext.EventSource` and the wrapper target's own definition policy. On a definition-routed command Arc still applies the command's selected definition dimensions, and builds the scope from the wrapper's own routing strings, always including the target event source id. Two analyzers help you migrate:

- [ARCCHR0021](/arc/backend/csharp/chronicle/code-analysis/arcchr0021/) (Info) finds literal strings that spell a declared stream. Its code fix replaces them with `EventSource = typeof(Definition)` and `EventStream = "stream"`, or you can use `IEventRoutes`. Review the concurrency change first, because the guard now comes from the definition, which Chronicle derives with exactly the declared dimensions rather than Arc's scope that always includes the event source id.
- [ARCCHR0020](/arc/backend/csharp/chronicle/code-analysis/arcchr0020/) (Warning) finds a literal stream name that the definition does not declare, which is usually a typo. At runtime a definition-routed command rejects such a wrapper with `EventStreamDoesNotBelongToEventSource` before it enrolls or appends anything.

```csharp
using Cratis.Arc.Chronicle.Commands;
using Cratis.Arc.Chronicle.Streams;
using Cratis.Arc.Commands.ModelBound;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.EventSources;

[EventSource("Account")]
[EventStream("Transactions")]
[EventStream("Adjustments")]
public class AccountEventSource : IEventSource;

[EventType]
public record BalanceAdjusted(decimal Amount);

[Command]
[EventSource<AccountEventSource>("Transactions")]
public record AdjustAccount(EventSourceId AccountId, decimal Amount)
{
    // Before: new EventForEventSourceId(AccountId, new BalanceAdjusted(Amount))
    //         { EventSourceType = "Account", EventStreamType = "Adjustments" }
    public EventForEventSourceId Handle(IEventRoutes routes) =>
        routes.For<AccountEventSource>("Adjustments").Route(AccountId, new BalanceAdjusted(Amount));
}
```

## Where to go next

- [Scenarios](/arc/backend/csharp/chronicle/commands/event-source-definition-scenarios/): a stream per entity, per owner and period, and per process, with a stream decision and a completed stream.
- [How routing works](/arc/backend/csharp/chronicle/commands/event-source-definition-routing/): how Arc resolves the definition, stream, stream id and scope.
- [Choosing how a command's rule holds](/arc/backend/csharp/chronicle/consistency/) and its [FAQ](/arc/backend/csharp/chronicle/consistency/faq/#event-source-definitions-and-stream-decisions): where definitions fit among the guards.
- Chronicle: [event sources](/chronicle/event-sources/), [Dynamic Consistency Boundary](/chronicle/dynamic-consistency-boundary/), [choosing a mechanism](/chronicle/dynamic-consistency-boundary/choosing/), [scenarios](/chronicle/dynamic-consistency-boundary/scenarios/) and [FAQ](/chronicle/dynamic-consistency-boundary/faq/).
