Skip to content

Appending many

Appending many events in a single call is useful for batch workflows such as imports, migrations, or multi-step business operations.

Batch appends preserve ordering and reduce per-event overhead compared to appending each event individually.

AppendMany is transactional all the way to storage. Either all events in the batch are committed, or none are. Chronicle assigns sequence numbers in order, persists the events and metadata atomically, and updates the sequence state once the batch succeeds.

  • Each item in the batch pairs an event with the event source it belongs to.
  • In the .NET client, each EventForEventSourceId can supply its own Causation. The current ambient causation chain is kept first, followed by that event’s explicit cause. Events without an explicit cause keep only the ambient chain; they do not inherit another event’s cause.
  • Per-event causation requires an updated Chronicle kernel. Older kernels ignore the per-event field and store only the batch’s ambient causation chain.
  • The batch is validated as a unit and written atomically.
  • Sequence numbers are assigned in the order provided.
  • The event sequence state is updated once the batch commits.

AppendManyOnce appends events for one event source with a single IdempotencyKey. Chronicle derives a different key for each event from the batch key and the event’s zero-based position (IdempotencyKey.ForIndex), so the batch is keyed per event. Retrying the same batch with the same key and the same events in the same order writes nothing new. There is also AppendManyOnceWithNamedTags. See Appending for keys, outcomes, and conflicts.

using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Events;
[EventType]
public record ImportedItemAdded(string Sku, int Quantity);
public class CartImportService(IEventLog eventLog)
{
public async Task<bool> Import(string cartId, IEnumerable<ImportedItemAdded> items, string importId)
{
// One key for the batch. Chronicle derives a distinct key for each event from its position.
var result = await eventLog.AppendManyOnce(
cartId,
items.Cast<object>(),
new IdempotencyKey($"import:{importId}"));
if (result.HasIdempotencyConflicts)
{
return false;
}
// Outcomes has one entry per event, in the order the events were given.
return result.Outcomes.All(outcome => outcome == AppendOutcome.Appended);
}
}

View C# snippet source on GitHub

AppendManyResult.Outcomes has one AppendOutcome per event, in the order you gave them. IdempotencyConflicts and HasIdempotencyConflicts report refused keys. Because a batch is atomic, check them before assuming the batch was written. WasAlreadyAppended is true when every event was already in the sequence.

When a batch spans event sources, set IdempotencyKey on each EventForEventSourceId instead. If any event in the batch has a key, the batch is appended once. Events without a key are appended normally, so a mixed batch is valid and Outcomes reports Appended for them.

using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Events;
public readonly record struct KeyedAccountId(string Value)
{
public static implicit operator EventSourceId(KeyedAccountId id) => new(id.Value);
}
[EventType]
public record KeyedMoneyWithdrawn(decimal Amount);
[EventType]
public record KeyedMoneyDeposited(decimal Amount);
public class KeyedTransferService(IEventLog eventLog)
{
public Task<AppendManyResult> Transfer(string transferId, KeyedAccountId fromAccount, KeyedAccountId toAccount, decimal amount)
{
var events = new[]
{
new EventForEventSourceId(fromAccount, new KeyedMoneyWithdrawn(amount))
{
IdempotencyKey = new IdempotencyKey($"transfer:{transferId}:withdrawn")
},
new EventForEventSourceId(toAccount, new KeyedMoneyDeposited(amount))
{
IdempotencyKey = new IdempotencyKey($"transfer:{transferId}:deposited")
}
};
// Any keyed event makes the batch keyed; events without a key are appended normally.
return eventLog.AppendMany(events);
}
}

View C# snippet source on GitHub

Keyed batches follow the same rules as a single keyed append: an older kernel throws IdempotentAppendsNotSupported, and keys are refused inside a unit of work and with effect receipts.

Use this approach when you already have a set of events that should be appended together. Each event can still target different event sources, and you can include concurrency scopes to validate the boundary across streams. See Concurrency for details.

using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Events;
public readonly record struct AccountId(string Value)
{
public static implicit operator EventSourceId(AccountId id) => new(id.Value);
}
[EventType]
public record MoneyWithdrawn(decimal Amount);
[EventType]
public record MoneyDeposited(decimal Amount);
public class TransferService(IEventLog eventLog)
{
public Task<AppendManyResult> Transfer(AccountId fromAccount, AccountId toAccount, decimal amount)
{
var events = new[]
{
new EventForEventSourceId(fromAccount, new MoneyWithdrawn(amount)),
new EventForEventSourceId(toAccount, new MoneyDeposited(amount))
};
return eventLog.AppendMany(events);
}
}

View C# snippet source on GitHub