Skip to content

Event Processing

This guide explains how reactors discover methods, which signatures are supported, and how event context and errors are handled.

Reactors use convention-based method discovery. Chronicle finds and invokes public methods that:

  • Accept the event type as the first parameter
  • Optionally accept further dependency parameters (the EventContext, read models, or services)
  • Return void, Task, Task<T>, or a synchronous side-effect type (see below)

Event parameter types must be marked with [EventType].

using Cratis.Chronicle.Events;
public interface ReactorHandlerSignatures<TEvent, TResult>
{
void MethodName(TEvent @event);
void MethodName(TEvent @event, EventContext context);
Task MethodNameAsync(TEvent @event);
Task MethodNameAsync(TEvent @event, EventContext context);
Task<TResult> MethodNameReturningAsync(TEvent @event);
Task<TResult> MethodNameReturningAsync(TEvent @event, EventContext context);
TResult MethodNameReturning(TEvent @event);
TResult MethodNameReturning(TEvent @event, EventContext context);
}

TResult can be an event type or IEnumerable<TEvent>. See Returning Side Effects for the full list of supported return types and metadata resolution.

This overload table is specific to C#‘s method-overload-based dispatch. Kotlin, Elixir, and TypeScript each use one canonical handler shape instead of a family of overloads — see Getting started with reactors for what that shape looks like in each client. None of them auto-append a handler’s return value the way C# does (see Returning side effects).

The optional EventContext parameter provides metadata about the event, including the event source ID, sequence number, timestamps, and correlation identifiers.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
[EventType]
public record ReactorAccountClosed(Guid AccountId);
public class AuditReactor : IReactor
{
public void AccountClosed(ReactorAccountClosed @event, EventContext context)
{
WriteAudit(@event.AccountId, context.Occurred, context.EventSourceId);
}
void WriteAudit(Guid accountId, DateTimeOffset occurred, EventSourceId eventSourceId) { }
}

Beyond the event and EventContext, a handler method can take additional parameters that Chronicle resolves when the method runs. Only the first parameter is fixed; it is the event that drives dispatch.

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
public interface IReactorShippingService
{
Task Schedule(EventProcessingOrder order, decimal price);
}
public interface IReactorPricingService
{
decimal PriceFor(EventProcessingOrder order);
}
[EventType]
public record EventProcessingOrderPlaced;
public record EventProcessingOrder(string Id, decimal Total);
public class EventProcessingOrderProcessor(IReactorShippingService shipping) : IReactor
{
public async Task OrderPlaced(
EventProcessingOrderPlaced @event,
EventContext context,
EventProcessingOrder order,
IReactorPricingService pricing)
{
await shipping.Schedule(order, pricing.PriceFor(order));
}
}

This is currently a C#-only capability. Kotlin, Elixir, and TypeScript reactor handlers accept only the event and, optionally, the event context — there’s no mechanism to have Chronicle resolve and inject further parameters (services or strongly-consistent read models) into the handler call.

Each parameter after the event resolves as follows:

  • An EventContext parameter receives the event context; its position does not matter.
  • A read model — a type with a reducer or projection — is materialized on demand from that reducer or projection, making it strongly consistent when the reactor runs. Read models are resolved directly by Chronicle, never through the service provider.
  • Any other type is resolved from the service provider.

By default the read model is materialized using the EventSourceId from the event context. When the key differs, implement ICanResolveReadModelKey on the reactor to return the ReadModelKey to use:

using Cratis.Chronicle.Events;
using Cratis.Chronicle.Reactors;
using Cratis.Chronicle.ReadModels;
[EventType]
public record ReactorOrderLineAdded(string OrderId);
public record ReactorOrderLine(string Id, string ProductId, int Quantity);
public class OrderLineProcessingReactor : IReactor, ICanResolveReadModelKey
{
public ReadModelKey Resolve(object @event, EventContext context) =>
((ReactorOrderLineAdded)@event).OrderId;
public Task OrderLineAdded(ReactorOrderLineAdded @event, ReactorOrderLine order) =>
Task.CompletedTask;
}

The resolved key applies to every read model parameter across all of the reactor’s handler methods. This is also currently C#-only, since it exists to customize the read-model-parameter resolution described above.

Events are delivered per event source and in sequence order. Each reactor method is called for the specific event source that produced the event.

If a reactor method throws an exception, the failing event source partition is marked as failed and processing for that partition is paused. Once the underlying issue is resolved, processing resumes from the last successful event.

  • CHR0004 - Reactor method signatures
  • CHR0005 - Event parameters require [EventType]