Skip to content

Define event sources

An event source definition names a kind of event source and the streams it owns. Chronicle registers the definitions discovered by the .NET client when the event store connects. An event source is optional: existing append APIs remain valid and append events without a definition.

using Cratis.Chronicle.EventSources;
[EventSource("ShoppingCart", Description = "A customer's cart", Concurrency = ConcurrencyDimensions.EventSourceId)]
[EventStream("Items", Description = "Cart item changes")]
[EventStream("Payment")]
public class ShoppingCartEventSource : IEventSource;

The name becomes the event source type stored with events appended through the definition. Stream names become event stream types. Chronicle does not impose a format for an event stream ID; the caller supplies it.

Append through the definition with the generic extensions:

await eventStore.EventLog.Append<ShoppingCartEventSource>(
cartId,
new ItemAdded(productId),
eventStream: "Items",
eventStreamId: cartId.ToString());

Chronicle rejects an undeclared stream and routing that contradicts the definition. It records the definition name in EventContext.EventSource, while EventSourceType and EventStreamType contain the resolved names.

When an append does not supply a concurrency scope, Chronicle narrows the optimistic scope to the dimensions declared on the stream. A stream without its own dimensions inherits the event source dimensions. ConcurrencyDimensions.None preserves the ordinary append behavior.

Build an explicit scope from the same declaration when needed:

var scope = await eventStore.EventLog.ConcurrencyScope()
.ForEventSource<ShoppingCartEventSource>("Items", cartId.ToString())
.Build();

Batches that need different guards for one event source id

Section titled “Batches that need different guards for one event source id”

An AppendMany call sends one concurrency scope per event source id. When a batch goes through event source definitions and you do not pass a scope for an id, Chronicle resolves the guard for every event with that id and compares them by what they check: event source id, event source type, event stream type, event stream id and event types. The expected sequence number is not part of the comparison, because it is read at a point in time.

  • Events whose guards check the same thing share one scope, and the first guard is kept.
  • An event whose strategy produces no guard, such as ConcurrencyScope.None, never hides a later event that does have one.
  • Events that need different guards cannot be sent, for example items for the streams 2025-01 and 2025-02 of the same cart, or two different event sources sharing an id. Chronicle throws IncompatibleConcurrencyScopesForEventSource before anything is appended instead of guarding one event and leaving the other unguarded.