Append through a definition
Use this page to write events through a definition. If you have not declared one yet, read Define an event source.
Append to a stream
Section titled “Append to a stream”Append<TSource> takes the event source id and the event as usual. The stream name and the stream id are optional.
using Cratis.Chronicle.Events;using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.EventSources;
[EventType]public record AccountOpened(string Owner);
[EventType]public record MoneyDeposited(decimal Amount);
[EventType]public record MoneyWithdrawn(decimal Amount);
[EventSource("Account", Description = "A customer's bank account", Concurrency = ConcurrencyDimensions.EventSourceId)][EventStream("Onboarding", Description = "Opening the account and verifying the customer")][EventStream( "Transactions", Description = "Deposits and withdrawals, one stream per month", Concurrency = ConcurrencyDimensions.EventSourceId | ConcurrencyDimensions.EventStreamType | ConcurrencyDimensions.EventStreamId)]public class AccountEventSource : IEventSource;
public class AccountWriter(IEventLog eventLog){ public Task<AppendResult> Deposit(EventSourceId accountId, decimal amount, string month) => eventLog.Append<AccountEventSource>( accountId, new MoneyDeposited(amount), eventStream: "Transactions", eventStreamId: month);}Chronicle fills in the routing from the definition:
| Value on the event | Taken from |
|---|---|
| Event source type | The definition’s name, Account here |
| Event stream type | The stream name you pass, or the default (All) when you pass none |
| Event stream id | The stream id you pass, or the default when you pass none |
EventContext.EventSource | The definition’s name |
You can pass eventSourceType or eventStreamType on the definition-less overloads, but not on the typed ones: they have no such parameters, and the definition decides.
Append without a stream
Section titled “Append without a stream”A stream is optional. An event appended without one carries the event source type and the default stream:
using Cratis.Chronicle.EventSequences;
public class AccountOpener(IEventLog eventLog){ // No stream: the event is written with the event source type "Account" and the default stream. public Task<AppendResult> Open(EventSourceId accountId, string owner) => eventLog.Append<AccountEventSource>(accountId, new AccountOpened(owner));}The concurrency guard then comes from the event source’s own dimensions, because no stream supplies others.
Append several events
Section titled “Append several events”AppendMany<TSource> writes a list of events for one event source id, to one stream and one stream id, in one atomic append.
using Cratis.Chronicle.EventSequences;
public class MonthlyPosting(IEventLog eventLog){ // Every event goes to the same event source id, stream and stream id. public Task<AppendManyResult> Post(EventSourceId accountId, string month, decimal deposit, decimal withdrawal) => eventLog.AppendMany<AccountEventSource>( accountId, [new MoneyDeposited(deposit), new MoneyWithdrawn(withdrawal)], eventStream: "Transactions", eventStreamId: month);}Append for several event sources
Section titled “Append for several event sources”To write events for several event sources in one atomic append, give each EventForEventSourceId the definition it goes through with EventSource, and its stream with EventStream and EventStreamId.
using Cratis.Chronicle.EventSequences;
public class Transfers(IEventLog eventLog){ // Two event sources, one atomic append: both events are written, or neither is. public Task<AppendManyResult> Transfer(EventSourceId from, EventSourceId to, decimal amount, string month) => eventLog.AppendMany( [ new EventForEventSourceId(from, new MoneyWithdrawn(amount)) { EventSource = typeof(AccountEventSource), EventStream = "Transactions", EventStreamId = month }, new EventForEventSourceId(to, new MoneyDeposited(amount)) { EventSource = typeof(AccountEventSource), EventStream = "Transactions", EventStreamId = month } ]);}Every event has exactly one event source. The append is all or nothing: if either event fails a constraint or a concurrency check, neither is written. Events in one batch can go through different definitions, and some can skip definitions altogether.
Each event source id in the batch is guarded by one concurrency scope. When events for the same id need different guards, Chronicle refuses the batch. See Concurrency.
Other overloads
Section titled “Other overloads”| API | Use |
|---|---|
Append(Type eventSource, …) and AppendMany(Type eventSource, …) | When the definition is known only at run time |
AppendPrepared<TSource> | When you prepared the event content on the same sequence |
EventForEventSourceId.EventSource and .EventStream | Batches across event sources |
The unit of work’s Transactional sequence has no typed overloads. To stage an event with the unit of work, pass the routing values yourself. They must equal what the definition declares.
What Chronicle rejects
Section titled “What Chronicle rejects”| Problem | What happens | When |
|---|---|---|
| The type is not a discovered definition | UnknownEventSource is thrown | Before anything is sent |
| The stream is not one the definition declares | EventStreamDoesNotBelongToEventSource is thrown | Before anything is sent |
An EventForEventSourceId carries an explicit EventSourceType or EventStreamType that contradicts the definition | EventRoutingContradictsEventSource is thrown | Before anything is sent |
| The kernel does not know the definition, the stream, or the event source type | The result carries an error that starts with UnknownEventSource, UnknownEventStreamForEventSource or EventSourceTypeDoesNotMatchEventSource | At append, for example when the definition was not registered yet |
| A constraint or a concurrency scope fails | IsSuccess is false and the result lists the violations | At append |
using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.EventSources;
public class CarefulDeposits(IEventLog eventLog){ public async Task<string> Deposit(EventSourceId accountId, decimal amount, string stream, string month) { try { var result = await eventLog.Append<AccountEventSource>( accountId, new MoneyDeposited(amount), eventStream: stream, eventStreamId: month);
return result.IsSuccess ? "Deposited" : "Rejected by the kernel"; } catch (EventStreamDoesNotBelongToEventSource) { // The stream is not one of the streams the definition declares. Nothing was sent. return $"'{stream}' is not a stream of the Account event source"; } catch (UnknownEventSource) { // The type is not a discovered definition. Nothing was sent. return "AccountEventSource is not discovered"; } }}Read the result. A constraint violation or a concurrency violation does not throw.
Next steps
Section titled “Next steps”- Concurrency: choose what an append conflicts with.
- Scenarios: complete examples.
- Dynamic Consistency Boundary: a guarded decision that appends through a definition. The wider picture is in Dynamic Consistency Boundary in Chronicle.
- Appending events