Grain persistence
Orleans grain persistence stores application state independently of a grain activation. When an activation starts, Orleans reads its configured state records before calling OnActivateAsync. The grain explicitly writes changes when the operation’s durability point is reached.
Persistence is intentionally a record-oriented abstraction, not an object-relational mapper. A grain can use multiple named state records, use different providers for different records, or access a database directly when it needs queries or data models that don’t fit grain storage.
Choose a provider
Section titled “Choose a provider”Officially maintained providers are available from NuGet and include:
| Provider | Package | Typical use |
|---|---|---|
| Azure Table and Blob Storage | Microsoft.Orleans.Persistence.AzureStorage | Azure-hosted state records |
| Azure Cosmos DB for NoSQL | Microsoft.Orleans.Persistence.Cosmos | Globally distributed Azure NoSQL storage |
| Amazon DynamoDB | Microsoft.Orleans.Persistence.DynamoDB | AWS-hosted key-value storage |
| Google Cloud Firestore | Microsoft.Orleans.Persistence.Firestore | Google Cloud-hosted document storage |
| ADO.NET | Microsoft.Orleans.Persistence.AdoNet | SQL Server, MySQL/MariaDB, PostgreSQL, Oracle, and SQLite |
| Redis | Microsoft.Orleans.Persistence.Redis | Low-latency state backed by Redis |
| Memory | Microsoft.Orleans.Persistence.Memory | Tests and disposable development environments |
Choose based on durability, availability, latency, record-size limits, operational tooling, and cost. Memory storage isn’t durable across cluster restarts. Redis expiration should only be used for state that is intentionally ephemeral.
Use persistent state
Section titled “Use persistent state”Inject IPersistentState<T> into the grain constructor and identify both the state record and provider with PersistentStateAttribute:
public class UserGrain : Grain, IUserGrain{ private readonly IPersistentState<ProfileState> _profile; private readonly IPersistentState<CartState> _cart;
public UserGrain( [PersistentState("profile", "profileStore")] IPersistentState<ProfileState> profile, [PersistentState("cart", "cartStore")] IPersistentState<CartState> cart) { _profile = profile; _cart = cart; }
public Task<string> GetNameAsync() => Task.FromResult(_profile.State.Name);
public async Task SetNameAsync(string name) { _profile.State.Name = name; await _profile.WriteStateAsync(); }}The state isn’t available inside the constructor. Orleans loads it before OnActivateAsync. From then on:
- State contains the in-memory value.
- RecordExists indicates whether the provider read an existing record.
- Etag contains the provider’s concurrency token, when supported.
- ReadStateAsync replaces the in-memory value with the latest stored value.
- WriteStateAsync persists the current value.
- ClearStateAsync clears or deletes the record according to provider configuration.
Each operation also has a CancellationToken overload. A provider can implement cancellation, but the default interface implementation delegates to the overload without a token.
Configure named state
Section titled “Configure named state”Configure every provider name referenced by [PersistentState] on the silo:
var tableEndpoint = new Uri(configuration["AZURE_TABLE_STORAGE_ENDPOINT"]!);var blobEndpoint = new Uri(configuration["AZURE_BLOB_STORAGE_ENDPOINT"]!);var credential = new DefaultAzureCredential();
var builder = Host.CreateApplicationBuilder();builder.UseOrleans(siloBuilder =>{ siloBuilder.AddAzureTableGrainStorage( name: "profileStore", configureOptions: options => { options.TableServiceClient = new TableServiceClient(tableEndpoint, credential); }) .AddAzureBlobGrainStorage( name: "cartStore", configureOptions: options => { options.BlobServiceClient = new BlobServiceClient(blobEndpoint, credential); });});
using var host = builder.Build();The state name distinguishes records owned by the same grain. The provider name selects a keyed IGrainStorage registration. Different records aren’t required to share a provider or backing store.
When [PersistentState] omits the storage name, Orleans resolves the default IGrainStorage registration instead. Configure that registration with the provider’s Add*GrainStorageAsDefault extension, or pass DEFAULT_STORAGE_PROVIDER_NAME ("Default") to the corresponding Add*GrainStorage extension. Any other name only adds that named provider; it doesn’t configure a default.
Default and named registrations represent separate roles even when they use the same provider type and backing database. For example, grain-based stream pub/sub conventionally uses the named provider PubSubStore, independently of the default provider used by grain state. Register both roles when a silo needs both:
Action<AdoNetGrainStorageOptions> configureStorage = options =>{ options.Invariant = "Npgsql"; options.ConnectionString = connectionString;};
siloBuilder .AddAdoNetGrainStorageAsDefault(configureStorage) .AddAdoNetGrainStorage("PubSubStore", configureStorage);The default and PubSubStore registrations can share configuration, but neither role aliases the other. For details about the streaming role, see Configure PubSub storage.
Consistency and atomicity
Section titled “Consistency and atomicity”A storage operation applies to one state record. Providers use the record’s Etag for optimistic concurrency where the backend supports it. A write or clear with a stale ETag fails with InconsistentStateException rather than overwriting a newer value.
The following aren’t one atomic operation:
- Writes to two IPersistentState<T> instances on the same grain.
- Writes to state owned by different grains.
- A storage write and an external side effect, such as publishing a message.
If an operation requires atomic updates across multiple grain states, use Orleans transactions and transactional state. For storage plus messaging, design an application-level outbox, inbox, or idempotency protocol.
Failure semantics
Section titled “Failure semantics”Activation reads
Section titled “Activation reads”If the initial read fails, activation fails and Orleans doesn’t call OnActivateAsync. The request that caused activation receives the failure. A bad or missing provider configuration results in BadProviderConfigException.
Explicit reads, writes, and clears
Section titled “Explicit reads, writes, and clears”Storage failures fault the returned task. Await each operation so that failures reach the grain call and its caller. After a failed write, don’t assume that the stored value changed. The precise outcome depends on the provider and underlying service failure.
An InconsistentStateException means another writer changed the record since this activation last read it. Don’t blindly retry the same write with the stale state. Re-read, re-evaluate the command against the new state, and write only if the operation is still valid.
For transient service failures, retries belong at an application boundary that understands idempotency. Prefer retrying the original command with an operation identifier over retrying an arbitrary storage write. Bound retries, add backoff, and preserve the exception when the retry budget is exhausted. Orleans doesn’t automatically retry IPersistentState<T> operations for the application.
State and schema evolution
Section titled “State and schema evolution”Persistence outlives activations and deployments. Treat the stored representation as a versioned contract:
- Add members in a backward-compatible form and preserve defaults for missing data.
- Deploy readers that accept both old and new representations before writing only the new representation.
- Don’t rename, remove, or reinterpret stored members without a migration plan.
- Test deserialization using data written by the currently deployed version.
- Retain old event types and transition behavior when using event sourcing.
Storage providers expose IGrainStorageSerializer through their options. The default provider serializer uses JSON. A custom serializer can implement explicit envelopes, version fields, or migrations, but changing serializers doesn’t migrate existing records automatically.
Redis configuration has moved to Redis grain persistence.
Memory storage configuration has moved to Memory grain persistence.
Legacy grain state base class
Section titled “Legacy grain state base class”The Grain<T> base class and StorageProviderAttribute remain supported for compatibility, but new code should use IPersistentState<T>. Constructor injection supports multiple state records and makes the storage dependency explicit.
Implement a storage provider
Section titled “Implement a storage provider”Custom providers implement IGrainStorage. Register a named provider using Orleans’ storage registration helper, which uses .NET keyed services:
siloBuilder.Services.AddGrainStorage<MyGrainStorage>( "custom", (services, name) => new MyGrainStorage(name));Providers must:
- Populate State, RecordExists, and ETag when reading.
- Preserve optimistic-concurrency semantics and throw InconsistentStateException on an ETag conflict.
- Complete each returned task only when the storage operation has completed.
- Surface backend failures instead of returning success.
- Define and document whether ClearStateAsync deletes or resets a record.
