-
Decide whether Orleans fits. Use it when the system has many loosely coupled interactive entities that can each stay small and single-threaded. Do not force Orleans onto shared-memory workloads, long batch jobs, or systems dominated by constant global coordination.
-
Model grain boundaries around business identity. Prefer one grain per user, cart, device, room, order, or other durable entity. Never create unique grains per request — use [StatelessWorker] for stateless fan-out. Grain identity types:
IGrainWithGuidKey — globally unique entities
IGrainWithIntegerKey — relational DB integration
IGrainWithStringKey — flexible string keys
IGrainWithGuidCompoundKey / IGrainWithIntegerCompoundKey — composite identity with extension string
-
Design coarse-grained async APIs. All grain interface methods must return Task, Task<T>, or ValueTask<T>. Use IAsyncEnumerable<T> for streaming responses. Avoid .Result, .Wait(), blocking I/O, lock-based coordination. Use Task.WhenAll for parallel cross-grain calls. Apply [ResponseTimeout("00:00:05")] on interface methods when needed.
-
Choose the right state pattern:
IPersistentState<TState> with [PersistentState("name", "provider")] for named persistent state (preferred)
- Multiple named states per grain for different storage providers
JournaledGrain<TState, TEvent> for event-sourced grains
ITransactionalState<TState> for ACID transactions across grains
Grain<TState> is legacy — use only when constrained by existing code
-
Pick the right runtime primitive deliberately:
- Standard grains for stateful request/response logic
[StatelessWorker] for pure stateless fan-out or compute helpers
- Orleans streams for decoupled event flow and pub/sub with
[ImplicitStreamSubscription]
- Broadcast channels for fire-and-forget fan-out with
[ImplicitChannelSubscription]
RegisterGrainTimer for activation-local periodic work (non-durable)
- Reminders via
IRemindable for durable low-frequency wakeups
- Observers via
IGrainObserver and ObserverManager<T> for one-way push notifications
-
Configure serialization correctly:
[GenerateSerializer] on all state and message types
[Id(N)] on each serialized member for stable identification
[Alias("name")] for safe type renaming
[Immutable] to skip copy overhead on immutable types
- Use surrogates (
IConverter<TOriginal, TSurrogate>) for types you don't own
-
Handle reentrancy and scheduling deliberately:
- Default is non-reentrant single-threaded execution (safe but deadlock-prone with circular calls)
[Reentrant] on grain class for full interleaving
[AlwaysInterleave] on interface method for specific method interleaving
[ReadOnly] for concurrent read-only methods
RequestContext.AllowCallChainReentrancy() for scoped reentrancy
- Native
CancellationToken support (last parameter, optional default)
-
Choose hosting intentionally.
UseOrleans for silos, UseOrleansClient for separate clients
- Co-hosted client runs in same process (reduced latency, no extra serialization)
- In Aspire, declare Orleans resource in AppHost, wire clustering/storage/reminders there, use
.AsClient() for frontend-only consumers
- In Aspire-backed tests, resolve Orleans backing-resource connection strings from the distributed app and feed them into the test host instead of duplicating local settings
- Prefer
TokenCredential with DefaultAzureCredential for Azure-backed providers
-
Configure providers with production realism.
- In-memory storage, reminders, and stream providers are dev/test only
- Persistence: Redis, Azure Table/Blob, Cosmos DB, ADO.NET, DynamoDB
- Reminders: Azure Table, Redis, Cosmos DB, ADO.NET
- Clustering: Azure Table, Redis, Cosmos DB, ADO.NET, Consul, Kubernetes
- Streams: Azure Event Hubs, Azure Queue, Memory (dev only)
-
Treat placement as an optimization tool, not a default to cargo-cult.
ResourceOptimizedPlacement is default since 9.2 (CPU, memory, activation count weighted)
RandomPlacement, PreferLocalPlacement, HashBasedPlacement, ActivationCountBasedPlacement
SiloRoleBasedPlacement for role-targeted placement
- Custom placement via
IPlacementDirector + PlacementStrategy + PlacementAttribute
- Placement filtering (9.0+) for zone-aware and hardware-affinity placement
- Activation repartitioning and rebalancing are experimental
-
Make the cluster observable.
- Standard
Microsoft.Extensions.Logging
System.Diagnostics.Metrics with meter "Microsoft.Orleans"
- OpenTelemetry export via
AddOtlpExporter + AddMeter("Microsoft.Orleans")
- Distributed tracing via
AddActivityPropagation() with sources "Microsoft.Orleans.Runtime" and "Microsoft.Orleans.Application"
- Orleans Dashboard for operational visibility (secure with ASP.NET Core auth)
- Health checks for cluster readiness
-
Test the cluster behavior you actually depend on.
Open only what you need. Each reference is topic-focused for token economy: