Skip to content

Observing Append Operations

IEventSequence exposes an AppendOperations observable that emits after every Append or AppendMany call on that sequence. Any code can subscribe to this observable to react to appended events in real time — without polling the event log.

using Cratis.Chronicle.EventSequences;
public interface IObservingAppendsEventSequence
{
IObservable<IEnumerable<AppendedEventWithResult>> AppendOperations { get; }
}

View C# snippet source on GitHub

The observable emits a collection of AppendedEventWithResult after each operation:

  • A single-event Append emits a collection containing one element.
  • A batch AppendMany emits the full batch as one collection.

Each AppendedEventWithResult pairs the appended event with its result:

MemberTypeDescription
EventAppendedEventThe appended event, including context and deserialized content
Event.ContentobjectThe deserialized event object
Event.ContextEventContextMetadata: event source, sequence number, correlation ID, causation chain
ResultAppendResultSuccess flag, sequence number, violations, and errors

Subscribers receive the notification after the operation has completed, whether it succeeded or failed. This observable does not fire for transactional appends through ITransactionalEventSequence.

Inject IEventSequence (or IEventLog) and subscribe to AppendOperations:

using Cratis.Chronicle.EventSequences;
public class ObservingAppendsMonitor(IEventLog eventLog) : IDisposable
{
readonly IDisposable _subscription = eventLog.AppendOperations.Subscribe(OnAppended);
static void OnAppended(IEnumerable<AppendedEventWithResult> operations)
{
foreach (var item in operations)
{
Console.WriteLine($"Event {item.Event.Content.GetType().Name} appended: success={item.Result.IsSuccess}");
}
}
public void Dispose() => _subscription.Dispose();
}

View C# snippet source on GitHub

Always dispose the subscription when you no longer need it to avoid resource leaks.

The most common use of AppendOperations in application code is through the IEventAppendCollection helper provided by Cratis.Chronicle.XUnit.Integration. It subscribes internally and provides a ready-to-assert collection of AppendedEventWithResult entries. See Chronicle.Testing.EventAppendCollection for full details.

Waiting for Observer Completion After Append

Section titled “Waiting for Observer Completion After Append”

When you need to wait until observers affected by an append operation have completed, use WaitForCompletion() from the Cratis.Chronicle.Observation namespace.

With the current .NET client and server, the wait includes observers that handle the appended event types, plus observers subscribed to all events. For AppendMany, each observer waits only through its last matching event in the batch. An observer whose tag, event source type, or event stream type filters exclude the appended events does not hold up the wait. If an affected observer fails while catching up, the result includes failed partitions. Check the append result before you wait: an append that failed never reached the log, so there is nothing to wait for and the wait reports success.

Only clients sending the first appended sequence number get filter-aware waits. The current .NET client sends it, including when the batch starts at sequence zero. TypeScript, Elixir, Kotlin, Python, and older .NET clients do not; filtered observers can hold up their waits until the timeout. A batch larger than the server’s bounded scan also falls back to waiting through its tail.

The wait gives up after five seconds unless you pass a longer timeout. A timeout returns a result with IsSuccess false and TimedOut true instead of throwing. Newer servers name the observers still behind in OutstandingObservers. TypeScript, Elixir, Kotlin, and Python clients also send no event types and consider every observer on the sequence rather than narrowing by event type. An older server ignores the new fields, so a newer .NET client can still time out on a filtered observer or a mixed-type batch there.

Waiting only means something when there are observers behind the connection. The in-process testing surfaces (EventScenario and EventStoreForTesting) run the event sequence without a silo, so an append there never reaches a projection, reducer, or reactor. Rather than report that everything completed, WaitForCompletion() throws CannotWaitForObserverCompletion on an append result that carries no observer surface — assert on the scenario’s own surface, or move the check to an out-of-process integration spec where observers genuinely run.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Observation;
[EventType]
public record ObservingAppendsSomeEvent(string Data);
public class ObservingAppendsCompletionWaiter(IEventLog eventLog)
{
public async Task AppendAndWait(EventSourceId eventSourceId)
{
var appendResult = await eventLog.Append(eventSourceId, new ObservingAppendsSomeEvent("example"));
// A failed append has nothing to wait for, and waiting on it reports success.
if (!appendResult.IsSuccess)
{
return;
}
var completion = await appendResult.WaitForCompletion();
if (!completion.IsSuccess)
{
foreach (var failedPartition in completion.FailedPartitions)
{
Console.WriteLine($"Observer {failedPartition.ObserverId} failed partition {failedPartition.Partition}");
}
}
}
}

View C# snippet source on GitHub

using Cratis.Chronicle.Events;
using Cratis.Chronicle.EventSequences;
using Cratis.Chronicle.Observation;
[EventType]
public record ObservingAppendsFirstEvent(string Data);
[EventType]
public record ObservingAppendsSecondEvent(string Data);
public class ObservingAppendsBatchCompletionWaiter(IEventLog eventLog)
{
public async Task AppendManyAndWait(EventSourceId eventSourceId)
{
var appendManyResult = await eventLog.AppendMany(eventSourceId, new object[]
{
new ObservingAppendsFirstEvent("first"),
new ObservingAppendsSecondEvent("second")
});
// A failed append has nothing to wait for, and waiting on it reports success.
if (!appendManyResult.IsSuccess)
{
return;
}
var completion = await appendManyResult.WaitForCompletion();
if (!completion.IsSuccess)
{
// Inspect failed partitions from affected observers
}
}
}

View C# snippet source on GitHub