Skip to content

The journaled grain API

Derive an event-sourced grain from JournaledGrain<T, U>:

public sealed class AccountGrain
: JournaledGrain<AccountState, AccountEvent>, IAccountGrain
{
}

TGrainState must be a class with a public parameterless constructor. TEventBase is the common class or interface for the grain’s events. State and event types must be serializable because providers can persist or send them.

The one-parameter JournaledGrain<T> form uses object as the event base type.

Don’t mutate State or TentativeState directly. Change state by raising events.

By default, Orleans dynamically invokes the closest Apply overload on the state:

[GenerateSerializer]
public sealed class AccountState
{
[Id(0)]
public decimal Balance { get; private set; }
public void Apply(Deposited deposited) =>
Balance += deposited.Amount;
public void Apply(Withdrawn withdrawn) =>
Balance -= withdrawn.Amount;
}

Alternatively, override TransitionState. Transition logic must be deterministic and must only mutate the supplied state. Providers can replay transitions more than once, so don’t perform I/O or other side effects from transition methods.

RaiseEvent submits an event but doesn’t wait for durable confirmation:

RaiseEvent(new Deposited(amount));
await ConfirmEvents();

Await ConfirmEvents before returning when the grain method promises that its events are confirmed. If confirmation isn’t awaited, Orleans continues confirmation in the background and callers can observe tentative behavior.

Submit a related sequence atomically with RaiseEvents:

RaiseEvents(events);
await ConfirmEvents();

The provider submits the sequence as one log append. The confirmed version advances by the number of events.

Use RaiseConditionalEvent or RaiseConditionalEvents when an event is valid only against the version currently observed:

if (!await RaiseConditionalEvent(new Withdrawn(amount)))
{
return false;
}

The returned task completes after the conditional append is resolved. false means another update won the version race and the event wasn’t appended. Re-evaluate the command using the refreshed state; don’t treat a conflict as success.

RefreshNow confirms submitted events and refreshes the view from storage:

await RefreshNow();

RetrieveConfirmedEvents returns a confirmed segment only when the provider retains and exposes it. State storage and custom storage don’t expose events through this API; log storage does.

ClearLogAsync resets state and discards confirmed and unconfirmed events only when supported by the provider. Clearing a log is destructive and isn’t a schema-migration mechanism.

For replay-based storage, historical events remain part of the durable contract. Keep their serialized shape readable and preserve transition behavior, or introduce explicit upcasting/migration in custom storage. Snapshot storage instead requires the stored state snapshot to remain readable. Test both activation and replay using production-shaped historical data.