Skip to content

Close a stream of a definition

Use this page to end one stream of a definition, such as one month of an account, and keep the rest of the event source open. The mechanism, its guarantees and its limits are the general ones in Closing streams. This page shows how to aim it at a definition’s streams.

A closure is a scope made of up to four values: event source id, event source type, event stream type and event stream id. A definition supplies the names for three of them. The closure refuses an append when every value the closure sets equals the append’s value. Closing the Transactions stream with id 2026-04 of account A refuses only that account’s April transactions.

When a domain fact ends the stream, declare it with [ClosesStream] on the event type. The kernel writes the event and the closure together. By default the closed scope is the appended event’s event source id, stream type and stream id, so append the closing event through the definition, to the stream it ends:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Events.Constraints;
using Cratis.Chronicle.EventSequences;
[EventType]
public record MoneyDeposited(decimal Amount);
// Appending this event closes the scope it is appended to: its event source id, stream type and stream id.
[EventType]
[ClosesStream]
public record MonthClosed(decimal ClosingBalance);
// AccountEventSource is the definition from the previous example.
public class MonthEndWithAnEvent(IEventLog eventLog)
{
public async Task<bool> CloseAndTryAgain(EventSourceId accountId, string month, decimal balance)
{
await eventLog.Append<AccountEventSource>(
accountId,
new MonthClosed(balance),
eventStream: "Transactions",
eventStreamId: month);
var late = await eventLog.Append<AccountEventSource>(
accountId,
new MoneyDeposited(10m),
eventStream: "Transactions",
eventStreamId: month);
// The late deposit is refused. The next month's stream is unaffected.
return late.ConstraintViolations.Any(violation => violation.ConstraintType == ConstraintType.StreamClosed);
}
}

View C# snippet source on GitHub

The closing event is accepted. Every later append to the same account’s 2026-04 stream is refused with a StreamClosed constraint violation. Other months and other accounts are unaffected. The declaration options, such as ReopenedBy and EventStreamIdFrom, are in Closing events.

For an ad-hoc close that no domain event represents, build a ClosedStreamScope and call CompleteStream. Take the names from the definition so that they cannot drift from what the appends write:

using Cratis.Chronicle;
using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.EventSources;
[EventSource("Account")]
[EventStream("Transactions")]
public class AccountEventSource : IEventSource;
public class MonthEnd(IEventStore eventStore)
{
public async Task<bool> CloseMonth(EventSourceId accountId, string month)
{
// Take the names from the definition, so a rename cannot leave a closure behind with the old name.
var definition = eventStore.EventSources.GetFor(typeof(AccountEventSource));
var transactions = definition.FindStream("Transactions")!;
var scope = new ClosedStreamScope(
EventSourceId: accountId,
EventSourceType: definition.EventSourceType,
EventStreamType: transactions.EventStreamType,
EventStreamId: month);
var closed = false;
var result = await eventStore.EventLog.CompleteStream(scope);
result.Switch(
_ => closed = true,
error => closed = error == CompleteStreamError.AlreadyCompleted);
return closed;
}
}

View C# snippet source on GitHub

EventSourceType here is the definition’s name. Leave it out to close the stream for every event source type that uses the same stream type and stream id. Add expectedTailSequenceNumber to close only if nothing was appended since the last fact you know of, as described in Scoped closing.

  • There is no typed CompleteStream<TSource>. You build the scope from the definition’s values.
  • Closing a stream does not touch the definition. A closure is stored by value, so renaming a stream later leaves the old closure on the old name. See Questions and pitfalls.
  • A scope that is only the default stream, with no event source id, is refused with DefaultStreamCannotBeCompleted. Appends without a stream go to the default stream, so close the whole event source by id if you want those closed.