Skip to content

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

View C# snippet source on GitHub

Chronicle fills in the routing from the definition:

Value on the eventTaken from
Event source typeThe definition’s name, Account here
Event stream typeThe stream name you pass, or the default (All) when you pass none
Event stream idThe stream id you pass, or the default when you pass none
EventContext.EventSourceThe 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.

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

View C# snippet source on GitHub

The concurrency guard then comes from the event source’s own dimensions, because no stream supplies others.

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

View C# snippet source on GitHub

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
}
]);
}

View C# snippet source on GitHub

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.

APIUse
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 .EventStreamBatches 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.

ProblemWhat happensWhen
The type is not a discovered definitionUnknownEventSource is thrownBefore anything is sent
The stream is not one the definition declaresEventStreamDoesNotBelongToEventSource is thrownBefore anything is sent
An EventForEventSourceId carries an explicit EventSourceType or EventStreamType that contradicts the definitionEventRoutingContradictsEventSource is thrownBefore anything is sent
The kernel does not know the definition, the stream, or the event source typeThe result carries an error that starts with UnknownEventSource, UnknownEventStreamForEventSource or EventSourceTypeDoesNotMatchEventSourceAt append, for example when the definition was not registered yet
A constraint or a concurrency scope failsIsSuccess is false and the result lists the violationsAt 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";
}
}
}

View C# snippet source on GitHub

Read the result. A constraint violation or a concurrency violation does not throw.