
Orleans
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with ai & agent building tasks.
About
orleans is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- orleans
- AI & Agent Building
- AI-coding skill
Orleans by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #10,886 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill orleansAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Microsoft Orleans
Trigger On
- building or reviewing
.NETcode that usesMicrosoft.Orleans.*,Grain,IGrainWith*,UseOrleans,UseOrleansClient,IGrainFactory,JournaledGrain,ITransactionalState, or Orleans silo/client builders - testing Orleans code with
InProcessTestCluster,Aspire.Hosting.Testing,WebApplicationFactory, or shared AppHost fixtures - modeling high-cardinality stateful entities such as users, carts, devices, rooms, orders, digital twins, sessions, or collaborative documents
- choosing between grains, streams, broadcast channels, reminders, stateless workers, persistence providers, placement strategies, transactions, event sourcing, and external client/frontend topologies
- deploying or operating Orleans with Redis, Azure Storage, Cosmos DB, ADO.NET, .NET Aspire, Kubernetes, Azure Container Apps, or built-in/dashboard observability
- designing grain serialization contracts, versioning grain interfaces, configuring custom placement, or implementing grain call filters and interceptors
Workflow
1. 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.
2. 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 entitiesIGrainWithIntegerKey— relational DB integrationIGrainWithStringKey— flexible string keysIGrainWithGuidCompoundKey/IGrainWithIntegerCompoundKey— composite identity with extension string
3. 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.
4. 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 grainsITransactionalState<TState>for ACID transactions across grainsGrain<TState>is legacy — use only when constrained by existing code
5. 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] RegisterGrainTimerfor activation-local periodic work (non-durable)- Reminders via
IRemindablefor durable low-frequency wakeups - Observers via
IGrainObserverandObserverManager<T>for one-way push notifications
6. 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
7. 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 methodsRequestContext.AllowCallChainReentrancy()for scoped reentrancy- Native
CancellationTokensupport (last parameter, optional default)
8. Choose hosting intentionally.
UseOrleansfor silos,UseOrleansClientfor 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
TokenCredentialwithDefaultAzureCredentialfor Azure-backed providers
9. 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)
10. Treat placement as an optimization tool, not a default to cargo-cult.
ResourceOptimizedPlacementis default since 9.2 (CPU, memory, activation count weighted)RandomPlacement,PreferLocalPlacement,HashBasedPlacement,ActivationCountBasedPlacementSiloRoleBasedPlacementfor 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
11. Make the cluster observable.
- Standard
Microsoft.Extensions.Logging System.Diagnostics.Metricswith 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
12. Apply 10.2 upgrade checks deliberately.
Orleans.Journalingnow defaults to JSON Lines for new writes; preserve the old binary format explicitly only when compatibility requires it.- Code that referenced the static Orleans metrics meter should resolve
OrleansInstrumentsfrom DI and use itsMeter. - Initial silo connectivity is validated before
BecomeActive, so startup diagnostics should distinguishJoiningfrom active workload readiness. - Redis providers that receive a DI-owned
IConnectionMultiplexerno longer dispose that shared multiplexer on shutdown; keep ownership assumptions explicit.
13. Test the cluster behavior you actually depend on.
InProcessTestClusterfor new tests- Shared Aspire/AppHost fixtures for real HTTP, SignalR, SSE, or UI flows that must exercise the co-hosted Orleans topology
WebApplicationFactory<TEntryPoint>layered over a shared AppHost when tests need Host DI services,IGrainFactory, or direct grain/runtime access while keeping real infrastructure- Multi-silo coverage when placement, reminders, persistence, or failover matters
- Benchmark hot grains before claiming the design scales
- Use memory providers in test, real providers in integration tests
Architecture
flowchart LR
A["Distributed requirement"] --> B{"Many independent<br/>interactive entities?"}
B -->|No| C["Plain service / worker / ASP.NET Core"]
B -->|Yes| D["Model one grain per business identity"]
D --> E{"State pattern?"}
E -->|"Persistent"| F["IPersistentState<T>"]
E -->|"Event-sourced"| F2["JournaledGrain<S,E>"]
E -->|"Transactional"| F3["ITransactionalState<T>"]
E -->|"In-memory only"| G["Activation state"]
D --> H{"Communication?"}
H -->|"Pub/sub"| I["Orleans streams"]
H -->|"Broadcast"| I2["Broadcast channels"]
H -->|"Push to client"| I3["Observers"]
H -->|"Request/response"| I4["Direct grain calls"]
D --> J{"Periodic work?"}
J -->|"Activation-local"| K["RegisterGrainTimer"]
J -->|"Durable wakeups"| L["Reminders"]
D --> M{"Client topology?"}
M -->|"Separate process"| N["UseOrleansClient / .AsClient()"]
M -->|"Same process"| O["Co-hosted silo+client"]
F & F2 & F3 & G & I & I2 & I3 & I4 & K & L & N & O --> P["Serialization → Placement → Observability → Testing → Deploy"]Deliver
- a justified Orleans fit, or a clear rejection when the problem should stay as plain
.NETcode - grain boundaries, grain identities, and activation behavior aligned to the domain model
- concrete choices for clustering, persistence, reminders, streams, placement, transactions, and hosting topology
- serialization contracts with
[GenerateSerializer],[Id], versioning via[Alias], and immutability annotations - an async-safe grain API surface with bounded state, proper reentrancy, and reduced hot-spot risk
- an explicit testing and observability plan for local development and production
- a test-harness choice that matches the assertion level: runtime-only, API/SignalR/UI, or direct Host DI/grain access
Validate
- Orleans is being used for many loosely coupled entities, not as a generic distributed hammer
- grain interfaces are coarse enough to avoid chatty cross-grain traffic
- no grain code blocks threads or mixes sync-over-async with runtime calls
- state is bounded, version-tolerant, and persisted only through intentional provider-backed writes
- all state and message types use
[GenerateSerializer]and[Id(N)]correctly - timers are not used where durable reminders are required; reminders are not used for high-frequency ticks
- in-memory storage, reminders, and stream providers are confined to dev/test usage
- Aspire projects register required keyed backing resources before
UseOrleans()orUseOrleansClient() - reentrancy is handled deliberately — circular call patterns use
[Reentrant],[AlwaysInterleave], orAllowCallChainReentrancy - transactional grains are marked
[Reentrant]and usePerformRead/PerformUpdate - hot grains, global coordinators, and affinity-heavy grains are measured and justified
- tests cover multi-silo behavior, persistence, and failover-sensitive logic when those behaviors matter
- Orleans 10.2 upgrade reviews cover journaling format, metrics meter access, client retry behavior, reminder lifecycle, and provider ownership changes
- Aspire-backed tests reuse one shared AppHost fixture and do not boot the distributed topology inside individual tests
- co-hosted Host tests do not start a redundant Orleans client unless external-client behavior is the thing under test
- Host or API test factories resolve connection strings from the AppHost resource graph instead of copied local config
- deployment uses production clustering, real providers, and proper GC configuration
Load References
Open only what you need. Each reference is topic-focused for token economy:
- references/official-docs-index.md — full Orleans documentation map with direct links to the official Learn tree
- references/grains.md — grain modeling, persistence, event sourcing, reminders, transactions, versioning links
- references/grain-api.md — grain identity, placement, lifecycle, reentrancy, cancellation API details with code
- references/persistence-api.md — IPersistentState API, provider configuration, event sourcing, transactions with code
- references/streaming-api.md — streams, broadcast channels, observers, IAsyncEnumerable patterns with code
- references/serialization-api.md — GenerateSerializer, Id, Alias, surrogates, copier, immutability details
- references/hosting.md — clients, Aspire, configuration, observability, dashboard, deployment links
- references/configuration-api.md — silo/client config, GC tuning, deployment targets, observability setup with code
- references/implementation.md — runtime internals, testing, load balancing, messaging guarantees
- references/testing-patterns.md — practical Orleans test harness selection with
InProcessTestCluster, shared AppHost fixtures,WebApplicationFactory, SignalR, and Playwright - references/patterns.md — grain, persistence, streaming, coordination, and performance patterns with code
- references/anti-patterns.md — blocking calls, unbounded state, chatty grains, bottlenecks, deadlocks with code
- references/examples.md — quickstarts, samples browser entries, and official Orleans example hubs
Official sources:
{
"version": "2.2.0",
"category": "Distributed",
"package_prefix": "Microsoft.Orleans"
}
Orleans Anti-Patterns
Common mistakes when building Orleans applications and how to avoid them.
---
Blocking Calls
Anti-Pattern: Synchronous Blocking
// BAD: Blocking call in async context
public class BadGrain : Grain, IBadGrain
{
public Task<string> GetData()
{
var client = new HttpClient();
var result = client.GetStringAsync("https://api.example.com/data").Result; // BLOCKS!
return Task.FromResult(result);
}
}Why it's bad:
- Blocks the grain's single-threaded scheduler
- Can cause deadlocks with Orleans runtime
- Prevents other grain calls from executing
- Severely degrades cluster throughput
Correct Approach
// GOOD: Fully async
public class GoodGrain : Grain, IGoodGrain
{
private readonly HttpClient _client;
public GoodGrain(HttpClient client)
{
_client = client;
}
public async Task<string> GetData()
{
return await _client.GetStringAsync("https://api.example.com/data");
}
}---
Large Grain State
Anti-Pattern: Unbounded State Growth
// BAD: State grows without limit
[GenerateSerializer]
public class ChatRoomState
{
[Id(0)] public List<ChatMessage> AllMessages { get; set; } = []; // Grows forever!
}
public class ChatRoomGrain : Grain, IChatRoomGrain
{
private readonly IPersistentState<ChatRoomState> _state;
public async Task SendMessage(ChatMessage message)
{
_state.State.AllMessages.Add(message); // Unbounded growth
await _state.WriteStateAsync(); // Gets slower over time
}
}Why it's bad:
- Serialization time increases linearly
- Memory usage grows unbounded
- Activation time becomes very slow
- Storage costs increase
Correct Approach
// GOOD: Bounded state with external storage for history
[GenerateSerializer]
public class ChatRoomState
{
[Id(0)] public List<ChatMessage> RecentMessages { get; set; } = [];
[Id(1)] public int TotalMessageCount { get; set; }
private const int MaxRecentMessages = 100;
public void AddMessage(ChatMessage message)
{
RecentMessages.Add(message);
TotalMessageCount++;
if (RecentMessages.Count > MaxRecentMessages)
{
RecentMessages.RemoveAt(0);
}
}
}
public class ChatRoomGrain : Grain, IChatRoomGrain
{
private readonly IPersistentState<ChatRoomState> _state;
private readonly IMessageArchive _archive; // External storage for old messages
public async Task SendMessage(ChatMessage message)
{
_state.State.AddMessage(message);
await _state.WriteStateAsync();
// Archive to external storage asynchronously
await _archive.StoreAsync(this.GetPrimaryKeyString(), message);
}
public async Task<List<ChatMessage>> GetHistory(int page, int pageSize)
{
return await _archive.GetPageAsync(this.GetPrimaryKeyString(), page, pageSize);
}
}---
Chatty Grain Communication
Anti-Pattern: Many Small Calls
// BAD: Multiple round-trips per operation
public class OrderGrain : Grain, IOrderGrain
{
public async Task<OrderSummary> GetOrderSummary()
{
var customer = GrainFactory.GetGrain<ICustomerGrain>(_customerId);
var product = GrainFactory.GetGrain<IProductGrain>(_productId);
var shipping = GrainFactory.GetGrain<IShippingGrain>(_shippingId);
// Sequential calls - very slow!
var customerName = await customer.GetName();
var customerEmail = await customer.GetEmail();
var customerAddress = await customer.GetAddress();
var productName = await product.GetName();
var productPrice = await product.GetPrice();
var shippingStatus = await shipping.GetStatus();
var shippingEta = await shipping.GetEta();
return new OrderSummary { /* ... */ };
}
}Why it's bad:
- Each call incurs network latency
- Sequential execution multiplies delay
- High overhead for small payloads
- Poor cluster resource utilization
Correct Approach
// GOOD: Batch operations and parallel calls
public class OrderGrain : Grain, IOrderGrain
{
public async Task<OrderSummary> GetOrderSummary()
{
var customer = GrainFactory.GetGrain<ICustomerGrain>(_customerId);
var product = GrainFactory.GetGrain<IProductGrain>(_productId);
var shipping = GrainFactory.GetGrain<IShippingGrain>(_shippingId);
// Parallel calls with batched data retrieval
var customerTask = customer.GetDetails(); // Returns all customer info
var productTask = product.GetDetails(); // Returns all product info
var shippingTask = shipping.GetStatus(); // Returns full status
await Task.WhenAll(customerTask, productTask, shippingTask);
return new OrderSummary
{
Customer = customerTask.Result,
Product = productTask.Result,
Shipping = shippingTask.Result
};
}
}---
Single Bottleneck Grain
Anti-Pattern: Hot Grain
// BAD: All operations go through one grain
public interface IGlobalCounterGrain : IGrainWithIntegerKey
{
Task<long> IncrementAndGet();
}
// Usage everywhere:
var counter = grainFactory.GetGrain<IGlobalCounterGrain>(0);
await counter.IncrementAndGet(); // ALL requests hit this single grainWhy it's bad:
- Single grain handles all load
- No horizontal scaling possible
- Becomes the bottleneck for entire system
- Single point of failure
Correct Approach
// GOOD: Partitioned counters with aggregation
public interface IPartitionedCounterGrain : IGrainWithIntegerKey
{
Task Increment();
Task<long> GetLocalCount();
}
public interface ICounterAggregatorGrain : IGrainWithIntegerKey
{
Task<long> GetTotalCount();
}
public class PartitionedCounterGrain : Grain, IPartitionedCounterGrain
{
private long _count;
public Task Increment()
{
_count++;
return Task.CompletedTask;
}
public Task<long> GetLocalCount() => Task.FromResult(_count);
}
public class CounterAggregatorGrain : Grain, ICounterAggregatorGrain
{
private const int PartitionCount = 100;
public async Task<long> GetTotalCount()
{
var tasks = Enumerable.Range(0, PartitionCount)
.Select(i => GrainFactory
.GetGrain<IPartitionedCounterGrain>(i)
.GetLocalCount());
var counts = await Task.WhenAll(tasks);
return counts.Sum();
}
}
// Usage: Distribute load across partitions
var partitionId = HashCode(userId) % PartitionCount;
var counter = grainFactory.GetGrain<IPartitionedCounterGrain>(partitionId);
await counter.Increment();---
Improper Grain Activation
Anti-Pattern: Short-Lived Grains
// BAD: Creating unique grains for each request
public class ApiController
{
public async Task<IActionResult> ProcessRequest(RequestData data)
{
// New unique grain for each request!
var processor = _grainFactory.GetGrain<IProcessorGrain>(Guid.NewGuid());
var result = await processor.Process(data);
return Ok(result);
}
}Why it's bad:
- Activation overhead on every request
- Grains never benefit from cached state
- Memory churn in silo
- Completely defeats Orleans' actor model benefits
Correct Approach
// GOOD: Reuse grains based on business identity
public class ApiController
{
public async Task<IActionResult> ProcessRequest(RequestData data)
{
// Grain identity based on logical entity
var processor = _grainFactory.GetGrain<IProcessorGrain>(data.CustomerId);
var result = await processor.Process(data);
return Ok(result);
}
}
// Or use StatelessWorker for truly stateless operations
[StatelessWorker]
public class ProcessorGrain : Grain, IProcessorGrain
{
public Task<Result> Process(RequestData data)
{
// Stateless processing, Orleans manages pooling
return Task.FromResult(DoProcess(data));
}
}---
Ignoring Reentrancy
Anti-Pattern: Deadlock-Prone Calls
// BAD: Can deadlock if A calls B and B calls A
public class GrainA : Grain, IGrainA
{
public async Task DoSomething()
{
var grainB = GrainFactory.GetGrain<IGrainB>(0);
await grainB.DoOther(); // GrainB might call back to GrainA!
}
public Task Callback()
{
// This will deadlock if called while DoSomething is waiting
return Task.CompletedTask;
}
}Why it's bad:
- Circular calls cause deadlock
- Grain waits for itself
- Hard to debug
- System appears hung
Correct Approach
// GOOD: Allow reentrancy for callbacks
[Reentrant] // Allows interleaved calls
public class GrainA : Grain, IGrainA
{
public async Task DoSomething()
{
var grainB = GrainFactory.GetGrain<IGrainB>(0);
await grainB.DoOther();
}
public Task Callback()
{
return Task.CompletedTask;
}
}
// Or use [AlwaysInterleave] for specific methods
public class GrainA : Grain, IGrainA
{
public async Task DoSomething()
{
var grainB = GrainFactory.GetGrain<IGrainB>(0);
await grainB.DoOther();
}
[AlwaysInterleave] // This method can always execute
public Task Callback()
{
return Task.CompletedTask;
}
}---
Misusing Timers and Reminders
Anti-Pattern: Timer for Persistence
// BAD: Using timer for critical persistence
public class BadGrain : Grain, IBadGrain
{
private int _importantData;
public override Task OnActivateAsync(CancellationToken ct)
{
// Timer is NOT persistent - data loss on silo crash!
RegisterGrainTimer(
SaveData,
default,
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(5));
return base.OnActivateAsync(ct);
}
public Task UpdateData(int value)
{
_importantData = value;
return Task.CompletedTask; // Not persisted until timer fires!
}
}Why it's bad:
- Timers don't survive grain deactivation
- Data lost if silo crashes
- No guarantee timer will fire
- Not suitable for critical operations
Correct Approach
// GOOD: Persist immediately for critical data, use reminders for scheduled work
public class GoodGrain : Grain, IGoodGrain, IRemindable
{
private readonly IPersistentState<GrainState> _state;
public async Task UpdateData(int value)
{
_state.State.ImportantData = value;
await _state.WriteStateAsync(); // Immediate persistence
}
// Use reminder for scheduled work that must survive failures
public async Task ScheduleDailyReport()
{
await this.RegisterOrUpdateReminder(
"daily-report",
TimeSpan.FromHours(24),
TimeSpan.FromHours(24));
}
public Task ReceiveReminder(string reminderName, TickStatus status)
{
if (reminderName == "daily-report")
{
return GenerateReport();
}
return Task.CompletedTask;
}
}---
Incorrect State Serialization
Anti-Pattern: Non-Serializable State
// BAD: Missing serialization attributes
public class PlayerState
{
public int Score { get; set; }
public HttpClient Client { get; set; } // Can't serialize!
public Action OnScoreChanged { get; set; } // Can't serialize!
}Why it's bad:
- Serialization fails at runtime
- State cannot be persisted
- Grain crashes on activation
Correct Approach
// GOOD: Proper serialization with Orleans attributes
[GenerateSerializer]
public class PlayerState
{
[Id(0)] public int Score { get; set; }
[Id(1)] public DateTime LastPlayed { get; set; }
[Id(2)] public List<string> Achievements { get; set; } = [];
// Non-serializable fields marked appropriately
[NonSerialized]
private HttpClient? _client;
[NonSerialized]
private Action? _onScoreChanged;
}
// Inject dependencies instead of storing them
public class PlayerGrain : Grain, IPlayerGrain
{
private readonly IPersistentState<PlayerState> _state;
private readonly HttpClient _client; // Injected, not in state
public PlayerGrain(
[PersistentState("player")] IPersistentState<PlayerState> state,
HttpClient client)
{
_state = state;
_client = client;
}
}---
Exception Handling
Anti-Pattern: Swallowing Exceptions
// BAD: Silent failures
public class BadGrain : Grain, IBadGrain
{
public async Task ProcessOrder(Order order)
{
try
{
await _paymentService.Charge(order.Amount);
await _inventoryService.Reserve(order.Items);
}
catch (Exception)
{
// Silently swallow - order appears successful but isn't!
}
}
}Why it's bad:
- Failures are hidden
- System enters inconsistent state
- Very hard to debug
- Breaks caller's error handling
Correct Approach
// GOOD: Proper exception handling and propagation
public class GoodGrain : Grain, IGoodGrain
{
private readonly ILogger<GoodGrain> _logger;
public async Task ProcessOrder(Order order)
{
try
{
await _paymentService.Charge(order.Amount);
}
catch (PaymentException ex)
{
_logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
throw new OrderProcessingException("Payment failed", ex);
}
try
{
await _inventoryService.Reserve(order.Items);
}
catch (InventoryException ex)
{
_logger.LogError(ex, "Inventory reservation failed for order {OrderId}", order.Id);
// Compensate for partial success
await _paymentService.Refund(order.Amount);
throw new OrderProcessingException("Inventory unavailable", ex);
}
}
}---
Cluster Configuration
Anti-Pattern: Dev Config in Production
// BAD: Localhost clustering in production
builder.UseOrleans(silo =>
{
silo.UseLocalhostClustering(); // Single-node only!
silo.AddMemoryGrainStorage("Default"); // No persistence!
});Why it's bad:
- Cannot scale beyond one silo
- Data lost on restart
- No fault tolerance
- Not suitable for production
Correct Approach
// GOOD: Environment-appropriate configuration
builder.UseOrleans((context, silo) =>
{
if (context.HostingEnvironment.IsDevelopment())
{
silo.UseLocalhostClustering();
silo.AddMemoryGrainStorage("Default");
}
else
{
silo.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(
context.Configuration.GetConnectionString("Orleans")));
silo.AddAzureTableGrainStorage("Default", options =>
options.ConfigureTableServiceClient(
context.Configuration.GetConnectionString("Orleans")));
silo.Configure<ClusterOptions>(options =>
{
options.ClusterId = context.Configuration["Orleans:ClusterId"];
options.ServiceId = context.Configuration["Orleans:ServiceId"];
});
}
});---
Summary: Quick Reference
| Anti-Pattern | Problem | Solution |
|---|---|---|
.Result / .Wait() | Deadlocks, blocks scheduler | Use async/await throughout |
| Unbounded state | Slow activation, memory bloat | Bound state, use external storage |
| Many small calls | High latency | Batch operations, parallel calls |
| Hot single grain | Bottleneck, no scaling | Partition across grains |
| Unique grain per request | Activation overhead | Reuse grains, use StatelessWorker |
| Circular calls | Deadlocks | Use [Reentrant] or redesign |
| Timer for persistence | Data loss | Use immediate persist + reminders |
| Missing [GenerateSerializer] | Runtime failures | Add proper serialization |
| Swallowing exceptions | Hidden failures | Log and propagate errors |
| Dev config in prod | No persistence/scaling | Environment-specific config |
Configuration, Deployment, and Observability API
Detailed configuration and operational patterns from official Orleans documentation.
Silo Configuration
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans((context, silo) =>
{
if (context.HostingEnvironment.IsDevelopment())
{
silo.UseLocalhostClustering();
silo.AddMemoryGrainStorage("Default");
silo.UseInMemoryReminderService();
}
else
{
// Production clustering
silo.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(
new DefaultAzureCredential(),
new Uri("https://mystorageaccount.table.core.windows.net")));
// Production persistence
silo.AddRedisGrainStorage("Default", options =>
options.ConfigurationOptions = ConfigurationOptions.Parse(redisConn));
// Production reminders
silo.UseRedisReminderService(options =>
options.ConfigurationOptions = ConfigurationOptions.Parse(redisConn));
silo.Configure<ClusterOptions>(options =>
{
options.ClusterId = "prod-cluster";
options.ServiceId = "my-service";
});
}
});Client Configuration
Co-hosted Client (Recommended)
Client runs in same process as silo. Get IClusterClient from DI:
var grain = serviceProvider.GetRequiredService<IClusterClient>()
.GetGrain<IMyGrain>("key");External Client
builder.UseOrleansClient(client =>
{
client.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(connectionString));
client.Configure<ClusterOptions>(options =>
{
options.ClusterId = "prod-cluster";
options.ServiceId = "my-service";
});
});Connection Retry
public class RetryFilter : IClientConnectionRetryFilter
{
private int _attempt;
public async Task<bool> ShouldRetryConnectionAttempt(
Exception exception, CancellationToken ct)
{
if (_attempt++ > 5) return false;
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, _attempt)), ct);
return true;
}
}.NET Aspire Integration
// AppHost
var storage = builder.AddAzureStorage("storage");
var clustering = storage.AddTables("clustering");
var grainStorage = storage.AddBlobs("grain-state");
var redis = builder.AddRedis("redis");
var orleans = builder.AddOrleans("my-cluster")
.WithClustering(clustering)
.WithGrainStorage("Default", grainStorage)
.WithReminders(redis)
.WithMemoryStreams("StreamProvider");
builder.AddProject<Projects.Silo>("silo")
.WithReference(orleans);
builder.AddProject<Projects.WebFrontend>("web")
.WithReference(orleans.AsClient()); // client-onlyClustering Providers
| Provider | Package | Use Case |
|---|---|---|
| Azure Table | Microsoft.Orleans.Clustering.AzureStorage | Azure-hosted |
| Redis | Microsoft.Orleans.Clustering.Redis | Redis-available environments |
| Cosmos DB | Microsoft.Orleans.Clustering.Cosmos | Cosmos-first architectures |
| ADO.NET | Microsoft.Orleans.Clustering.AdoNet | SQL Server / PostgreSQL |
| Consul | Microsoft.Orleans.Clustering.Consul | Consul-based infrastructure |
| Kubernetes | via sidecar | K8s native |
| Localhost | built-in | Dev only |
GC Configuration
Critical for Orleans performance. Configure in project file:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
</PropertyGroup>Or via runtimeconfig.json:
{
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true,
"System.GC.Concurrent": true
}
}
}Observability
Metrics (System.Diagnostics.Metrics)
Meter name: "Microsoft.Orleans"
// OpenTelemetry export
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter("Microsoft.Orleans");
metrics.AddOtlpExporter();
});Monitor from CLI:
dotnet counters monitor -n <Process> --counters Microsoft.OrleansMeter categories: Networking, Messaging, Gateway, Runtime, Catalog (activations), Directory, Consistent Ring, Watchdog, Client, Grains, App Requests, Reminders, Storage, Streams, Transactions.
Distributed Tracing
// Enable
siloBuilder.AddActivityPropagation();
clientBuilder.AddActivityPropagation();
// Or via options
services.Configure<ActivityPropagationGrainCallFilterOptions>(o =>
o.EnableDistributedTracing = true);
// Export
builder.Services.AddOpenTelemetry()
.WithTracing(tracing =>
{
tracing.AddSource("Microsoft.Orleans.Runtime");
tracing.AddSource("Microsoft.Orleans.Application");
tracing.AddOtlpExporter();
});Activity sources: "Microsoft.Orleans.Runtime", "Microsoft.Orleans.Application".
Dashboard
siloBuilder.UseDashboard(options =>
{
options.Port = 8080;
options.Host = "*";
});Secure with ASP.NET Core authorization middleware.
Deployment Targets
Azure Container Apps
siloBuilder.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(new DefaultAzureCredential(), tableUri));
// ACA provides automatic scaling, zero-downtime deploymentsKubernetes
siloBuilder.UseKubernetesHosting(); // auto-configures from K8s environment
// Requires proper RBAC, service account, and headless service
// Configure liveness and readiness probesAzure App Service
// Use Azure Storage for clustering (no multicast)
// Configure sticky sessions for client gateway affinity
// Use deployment slots for zero-downtime upgradesGraceful Shutdown
// Automatic with UseConsoleLifetime() or ASP.NET Core host
// Manual: await host.StopAsync(cancellationToken)
// Configure drain period
services.Configure<SiloMessagingOptions>(options =>
{
options.ShutdownGracePeriod = TimeSpan.FromSeconds(30);
});Heterogeneous Silos
Different silos can support different grain types. All must reference grain interfaces.
services.Configure<GrainClassOptions>(options =>
{
options.ExcludedGrainTypes.Add("MyHeavyGrain");
});
services.Configure<TypeManagementOptions>(options =>
{
options.TypeMapRefreshInterval = TimeSpan.FromMinutes(1);
});Limitations: stateless grains and [ImplicitStreamSubscription] not supported in heterogeneous mode.
Silo Metadata (Orleans 9+)
Label silos for placement filtering:
siloBuilder.Configure<SiloMetadataOptions>(options =>
{
options.Metadata["zone"] = "us-east-1a";
options.Metadata["tier"] = "compute";
});Used with placement filtering for zone-aware and hardware-affinity placement.
Silo Lifecycle
Ordered startup/shutdown via observable lifecycle. Components participate via ILifecycleParticipant<ISiloLifecycle>.
Lifecycle Stages
| Stage | Value | Purpose |
|---|---|---|
First | int.MinValue | Earliest stage |
RuntimeInitialize | 2000 | Threading init |
RuntimeServices | 4000 | Networking, agents |
RuntimeStorageServices | 6000 | Storage init |
RuntimeGrainServices | 8000 | Grain type management, membership, directory |
ApplicationServices | 10000 | Application layer |
BecomeActive | Active - 1 | Join cluster |
Active | 20000 | Ready for workload |
Last | int.MaxValue | Latest stage |
Grain Directory
Maps grain identity → activation location (silo). Ensures at most one activation.
| Implementation | Package | Notes |
|---|---|---|
| Distributed In-Cluster (default) | built-in | Eventually consistent DHT |
| Strongly-Consistent (Orleans 10 preview) | built-in | Versioned range locks, prevents duplicates |
| ADO.NET | Microsoft.Orleans.GrainDirectory.AdoNet | SQL Server, PostgreSQL, MySQL, Oracle |
| Azure Table | Microsoft.Orleans.GrainDirectory.AzureStorage | Azure-hosted |
| Redis | Microsoft.Orleans.GrainDirectory.Redis | Redis-available |
// Per-grain-type directory
[GrainDirectory(GrainDirectoryName = "my-directory")]
public class MyGrain : Grain, IMyGrain { }
siloBuilder.AddRedisGrainDirectory("my-directory", options => { });TLS Configuration
Package: Microsoft.Orleans.Connections.Security
// Silo — certificate from file
var cert = X509CertificateLoader.LoadPkcs12FromFile("cert.pfx", "password");
siloBuilder.UseTls(cert, options =>
{
options.OnAuthenticateAsClient = (conn, ssl) =>
ssl.TargetHost = "my-service";
});
// Client
clientBuilder.UseTls(cert, options =>
options.AllowAnyRemoteCertificate()); // dev only
// Mutual TLS — client sends certificate for silo verificationDashboard (Orleans 10.0)
Packages: Microsoft.Orleans.Dashboard, Microsoft.Orleans.Dashboard.Abstractions
siloBuilder.AddDashboard();
app.MapOrleansDashboard(); // default /
app.MapOrleansDashboard(routePrefix: "/dashboard");
// Authorization
app.MapOrleansDashboard().RequireAuthorization("AdminPolicy");Features: cluster overview, grain monitoring, method profiling, reminder management, live log streaming, grain state inspection.
Options: HideTrace (bool), CounterUpdateIntervalMs (int, default 1000), HistoryLength (int, default 100).
Exclude grains from profiling: [NoProfiling] attribute.
Startup Tasks
Preferred: use .NET BackgroundService or IHostedService. Register after UseOrleans().
// BackgroundService approach
public class GrainInitializer : BackgroundService
{
private readonly IGrainFactory _grainFactory;
public GrainInitializer(IGrainFactory grainFactory) => _grainFactory = grainFactory;
protected override async Task ExecuteAsync(CancellationToken ct)
{
var grain = _grainFactory.GetGrain<IInitGrain>(0);
await grain.Initialize();
}
}
// Legacy startup task
siloBuilder.AddStartupTask(async (IServiceProvider sp, CancellationToken ct) =>
{
var grain = sp.GetRequiredService<IGrainFactory>().GetGrain<IInitGrain>(0);
await grain.Initialize();
});Warning: exceptions from startup tasks stop the silo (fail-fast).
ADO.NET Configuration
NuGet packages: Microsoft.Orleans.Clustering.AdoNet, Microsoft.Orleans.Persistence.AdoNet, Microsoft.Orleans.Reminders.AdoNet
siloBuilder.UseAdoNetClustering(options =>
{
options.Invariant = "Microsoft.Data.SqlClient"; // Orleans 10.0
options.ConnectionString = connectionString;
});
siloBuilder.AddAdoNetGrainStorage("Default", options =>
{
options.Invariant = "Microsoft.Data.SqlClient";
options.ConnectionString = connectionString;
});
siloBuilder.UseAdoNetReminderService(options =>
{
options.Invariant = "Microsoft.Data.SqlClient";
options.ConnectionString = connectionString;
});Supported databases and invariants:
| Database | Invariant |
|---|---|
| SQL Server | Microsoft.Data.SqlClient (10.0) / System.Data.SqlClient (7-9) |
| PostgreSQL | Npgsql |
| MySQL/MariaDB | MySql.Data.MySqlClient |
| Oracle | Oracle.DataAccess.Client |
Prerequisite: run SQL setup scripts from dotnet/orleans repo before use.
Azure App Service Deployment
Requires VNet integration and private ports for silo-to-silo communication.
var endpointAddress = IPAddress.Parse(builder.Configuration["WEBSITE_PRIVATE_IP"]!);
var strPorts = builder.Configuration["WEBSITE_PRIVATE_PORTS"]!.Split(',');
var (siloPort, gatewayPort) = (int.Parse(strPorts[0]), int.Parse(strPorts[1]));
siloBuilder.ConfigureEndpoints(endpointAddress, siloPort, gatewayPort,
listenOnAnyHostAddress: true);Enable private ports: az webapp config set --generic-configurations '{"vnetPrivatePortsCount": "2"}'
Kubernetes Deployment
Package: Microsoft.Orleans.Hosting.Kubernetes
siloBuilder.UseKubernetesHosting();
// Auto-configures SiloName, AdvertisedIPAddress, endpoints from K8s env
// Still need separate clustering provider (Redis, Azure Table, etc.)Required pod labels: orleans/serviceId, orleans/clusterId. Required env: POD_NAME, POD_NAMESPACE, POD_IP.
Key YAML: terminationGracePeriodSeconds: 180, DOTNET_SHUTDOWNTIMEOUTSECONDS: "120", maxUnavailable: 0, maxSurge: 1, minReadySeconds: 60.
RBAC: get, watch, list, delete, patch on pods.
Handling Failures
- Method calls return exceptions; Orleans propagates across silos
- Getting a grain reference always succeeds locally (lazy activation)
- Orleans auto-reactivates failed grains on next call on another silo
- At-most-once message delivery by default (no automatic retries)
Recovery strategies: 1. Retry — suitable when no half-done state changes 2. Reload state — ReadStateAsync() to refresh from storage 3. Transactions — for multi-grain atomicity 4. Process Manager / Saga — for complex multi-grain orchestration
Cluster Management
Fully distributed peer-to-peer membership protocol.
siloBuilder.Configure<ClusterMembershipOptions>(options =>
{
options.NumProbedSilos = 10; // default (Orleans 9+)
options.NumVotesForDeathDeclaration = 2;
options.DeathVoteExpirationTimeout = TimeSpan.FromSeconds(180);
options.ProbeTimeout = TimeSpan.FromSeconds(10);
options.NumMissedProbesLimit = 3;
});Typical failure detection: ~15 seconds (Orleans 9+). Properties: handles any number of failures, self-monitoring with health scoring, indirect probing, table unavailability never causes false death declarations.
Messaging Delivery Guarantees
Default: at-most-once (no automatic retries). Message delivered once or not at all, never twice.
With retries: at-least-once (may arrive multiple times, no dedup). Every message has automatic configurable timeout.
Migration Guide (7.x → 10.x)
| Change | Migration |
|---|---|
AddGrainCallFilter removed | Use AddIncomingGrainCallFilter |
RegisterTimer obsoleted | Use RegisterGrainTimer with GrainTimerCreationOptions |
| ADO.NET invariant | Use Microsoft.Data.SqlClient instead of System.Data.SqlClient |
CancelRequestOnTimeout default → false | Set true explicitly if needed |
Default placement → ResourceOptimized (9.2) | Explicitly set if different behavior needed |
Rolling upgrades 7.x → 10.0 NOT recommended due to protocol changes. Deploy new cluster, migrate state, switch traffic.
Testing
InProcessTestCluster (Orleans 9+, recommended)
var builder = new InProcessTestClusterBuilder();
builder.ConfigureSilo((options, siloBuilder) =>
{
siloBuilder.AddMemoryGrainStorage("Default");
});
var cluster = builder.Build();
await cluster.DeployAsync();
var grain = cluster.Client.GetGrain<IMyGrain>(0);
var result = await grain.DoWork();
// Dynamic silo management
var newSilo = await cluster.StartSiloAsync();
await cluster.StopSiloAsync(newSilo);TestCluster (legacy, still supported)
var builder = new TestClusterBuilder();
builder.AddSiloBuilderConfigurator<TestSiloConfig>();
var cluster = builder.Build();
cluster.Deploy();xUnit Sharing
[CollectionDefinition("Orleans")]
public class ClusterCollection : ICollectionFixture<ClusterFixture> { }
[Collection("Orleans")]
public class MyTests
{
private readonly TestCluster _cluster;
public MyTests(ClusterFixture fixture) => _cluster = fixture.Cluster;
}Best Practices Summary
Good fit: millions of loosely coupled entities, small single-threaded, interactive workloads.
Bad fit: shared memory between entities, few large multithreaded entities, global coordination, long-running batch jobs.
Rules:
- Avoid chatty inter-grain communication
- Avoid bottleneck grains — use staged aggregation
- Never block threads
- Use
[StatelessWorker]for stateless operations - Initial
ReadStateAsynchappens automatically beforeOnActivateAsync - Call
WriteStateAsync()after state changes - Use Polly for retry logic
NuGet Packages Map
Core
| Package | Purpose |
|---|---|
Microsoft.Orleans.Server | Silo hosting (includes Client) |
Microsoft.Orleans.Client | Standalone client |
Microsoft.Orleans.Sdk | Grain development metapackage |
Dashboard
| Package | Purpose |
|---|---|
Microsoft.Orleans.Dashboard | Built-in dashboard (10.0) |
Microsoft.Orleans.Dashboard.Abstractions | Dashboard abstractions |
Clustering
| Package | Backend |
|---|---|
Microsoft.Orleans.Clustering.AzureStorage | Azure Table |
Microsoft.Orleans.Clustering.AdoNet | SQL Server, PostgreSQL, MySQL, Oracle |
Microsoft.Orleans.Clustering.Redis | Redis |
Microsoft.Orleans.Clustering.Cosmos | Cosmos DB |
Microsoft.Orleans.Clustering.Consul | Consul |
Persistence
| Package | Backend |
|---|---|
Microsoft.Orleans.Persistence.AzureStorage | Azure Table/Blob |
Microsoft.Orleans.Persistence.AdoNet | SQL |
Microsoft.Orleans.Persistence.Redis | Redis |
Microsoft.Orleans.Persistence.Cosmos | Cosmos DB |
Microsoft.Orleans.Persistence.DynamoDB | DynamoDB |
Reminders
| Package | Backend |
|---|---|
Microsoft.Orleans.Reminders.AzureStorage | Azure Table |
Microsoft.Orleans.Reminders.AdoNet | SQL |
Microsoft.Orleans.Reminders.Redis | Redis |
Microsoft.Orleans.Reminders.Cosmos | Cosmos DB |
Grain Directory
| Package | Backend |
|---|---|
Microsoft.Orleans.GrainDirectory.AzureStorage | Azure Table |
Microsoft.Orleans.GrainDirectory.AdoNet | SQL |
Microsoft.Orleans.GrainDirectory.Redis | Redis |
Streaming
| Package | Backend |
|---|---|
Microsoft.Orleans.Streaming.EventHubs | Azure Event Hubs |
Microsoft.Orleans.Streaming.AzureStorage | Azure Queue |
Serializers
| Package | Format |
|---|---|
Microsoft.Orleans.Serialization.SystemTextJson | System.Text.Json |
Microsoft.Orleans.Serialization.NewtonsoftJson | Newtonsoft.Json |
Microsoft.Orleans.Serialization.MessagePack | MessagePack |
Microsoft.Orleans.Serialization.Protobuf | Protobuf |
Other
| Package | Purpose |
|---|---|
Microsoft.Orleans.Transactions | ACID transactions |
Microsoft.Orleans.EventSourcing | JournaledGrain |
Microsoft.Orleans.Connections.Security | TLS |
Microsoft.Orleans.Hosting.Kubernetes | K8s hosting |
Microsoft.Orleans.Analyzers | Code analyzers |
Microsoft.Orleans.TestingHost | TestCluster |
Local Development Configuration
// Silo — single-node, in-memory everything
await Host.CreateDefaultBuilder(args)
.UseOrleans(silo => silo.UseLocalhostClustering())
.RunConsoleAsync();
// Client — connect to local cluster
using IHost host = Host.CreateDefaultBuilder(args)
.UseOrleansClient(client => client.UseLocalhostClustering())
.UseConsoleLifetime()
.Build();
await host.StartAsync();Packages: Microsoft.Orleans.Server for silo, Microsoft.Orleans.Client for client.
Server Configuration
builder.UseOrleans((context, silo) =>
{
silo.Configure<ClusterOptions>(options =>
{
options.ClusterId = "my-cluster";
options.ServiceId = "my-service";
});
silo.Configure<EndpointOptions>(options =>
{
options.SiloPort = 11111;
options.GatewayPort = 30000;
options.AdvertisedIPAddress = IPAddress.Loopback;
});
// Clustering, persistence, reminders, streams...
});Client Configuration
builder.UseOrleansClient(client =>
{
client.Configure<ClusterOptions>(options =>
{
options.ClusterId = "my-cluster";
options.ServiceId = "my-service";
});
// Clustering provider must match silo
client.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(connectionString));
});Prefer TokenCredential with DefaultAzureCredential over connection strings for Azure providers.
Typical Configurations
Aspire + Redis (Recommended for Orleans 8+)
// AppHost
var redis = builder.AddRedis("redis");
var orleans = builder.AddOrleans("cluster")
.WithClustering(redis)
.WithGrainStorage("Default", redis)
.WithReminders(redis);
// Silo
builder.AddKeyedRedisClient("redis");
builder.UseOrleans();Azure Storage Production
silo.UseAzureStorageClustering(options =>
options.ConfigureTableServiceClient(
new DefaultAzureCredential(),
new Uri("https://account.table.core.windows.net")));
silo.AddAzureBlobGrainStorage("Default", options =>
options.ConfigureBlobServiceClient(
new DefaultAzureCredential(),
new Uri("https://account.blob.core.windows.net")));SQL Server Production
silo.UseAdoNetClustering(options =>
{
options.Invariant = "Microsoft.Data.SqlClient"; // Orleans 10.0
options.ConnectionString = connectionString;
});Unreliable Test Cluster (No External Deps)
// Silo
silo.UseDevelopmentClustering(primarySiloEndpoint);
// Client
client.UseStaticClustering(gateways);Service Fabric Deployment
Orleans integrates with Azure Service Fabric for deployment, service discovery, and failover.
// Use Service Fabric's membership system
siloBuilder.UseServiceFabricClustering(serviceContext);Key considerations:
- Service Fabric manages silo lifecycle through reliable services
- Use Service Fabric's naming service for cluster membership
- Co-locate silos with Service Fabric partitions for locality
- Configure endpoints through Service Fabric service manifests
Consul Deployment
Package: Microsoft.Orleans.Clustering.Consul
// Silo
silo.UseConsulSiloClustering(options =>
options.ConfigureConsulClient(new Uri("http://localhost:8500")));
// Client
client.UseConsulClientClustering(options =>
options.ConfigureConsulClient(new Uri("http://localhost:8500")));Uses Consul Key/Value store with Check-And-Set (CAS) operations. Keys prefixed with orleans/. Each silo registers silo details + last alive timestamp.
Limitations: only basic membership protocol (no atomic multi-key updates), KV not replicated between Consul data centers.
Troubleshooting Deployments
Common SiloUnavailableException Causes
- Silo crashed/terminated and evicted from cluster
- Network partition between silos
- Silo shutting down during request
- No silos available for client connection
Configuration Issues
- Mismatched clustering provider between silos and clients
- Local/dev config used in cloud environments
- Missing/incorrect connection strings
ClusterId/ServiceIdmismatch between silo and client
Container/K8s Issues
- Insufficient resource requests/limits
- Clustering provider connectivity failure
- SiloPort (11111) / GatewayPort (30000) not correctly exposed
- Missing liveness/readiness probes
Debugging
builder.Logging.SetMinimumLevel(LogLevel.Information);
// For Orleans internals:
builder.Logging.AddFilter("Orleans", LogLevel.Debug);Observability Details
Silo Error Code Monitoring
Orleans silos emit structured error codes with categories:
Runtime— activation, deactivation, messaging errorsCatalog— grain directory, activation catalogNetworking— connection, socket errorsMembership— cluster membership, failure detectionStorage— persistence provider errors
Monitor via standard Microsoft.Extensions.Logging — error codes appear in log messages.
Client Error Code Monitoring
Client-side error categories:
Gateway— connection to silo gatewaysMessaging— request/response failures, timeoutsRuntime— client lifecycle errors
OpenTelemetry Full Setup
builder.Services.AddOpenTelemetry()
.WithMetrics(metrics =>
{
metrics.AddMeter("Microsoft.Orleans");
metrics.AddOtlpExporter();
})
.WithTracing(tracing =>
{
tracing.AddSource("Microsoft.Orleans.Runtime");
tracing.AddSource("Microsoft.Orleans.Application");
tracing.AddOtlpExporter();
});Metrics Categories
| Category | What It Tracks |
|---|---|
| Networking | Connections, bytes sent/received |
| Messaging | Messages sent/received, queue lengths |
| Gateway | Client connections, active gateways |
| Runtime | Thread pool, memory, CPU |
| Catalog | Activations, activation creation/destruction |
| Directory | Directory lookups, registrations |
| Grains | Per-grain-type activation counts |
| App Requests | Grain method call latency and throughput |
| Reminders | Active reminders, ticks |
| Storage | Read/write latency, failures |
| Streams | Events processed, subscription counts |
| Transactions | Commit/abort rates |
Key Options Classes
| Class | Purpose |
|---|---|
ClusterOptions | ClusterId, ServiceId |
ClusterMembershipOptions | Probing, death votes, failure detection |
SiloMessagingOptions | ResponseTimeout, ShutdownGracePeriod |
ClientMessagingOptions | ResponseTimeout, CancelRequestOnTimeout |
GrainCollectionOptions | CollectionAge, memory shedding |
GrainClassOptions | ExcludedGrainTypes |
GrainVersioningOptions | Compatibility and version selector strategies |
TypeManagementOptions | TypeMapRefreshInterval |
SiloMetadataOptions | Metadata labels for placement filtering |
ResourceOptimizedPlacementOptions | Placement weights (CPU, memory, etc.) |
EndpointOptions | Silo/gateway ports |
LoadSheddingOptions | CPU threshold for load shedding |
SchedulingOptions | Scheduler behavior |
NetworkingOptions | Socket/connection timeouts |
StatisticsOptions | Statistics output |
ActivityPropagationGrainCallFilterOptions | Distributed tracing |
DashboardOptions | Dashboard port, trace hiding, update interval |
GrainProfilerOptions | Profiling behavior |
Orleans Examples
Use this reference when the user needs an example-first entry point instead of conceptual guidance.
Official Sample Hubs
- Microsoft Learn Orleans samples browser
- dotnet/samples Orleans directory
- Orleans repository samples README
Getting Started Examples
Domain And Architecture Samples
- Adventure game
- Chirper social media sample
- GPS device tracker
- Presence service
- Tic Tac Toe web game
- Stocks sample
Hosting And UI Samples
- Deploy and scale an Orleans app on Azure
- Voting app on Kubernetes
- Blazor Server + Orleans
- Blazor WebAssembly + Orleans
- Transport Layer Security sample
Streams, Observers, And Real-Time
- Chat Room sample
- Streaming pub/sub with Azure Event Hubs
- Chirper social sample
- GPS Tracker with SignalR
State, Persistence, And Transactions
Testing
Repo-Grounded Integration Harness Patterns
AIBase: sharedAspireTestFixturefor API/UI suites plusAIBaseTestApplication : WebApplicationFactory<...>for direct DI and grain accessWA.Storied.Agents: sharedAspireTestFixturefor AppHost lifecycle and Playwright, plusAIBaseTestApplication : WebApplicationFactory<HostEntryPointMarker>for Host services and Orleans runtime access- Load
testing-patterns.mdwhen you need working snippets for shared AppHost fixtures,WebApplicationFactory, SignalR, or browser automation instead of standaloneInProcessTestClusterexamples
Additional Community-Oriented Example Lists Mentioned By Official Samples
Usage Guidance
- Pick the example that matches the dominant concern before reading broad docs.
- Use quickstarts for first wiring, tutorial pages for guided walkthroughs, and sample-browser entries for concrete repo layouts.
- Cross-check sample age and package names against the live Orleans docs when copying code into a modern project.
Grain API Reference
Detailed grain API patterns extracted from official Orleans documentation. Use when you need exact API shapes, not just link navigation.
Grain Identity
Identity structure: type/key (e.g., shoppingcart/bob65).
Grain type name derived from class name by removing "Grain" suffix and lowercasing. Customize with [GrainType("cart")].
Key Types
| Interface | Key Type | Access | Factory |
|---|---|---|---|
IGrainWithGuidKey | Guid | this.GetPrimaryKey() | GetGrain<T>(guid) |
IGrainWithIntegerKey | long | this.GetPrimaryKeyLong() | GetGrain<T>(longId) |
IGrainWithStringKey | string | this.GetPrimaryKeyString() | GetGrain<T>(stringId) |
IGrainWithGuidCompoundKey | Guid+string | this.GetPrimaryKey(out string ext) | GetGrain<T>(guid, "ext", null) |
IGrainWithIntegerCompoundKey | long+string | this.GetPrimaryKeyLong(out string ext) | GetGrain<T>(0, "ext", null) |
Singleton pattern: use a well-known fixed key like "default" or 0.
Grain Interface Rules
public interface IHello : IGrainWithIntegerKey
{
ValueTask<string> SayHello(string greeting);
Task DoWork();
IAsyncEnumerable<int> StreamData(int count, [EnumeratorCancellation] CancellationToken ct = default);
}
[ResponseTimeout("00:00:05")]
Task<Result> TimeSensitiveCall();- All methods must return
Task,Task<T>,ValueTask<T>, orIAsyncEnumerable<T> CancellationTokenas last parameter, optional with default[ResponseTimeout]on interface methods only (not implementations)- Default response timeout: 30 seconds
Grain Lifecycle
flowchart LR
S1["First<br/>(int.MinValue)"] --> S2["SetupState<br/>(1000)<br/>loads persistent state"]
S2 --> S3["Activate<br/>(2000)<br/>OnActivateAsync"]
S3 --> S4["Active<br/>serving requests"]
S4 --> S5["Deactivate<br/>OnDeactivateAsync"]
S5 --> S6["Last<br/>(int.MaxValue)"]public override Task OnActivateAsync(CancellationToken ct)
{
// Called during activation (stage 2000)
return base.OnActivateAsync(ct);
}
public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken ct)
{
// Best-effort, not guaranteed on crash
return base.OnDeactivateAsync(reason, ct);
}Memory-Based Activation Shedding (Orleans 9+)
Auto-deactivates grains under memory pressure.
// Configure via GrainCollectionOptions
services.Configure<GrainCollectionOptions>(options =>
{
options.EnableActivationSheddingOnMemoryPressure = true;
options.MemoryUsageLimitPercentage = 80; // default
options.MemoryUsageTargetPercentage = 75; // default
options.CollectionAge = TimeSpan.FromMinutes(15); // default
});Grain Migration (Orleans 8+)
Move activations between silos preserving in-memory state.
// Implement IGrainMigrationParticipant
public class MyGrain : Grain, IMyGrain, IGrainMigrationParticipant
{
public void OnDehydrate(IDehydrationContext context)
{
context.TryAddValue("key", _myValue);
}
public void OnRehydrate(IRehydrationContext context)
{
context.TryGetValue("key", out _myValue);
}
}
// Trigger migration
this.MigrateOnIdle();
// Prevent migration
[Immovable]
public class PinnedGrain : Grain, IPinnedGrain { }Placement Strategies
| Strategy | Attribute | Behavior |
|---|---|---|
| Resource-Optimized (default 9.2+) | [ResourceOptimizedPlacement] | CPU + memory + activation count weighted scoring |
| Random | [RandomPlacement] | Random compatible server |
| Prefer Local | [PreferLocalPlacement] | Local if compatible, else random |
| Hash-Based | [HashBasedPlacement] | Hash grain ID mod server count |
| Activation-Count-Based | [ActivationCountBasedPlacement] | Power of Two Choices algorithm |
| Stateless Worker | [StatelessWorker] | Multiple activations per server |
| Silo-Role-Based | [SiloRoleBasedPlacement] | Deterministic on silos with role |
Custom Placement
// 1. Define strategy
public class MyPlacementStrategy : PlacementStrategy { }
// 2. Define attribute
[AttributeUsage(AttributeTargets.Class)]
public class MyPlacementAttribute : PlacementAttribute
{
public MyPlacementAttribute() : base(new MyPlacementStrategy()) { }
}
// 3. Implement director
public class MyPlacementDirector : IPlacementDirector
{
public Task<SiloAddress> OnAddActivation(
PlacementStrategy strategy, PlacementTarget target,
IPlacementContext context) { /* ... */ }
}
// 4. Register
services.AddPlacementDirector<MyPlacementStrategy, MyPlacementDirector>();Request Scheduling and Reentrancy
Default: single-threaded, non-reentrant. Each request runs to completion before the next starts. Safe but can deadlock with circular call patterns (A → B → A).
Reentrancy Mechanisms
| Mechanism | Scope | Effect |
|---|---|---|
[Reentrant] | Grain class | All methods interleave freely |
[AlwaysInterleave] | Interface method | Method always interleaves even on non-reentrant grains |
[ReadOnly] | Interface method | Concurrent with other [ReadOnly] methods |
[MayInterleave(nameof(P))] | Grain class | Per-call predicate decides interleaving |
AllowCallChainReentrancy() | Scoped | Reentrancy for current call chain only |
SuppressCallChainReentrancy() | Scoped | Disables call chain reentrancy |
Deadlock Prevention
// Problem: A calls B, B calls back to A → deadlock (A is blocked waiting for B)
// Solution 1: Mark entire grain as reentrant
[Reentrant]
public class GrainA : Grain, IGrainA { }
// Solution 2: Mark specific callback method
public interface IGrainA : IGrainWithIntegerKey
{
Task DoWork();
[AlwaysInterleave] Task Callback(); // always interleaves
}
// Solution 3: Scoped reentrancy (best — minimal surface)
public async Task DoWork()
{
using var _ = RequestContext.AllowCallChainReentrancy();
await otherGrain.MethodThatMightCallBack();
}ReadOnly Methods
public interface ICounterGrain : IGrainWithIntegerKey
{
[ReadOnly] Task<int> GetCount(); // concurrent with other ReadOnly
Task Increment(); // exclusive access
}MayInterleave Predicate
[MayInterleave(nameof(ArgHasInterleaveFlag))]
public class MyGrain : Grain, IMyGrain
{
private static bool ArgHasInterleaveFlag(IInvokable req)
{
return req.GetArgument<MyRequest>(0)?.AllowInterleave == true;
}
}Tradeoff
| Approach | Liveness | Safety |
|---|---|---|
| Non-reentrant (default) | Risk of deadlocks | No concurrent state mutation |
[Reentrant] | No deadlocks | Must handle concurrent state access |
[AlwaysInterleave] on method | Targeted | Only that method interleaves |
[ReadOnly] | Concurrent reads | Only safe for read-only operations |
AllowCallChainReentrancy | Targeted | Only the originating call chain reenters |
Code Generation
Orleans 7+ uses C# source generators at build time. No runtime code generation.
NuGet Packages
| Package | Use |
|---|---|
Microsoft.Orleans.Sdk | Shared — grain interfaces, state types, serialization |
Microsoft.Orleans.Server | Silo — includes Sdk |
Microsoft.Orleans.Client | External client — includes Sdk |
Key Attributes
// Required on all serialized types (state, messages, events)
[GenerateSerializer]
public class MyState
{
[Id(0)] public string Name { get; set; } = "";
[Id(1)] public int Count { get; set; }
}Source generators create:
- Grain reference proxies (method invokers)
- Serializers and copiers for
[GenerateSerializer]types - Method metadata for interceptors and profiling
F# and VB.NET
[<assembly: Orleans.GenerateCodeForDeclaringAssembly(typeof<IMyGrainInterface>)>]
do ()Build-Time Only
No runtime IL emission or reflection-based generation. All code generated as part of compilation. Analyzers (Microsoft.Orleans.Analyzers) provide warnings for missing [GenerateSerializer], [Id], etc.
Cancellation Tokens
Native CancellationToken support in grain interface methods (Orleans 7+). Cooperative cancellation.
// Interface — CancellationToken as last parameter, optional with default
public interface IProcessGrain : IGrainWithStringKey
{
Task<Result> Process(RequestData data, CancellationToken ct = default);
IAsyncEnumerable<Item> StreamItems(int count,
[EnumeratorCancellation] CancellationToken ct = default);
}
// Grain implementation
public class ProcessGrain : Grain, IProcessGrain
{
public async Task<Result> Process(RequestData data, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var step1 = await DoStep1(ct);
ct.ThrowIfCancellationRequested();
return await DoStep2(step1, ct);
}
public async IAsyncEnumerable<Item> StreamItems(int count,
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < count && !ct.IsCancellationRequested; i++)
yield return await FetchItem(i);
}
}
// Client usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var result = await grain.Process(data, cts.Token);
// IAsyncEnumerable with cancellation
await foreach (var item in grain.StreamItems(100).WithCancellation(cts.Token))
Process(item);Cancellation Behavior
| Scenario | Behavior |
|---|---|
| Token cancelled before call | Throws OperationCanceledException immediately |
| Token cancelled during enqueued (not started) request | Request cancelled |
| Token cancelled during active call | Signal propagated, grain cooperatively cancels |
Adding/removing CancellationToken parameter | Backward compatible, doesn't break existing callers |
Multiple CancellationToken params | Compiler error ORLEANS0109 |
Configuration
// SiloMessagingOptions / ClientMessagingOptions
options.CancelRequestOnTimeout = true; // default: auto-cancel on timeout
options.WaitForCancellationAcknowledgement = false; // default: don't wait for ackMetric: orleans-app-requests-canceled.
Legacy GrainCancellationToken / GrainCancellationTokenSource still available but deprecated.
Timers
Non-durable, activation-local periodic work. Stops on deactivation or silo crash.
// Modern API (Orleans 8+)
IGrainTimer timer = this.RegisterGrainTimer<MyState>(
async (state, ct) => { /* callback with CancellationToken */ },
myState,
new GrainTimerCreationOptions
{
DueTime = TimeSpan.FromSeconds(5),
Period = TimeSpan.FromMinutes(1),
Interleave = false, // default — respects single-threaded execution
KeepAlive = false // default — doesn't prevent deactivation
});
// Update timer at runtime
timer.Change(newDueTime, newPeriod);
// Dispose to stop
timer.Dispose();Timer Properties
- Period measured from callback completion to next invocation (not fixed interval)
Interleave = false(default): callback waits for turn like normal grain callsInterleave = true: callback interleaves with other grain calls (oldRegisterTimerbehavior)KeepAlive = true: prevents grain deactivation while timer is active- Callback receives
CancellationTokenthat is cancelled when timer is disposed or grain deactivates
Migration from Legacy RegisterTimer
Legacy RegisterTimer | Modern RegisterGrainTimer |
|---|---|
Returns IDisposable | Returns IGrainTimer |
Default interleave: true | Default interleave: false |
| No cancellation token | Callback receives CancellationToken |
No KeepAlive option | KeepAlive available |
POCO Grains — Timer via DI
public class MyPocoGrain : IMyGrain
{
private readonly ITimerRegistry _timers;
private readonly IGrainContext _context;
public MyPocoGrain(ITimerRegistry timers, IGrainContext context)
{
_timers = timers;
_context = context;
}
public Task Start()
{
_timers.RegisterGrainTimer(_context, async (state, ct) => { },
new object(), new GrainTimerCreationOptions { Period = TimeSpan.FromMinutes(1) });
return Task.CompletedTask;
}
}Reminders
Durable, persistent periodic wakeups. Survive activation/deactivation and cluster restarts. Minimum granularity: minutes/hours/days (not for high-frequency).
public class MyGrain : Grain, IMyGrain, IRemindable
{
public async Task StartReminder()
{
// Register or update (idempotent)
IGrainReminder reminder = await RegisterOrUpdateReminder(
"daily-check",
dueTime: TimeSpan.FromHours(1),
period: TimeSpan.FromHours(24));
}
public async Task StopReminder()
{
// Lookup by name
IGrainReminder? reminder = await GetReminder("daily-check");
if (reminder is not null)
await UnregisterReminder(reminder);
}
public Task ReceiveReminder(string reminderName, TickStatus status)
{
// Reactivates idle grains when reminder ticks
return reminderName switch
{
"daily-check" => DoCheck(),
_ => Task.CompletedTask
};
}
}Reminder Operations
| Operation | Method |
|---|---|
| Register/update | RegisterOrUpdateReminder(name, dueTime, period) → IGrainReminder |
| Cancel | UnregisterReminder(reminder) |
| Lookup by name | GetReminder(name) → IGrainReminder? |
| List all | GetReminders() → List<IGrainReminder> |
Reminder Storage Providers
| Provider | Registration Method |
|---|---|
| Azure Table Storage | UseAzureTableReminderService(options) |
| Redis | UseRedisReminderService(options) |
| Cosmos DB | UseCosmosReminderService(options) |
| ADO.NET | UseAdoNetReminderService(options) |
| In-Memory (dev only) | UseInMemoryReminderService() |
Aspire Integration
// AppHost
var redis = builder.AddRedis("redis");
var orleans = builder.AddOrleans("cluster")
.WithReminders(redis);
// or .WithMemoryReminders() for devPOCO Grains — Reminder via DI
Inject IReminderRegistry instead of using Grain base class methods.
Timers vs Reminders Decision
| Need | Use |
|---|---|
| High-frequency ticks (seconds) | Timer |
| Must survive deactivation/restart | Reminder |
| Activation-local periodic work | Timer |
| Durable low-frequency wakeups | Reminder |
| Should prevent deactivation | Timer with KeepAlive = true |
Interceptors
// Silo-wide incoming filter
public class AuthFilter : IIncomingGrainCallFilter
{
public async Task Invoke(IIncomingGrainCallContext context)
{
// context.Grain, context.InterfaceMethod, context.Arguments, context.Result
await context.Invoke(); // call next filter or grain method
}
}
siloBuilder.AddIncomingGrainCallFilter<AuthFilter>();
// Per-grain filter: grain implements IIncomingGrainCallFilter
// Outgoing filter: IOutgoingGrainCallFilter on silo or client
// Execution order: DI-registered → grain-level → grain methodPOCO Grains
For grains that don't inherit from Grain:
public class MyPocoGrain : IMyGrain
{
private readonly ITimerRegistry _timers;
private readonly IReminderRegistry _reminders;
private readonly IGrainContext _context;
public MyPocoGrain(ITimerRegistry timers, IReminderRegistry reminders, IGrainContext context)
{
_timers = timers;
_reminders = reminders;
_context = context;
}
}Grain References
Proxy objects that encapsulate logical identity (type + key). Location-independent, survive restarts, serializable.
// From grain code
var grain = GrainFactory.GetGrain<IMyGrain>("key");
// From client code
var grain = client.GetGrain<IMyGrain>("key");
// Disambiguation when multiple implementations exist
var grain = GrainFactory.GetGrain<ICounterGrain>("key", grainClassNamePrefix: "Up");
// Or via explicit GrainId
var grain = GrainFactory.GetGrain<ICounterGrain>(GrainId.Create("up-counter", "key"));
// Or via DefaultGrainType attribute on interface
[DefaultGrainType("up-counter")]
public interface ICounterGrain : IGrainWithStringKey { }
// Or via unique marker interfaces
public interface IUpCounterGrain : ICounterGrain, IGrainWithStringKey { }Grain Extensions
Add behavior to grains without modifying the grain class via IGrainExtension.
// Define extension interface
public interface IDeactivateExtension : IGrainExtension
{
Task Deactivate(string msg);
}
// Implement
public sealed class DeactivateExtension : IDeactivateExtension
{
private readonly IGrainContext _context;
public DeactivateExtension(IGrainContext context) => _context = context;
public Task Deactivate(string msg)
{
_context.Deactivate(new DeactivationReason(
DeactivationReasonCode.ApplicationRequested, msg));
return Task.CompletedTask;
}
}
// Register globally
siloBuilder.AddGrainExtension<IDeactivateExtension, DeactivateExtension>();
// Use from anywhere
var ext = grain.AsReference<IDeactivateExtension>();
await ext.Deactivate("cleanup");Per-grain registration: call GrainContext.SetComponent<T>(instance) in OnActivateAsync.
Stateless Worker Grains
Multiple activations per silo, requests dispatched locally, auto-scaling.
[StatelessWorker] // scales up to CPU core count per silo
public class ProcessorGrain : Grain, IProcessorGrain { }
[StatelessWorker(4)] // max 4 activations per silo
public class LimitedWorker : Grain, ILimitedWorker { }
// Typically called with fixed key
var worker = GrainFactory.GetGrain<IProcessorGrain>(0);- Pool expands when all activations busy (up to limit), shrinks via idle deactivation
- Not individually addressable — two requests may hit different activations
- Useful for: CPU-bound stateless ops, hot cache items, reduce-style pre-aggregation
- Limitations: versioning does not apply, not supported in heterogeneous mode
One-Way Requests
Fire-and-forget: returns immediately, no completion signal, no error propagation.
public interface INotifyGrain : IGrainWithGuidKey
{
[OneWay]
Task Notify(MyData data); // must return non-generic Task or ValueTask
}Advanced feature — prefer regular bidirectional requests by default.
External Tasks and Grains
Orleans grain scheduler is single-threaded. Understanding which APIs stay on it is critical.
| API | Scheduler |
|---|---|
await, Task.Factory.StartNew, ContinueWith, WhenAny, WhenAll, Task.Delay | Stays on grain scheduler |
Task.Run, TaskFactory.FromAsync endMethod | Escapes to thread pool |
ConfigureAwait(false) | NEVER use in grain code |
async void | NEVER use in grain code |
// WRONG — unwrapped async delegate
var bad = Task.Factory.StartNew(SomeDelegateAsync);
// CORRECT
var good = Task.Factory.StartNew(SomeDelegateAsync).Unwrap();Never use Task.Wait(), .Result, WaitAny, WaitAll, or GetAwaiter().GetResult() in grain code.
Request Context
Ambient metadata flowing with requests (client → grain, grain → grain). Does NOT flow back with responses.
// Set on client or calling grain
RequestContext.Set("TraceId", Guid.NewGuid().ToString());
// Read in target grain
var traceId = RequestContext.Get("TraceId") as string;Values must be serializable. Prefer simple types (string, Guid, numeric) to minimize overhead.
GrainServices
Special grains running on every silo from startup to shutdown. Not individually addressable, not collected when idle. Used for distributed per-silo services.
// 1. Service interface
public interface IDataService : IGrainService { Task MyMethod(); }
// 2. Implementation
[Reentrant]
public class DataService : GrainService, IDataService
{
private readonly IGrainFactory _grainFactory;
public DataService(IServiceProvider services, GrainId id, Silo silo,
ILoggerFactory loggerFactory, IGrainFactory grainFactory)
: base(id, silo, loggerFactory) => _grainFactory = grainFactory;
public override Task Init(IServiceProvider serviceProvider) => base.Init(serviceProvider);
public override Task Start() => base.Start();
public override Task Stop() => base.Stop();
public Task MyMethod() => Task.CompletedTask;
}
// 3. Client interface
public interface IDataServiceClient : IGrainServiceClient<IDataService>, IDataService { }
// 4. Client implementation
public class DataServiceClient : GrainServiceClient<IDataService>, IDataServiceClient
{
public DataServiceClient(IServiceProvider sp) : base(sp) { }
private IDataService GrainService => GetGrainService(CurrentGrainReference.GrainId);
public Task MyMethod() => GrainService.MyMethod();
}
// 5. Register
siloBuilder.AddGrainService<DataService>();
builder.Services.AddSingleton<IDataServiceClient, DataServiceClient>();Grain Versioning
Different silos can support different versions of a grain interface.
[Version(2)]
public interface IMyGrain : IGrainWithIntegerKey
{
Task OriginalMethod(int arg); // from V1
Task NewMethod(int arg, obj o); // added in V2
}Compatibility Strategies
| Strategy | Behavior |
|---|---|
BackwardCompatible (default) | V2 handles V1 requests; V1 cannot handle V2 |
FullyCompatible | Bidirectional if no new methods added |
Version Selector Strategies
| Strategy | Behavior |
|---|---|
AllCompatibleVersions (default) | Random selection proportional to silo count |
LatestVersion | Always newest compatible version |
MinimumVersion | Always minimum compatible version |
siloBuilder.Configure<GrainVersioningOptions>(options =>
{
options.DefaultCompatibilityStrategy = nameof(BackwardCompatible);
options.DefaultVersionSelectorStrategy = nameof(AllCompatibleVersions);
});Backward Compatibility Rules
- Never change signatures of existing methods
- Never rename parameters (position-based serialization)
- Add new methods in new versions instead of modifying existing
- Use two-step deprecation: mark
[Obsolete]in V2, remove in V3 after V1 decommissioned - Limitations: no versioning on stateless workers
Deployment Strategies
- Rolling upgrade: deploy newer silos directly, use
BackwardCompatible+AllCompatibleVersions - Staging environment: deploy V2 in staging slot joining same cluster, use
BackwardCompatible+MinimumVersion, VIP swap when validated
Activation Collection
Automatic removal of idle grain activations. Default collection age: 15 minutes (Orleans 7+).
siloBuilder.Configure<GrainCollectionOptions>(options =>
{
options.CollectionAge = TimeSpan.FromMinutes(10);
options.ClassSpecificCollectionAge[typeof(MyGrain).FullName!] =
TimeSpan.FromMinutes(5);
});
// In grain code
this.DelayDeactivation(TimeSpan.FromHours(1)); // delay collection
this.DeactivateOnIdle(); // deactivate ASAP
// Prevent collection for a grain type
[KeepAlive]
public class PermanentGrain : Grain, IPermanentGrain { }What counts as active: receiving a method call, reminder, or streaming event. NOT: outbound calls, timer events, arbitrary I/O.
Error Handling
- Exceptions propagate across hosts with async/distributed try/catch semantics
InconsistentStateExceptioncauses grain deactivation; other exceptions do not- Read failures during activation fail the activation
- Write failures fault the
WriteStateAsyncTask
Grains, State, and Runtime Primitives
Use this reference when the main question is inside grain design rather than hosting or deployment.
Core Grain Modeling
| Need | Official Source | What It Covers |
|---|---|---|
| Start with the grain programming model | Develop grains | Grain classes, interfaces, and the core programming surface |
| Understand grain references | Grain references | How grains are addressed and invoked |
| Pick the right identity shape | Grain identity | Keys, namespaces, and identity semantics |
| Understand default placement | Grain placement | Runtime placement model and locality tradeoffs |
| Filter or constrain placement | Grain placement filtering | Placement filters and targeting rules |
| Add extension points to grains | Grain extensions | Grain extension patterns |
| Generate serializers and proxies correctly | Code generation | Codegen expectations and generated artifacts |
Timers, Reminders, and Execution Flow
| Need | Official Source | What It Covers |
|---|---|---|
| Choose timers vs reminders | Timers and reminders | Activation-local timers versus durable reminders |
| Push updates back to clients | Observers | Grain observers and callback patterns |
| Cancel grain work safely | Cancellation tokens | Cancellation behavior across grain calls |
| Reason about reentrancy and ordering | Request scheduling | Scheduler rules, interleaving, and request ordering |
| Flow ambient metadata | Request context | Request-scoped metadata across calls |
| Hook into activation stages | Grain lifecycle | Lifecycle stages and activation events |
| Offload stateless fan-out | Stateless worker grains | Stateless scaling patterns |
| Use external tasks safely | External tasks and grains | Mixing Orleans scheduling with external async work |
| Add interceptors or filters | Interceptors | Cross-cutting interception points |
| Create runtime helper services | GrainServices | Cluster-local services for shared runtime behavior |
| Use fire-and-forget deliberately | One-way requests | One-way call semantics and limits |
Persistence and State
| Need | Official Source | What It Covers |
|---|---|---|
| Persist grain state | Grain persistence | Persistent state model and provider wiring |
| Use Azure Cosmos DB storage | Azure Cosmos DB persistence | Cosmos-backed state provider setup |
| Use relational storage | Relational storage (ADO.NET) | SQL-backed provider options |
| Use Azure Storage | Azure storage persistence | Azure Table/Blob-backed state provider guidance |
| Use DynamoDB | Amazon DynamoDB storage | DynamoDB-backed persistence options |
Event Sourcing
| Need | Official Source | What It Covers |
|---|---|---|
| Decide whether to use event sourcing | Event sourcing overview | Journaled grain model and tradeoffs |
Start with JournaledGrain | JournaledGrain basics | Core journaled grain API and state evolution |
| Diagnose journaled grains | JournaledGrain diagnostics | Troubleshooting and diagnostics for journaled grains |
| Choose confirmation mode | Immediate vs delayed confirmation | Consistency and confirmation tradeoffs |
| Publish event notifications | Notifications | Observer/notification patterns for journaled grains |
| Configure event sourcing | Event sourcing configuration | Provider and configuration model |
| Review built-in providers | Built-in log-consistency providers | Available log consistency implementations |
| Understand replicated instances | Replicated instances | Multi-instance replication behavior |
Transactions and Versioning
| Need | Official Source | What It Covers |
|---|---|---|
| Use transactional state | Transactions | Orleans ACID transaction model |
| Plan contract evolution | Grain versioning overview | Interface and implementation versioning |
| Preserve compatibility | Backward compatibility guidelines | Safe versioning rules |
| Mark compatible implementations | Compatible grains | Compatibility declarations |
| Control version selection | Version selector strategy | Version routing rules |
| Roll out new grain versions | Deploying new versions of grains | Deployment workflow for upgrades |
Usage Guidance
- Start here when the dominant question is grain boundaries, runtime primitives, or state semantics.
- Jump to hosting.md when the problem is cluster wiring, clients, observability, or deployment.
- Jump to implementation.md when you need runtime-internals or testing details.
Hosting, Configuration, and Operations
Use this reference when the main question is about running Orleans, wiring providers, or operating a cluster.
Host and Client Entry Points
| Need | Official Source | What It Covers |
|---|---|---|
| Connect external processes to a cluster | Clients | UseOrleansClient, gateways, and client topology |
| Add operational visibility | Dashboard | Orleans Dashboard setup and operational usage |
| Wire Orleans through Aspire | .NET Aspire integration | AppHost resources, .AsClient(), and orchestration wiring |
| Understand silo host stages | Silo lifecycle | Silo startup and shutdown lifecycle |
| Run mixed silo roles | Heterogeneous silos | Different silo capabilities in one cluster |
| Reason about activation lookups | Grain directory | Directory behavior and placement lookup mechanics |
| Secure transport | Transport Layer Security (TLS) | TLS between Orleans cluster participants |
Configuration Guide
| Need | Official Source | What It Covers |
|---|---|---|
| Start with cluster configuration | Configuration overview | Main configuration surface |
| Set up local development | Local development configuration | Dev cluster setup and local defaults |
| Configure clients | Client configuration | Client-side settings and connectivity |
| Configure silos | Server configuration | Silo-side options and runtime wiring |
| Review common recipes | Typical configurations | Canonical configuration examples |
| Look up available options | List of options classes | Option types exposed by Orleans |
| Add metadata to silos | Silo metadata | Metadata and node labeling |
| Tune deactivation | Activation collection | Activation cleanup and collection rules |
| Tune GC for Orleans | Configure .NET garbage collection | GC recommendations for Orleans hosts |
| Configure relational providers | Configure ADO.NET providers | ADO.NET provider registration and setup |
| Set up ADO.NET databases | ADO.NET database configuration | Database-side setup for Orleans SQL providers |
| Understand Orleans serialization | Serialization overview | Serializer model and contracts |
| Use immutable types | Serialization of immutable types | Immutable-type handling |
| Configure serialization | Configure serialization | Serializer configuration switches |
| Customize serializers | Customize serialization | Custom serializers and codecs |
| Run startup hooks | Startup tasks | Startup task registration |
| Shut clusters down cleanly | Graceful shutdown | Drain and shutdown behavior |
Observability
| Need | Official Source | What It Covers |
|---|---|---|
| Start with monitoring | Observability overview | Logs, metrics, and monitoring guidance |
| Decode silo-side errors | Silo error code monitoring | Error-code reference for silo issues |
| Decode client-side errors | Client error code monitoring | Error-code reference for clients |
Orleans 10.2 moved metrics access away from the removed static meter path. When upgrading code that directly referenced Orleans runtime metrics, resolve OrleansInstruments from DI and use its Meter instead of hardcoded static access.
For Orleans.Journaling, JSON Lines is the default format for new writes in 10.2. Existing journals with stored format metadata continue to read correctly; pin JournaledStateManagerOptions.JournalFormatKey only when a deployment must keep writing the old binary format.
Deployment and Failures
| Need | Official Source | What It Covers |
|---|---|---|
| Start from deployment basics | Running the app | Deployment overview and entry points |
| Deploy to Azure App Service | Azure App Service | App Service hosting guidance |
| Deploy to Azure Container Apps | Azure Container Apps | ACA deployment shape |
| Deploy to Kubernetes | Kubernetes | K8s deployment and clustering |
| Deploy to Service Fabric | Service Fabric | Service Fabric runtime integration |
| Handle cluster failures | Handle failures | Failure modes and recovery patterns |
| Deploy with Consul | Consul deployments | Consul-based clustering |
| Troubleshoot deployments | Troubleshoot deployments | Common deployment diagnostics |
| Troubleshoot legacy Azure Cloud Services | Azure Cloud Services troubleshooting | Legacy deployment troubleshooting |
Usage Guidance
- Start here when the problem is cluster wiring, clients, provider registration, or operational readiness.
- Use grains.md when the problem is inside a grain rather than in the hosting model.
- Use implementation.md when you need runtime internals, messaging guarantees, or testing behavior.
- Use testing-patterns.md when the hosting question is specifically about mixing Orleans, Aspire,
WebApplicationFactory, SignalR, or Playwright in integration tests.
Implementation Details, Runtime Internals, and Testing
Use this reference when architecture decisions depend on Orleans internals, scheduler rules, delivery guarantees, stream internals, or test-cluster behavior.
Implementation Overview
Orleans runtime is built on a few core subsystems:
flowchart TD
C["Client / Gateway"] --> M["Messaging Layer"]
M --> S["Scheduler"]
S --> A["Activation Catalog"]
A --> G["Grain Directory"]
G --> P["Placement Director"]
M --> ST["Streaming Runtime"]
A --> PS["Persistence / State"]
CLM["Cluster Management"] --> G
CLM --> LB["Load Balancer"]Key principle: grains are virtual — they always exist logically, and the runtime activates/deactivates physical instances as needed. The grain directory maps identities to activations, the scheduler enforces single-threaded execution per grain, and the messaging layer routes calls across silos.
Grain Directory Internals
The grain directory maintains a mapping: GrainId → (SiloAddress, ActivationId).
How Activation Works
1. Client or grain makes a call to GrainId 2. Runtime checks local cache for existing activation 3. On cache miss, queries the grain directory 4. If no activation exists, directory picks a silo (via placement) and creates one 5. Directory registers the new activation 6. Future calls route directly to the registered silo
Directory Partitioning
The default distributed directory uses a DHT (Distributed Hash Table):
- Each silo owns a range of the grain ID hash space
- Directory lookups are single-hop: hash the grain ID → find owning silo → query that silo
- On silo failure, its directory partition is rebuilt from surviving silos
Consistency
- Default (eventually consistent): may allow brief duplicate activations during cluster instability. The duplicate is detected and one is deactivated.
- Strong consistency (Orleans 10 preview): uses versioned range locks, 30 virtual nodes per silo. Prevents duplicate activations entirely. Enable with
builder.AddDistributedGrainDirectory().
Per-Grain-Type Directory
[GrainDirectory(GrainDirectoryName = "my-directory")]
public class MyGrain : Grain, IMyGrain { }
// Register external directory
siloBuilder.AddRedisGrainDirectory("my-directory", options => { });Available backends: In-Cluster (default), ADO.NET, Azure Table, Redis, Cosmos DB.
Orleans Lifecycle
Orleans uses an observable lifecycle pattern for ordered startup and shutdown of components.
Silo Lifecycle Stages
flowchart LR
S1["First<br/>int.MinValue"] --> S2["RuntimeInitialize<br/>2000"]
S2 --> S3["RuntimeServices<br/>4000"]
S3 --> S4["RuntimeStorageServices<br/>6000"]
S4 --> S5["RuntimeGrainServices<br/>8000"]
S5 --> S6["ApplicationServices<br/>10000"]
S6 --> S7["BecomeActive<br/>Active-1"]
S7 --> S8["Active<br/>20000"]
S8 --> S9["Last<br/>int.MaxValue"]| Stage | Value | What Happens |
|---|---|---|
First | int.MinValue | Earliest possible stage |
RuntimeInitialize | 2000 | Threading initialization |
RuntimeServices | 4000 | Networking, messaging, agents started |
RuntimeStorageServices | 6000 | Storage providers initialized |
RuntimeGrainServices | 8000 | Grain type management, membership joined, grain directory started |
ApplicationServices | 10000 | Application-layer services initialized |
ValidateInitialConnectivity | before BecomeActive | Orleans 10.2 validates initial peer connectivity while the silo is still Joining |
BecomeActive | Active - 1 | Silo joins the cluster |
Active | 20000 | Ready for workload — grains can be activated |
Last | int.MaxValue | Latest possible stage |
Shutdown reverses the order.
Participation API
Components participate via ILifecycleParticipant<ISiloLifecycle>:
public class MyComponent : ILifecycleParticipant<ISiloLifecycle>
{
public void Participate(ISiloLifecycle lifecycle)
{
lifecycle.Subscribe<MyComponent>(
ServiceLifecycleStage.ApplicationServices,
onStart: async ct =>
{
// Initialization logic
},
onStop: async ct =>
{
// Cleanup logic
});
}
}
// Register in DI
services.AddSingleton<ILifecycleParticipant<ISiloLifecycle>, MyComponent>();Grain Lifecycle Stages
Grain-level lifecycle (distinct from silo lifecycle):
| Stage | Value | What Happens |
|---|---|---|
First | int.MinValue | Earliest |
SetupState | 1000 | Loads persistent state from storage |
Activate | 2000 | Calls OnActivateAsync / OnDeactivateAsync |
Last | int.MaxValue | Latest |
Override Grain.Participate(IGrainLifecycle) to hook into grain-level lifecycle.
Logging
At Information level on Orleans.Runtime.SiloLifecycleSubject, logs which components participate at each stage and timing.
Messaging Delivery Guarantees
Default: At-Most-Once
Orleans delivers messages at most once by default. A message is either delivered exactly once or not at all — never duplicated.
- Every message has an automatic configurable timeout
- On timeout, the caller's
Taskis faulted with a timeout exception - No automatic retries by default
With Retries: At-Least-Once
If the application implements retry logic (e.g., via Polly), delivery becomes at-least-once: the message may arrive multiple times. Orleans does not deduplicate messages.
Timeout Behavior
// Global timeout
siloBuilder.Configure<SiloMessagingOptions>(options =>
{
options.ResponseTimeout = TimeSpan.FromSeconds(30); // default
});
// Per-method timeout
[ResponseTimeout("00:00:05")]
Task<Result> TimeSensitiveCall();Failure Scenarios
| Scenario | What Happens |
|---|---|
| Target silo alive | Message delivered, response returned |
| Target silo dead (detected) | Grain reactivated on another silo, message re-routed |
| Target silo dead (not yet detected) | Timeout → exception → caller retries → grain activates elsewhere |
| Network partition | Timeout → exception to caller |
| Grain method throws | Exception propagated back to caller |
Grain OnActivateAsync throws | Activation fails, exception to caller |
Key Guarantee
With infinite retries, eventual delivery is guaranteed because grains never enter a permanent failure state — failed grains reactivate on another silo automatically.
Scheduler
Orleans uses a cooperative, single-threaded-per-grain scheduler built on the .NET Thread Pool.
Core Rules
1. Single-threaded execution: a grain never executes on more than one thread simultaneously (unless [Reentrant]) 2. Turn-based: each "turn" runs to the next await or completion. State only changes between turns. 3. No preemption: long-running synchronous code blocks the grain's scheduler slot 4. Cooperative multitasking: grains yield at await points
Task Scheduling Behavior Within Grain Code
| API | Where It Runs |
|---|---|
await task | Resumes on grain scheduler |
Task.Factory.StartNew(delegate) | Runs on grain scheduler |
ContinueWith(delegate) | Runs on grain scheduler |
Task.WhenAll, Task.WhenAny | Continuation on grain scheduler |
Task.Delay | Continuation on grain scheduler |
Task.Run(delegate) | Delegate on thread pool; await resumes on grain scheduler |
ConfigureAwait(false) | NEVER use — escapes grain scheduler |
async void | NEVER use in grain code |
Thread Pool Usage
Orleans runs grain turns on the .NET Thread Pool with cooperative scheduling. With proper async code, can achieve 90%+ CPU utilization with stability.
// Background work on thread pool (OK)
var result = await Task.Run(() => CpuBoundWork(data));
// Resumes here on grain scheduler
// WRONG — deadlock risk
var bad = task.Result; // NEVER block in grain codeScheduling Options
siloBuilder.Configure<SchedulingOptions>(options =>
{
options.AllowCallChainReentrancy = false; // default
options.PerformDeadlockDetection = true; // default (dev)
});Reentrancy and Interleaving
Default non-reentrant: each request runs to completion. With [Reentrant], multiple requests can interleave at await points. See grain-api.md ## Request Scheduling and Reentrancy for full details.
Cluster Management
Fully distributed peer-to-peer membership protocol with no central coordinator.
Protocol Overview
flowchart TD
S1["Silo 1"] -->|"Probe every 10s"| S2["Silo 2"]
S1 -->|"Probe every 10s"| S3["Silo 3"]
S2 -->|"Probe every 10s"| S1
S2 -->|"Probe every 10s"| S3
S3 -->|"Probe every 10s"| S1
S3 -->|"Probe every 10s"| S2
S1 & S2 & S3 -->|"IAmAlive every 30s"| MT["IMembershipTable<br/>(Azure Table / Redis / SQL / ...)"]Configuration
siloBuilder.Configure<ClusterMembershipOptions>(options =>
{
options.NumProbedSilos = 10; // how many silos monitor each silo (default 10 in 9.x+, was 3)
options.NumVotesForDeathDeclaration = 2; // votes needed to declare dead
options.DeathVoteExpirationTimeout = TimeSpan.FromSeconds(180); // vote TTL
options.ProbeTimeout = TimeSpan.FromSeconds(10); // probe interval
options.NumMissedProbesLimit = 3; // missed probes before suspicion
});Failure Detection Timeline
Typical: ~15 seconds (Orleans 9+) from silo crash to detection.
1. Monitoring silos send probes every 10 seconds 2. After 3 missed probes (30s), silo is suspected 3. 2 independent suspicions trigger death declaration 4. Dead silo evicted from cluster, its activations destroyed 5. Grains reactivate on other silos on next call
Protocol Properties
- Handles any number of simultaneous failures (f ≤ n), including full cluster restart
- Light table traffic: probes go direct silo-to-silo, not through
IMembershipTable - Self-monitoring with Lifeguard-inspired health scoring (unhealthy silos get increased probe timeouts)
- Indirect probing for accuracy improvement
- Table unavailability never causes false death declarations
IAmAlivewrites every 30 seconds for diagnostics and disaster recovery- Ordered membership views with guaranteed connectivity on join
- Dead silos forced to terminate and restart as new processes
IMembershipTable Implementations
| Provider | Package |
|---|---|
| Azure Table Storage | Microsoft.Orleans.Clustering.AzureStorage |
| Redis | Microsoft.Orleans.Clustering.Redis |
| ADO.NET (SQL/PostgreSQL/MySQL/Oracle) | Microsoft.Orleans.Clustering.AdoNet |
| Cosmos DB | Microsoft.Orleans.Clustering.Cosmos |
| DynamoDB | Microsoft.Orleans.Clustering.DynamoDB |
| Consul | Microsoft.Orleans.Clustering.Consul |
| ZooKeeper | Microsoft.Orleans.Clustering.ZooKeeper |
| In-Memory (dev only) | built-in |
Streams Implementation
Architecture
The streaming runtime uses a pulling model with agents inside silos:
flowchart LR
P["Producers"] -->|"OnNextAsync"| Q["Queue<br/>(Event Hubs / Azure Queue / Memory)"]
Q -->|"Pull"| PA["Pulling Agent<br/>(per silo)"]
PA -->|"Deliver"| G["Consumer Grains"]
PA -->|"Checkpoint"| CP["Checkpoint Store"]- Pulling agents run inside each silo, one per queue partition
- Agents pull batches of events from the underlying queue
- Events are dispatched to consumer grains via
IAsyncObserver<T> - Checkpoint position tracked in a separate storage provider
Pub-Sub System
Stream subscriptions are managed by PubSubRendezvousGrain:
- One rendezvous grain per
StreamId - Stores list of subscribers
- Persisted via the
"PubSubStore"storage provider - Implicit subscriptions (
[ImplicitStreamSubscription]) registered automatically
Azure Queue Streams Implementation
NuGet: Microsoft.Orleans.Streaming.AzureStorage
siloBuilder.AddAzureQueueStreams("AQProvider", optionsBuilder =>
optionsBuilder.ConfigureAzureQueue(options =>
options.Configure(opt =>
{
opt.QueueServiceClient = new QueueServiceClient(endpoint, credential);
opt.QueueNames = new List<string> { "queue1", "queue2" }; // optional
})));Behavior:
- Uses Azure Storage Queues as the backing store
- Pulling agents poll queues at configurable intervals
- Not rewindable — cannot replay from arbitrary position
- Does not guarantee FIFO on failures (poison messages re-queued)
- Multiple queues for parallelism
- Automatic queue assignment to silo agents via consistent hashing
Event Hubs vs Azure Queue
| Feature | Event Hubs | Azure Queue |
|---|---|---|
| Rewindable | Yes (replay from token) | No |
| FIFO guarantee | Per partition | Not on failures |
| Throughput | High (millions/sec) | Moderate |
| Cost | Higher | Lower |
| Checkpointing | Yes (via checkpoint store) | Via queue dequeue |
| Use case | High-volume event streaming | Simple message queuing |
Load Balancing
Orleans uses multiple mechanisms to distribute load across the cluster:
Placement-Based Balancing
The placement strategy determines where new activations are created:
| Strategy | Load Distribution |
|---|---|
ResourceOptimizedPlacement (default 9.2+) | Weighted scoring: CPU (40), memory (20), available memory (20), max memory (5), activation count (15) |
ActivationCountBasedPlacement | Power of Two Choices — pick two random silos, place on the one with fewer activations |
RandomPlacement | Uniform random across compatible silos |
PreferLocalPlacement | Local first, then random |
Activation Repartitioning (Experimental)
Monitors grain-to-grain communication patterns and migrates grains closer to frequent communication partners.
#pragma warning disable ORLEANSEXP001
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001Uses probabilistic tracking (sampling) and anchoring filters to limit migration churn.
Activation Rebalancing (Experimental, Orleans 10)
Cluster-wide redistribution for memory and activation count balance.
#pragma warning disable ORLEANSEXP002
siloBuilder.AddActivationRebalancer();
#pragma warning restore ORLEANSEXP002Uses entropy calculations to detect imbalance and session-based execution to coordinate rebalancing.
Memory-Based Activation Shedding (Orleans 9+)
Auto-deactivates least-recently-used grains when memory exceeds threshold:
services.Configure<GrainCollectionOptions>(options =>
{
options.EnableActivationSheddingOnMemoryPressure = true;
options.MemoryUsageLimitPercentage = 80; // start shedding
options.MemoryUsageTargetPercentage = 75; // stop shedding
options.MemoryUsagePollingPeriod = TimeSpan.FromSeconds(5);
});Unit Testing
InProcessTestCluster (Orleans 9+, Recommended)
// Setup
var builder = new InProcessTestClusterBuilder();
builder.ConfigureSilo((options, siloBuilder) =>
{
siloBuilder.AddMemoryGrainStorage("Default");
siloBuilder.AddMemoryGrainStorage("PubSubStore");
siloBuilder.UseInMemoryReminderService();
});
var cluster = builder.Build();
await cluster.DeployAsync();
// Test
var grain = cluster.Client.GetGrain<IPlayerGrain>("player-1");
await grain.UpdateScore(100);
var state = await grain.GetState();
Assert.Equal(100, state.Score);
// Cleanup
await cluster.DisposeAsync();Options: InitialSilosCount (default 1), InitializeClientOnDeploy (default true), ConfigureFileLogging (default true), GatewayPerSilo (default true).
Dynamic Silo Management
// Add silo to running cluster
var newSilo = await cluster.StartSiloAsync();
// Stop specific silo (simulate failure)
await cluster.StopSiloAsync(newSilo);
// Restart entire cluster
await cluster.RestartAsync();TestCluster (Legacy, Still Supported)
var builder = new TestClusterBuilder();
builder.AddSiloBuilderConfigurator<TestSiloConfigurator>();
var cluster = builder.Build();
cluster.Deploy(); // synchronous
public class TestSiloConfigurator : ISiloConfigurator
{
public void Configure(ISiloBuilder siloBuilder)
{
siloBuilder.AddMemoryGrainStorage("Default");
}
}Multi-Silo Testing
var builder = new InProcessTestClusterBuilder();
builder.InitialSilosCount = 3; // start with 3 silos
// Test placement, failover, reminders, etc.
var grain = cluster.Client.GetGrain<IMyGrain>("key");
await grain.DoWork();
// Kill a silo and verify grain reactivates
var siloToKill = cluster.Silos[1];
await cluster.StopSiloAsync(siloToKill);
// Grain auto-reactivates on next call
var result = await grain.DoWork(); // succeeds on different siloxUnit Fixture Sharing
public class ClusterFixture : IAsyncLifetime
{
public InProcessTestCluster Cluster { get; private set; } = null!;
public async Task InitializeAsync()
{
var builder = new InProcessTestClusterBuilder();
builder.ConfigureSilo((_, silo) => silo.AddMemoryGrainStorage("Default"));
Cluster = builder.Build();
await Cluster.DeployAsync();
}
public async Task DisposeAsync() => await Cluster.DisposeAsync();
}
[CollectionDefinition("Orleans")]
public class ClusterCollection : ICollectionFixture<ClusterFixture> { }
[Collection("Orleans")]
public class MyGrainTests
{
private readonly InProcessTestCluster _cluster;
public MyGrainTests(ClusterFixture fixture) => _cluster = fixture.Cluster;
[Fact]
public async Task TestGrainBehavior()
{
var grain = _cluster.Client.GetGrain<IMyGrain>("test");
var result = await grain.DoWork();
Assert.NotNull(result);
}
}Mocking Approach
// Override GrainFactory for mocking
public class TestableOrderGrain : OrderGrain
{
public new virtual IGrainFactory GrainFactory { get; set; }
}
// With Moq
var mockInventory = new Mock<IInventoryGrain>();
mockInventory.Setup(i => i.Reserve(It.IsAny<int>())).Returns(Task.CompletedTask);
var mockFactory = new Mock<IGrainFactory>();
mockFactory.Setup(f => f.GetGrain<IInventoryGrain>(It.IsAny<string>(), null))
.Returns(mockInventory.Object);Alternative: OrleansTestKit from OrleansContrib provides unit-test-friendly grain activation with less ceremony.
Tutorials, Samples, and Resource Pages
| Need | Official Source |
|---|---|
| Browse tutorials and samples | Code samples overview |
| Hello World tutorial | Hello World |
| Orleans basics tutorial | Tutorial 1 |
| Adventure game sample | Adventure |
| Custom grain storage | Custom storage sample |
| Design principles | Architecture principles |
| When Orleans fits | Applicability |
| NuGet package map | NuGet packages |
| Best practices | Best practices |
| FAQ | FAQ |
| External links | Links |
API Reference and Source Entry Points
| Need | Official Source |
|---|---|
| Core API | Orleans.Core |
| Runtime API | Orleans.Runtime |
| Streams API | Orleans.Streams |
| Source repo | dotnet/orleans |
| Official samples | dotnet/samples Orleans |
| Repo samples README | Samples README |
Usage Guidance
- Start here when the problem depends on scheduler rules, runtime delivery guarantees, stream internals, cluster management, or test-cluster behavior.
- Use grain-api.md for grain-level API details (reentrancy, timers, placement).
- Use configuration-api.md for operational setup (deployment, observability, providers).
- Use examples.md for example-first navigation.
- Use official-docs-index.md for the full documentation tree.
Serialization API Reference
Detailed serialization patterns from official Orleans documentation. Orleans uses two kinds of serialization: grain call serialization (between grains/clients) and grain storage serialization (persistence).
Core Attributes
[GenerateSerializer]
Required on all types passed between grains, stored in state, or used in streams.
[GenerateSerializer]
public class PlayerState
{
[Id(0)] public string Name { get; set; } = "";
[Id(1)] public int Level { get; set; }
[Id(2)] public List<string> Achievements { get; set; } = [];
}[Id(N)]
Stable member identification. Rules:
- Each serialized member needs a unique
[Id(N)] - Adding new
[Id]members is safe (backward compatible) - Removing members is safe if the
[Id]is not reused - Changing member types is a breaking change
- IDs are per-type, not global
[Alias("name")]
Type aliases for safe renaming:
[GenerateSerializer]
[Alias("player-state-v1")]
public class PlayerState { }Allows type to be renamed without breaking deserialization.
[Immutable]
Skip copy overhead for immutable types:
[Immutable]
[GenerateSerializer]
public record SensorReading(
[property: Id(0)] string SensorId,
[property: Id(1)] double Value,
[property: Id(2)] DateTime Timestamp);Can also be applied per-parameter or per-property:
Task ProcessData([Immutable] LargePayload data);Versioning Rules
| Change | Safe? |
|---|---|
Add new [Id] member | Yes |
| Remove member (don't reuse ID) | Yes |
| Rename member (keep same ID) | Yes |
| Change member type | No |
Rename type (with [Alias]) | Yes |
Rename type (without [Alias]) | No |
Surrogates
Serialize types you don't own:
// Surrogate for DateTimeOffset
[GenerateSerializer]
public struct DateTimeOffsetSurrogate
{
[Id(0)] public long Ticks;
[Id(1)] public short OffsetMinutes;
}
[RegisterConverter]
public sealed class DateTimeOffsetConverter :
IConverter<DateTimeOffset, DateTimeOffsetSurrogate>
{
public DateTimeOffset ConvertFromSurrogate(in DateTimeOffsetSurrogate s) =>
new(s.Ticks, TimeSpan.FromMinutes(s.OffsetMinutes));
public DateTimeOffsetSurrogate ConvertToSurrogate(in DateTimeOffset value) =>
new() { Ticks = value.Ticks, OffsetMinutes = (short)value.Offset.TotalMinutes };
}Copier
Orleans copies objects by default to prevent grain state corruption from accidental mutation.
// Custom copier
[RegisterCopier]
public sealed class MyTypeCopier : IDeepCopier<MyType>
{
public MyType DeepCopy(MyType input, CopyContext context) =>
new MyType { Value = input.Value };
}Use [Immutable] to skip copying entirely for types that are never mutated after creation.
Grain Storage Serialization
Configurable per provider. Default uses Newtonsoft.Json for stored state.
// Use Orleans native format
siloBuilder.AddRedisGrainStorage("redis", options =>
{
options.GrainStorageSerializer = new OrleansGrainStorageSerializer(
serializerSessionPool);
});
// Custom serializer
public class MySerializer : IGrainStorageSerializer
{
public BinaryData Serialize<T>(T value) { }
public T Deserialize<T>(BinaryData data) { }
}Serialization of Immutable Types
Immutable types skip the copy step, improving performance for read-heavy grain communication.
Type-Level Immutability
[Immutable]
[GenerateSerializer]
public record SensorReading(
[property: Id(0)] string SensorId,
[property: Id(1)] double Value,
[property: Id(2)] DateTime Timestamp);Member-Level Immutability
[GenerateSerializer]
public class MyGrainState
{
[Id(0), Immutable] public IReadOnlyList<string> Tags { get; set; } = [];
[Id(1)] public int MutableCount { get; set; }
}Parameter-Level Immutability
public interface IMyGrain : IGrainWithStringKey
{
Task ProcessData([Immutable] LargePayload data);
}When [Immutable] is applied, Orleans trusts that the object will not be mutated after it's passed. Violating this contract can corrupt grain state.
Configure Serialization
Serializer Selection
Orleans uses its own high-performance serializer by default. Configuration options:
// Use System.Text.Json for specific types
siloBuilder.Services.AddSerializer(builder =>
{
builder.AddJsonSerializer(
isSupported: type => type.Namespace?.StartsWith("MyApp.Dto") == true);
});
// Use Newtonsoft.Json for specific types
siloBuilder.Services.AddSerializer(builder =>
{
builder.AddNewtonsoftJsonSerializer(
isSupported: type => type.GetCustomAttribute<JsonObjectAttribute>() != null);
});External Serializer Packages
| Package | Serializer |
|---|---|
Microsoft.Orleans.Serialization.SystemTextJson | System.Text.Json |
Microsoft.Orleans.Serialization.NewtonsoftJson | Newtonsoft.Json |
Microsoft.Orleans.Serialization.MessagePack | MessagePack |
Microsoft.Orleans.Serialization.Protobuf | Protobuf |
Microsoft.Orleans.Serialization.FSharp | F# types |
Customize Serialization
Custom Serializer
[RegisterSerializer]
public sealed class MyTypeSerializer : IFieldCodec<MyType>
{
public void WriteField<TBufferWriter>(ref Writer<TBufferWriter> writer,
uint fieldIdDelta, Type expectedType, MyType value)
where TBufferWriter : IBufferWriter<byte>
{
StringCodec.WriteField(ref writer, fieldIdDelta, value.ToString());
}
public MyType ReadValue<TInput>(ref Reader<TInput> reader, Field field)
{
return MyType.Parse(StringCodec.ReadValue(ref reader, field));
}
}Custom Copier
[RegisterCopier]
public sealed class MyTypeCopier : IDeepCopier<MyType>
{
public MyType DeepCopy(MyType input, CopyContext context) =>
new MyType(input.Value); // create independent copy
}Surrogates for External Types
For types you don't own, create a surrogate with converter:
[GenerateSerializer]
public struct DateTimeOffsetSurrogate
{
[Id(0)] public long Ticks;
[Id(1)] public short OffsetMinutes;
}
[RegisterConverter]
public sealed class DateTimeOffsetConverter :
IConverter<DateTimeOffset, DateTimeOffsetSurrogate>
{
public DateTimeOffset ConvertFromSurrogate(in DateTimeOffsetSurrogate s) =>
new(s.Ticks, TimeSpan.FromMinutes(s.OffsetMinutes));
public DateTimeOffsetSurrogate ConvertToSurrogate(in DateTimeOffset value) =>
new() { Ticks = value.Ticks, OffsetMinutes = (short)value.Offset.TotalMinutes };
}Grain Storage Serializer Override
// Default: Newtonsoft.Json for stored state
// Override per provider:
siloBuilder.AddRedisGrainStorage("redis", options =>
{
// Use Orleans native format instead of JSON
options.GrainStorageSerializer = new OrleansGrainStorageSerializer(
serializerSessionPool);
});
// Or implement custom
public class MyGrainStorageSerializer : IGrainStorageSerializer
{
public BinaryData Serialize<T>(T value) { /* ... */ }
public T Deserialize<T>(BinaryData data) { /* ... */ }
}Common Serialization Mistakes
| Mistake | Fix |
|---|---|
Missing [GenerateSerializer] | Add to all grain state and message types |
Missing [Id(N)] | Add unique IDs to all serialized members |
| Non-serializable fields in state | Mark with [NonSerialized] or inject via DI |
Reusing [Id] after removal | Use a new unused ID number |
Storing HttpClient / Action in state | Inject as service, not in state |