Skip to content

Use durable state

Install the pre-release Microsoft.Orleans.Journaling package in the silo project. Install a journal storage provider and configure it before activating grains which use durable state.

All Journaling APIs are experimental and carry diagnostic ORLEANSEXP005.

Inject IDurableStateManager into an ordinary Grain and declare named state components during construction. The manager creates each component once and Orleans recovers the grain’s state before application methods run:

public interface IShoppingCartGrain : IGrainWithStringKey
{
ValueTask AddItem(string itemId, int quantity, CancellationToken cancellationToken);
ValueTask<Dictionary<string, int>> GetItems(CancellationToken cancellationToken);
}
public sealed class ShoppingCartGrain(IDurableStateManager stateManager)
: Grain, IShoppingCartGrain
{
private readonly IDurableDictionary<string, int> _cart =
stateManager.GetOrAddDictionary<string, int>("cart");
public async ValueTask AddItem(string itemId, int quantity, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
_cart[itemId] = quantity;
await stateManager.WriteStateAsync(cancellationToken);
}
public ValueTask<Dictionary<string, int>> GetItems(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return new(_cart.ToDictionary());
}
}

The standard manager enrolls itself in the grain lifecycle when constructed with the activation’s IGrainContext, before resolution returns. Declare durable state components during construction or synchronous activation setup. Recovery at SetupState finishes before OnActivateAsync and request processing. The same composition works with an application-owned grain base class.

The dictionary mutation is immediately visible to the current activation. Awaiting WriteStateAsync establishes the durability point for pending mutations to the grain’s state. Accept a CancellationToken on grain operations and flow it through state-manager calls so cancellation follows the caller’s operation lifetime.

The composition example is compiled against repository source so it exercises constructor-owned lifecycle enrollment.

GetOrAddState accepts an application contract, such as IDurableDictionary<string, int>. A registered factory supplies the implementation. The DurableStateManagerExtensions helpers provide the same access with discoverable names.

Use keyed injection and the convenience base class

Section titled “Use keyed injection and the convenience base class”

DurableGrain supplies a StateManager property typed as IDurableStateManager and a protected WriteStateAsync forwarding helper. Inject a state component with FromKeyedServicesAttribute when its name is fixed:

public sealed class KeyedCounterGrain(
[FromKeyedServices("count")] IDurableValue<int> count)
: DurableGrain, IKeyedCounterGrain
{
public async ValueTask<int> Increment(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
count.Value++;
await WriteStateAsync(cancellationToken);
return count.Value;
}
}

Keyed injection of IDurableValue<int> with the key "count" and stateManager.GetOrAddValue<int>("count") return the same object within a manager, in either resolution order. Both construction paths use the manager’s registry and configured format. Choose the convenience base when its helpers fit the application; constructor-injected composition gives existing grain hierarchies the same standard recovery behavior.

State typeSetup-time helperIn-memory APIJournaled operations
IDurableValue<T>GetOrAddValueOne mutable valueSet
IDurableDictionary<T, U>GetOrAddDictionaryIDictionary<T, U>Set, remove, clear, snapshot
IDurableList<T>GetOrAddListIList<T> plus AddRangeAdd, insert, set, remove, clear, snapshot
IDurableQueue<T>GetOrAddQueueQueue operationsEnqueue, dequeue, clear, snapshot
IDurableSet<T>GetOrAddSetISet<T>Add, remove, clear, snapshot
IDurableTaskCompletionSource<T>GetOrAddTaskCompletionSourceDurable task completionComplete, fault, or cancel
IPersistentState<T>GetOrAddPersistentStateRecord-style stateSet or clear a versioned state value

All named state components registered with a manager share its pending journal and write acknowledgement boundary. A caller’s write can include changes staged by interleaved callers. Prepare fallible work before staging mutations and await WriteStateAsync before returning success. Cancelling the caller’s wait leaves an already queued write running to its storage outcome. A failed journal operation fences the manager; a fresh activation recovers the durable outcome. See Runtime behavior and consistency for failure handling and safe staging.

Coordination with another grain or an external service requires an application protocol such as idempotency, an inbox, or an outbox.

An IDurableTaskCompletionSource<T> changes status in memory when TrySetResult, TrySetException, or TrySetCanceled succeeds. Its Task completes after a write acknowledges that status or recovery replays it, allowing waiters to observe a durable completion.

The name supplied to the manager or keyed service identifies a durable state component across activations and deployments. Apply these rules:

  • Keep each name unique within the grain.
  • Preserve names when changing constructors or refactoring fields.
  • Keep JSON key, value, and record schemas backward readable during rolling upgrades.
  • Register every JSON payload type in the configured source-generated serializer context when trimming or using Native AOT.
  • Retain removed state component definitions through the retirement grace period when a rollback can reintroduce them.

Names use ordinal comparison. Repeated requests for a name and compatible application contract return the same instance. An incompatible contract or closed generic type for that name fails immediately. An unsupported application contract produces an explicit factory-registration error.

Declare all state components during construction or synchronous activation setup, before initialization begins. Use recovered state after initialization succeeds. Later GetOrAddState calls resolve existing components; a missing name fails immediately without changing the registry. Use TryGetState for lookup without creation. Put runtime-varying keys inside a declared durable dictionary rather than creating a new named component for each key.

A reusable feature can own durable state independently of the grain’s constructor and base class. Select the grain implementation once per grain type using GrainClassMap in an IConfigureGrainTypeComponents implementation, then register a shared setup action. This example selects the existing shopping-cart interface and records an activation count:

public sealed class CartActivationCounter(
IDurableStateManager stateManager,
[FromKeyedServices("activation-count")] IDurableValue<int> count)
: ILifecycleParticipant<IGrainLifecycle>
{
public void Participate(IGrainLifecycle lifecycle)
{
lifecycle.Subscribe<CartActivationCounter>(
GrainLifecycleStage.Activate - 1,
async cancellationToken =>
{
count.Value++;
await stateManager.WriteStateAsync(cancellationToken);
});
}
}
public sealed class CartFeatureConfigurator(GrainClassMap grainClasses)
: IConfigureGrainTypeComponents
{
public void Configure(
GrainType grainType,
GrainProperties properties,
GrainTypeSharedContext shared)
{
if (grainClasses.TryGetGrainClass(grainType, out var grainClass)
&& typeof(IShoppingCartGrain).IsAssignableFrom(grainClass))
{
shared.AddActivationSetup(static context =>
{
var feature = context.ActivationServices
.GetRequiredService<CartActivationCounter>();
feature.Participate(context.ObservableLifecycle);
});
}
}
}

Register the feature as scoped and the configurator as singleton:

siloBuilder.ConfigureServices(services =>
{
services.AddScoped<CartActivationCounter>();
services.AddSingleton<IConfigureGrainTypeComponents, CartFeatureConfigurator>();
});

The synchronous action resolves the feature from ActivationServices and enrolls its lifecycle participant. Resolving the feature’s dependencies constructs the standard state manager if needed, and that manager enrolls itself before resolution returns. A setup action can be the first place an activation resolves its manager or state. The feature uses a stage after SetupState and before Activate so it increments recovered state before application activation begins.

Keep one enrollment owner per participant. Setup actions are shared across concurrent activations; resolve activation-specific data from the supplied context and keep shared callbacks stateless. See Shared activation setup and Journaling activation and recovery for ordering and failure behavior.

Obtain journal-backed IPersistentState<T> through keyed injection or GetOrAddPersistentState<T>(name) during setup. Its familiar State, WriteStateAsync, and ClearStateAsync members write through the same journal manager as the durable collections. ReadStateAsync completes from the already-recovered in-memory state because activation setup replayed the grain journal.

Use a unique keyed service name exactly as you would for another durable state component. The ETag is the journal-backed state’s recovered version and RecordExists indicates whether a stored value is present.

Define a grain-facing state contract and an implementation of that contract and IStateMachine. Register the mapping on IServiceCollection with AddStateMachine using the application contract and implementation as its two type arguments. Both types are reference types. In silo configuration, call siloBuilder.AddJournaling() for core setup and siloBuilder.Services.AddStateMachine<TState, TImplementation>() for the grain state component mapping. A storage-provider registration already performs the core setup. Grain code obtains the component with GetOrAddState<TState>(name) during setup. The manager constructs and registers the implementation once using the existing activation scope, then binds its journal stream.

The parameterless registration overload resolves the implementation’s constructor dependencies from dependency injection. When construction needs the state name, use the factory overload: its callback receives the owning service provider and the requested state name and returns the implementation.

The state-machine protocol owns operation encoding, snapshots, replay, and volatile bookkeeping:

MemberResponsibility
ResetReset in-memory state and bind the supplied journal stream writer.
ReplayEntryApply a recorded operation during recovery.
WritePendingEntriesEmit pending operations into the supplied writer.
WriteSnapshotEmit the state needed to reconstruct the current contents.
OnRecoveryCompletedFinish reconstruction before application use.
OnWriteCompletedPublish effects which depend on storage acknowledgement.

An implementation runs on one logical grain thread. Recovery uses fresh instances and replay. After a journal operation fails, the manager remains fenced and its owner creates a new manager and state instances.

IJournaledStateManager is the journal-owner contract, independent of the grain-facing IDurableStateManager. It provides RegisterStateMachine, TryGetStateMachine, InitializeAsync, WriteStateAsync, whole-journal DeleteStateAsync, asynchronous disposal, and PendingWriteByteCount diagnostics.

Use CreateStandalone when an integration owns a journal independently of a grain activation. It returns the owner contract. Construct state machine components with caller-supplied dependencies and register them before initialization. Await recovery before using their contents, and await writes before reporting durable changes:

public static async ValueTask<int> Increment(
IJournaledStateManagerFactory factory,
JournalId journalId,
IDurableValueCommandCodec<int> codec,
CancellationToken cancellationToken)
{
await using var stateManager = factory.CreateStandalone(journalId);
var component = new CounterState(codec);
stateManager.RegisterStateMachine("count", component);
await stateManager.InitializeAsync(cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
component.Value++;
await stateManager.WriteStateAsync(cancellationToken);
return component.Value;
}

The example explicitly constructs a counter component implementing IStateMachine and IDurableValueCommandHandler<T>. Its supplied IDurableValueCommandCodec<T> must correspond to the journal’s configured write format. The component applies replayed set commands and emits its current value for pending writes and snapshots.

The caller owns the manually supplied state components and their dependencies, including any scopes used to construct them. Disposing the journal owner stops processing and releases journal resources; the caller arranges component and dependency disposal. The standalone owner never creates or disposes DI scopes. Use TryGetStateMachine to find a component registered with that owner.

For grains, the default manager implements both independent interfaces on the same activation-scoped object. IDurableStateManager supplies GetOrAdd, typed lookup, and writes; IJournaledStateManager supplies journal ownership operations. The runtime-owned activation scope controls the lifetime of DI-created state components, their dependencies, and the manager.