Closing events
A manual CompleteStream call writes nothing to the log, cannot be rebuilt from events and is a second step after the final append. A closing event removes all three problems. You declare that appending a particular event type closes a scope. The kernel then writes the event and the closure together, and the closure can be rebuilt from the log.
Use it when a domain fact ends something: ReportDelivered, BooksClosed, VersionSuperseded.
Declare a closing event
Section titled “Declare a closing event”With the attribute, the declaration sits on the event:
using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType][ClosesStream(Dimensions = ClosedStreamDimensions.EventSourceId | ClosedStreamDimensions.EventStreamType)]public record ClosingEventsReportDelivered(string Recipient);With the builder, the declaration is a constraint class:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ClosingEventsVersionSuperseded(string Version);
[EventType]public record ClosingEventsVersionReinstated(string Version);
public class ClosingEventsVersionClosing : IConstraint{ public void Define(IConstraintBuilder builder) => builder.ClosesStreamOn<ClosingEventsVersionSuperseded>( closes => closes .PerEventSourceId() .EventStreamIdFrom(@event => @event.Version) .ReopenedBy<ClosingEventsVersionReinstated>(), name: "close-superseded-version");}Both forms compile to the same constraint. Details of each style are in the model-bound and declarative reference pages.
Choose the scope
Section titled “Choose the scope”By default the closed scope is the appended event’s event source id, stream type and stream id, taken from the append. To close something else, choose dimensions. The attribute takes a Dimensions value and the builder has PerEventSourceId(), PerEventSourceType(), PerEventStreamType() and PerEventStreamId().
To take the stream id from the event’s payload instead of the append, use EventStreamIdFrom (attribute) or EventStreamIdFrom(@event => @event.Period) (builder). That implies the stream id dimension. This is useful when the closing fact names the period it closes while it is appended somewhere else:
using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType][ClosesStream(EventStreamIdFrom = nameof(Period), ReopenedBy = [typeof(ClosingEventsBooksReopened)])]public record ClosingEventsBooksClosed(string Period, decimal ClosingBalance);
[EventType]public record ClosingEventsBooksReopened(string Period, string Reason);A scope resolving to only default-stream values is refused for both closing and reopening events; it cannot close the default stream for every event source.
A property-sourced value must be present. If it is missing, null or empty, the kernel refuses the append with a violation naming the property rather than closing something broader. Make the property non-nullable. The analyzer CHR0012 warns on nullable event properties.
What happens on append
Section titled “What happens on append”The closing event is accepted. Every later event inside the closed scope is refused with StreamClosed, and events elsewhere are not affected:
using Cratis.Chronicle.EventSequences;using Cratis.Chronicle.Events;using Cratis.Chronicle.Events.Constraints;
[EventType]public record ClosingEventsEntryPosted(string Description);
public class ClosingEventsAfterClose(IEventLog eventLog){ public async Task<bool> Run(EventSourceId ledgerId) { await eventLog.Append( ledgerId, new ClosingEventsBooksClosed("2026-04", 1200m), new EventStreamType("ledger"), new EventStreamId("2026-04"));
var late = await eventLog.Append( ledgerId, new ClosingEventsEntryPosted("Late invoice"), new EventStreamType("ledger"), new EventStreamId("2026-04"));
// The late entry is refused; the next period is unaffected. return late.ConstraintViolations.Any(violation => violation.ConstraintType == ConstraintType.StreamClosed); }}Inside one AppendMany, events after the closing event that fall inside the scope are refused, and the batch result names them. A batch that closes, reopens and then appends a covered event succeeds for the covered event, because the batch is evaluated in order.
A closing event cannot be appended into a scope that is already closed. The closed-stream check applies to it like to any other event.
Reopen with an event
Section titled “Reopen with an event”Name the event types that lift the closure with ReopenedBy (attribute) or ReopenedBy<T>() (builder). The reopening fact records who reopened and why in the log, which is the reason to prefer it over a kernel call.
A ReopenedBy event:
- removes only the closure owned by its own constraint, for the exact scope it resolves to;
- is accepted inside the scope it reopens, because it is exempt from its own constraint’s closure;
- is not exempt from a manual closure or from another constraint’s closure. If an operator also closed the scope with
CompleteStream, the reopening event is refused; - must carry the same
EventStreamIdFromproperty as the closing event. The client throwsPropertyDoesNotExistOnEventTypewhen it discovers the constraint if the property is missing.
Ownership and names
Section titled “Ownership and names”Each closure belongs to its constraint. The constraint name defaults to the event type name. Set Name on the attribute or pass name to the builder to choose another. Two declarations that share a name merge into one constraint, with their closing and reopening event types combined. They must use the same dimensions and stream id property, otherwise the client throws ConflictingClosesStreamDeclarations. The empty name is reserved for manual closures.
Set EventSequences on the attribute to restrict a declaration to specific event sequences. Empty means all.
Not retroactive
Section titled “Not retroactive”Registering or changing a declaration closes nothing in history. Only events appended afterwards close scopes. A reindex that names the constraint rebuilds its closures from the log, see Closing streams for the guarantees, the crash window and the redaction limit. A closing-constraint rebuild must not run while appends to the sequence are in flight.
Test it
Section titled “Test it”EventScenario supports closing and reopening events. See EventScenario.