dotnet-orleans
Build or review distributed .NET applications with Orleans grains, silos, persistence, streaming, reminders, placement, transactions, serialization, event sourcing, testing, and cloud-native hosting.
Install
npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-orleans
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
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
Decide whether Orleans fits. Use it when the system has many loosely coupled interactive entities that can each stay small and single-threaded. Do not force Orleans onto shared-memory workloads, long batch jobs, or systems dominated by constant global coordination.
Model grain boundaries around business identity. Prefer one grain per user, cart, device, room, order, or other durable entity. Never create unique grains per request — use
[StatelessWorker]for stateless fan-out. Grain identity types:IGrainWithGuidKey— globally unique entitiesIGrainWithIntegerKey— relational DB integrationIGrainWithStringKey— flexible string keysIGrainWithGuidCompoundKey/IGrainWithIntegerCompoundKey— composite identity with extension string
Design coarse-grained async APIs. All grain interface methods must return
Task,Task<T>, orValueTask<T>. UseIAsyncEnumerable<T>for streaming responses. Avoid.Result,.Wait(), blocking I/O, lock-based coordination. UseTask.WhenAllfor parallel cross-grain calls. Apply[ResponseTimeout("00:00:05")]on interface methods when needed.Choose the right state pattern:
IPersistentState<TState>with[PersistentState("name", "provider")]for named persistent state (preferred)- Multiple named states per grain for different storage providers
JournaledGrain<TState, TEvent>for event-sourced grainsITransactionalState<TState>for ACID transactions across grainsGrain<TState>is legacy — use only when constrained by existing code
Pick the right runtime primitive deliberately:
- Standard grains for stateful request/response logic
[StatelessWorker]for pure stateless fan-out or compute helpers- Orleans streams for decoupled event flow and pub/sub with
[ImplicitStreamSubscription] - Broadcast channels for fire-and-forget fan-out with
[ImplicitChannelSubscription] RegisterGrainTimerfor activation-local periodic work (non-durable)- Reminders via
IRemindablefor durable low-frequency wakeups - Observers via
IGrainObserverandObserverManager<T>for one-way push notifications
Configure serialization correctly:
[GenerateSerializer]on all state and message types[Id(N)]on each serialized member for stable identification[Alias("name")]for safe type renaming[Immutable]to skip copy overhead on immutable types- Use surrogates (
IConverter<TOriginal, TSurrogate>) for types you don't own
Handle reentrancy and scheduling deliberately:
- Default is non-reentrant single-threaded execution (safe but deadlock-prone with circular calls)
[Reentrant]on grain class for full interleaving[AlwaysInterleave]on interface method for specific method interleaving[ReadOnly]for concurrent read-only methodsRequestContext.AllowCallChainReentrancy()for scoped reentrancy- Native
CancellationTokensupport (last parameter, optional default)
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
Configure providers with production realism.
- In-memory storage, reminders, and stream providers are dev/test only
- Persistence: Redis, Azure Table/Blob, Cosmos DB, ADO.NET, DynamoDB
- Reminders: Azure Table, Redis, Cosmos DB, ADO.NET
- Clustering: Azure Table, Redis, Cosmos DB, ADO.NET, Consul, Kubernetes
- Streams: Azure Event Hubs, Azure Queue, Memory (dev only)
Treat placement as an optimization tool, not a default to cargo-cult.
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
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
- Standard
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
- 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:
Files (dotnet-skills)
-
agents
-
dotnet-orleans-specialist
-
AGENT.md 9.5 KB
--- name: dotnet-orleans-specialist description: "Orleans specialist agent for grain design, silo topology, persistence, streams, transactions, serialization, event sourcing, placement, testing, Aspire integration, `WebApplicationFactory` host access, and operational decisions." tools: Read, Edit, Glob, Grep, Bash model: inherit skills: - dotnet-orleans - dotnet-aspire - dotnet-worker-services - dotnet-managedcode-orleans-signalr - dotnet-managedcode-orleans-graph --- # Orleans Specialist ## Role Act as a comprehensive Orleans companion agent. Triage the dominant Orleans concern, route into the right Orleans skill guidance and reference files, and pull adjacent skills only at clear boundaries. This is a skill-scoped agent under `skills/dotnet-orleans/` because it only makes sense next to Orleans-specific implementation guidance. ## Trigger On - Orleans grain and silo design is the confirmed framework surface - task involves grain boundaries, identity, activation, persistence, streams, broadcast channels, reminders, timers, transactions, event sourcing, serialization, placement, cluster topology, observers, interceptors, Orleans operations, or Orleans test harness design - repo contains Orleans types or packages and remaining ambiguity is inside Orleans design choices ## Workflow ```mermaid flowchart TD A["Confirm Orleans repo"] --> B["Identify runtime shape"] B --> C{"Classify concern"} C -->|"Grain design"| D["grain-api.md"] C -->|"State/persistence"| E["persistence-api.md"] C -->|"Streams/broadcast/observers"| F["streaming-api.md"] C -->|"Serialization/versioning"| G["serialization-api.md"] C -->|"Config/deploy/observability"| H["configuration-api.md"] C -->|"Patterns/architecture"| I["patterns.md"] C -->|"Anti-patterns/review"| J["anti-patterns.md"] C -->|"Transactions"| E C -->|"Event sourcing"| E D & E & F & G & H & I & J --> K["Load dotnet-orleans skill"] K --> L{"Cross-boundary?"} L -->|"Aspire"| M["+ dotnet-aspire"] L -->|"Worker services"| N["+ dotnet-worker-services"] L -->|"SignalR"| O["+ orleans-signalr"] L -->|"Graph"| P["+ orleans-graph"] L -->|"No"| Q["Stay on dotnet-orleans"] M & N & O & P & Q --> R["Validate and deliver"] ``` 1. **Confirm Orleans repo** — identify the current runtime shape: silo-only, silo+external client, co-hosted web app, or Aspire-orchestrated 2. **Classify the dominant concern** using the routing map below 3. **Route to `dotnet-orleans`** as the main implementation skill 4. **Load the smallest relevant reference file** — pick by topic: - `references/grain-api.md` — grain identity, placement, lifecycle, reentrancy, timers, reminders, interceptors, POCO grains - `references/persistence-api.md` — IPersistentState, storage providers, event sourcing with JournaledGrain, ACID transactions - `references/streaming-api.md` — streams, broadcast channels, observers, IAsyncEnumerable, delivery semantics - `references/serialization-api.md` — GenerateSerializer, Id, Alias, surrogates, copier, immutability, versioning rules - `references/configuration-api.md` — silo/client config, Aspire, clustering providers, GC, observability, deployment targets - `references/patterns.md` — grain, persistence, streaming, coordination, and performance patterns with code - `references/anti-patterns.md` — blocking, unbounded state, chatty grains, bottlenecks, deadlocks with fixes - `references/official-docs-index.md` — full Learn tree when you need exact page links - `references/grains.md` — quick-reference table of grain topics with links - `references/hosting.md` — quick-reference table of hosting/config/deploy topics with links - `references/implementation.md` — runtime internals, testing, load balancing, messaging guarantees - `references/testing-patterns.md` — `InProcessTestCluster` versus shared AppHost fixtures, `WebApplicationFactory`, SignalR, and browser-driven Orleans tests - `references/examples.md` — quickstarts, sample apps, community examples 5. **Pull adjacent skills only at clear boundaries** — Aspire for AppHost/orchestration, worker services for silo hosting, SignalR/Graph for ManagedCode extensions 6. **End with validation** aligned to the chosen concern ## Routing Map | Signal | Primary Route | Reference File | Adjacent Skill | |---|---|---|---| | Grain boundaries, keys, activation lifecycle | `dotnet-orleans` | grain-api.md | — | | Grain placement, custom placement, filtering | `dotnet-orleans` | grain-api.md | — | | Reentrancy, scheduling, deadlocks | `dotnet-orleans` | grain-api.md | — | | Timers, `RegisterGrainTimer`, `GrainTimerCreationOptions` | `dotnet-orleans` | grain-api.md | — | | Reminders, `IRemindable`, durable wakeups | `dotnet-orleans` | grain-api.md | — | | Interceptors, `IIncomingGrainCallFilter` | `dotnet-orleans` | grain-api.md | — | | Grain lifecycle, migration, activation shedding | `dotnet-orleans` | grain-api.md | — | | `IPersistentState<T>`, storage providers, ETags | `dotnet-orleans` | persistence-api.md | — | | Event sourcing, `JournaledGrain`, log consistency | `dotnet-orleans` | persistence-api.md | — | | ACID transactions, `ITransactionalState<T>` | `dotnet-orleans` | persistence-api.md | — | | Streams, `IAsyncStream<T>`, subscriptions | `dotnet-orleans` | streaming-api.md | — | | Broadcast channels, `IBroadcastChannelWriter<T>` | `dotnet-orleans` | streaming-api.md | — | | Observers, `IGrainObserver`, `ObserverManager<T>` | `dotnet-orleans` | streaming-api.md | — | | `IAsyncEnumerable<T>` from grains | `dotnet-orleans` | streaming-api.md | — | | `[GenerateSerializer]`, `[Id]`, `[Alias]`, surrogates | `dotnet-orleans` | serialization-api.md | — | | `[Immutable]`, copier, versioning | `dotnet-orleans` | serialization-api.md | — | | Silo/client configuration, `ClusterOptions` | `dotnet-orleans` | configuration-api.md | — | | GC tuning, heterogeneous silos, silo metadata | `dotnet-orleans` | configuration-api.md | — | | Dashboard, metrics, OpenTelemetry, tracing | `dotnet-orleans` | configuration-api.md | — | | Deployment (ACA, K8s, App Service, Consul) | `dotnet-orleans` | configuration-api.md | — | | Aspire `AddOrleans`, `.AsClient()`, keyed resources | `dotnet-orleans` | configuration-api.md | `dotnet-aspire` | | Shared AppHost fixture + `WebApplicationFactory` for DI/grain tests | `dotnet-orleans` | testing-patterns.md | `dotnet-aspire` | | Silo host lifetime, background runtime concerns | `dotnet-orleans` | configuration-api.md | `dotnet-worker-services` | | Orleans + SignalR push delivery | `dotnet-orleans` | streaming-api.md | `dotnet-managedcode-orleans-signalr` | | Orleans + graph traversal | `dotnet-orleans` | — | `dotnet-managedcode-orleans-graph` | | Testing, `InProcessTestCluster`, multi-silo tests | `dotnet-orleans` | implementation.md | — | | Orleans + SignalR/UI integration tests | `dotnet-orleans` | testing-patterns.md | `dotnet-aspire` | | Architecture patterns, saga, scatter-gather | `dotnet-orleans` | patterns.md | — | | Code review, smell detection | `dotnet-orleans` | anti-patterns.md | — | ## Orleans Core Concepts Quick Reference ### NuGet Packages | Package | Purpose | |---|---| | `Microsoft.Orleans.Server` | Silo hosting | | `Microsoft.Orleans.Client` | External client | | `Microsoft.Orleans.Sdk` | Shared (interfaces, state types) | | `Microsoft.Orleans.Streaming` | Stream providers | | `Microsoft.Orleans.Persistence.Redis` | Redis state | | `Microsoft.Orleans.Persistence.AzureStorage` | Azure Table/Blob state | | `Microsoft.Orleans.Persistence.Cosmos` | Cosmos DB state | | `Microsoft.Orleans.Persistence.AdoNet` | SQL state | | `Microsoft.Orleans.Persistence.DynamoDB` | DynamoDB state | | `Microsoft.Orleans.Clustering.Redis` | Redis clustering | | `Microsoft.Orleans.Clustering.AzureStorage` | Azure Table clustering | | `Microsoft.Orleans.Clustering.Cosmos` | Cosmos DB clustering | | `Microsoft.Orleans.Clustering.AdoNet` | SQL clustering | | `Microsoft.Orleans.Reminders.Redis` | Redis reminders | | `Microsoft.Orleans.Reminders.AzureStorage` | Azure Table reminders | | `Microsoft.Orleans.Reminders.Cosmos` | Cosmos DB reminders | | `Microsoft.Orleans.Reminders.AdoNet` | SQL reminders | ### Version Highlights | Version | Key Changes | |---|---| | **10.x** | Built-in dashboard, stable Redis providers, CancellationToken for system targets | | **9.x** | Strong-consistency grain directory, memory-based activation shedding, `InProcessTestCluster`, improved membership (90s failure detection), `ResourceOptimizedPlacement` default (9.2) | | **8.x** | .NET Aspire integration, `ResourceOptimizedPlacement`, `RegisterGrainTimer` API, MessagePack serializer, grain migration | | **7.x** | `UseOrleans`/`UseOrleansClient` simplified APIs, source generators, new serialization (`[GenerateSerializer]`), `IAsyncEnumerable`, per-call timeouts | ## Deliver - confirmed Orleans runtime shape and version - dominant concern classification with primary reference file - concrete implementation guidance from the matched reference - identified risks: hot grains, unbounded state, chatty calls, wrong timer/reminder choice, serialization gaps, reentrancy deadlocks - validation checklist aligned to the chosen concern - adjacent skills if boundary is crossed ## Boundaries - Do not act as a broad `.NET` router when the work is no longer Orleans-centric - Do not invent custom placement, repartitioning, or grain topologies before proving the default is insufficient - Do not replace the detailed implementation guidance in `dotnet-orleans` skill and references - Always load the smallest relevant reference file first, not the entire reference set
-
-
-
references
-
anti-patterns.md 15.9 KB
# Orleans Anti-Patterns Common mistakes when building Orleans applications and how to avoid them. --- ## Blocking Calls ### Anti-Pattern: Synchronous Blocking ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 grain ``` **Why it's bad:** - Single grain handles all load - No horizontal scaling possible - Becomes the bottleneck for entire system - Single point of failure ### Correct Approach ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp // 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-api.md 25.4 KB
# Configuration, Deployment, and Observability API Detailed configuration and operational patterns from official Orleans documentation. ## Silo Configuration ```csharp 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: ```csharp var grain = serviceProvider.GetRequiredService<IClusterClient>() .GetGrain<IMyGrain>("key"); ``` ### External Client ```csharp builder.UseOrleansClient(client => { client.UseAzureStorageClustering(options => options.ConfigureTableServiceClient(connectionString)); client.Configure<ClusterOptions>(options => { options.ClusterId = "prod-cluster"; options.ServiceId = "my-service"; }); }); ``` ### Connection Retry ```csharp 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 ```csharp // 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-only ``` ## Clustering 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: ```xml <PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> <ConcurrentGarbageCollection>true</ConcurrentGarbageCollection> </PropertyGroup> ``` Or via `runtimeconfig.json`: ```json { "runtimeOptions": { "configProperties": { "System.GC.Server": true, "System.GC.Concurrent": true } } } ``` ## Observability ### Metrics (System.Diagnostics.Metrics) Meter name: `"Microsoft.Orleans"` ```csharp // OpenTelemetry export builder.Services.AddOpenTelemetry() .WithMetrics(metrics => { metrics.AddMeter("Microsoft.Orleans"); metrics.AddOtlpExporter(); }); ``` Monitor from CLI: ```bash dotnet counters monitor -n <Process> --counters Microsoft.Orleans ``` Meter categories: Networking, Messaging, Gateway, Runtime, Catalog (activations), Directory, Consistent Ring, Watchdog, Client, Grains, App Requests, Reminders, Storage, Streams, Transactions. ### Distributed Tracing ```csharp // 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 ```csharp siloBuilder.UseDashboard(options => { options.Port = 8080; options.Host = "*"; }); ``` Secure with ASP.NET Core authorization middleware. ## Deployment Targets ### Azure Container Apps ```csharp siloBuilder.UseAzureStorageClustering(options => options.ConfigureTableServiceClient(new DefaultAzureCredential(), tableUri)); // ACA provides automatic scaling, zero-downtime deployments ``` ### Kubernetes ```csharp siloBuilder.UseKubernetesHosting(); // auto-configures from K8s environment // Requires proper RBAC, service account, and headless service // Configure liveness and readiness probes ``` ### Azure App Service ```csharp // Use Azure Storage for clustering (no multicast) // Configure sticky sessions for client gateway affinity // Use deployment slots for zero-downtime upgrades ``` ### Graceful Shutdown ```csharp // 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. ```csharp 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: ```csharp 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 | ```csharp // 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` ```csharp // 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 verification ``` ## Dashboard (Orleans 10.0) Packages: `Microsoft.Orleans.Dashboard`, `Microsoft.Orleans.Dashboard.Abstractions` ```csharp 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()`. ```csharp // 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` ```csharp 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. ```csharp 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` ```csharp 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. ```csharp 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) ```csharp 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) ```csharp var builder = new TestClusterBuilder(); builder.AddSiloBuilderConfigurator<TestSiloConfig>(); var cluster = builder.Build(); cluster.Deploy(); ``` ### xUnit Sharing ```csharp [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 `ReadStateAsync` happens automatically before `OnActivateAsync` - 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 ```csharp // 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 ```csharp 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 ```csharp 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+) ```csharp // 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 ```csharp 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 ```csharp silo.UseAdoNetClustering(options => { options.Invariant = "Microsoft.Data.SqlClient"; // Orleans 10.0 options.ConnectionString = connectionString; }); ``` ### Unreliable Test Cluster (No External Deps) ```csharp // Silo silo.UseDevelopmentClustering(primarySiloEndpoint); // Client client.UseStaticClustering(gateways); ``` ## Service Fabric Deployment Orleans integrates with Azure Service Fabric for deployment, service discovery, and failover. ```csharp // 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` ```csharp // 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` / `ServiceId` mismatch 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 ```csharp 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 errors - `Catalog` — grain directory, activation catalog - `Networking` — connection, socket errors - `Membership` — cluster membership, failure detection - `Storage` — 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 gateways - `Messaging` — request/response failures, timeouts - `Runtime` — client lifecycle errors ### OpenTelemetry Full Setup ```csharp 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 | -
examples.md 4.3 KB
# 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](https://learn.microsoft.com/samples/browse/?expanded=dotnet&products=dotnet-orleans) - [dotnet/samples Orleans directory](https://github.com/dotnet/samples/tree/main/orleans) - [Orleans repository samples README](https://github.com/dotnet/orleans/blob/main/samples/README.md) ## Getting Started Examples - [Build your first Orleans app](https://learn.microsoft.com/dotnet/orleans/quickstarts/build-your-first-orleans-app) - [Hello, World sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-hello-world-sample-app) - [Visual Basic Hello World source](https://github.com/dotnet/samples/tree/main/orleans/VBHelloWorld/README.md) - [F# Hello World source](https://github.com/dotnet/samples/tree/main/orleans/FSharpHelloWorld/README.md) ## Domain And Architecture Samples - [Adventure game](https://learn.microsoft.com/samples/dotnet/samples/orleans-text-adventure-game) - [Chirper social media sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-chirper-social-media-sample-app) - [GPS device tracker](https://learn.microsoft.com/samples/dotnet/samples/orleans-gps-device-tracker-sample) - [Presence service](https://learn.microsoft.com/samples/dotnet/samples/orleans-gaming-presence-service-sample) - [Tic Tac Toe web game](https://learn.microsoft.com/samples/dotnet/samples/orleans-tictactoe-web-based-game) - [Stocks sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-stocks-sample-app) ## Hosting And UI Samples - [Deploy and scale an Orleans app on Azure](https://learn.microsoft.com/dotnet/orleans/quickstarts/deploy-scale-orleans-on-azure) - [Voting app on Kubernetes](https://learn.microsoft.com/samples/dotnet/samples/orleans-voting-sample-app-on-kubernetes) - [Blazor Server + Orleans](https://learn.microsoft.com/samples/dotnet/samples/orleans-aspnet-core-blazor-server-sample) - [Blazor WebAssembly + Orleans](https://learn.microsoft.com/samples/dotnet/samples/orleans-aspnet-core-blazor-wasm-sample) - [Transport Layer Security sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-transport-layer-security-tls) ## Streams, Observers, And Real-Time - [Chat Room sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-chat-room-sample) - [Streaming pub/sub with Azure Event Hubs](https://learn.microsoft.com/samples/dotnet/samples/orleans-streaming-pubsub-with-azure-event-hub) - [Chirper social sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-chirper-social-media-sample-app) - [GPS Tracker with SignalR](https://learn.microsoft.com/samples/dotnet/samples/orleans-gps-device-tracker-sample) ## State, Persistence, And Transactions - [Bank Account ACID transactions](https://learn.microsoft.com/samples/dotnet/samples/orleans-bank-account-acid-transactions) - [Custom grain storage sample page](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/custom-grain-storage) ## Testing - [Unit testing documentation and example](https://learn.microsoft.com/dotnet/orleans/implementation/testing) ## Repo-Grounded Integration Harness Patterns - `AIBase`: shared `AspireTestFixture` for API/UI suites plus `AIBaseTestApplication : WebApplicationFactory<...>` for direct DI and grain access - `WA.Storied.Agents`: shared `AspireTestFixture` for AppHost lifecycle and Playwright, plus `AIBaseTestApplication : WebApplicationFactory<HostEntryPointMarker>` for Host services and Orleans runtime access - Load `testing-patterns.md` when you need working snippets for shared AppHost fixtures, `WebApplicationFactory`, SignalR, or browser automation instead of standalone `InProcessTestCluster` examples ## Additional Community-Oriented Example Lists Mentioned By Official Samples - [Road to Orleans](https://github.com/PiotrJustyna/road-to-orleans/) - [HanBaoBao Kubernetes sample](https://github.com/ReubenBond/hanbaobao-web) ## 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.md 23.2 KB
# 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 ```csharp 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>`, or `IAsyncEnumerable<T>` - `CancellationToken` as last parameter, optional with default - `[ResponseTimeout]` on interface methods only (not implementations) - Default response timeout: 30 seconds ## Grain Lifecycle ```mermaid 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)"] ``` ```csharp 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. ```csharp // 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. ```csharp // 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 ```csharp // 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 ```csharp // 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 ```csharp public interface ICounterGrain : IGrainWithIntegerKey { [ReadOnly] Task<int> GetCount(); // concurrent with other ReadOnly Task Increment(); // exclusive access } ``` ### MayInterleave Predicate ```csharp [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 ```csharp // 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 ```fsharp [<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. ```csharp // 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 ```csharp // SiloMessagingOptions / ClientMessagingOptions options.CancelRequestOnTimeout = true; // default: auto-cancel on timeout options.WaitForCancellationAcknowledgement = false; // default: don't wait for ack ``` Metric: `orleans-app-requests-canceled`. Legacy `GrainCancellationToken` / `GrainCancellationTokenSource` still available but deprecated. ## Timers Non-durable, activation-local periodic work. Stops on deactivation or silo crash. ```csharp // 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 calls - `Interleave = true`: callback interleaves with other grain calls (old `RegisterTimer` behavior) - `KeepAlive = true`: prevents grain deactivation while timer is active - Callback receives `CancellationToken` that 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 ```csharp 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). ```csharp 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 ```csharp // AppHost var redis = builder.AddRedis("redis"); var orleans = builder.AddOrleans("cluster") .WithReminders(redis); // or .WithMemoryReminders() for dev ``` ### POCO 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 ```csharp // 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 method ``` ## POCO Grains For grains that don't inherit from `Grain`: ```csharp 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. ```csharp // 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`. ```csharp // 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. ```csharp [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. ```csharp 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 | ```csharp // 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. ```csharp // 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. ```csharp // 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. ```csharp [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 | ```csharp 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+). ```csharp 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 - `InconsistentStateException` causes grain deactivation; other exceptions do not - Read failures during activation fail the activation - Write failures fault the `WriteStateAsync` Task -
grains.md 7.2 KB
# 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](https://learn.microsoft.com/dotnet/orleans/grains/) | Grain classes, interfaces, and the core programming surface | | Understand grain references | [Grain references](https://learn.microsoft.com/dotnet/orleans/grains/grain-references) | How grains are addressed and invoked | | Pick the right identity shape | [Grain identity](https://learn.microsoft.com/dotnet/orleans/grains/grain-identity) | Keys, namespaces, and identity semantics | | Understand default placement | [Grain placement](https://learn.microsoft.com/dotnet/orleans/grains/grain-placement) | Runtime placement model and locality tradeoffs | | Filter or constrain placement | [Grain placement filtering](https://learn.microsoft.com/dotnet/orleans/grains/grain-placement-filtering) | Placement filters and targeting rules | | Add extension points to grains | [Grain extensions](https://learn.microsoft.com/dotnet/orleans/grains/grain-extensions) | Grain extension patterns | | Generate serializers and proxies correctly | [Code generation](https://learn.microsoft.com/dotnet/orleans/grains/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](https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders) | Activation-local timers versus durable reminders | | Push updates back to clients | [Observers](https://learn.microsoft.com/dotnet/orleans/grains/observers) | Grain observers and callback patterns | | Cancel grain work safely | [Cancellation tokens](https://learn.microsoft.com/dotnet/orleans/grains/cancellation-tokens) | Cancellation behavior across grain calls | | Reason about reentrancy and ordering | [Request scheduling](https://learn.microsoft.com/dotnet/orleans/grains/request-scheduling) | Scheduler rules, interleaving, and request ordering | | Flow ambient metadata | [Request context](https://learn.microsoft.com/dotnet/orleans/grains/request-context) | Request-scoped metadata across calls | | Hook into activation stages | [Grain lifecycle](https://learn.microsoft.com/dotnet/orleans/grains/grain-lifecycle) | Lifecycle stages and activation events | | Offload stateless fan-out | [Stateless worker grains](https://learn.microsoft.com/dotnet/orleans/grains/stateless-worker-grains) | Stateless scaling patterns | | Use external tasks safely | [External tasks and grains](https://learn.microsoft.com/dotnet/orleans/grains/external-tasks-and-grains) | Mixing Orleans scheduling with external async work | | Add interceptors or filters | [Interceptors](https://learn.microsoft.com/dotnet/orleans/grains/interceptors) | Cross-cutting interception points | | Create runtime helper services | [GrainServices](https://learn.microsoft.com/dotnet/orleans/grains/grainservices) | Cluster-local services for shared runtime behavior | | Use fire-and-forget deliberately | [One-way requests](https://learn.microsoft.com/dotnet/orleans/grains/oneway) | One-way call semantics and limits | ## Persistence and State | Need | Official Source | What It Covers | |---|---|---| | Persist grain state | [Grain persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/) | Persistent state model and provider wiring | | Use Azure Cosmos DB storage | [Azure Cosmos DB persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/azure-cosmos-db) | Cosmos-backed state provider setup | | Use relational storage | [Relational storage (ADO.NET)](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/relational-storage) | SQL-backed provider options | | Use Azure Storage | [Azure storage persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/azure-storage) | Azure Table/Blob-backed state provider guidance | | Use DynamoDB | [Amazon DynamoDB storage](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/dynamodb-storage) | DynamoDB-backed persistence options | ## Event Sourcing | Need | Official Source | What It Covers | |---|---|---| | Decide whether to use event sourcing | [Event sourcing overview](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/) | Journaled grain model and tradeoffs | | Start with `JournaledGrain` | [JournaledGrain basics](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/journaledgrain-basics) | Core journaled grain API and state evolution | | Diagnose journaled grains | [JournaledGrain diagnostics](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/journaledgrain-diagnostics) | Troubleshooting and diagnostics for journaled grains | | Choose confirmation mode | [Immediate vs delayed confirmation](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation) | Consistency and confirmation tradeoffs | | Publish event notifications | [Notifications](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/notifications) | Observer/notification patterns for journaled grains | | Configure event sourcing | [Event sourcing configuration](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/event-sourcing-configuration) | Provider and configuration model | | Review built-in providers | [Built-in log-consistency providers](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/log-consistency-providers) | Available log consistency implementations | | Understand replicated instances | [Replicated instances](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/replicated-instances) | Multi-instance replication behavior | ## Transactions and Versioning | Need | Official Source | What It Covers | |---|---|---| | Use transactional state | [Transactions](https://learn.microsoft.com/dotnet/orleans/grains/transactions) | Orleans ACID transaction model | | Plan contract evolution | [Grain versioning overview](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/grain-versioning) | Interface and implementation versioning | | Preserve compatibility | [Backward compatibility guidelines](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/backward-compatibility-guidelines) | Safe versioning rules | | Mark compatible implementations | [Compatible grains](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/compatible-grains) | Compatibility declarations | | Control version selection | [Version selector strategy](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/version-selector-strategy) | Version routing rules | | Roll out new grain versions | [Deploying new versions of grains](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/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](hosting.md) when the problem is cluster wiring, clients, observability, or deployment. - Jump to [implementation.md](implementation.md) when you need runtime-internals or testing details. -
hosting.md 7.1 KB
# 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](https://learn.microsoft.com/dotnet/orleans/host/client) | `UseOrleansClient`, gateways, and client topology | | Add operational visibility | [Dashboard](https://learn.microsoft.com/dotnet/orleans/dashboard/) | Orleans Dashboard setup and operational usage | | Wire Orleans through Aspire | [.NET Aspire integration](https://learn.microsoft.com/dotnet/orleans/host/aspire-integration) | AppHost resources, `.AsClient()`, and orchestration wiring | | Understand silo host stages | [Silo lifecycle](https://learn.microsoft.com/dotnet/orleans/host/silo-lifecycle) | Silo startup and shutdown lifecycle | | Run mixed silo roles | [Heterogeneous silos](https://learn.microsoft.com/dotnet/orleans/host/heterogeneous-silos) | Different silo capabilities in one cluster | | Reason about activation lookups | [Grain directory](https://learn.microsoft.com/dotnet/orleans/host/grain-directory) | Directory behavior and placement lookup mechanics | | Secure transport | [Transport Layer Security (TLS)](https://learn.microsoft.com/dotnet/orleans/host/transport-layer-security) | TLS between Orleans cluster participants | ## Configuration Guide | Need | Official Source | What It Covers | |---|---|---| | Start with cluster configuration | [Configuration overview](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/) | Main configuration surface | | Set up local development | [Local development configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/local-development-configuration) | Dev cluster setup and local defaults | | Configure clients | [Client configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/client-configuration) | Client-side settings and connectivity | | Configure silos | [Server configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/server-configuration) | Silo-side options and runtime wiring | | Review common recipes | [Typical configurations](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/typical-configurations) | Canonical configuration examples | | Look up available options | [List of options classes](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/list-of-options-classes) | Option types exposed by Orleans | | Add metadata to silos | [Silo metadata](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/silo-metadata) | Metadata and node labeling | | Tune deactivation | [Activation collection](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/activation-collection) | Activation cleanup and collection rules | | Tune GC for Orleans | [Configure .NET garbage collection](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/configuring-garbage-collection) | GC recommendations for Orleans hosts | | Configure relational providers | [Configure ADO.NET providers](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/configuring-ado-dot-net-providers) | ADO.NET provider registration and setup | | Set up ADO.NET databases | [ADO.NET database configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/adonet-configuration) | Database-side setup for Orleans SQL providers | | Understand Orleans serialization | [Serialization overview](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization) | Serializer model and contracts | | Use immutable types | [Serialization of immutable types](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-immutability) | Immutable-type handling | | Configure serialization | [Configure serialization](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-configuration) | Serializer configuration switches | | Customize serializers | [Customize serialization](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-customization) | Custom serializers and codecs | | Run startup hooks | [Startup tasks](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/startup-tasks) | Startup task registration | | Shut clusters down cleanly | [Graceful shutdown](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/shutting-down-orleans) | Drain and shutdown behavior | ## Observability | Need | Official Source | What It Covers | |---|---|---| | Start with monitoring | [Observability overview](https://learn.microsoft.com/dotnet/orleans/host/monitoring/) | Logs, metrics, and monitoring guidance | | Decode silo-side errors | [Silo error code monitoring](https://learn.microsoft.com/dotnet/orleans/host/monitoring/silo-error-code-monitoring) | Error-code reference for silo issues | | Decode client-side errors | [Client error code monitoring](https://learn.microsoft.com/dotnet/orleans/host/monitoring/client-error-code-monitoring) | Error-code reference for clients | ## Deployment and Failures | Need | Official Source | What It Covers | |---|---|---| | Start from deployment basics | [Running the app](https://learn.microsoft.com/dotnet/orleans/deployment/) | Deployment overview and entry points | | Deploy to Azure App Service | [Azure App Service](https://learn.microsoft.com/dotnet/orleans/deployment/deploy-to-azure-app-service) | App Service hosting guidance | | Deploy to Azure Container Apps | [Azure Container Apps](https://learn.microsoft.com/dotnet/orleans/deployment/deploy-to-azure-container-apps) | ACA deployment shape | | Deploy to Kubernetes | [Kubernetes](https://learn.microsoft.com/dotnet/orleans/deployment/kubernetes) | K8s deployment and clustering | | Deploy to Service Fabric | [Service Fabric](https://learn.microsoft.com/dotnet/orleans/deployment/service-fabric) | Service Fabric runtime integration | | Handle cluster failures | [Handle failures](https://learn.microsoft.com/dotnet/orleans/deployment/handling-failures) | Failure modes and recovery patterns | | Deploy with Consul | [Consul deployments](https://learn.microsoft.com/dotnet/orleans/deployment/consul-deployment) | Consul-based clustering | | Troubleshoot deployments | [Troubleshoot deployments](https://learn.microsoft.com/dotnet/orleans/deployment/troubleshooting-deployments) | Common deployment diagnostics | | Troubleshoot legacy Azure Cloud Services | [Azure Cloud Services troubleshooting](https://learn.microsoft.com/dotnet/orleans/deployment/troubleshooting-azure-cloud-services-deployments) | Legacy deployment troubleshooting | ## Usage Guidance - Start here when the problem is cluster wiring, clients, provider registration, or operational readiness. - Use [grains.md](grains.md) when the problem is inside a grain rather than in the hosting model. - Use [implementation.md](implementation.md) when you need runtime internals, messaging guarantees, or testing behavior. - Use [testing-patterns.md](testing-patterns.md) when the hosting question is specifically about mixing Orleans, Aspire, `WebApplicationFactory`, SignalR, or Playwright in integration tests. -
implementation.md 20.2 KB
# 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: ```mermaid 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 ```csharp [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 ```mermaid 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 | | `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>`: ```csharp 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 `Task` is 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 ```csharp // 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. ```csharp // 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 code ``` ### Scheduling Options ```csharp 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 ```mermaid 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 ```csharp 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 - `IAmAlive` writes 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: ```mermaid 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` ```csharp 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. ```csharp #pragma warning disable ORLEANSEXP001 siloBuilder.AddActivationRepartitioner(); #pragma warning restore ORLEANSEXP001 ``` Uses probabilistic tracking (sampling) and anchoring filters to limit migration churn. ### Activation Rebalancing (Experimental, Orleans 10) Cluster-wide redistribution for memory and activation count balance. ```csharp #pragma warning disable ORLEANSEXP002 siloBuilder.AddActivationRebalancer(); #pragma warning restore ORLEANSEXP002 ``` Uses 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: ```csharp 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) ```csharp // 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 ```csharp // 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) ```csharp 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 ```csharp 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 silo ``` ### xUnit Fixture Sharing ```csharp 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 ```csharp // 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](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/) | | Hello World tutorial | [Hello World](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/overview-helloworld) | | Orleans basics tutorial | [Tutorial 1](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/tutorial-1) | | Adventure game sample | [Adventure](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/adventure) | | Custom grain storage | [Custom storage sample](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/custom-grain-storage) | | Design principles | [Architecture principles](https://learn.microsoft.com/dotnet/orleans/resources/orleans-architecture-principles-and-approach) | | When Orleans fits | [Applicability](https://learn.microsoft.com/dotnet/orleans/resources/orleans-thinking-big-and-small) | | NuGet package map | [NuGet packages](https://learn.microsoft.com/dotnet/orleans/resources/nuget-packages) | | Best practices | [Best practices](https://learn.microsoft.com/dotnet/orleans/resources/best-practices) | | FAQ | [FAQ](https://learn.microsoft.com/dotnet/orleans/resources/frequently-asked-questions) | | External links | [Links](https://learn.microsoft.com/dotnet/orleans/resources/links) | ## API Reference and Source Entry Points | Need | Official Source | |---|---| | Core API | [Orleans.Core](https://learn.microsoft.com/dotnet/api/orleans.core) | | Runtime API | [Orleans.Runtime](https://learn.microsoft.com/dotnet/api/orleans.runtime) | | Streams API | [Orleans.Streams](https://learn.microsoft.com/dotnet/api/orleans.streams) | | Source repo | [dotnet/orleans](https://github.com/dotnet/orleans) | | Official samples | [dotnet/samples Orleans](https://github.com/dotnet/samples/tree/main/orleans) | | Repo samples README | [Samples README](https://github.com/dotnet/orleans/blob/main/samples/README.md) | ## 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. -
official-docs-index.md 15 KB
# Official Docs Index Use this reference when the summarized guidance in the skill is not enough and you need the official Orleans documentation tree through direct links. This skill keeps a live-link map for Orleans instead of a mirrored local docs snapshot. ## Scope - Full Orleans Microsoft Learn tree mapped through links, including getting started, grains, streaming, host, deployment, implementation details, resources, quickstarts, and code samples - Official examples and samples entry points from Microsoft Learn, `dotnet/samples`, and the Orleans repository sample index - GitHub repository entry points for repo-level docs, releases, and sample navigation ## Primary Entry Points - [Microsoft Orleans documentation root](https://learn.microsoft.com/dotnet/orleans/) - [Orleans overview](https://learn.microsoft.com/dotnet/orleans/overview) - [Benefits](https://learn.microsoft.com/dotnet/orleans/benefits) - [Migration guide](https://learn.microsoft.com/dotnet/orleans/migration-guide) - [Best practices](https://learn.microsoft.com/dotnet/orleans/resources/best-practices) - [Orleans GitHub repository](https://github.com/dotnet/orleans) - [Latest Orleans release](https://github.com/dotnet/orleans/releases/latest) ## Get Started - [Overview](https://learn.microsoft.com/dotnet/orleans/overview) - [Benefits](https://learn.microsoft.com/dotnet/orleans/benefits) - [Migration guide](https://learn.microsoft.com/dotnet/orleans/migration-guide) ## Quickstarts - [Build your first Orleans app](https://learn.microsoft.com/dotnet/orleans/quickstarts/build-your-first-orleans-app) - [Deploy and scale an Orleans app on Azure](https://learn.microsoft.com/dotnet/orleans/quickstarts/deploy-scale-orleans-on-azure) ## Grains - [Develop grains](https://learn.microsoft.com/dotnet/orleans/grains/) - [Grain references](https://learn.microsoft.com/dotnet/orleans/grains/grain-references) - [Grain identity](https://learn.microsoft.com/dotnet/orleans/grains/grain-identity) - [Grain placement overview](https://learn.microsoft.com/dotnet/orleans/grains/grain-placement) - [Grain placement filtering](https://learn.microsoft.com/dotnet/orleans/grains/grain-placement-filtering) - [Grain extensions](https://learn.microsoft.com/dotnet/orleans/grains/grain-extensions) - [Timers and reminders](https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders) - [Observers](https://learn.microsoft.com/dotnet/orleans/grains/observers) - [Cancellation tokens](https://learn.microsoft.com/dotnet/orleans/grains/cancellation-tokens) - [Request scheduling](https://learn.microsoft.com/dotnet/orleans/grains/request-scheduling) - [Request context](https://learn.microsoft.com/dotnet/orleans/grains/request-context) - [Code generation](https://learn.microsoft.com/dotnet/orleans/grains/code-generation) ### Persistence - [Grain persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/) - [Azure Cosmos DB persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/azure-cosmos-db) - [Relational storage (ADO.NET)](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/relational-storage) - [Azure storage persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/azure-storage) - [Amazon DynamoDB storage](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence/dynamodb-storage) ### Event Sourcing - [Event sourcing overview](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/) - [JournaledGrain basics](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/journaledgrain-basics) - [JournaledGrain diagnostics](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/journaledgrain-diagnostics) - [Immediate vs delayed confirmation](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/immediate-vs-delayed-confirmation) - [Notifications](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/notifications) - [Event sourcing configuration](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/event-sourcing-configuration) - [Built-in log-consistency providers](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/log-consistency-providers) - [Replicated instances](https://learn.microsoft.com/dotnet/orleans/grains/event-sourcing/replicated-instances) ### Advanced Grain Features - [External tasks and grains](https://learn.microsoft.com/dotnet/orleans/grains/external-tasks-and-grains) - [Interceptors](https://learn.microsoft.com/dotnet/orleans/grains/interceptors) - [GrainServices](https://learn.microsoft.com/dotnet/orleans/grains/grainservices) - [Stateless worker grains](https://learn.microsoft.com/dotnet/orleans/grains/stateless-worker-grains) - [Transactions](https://learn.microsoft.com/dotnet/orleans/grains/transactions) - [One-way requests](https://learn.microsoft.com/dotnet/orleans/grains/oneway) - [Grain lifecycle](https://learn.microsoft.com/dotnet/orleans/grains/grain-lifecycle) ### Grain Versioning - [Grain versioning overview](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/grain-versioning) - [Backward compatibility guidelines](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/backward-compatibility-guidelines) - [Compatible grains](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/compatible-grains) - [Version selector strategy](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/version-selector-strategy) - [Deploying new versions of grains](https://learn.microsoft.com/dotnet/orleans/grains/grain-versioning/deploying-new-versions-of-grains) ## Streaming - [Streaming overview](https://learn.microsoft.com/dotnet/orleans/streaming/) - [Streams quick start](https://learn.microsoft.com/dotnet/orleans/streaming/streams-quick-start) - [Why streams?](https://learn.microsoft.com/dotnet/orleans/streaming/streams-why) - [Broadcast channels](https://learn.microsoft.com/dotnet/orleans/streaming/broadcast-channel) - [Streams APIs](https://learn.microsoft.com/dotnet/orleans/streaming/streams-programming-apis) - [Stream providers](https://learn.microsoft.com/dotnet/orleans/streaming/stream-providers) ## Host - [Clients](https://learn.microsoft.com/dotnet/orleans/host/client) - [Dashboard](https://learn.microsoft.com/dotnet/orleans/dashboard/) - [.NET Aspire integration](https://learn.microsoft.com/dotnet/orleans/host/aspire-integration) - [Silo lifecycle](https://learn.microsoft.com/dotnet/orleans/host/silo-lifecycle) - [Heterogeneous silos](https://learn.microsoft.com/dotnet/orleans/host/heterogeneous-silos) - [Grain directory](https://learn.microsoft.com/dotnet/orleans/host/grain-directory) - [Transport Layer Security (TLS)](https://learn.microsoft.com/dotnet/orleans/host/transport-layer-security) ### Configuration Guide - [Configuration overview](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/) - [Local development configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/local-development-configuration) - [Client configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/client-configuration) - [Server configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/server-configuration) - [Typical configurations](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/typical-configurations) - [List of options classes](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/list-of-options-classes) - [Silo metadata](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/silo-metadata) - [Activation collection](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/activation-collection) - [Configure .NET garbage collection](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/configuring-garbage-collection) - [Configure ADO.NET providers](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/configuring-ado-dot-net-providers) - [ADO.NET database configuration](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/adonet-configuration) - [Serialization overview](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization) - [Serialization of immutable types](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-immutability) - [Configure serialization](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-configuration) - [Customize serialization](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization-customization) - [Startup tasks](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/startup-tasks) - [Graceful shutdown](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/shutting-down-orleans) ### Observability - [Observability overview](https://learn.microsoft.com/dotnet/orleans/host/monitoring/) - [Silo error code monitoring](https://learn.microsoft.com/dotnet/orleans/host/monitoring/silo-error-code-monitoring) - [Client error code monitoring](https://learn.microsoft.com/dotnet/orleans/host/monitoring/client-error-code-monitoring) ## Deployment - [Running the app](https://learn.microsoft.com/dotnet/orleans/deployment/) - [Azure App Service](https://learn.microsoft.com/dotnet/orleans/deployment/deploy-to-azure-app-service) - [Azure Container Apps](https://learn.microsoft.com/dotnet/orleans/deployment/deploy-to-azure-container-apps) - [Kubernetes](https://learn.microsoft.com/dotnet/orleans/deployment/kubernetes) - [Service Fabric](https://learn.microsoft.com/dotnet/orleans/deployment/service-fabric) - [Handle failures](https://learn.microsoft.com/dotnet/orleans/deployment/handling-failures) - [Troubleshooting Azure Cloud Services (Legacy)](https://learn.microsoft.com/dotnet/orleans/deployment/troubleshooting-azure-cloud-services-deployments) - [Consul deployments](https://learn.microsoft.com/dotnet/orleans/deployment/consul-deployment) - [Troubleshoot deployments](https://learn.microsoft.com/dotnet/orleans/deployment/troubleshooting-deployments) ## Code Samples And Tutorials - [Code samples overview](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/) - [Hello World tutorial](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/overview-helloworld) - [Orleans basics tutorial](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/tutorial-1) - [Adventure game sample](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/adventure) - [Unit testing](https://learn.microsoft.com/dotnet/orleans/implementation/testing) - [Custom grain storage sample](https://learn.microsoft.com/dotnet/orleans/tutorials-and-samples/custom-grain-storage) ## Implementation Details - [Implementation overview](https://learn.microsoft.com/dotnet/orleans/implementation/) - [Implementation grain directory](https://learn.microsoft.com/dotnet/orleans/implementation/grain-directory) - [Orleans lifecycle](https://learn.microsoft.com/dotnet/orleans/implementation/orleans-lifecycle) - [Messaging delivery guarantees](https://learn.microsoft.com/dotnet/orleans/implementation/messaging-delivery-guarantees) - [Scheduler](https://learn.microsoft.com/dotnet/orleans/implementation/scheduler) - [Cluster management](https://learn.microsoft.com/dotnet/orleans/implementation/cluster-management) - [Streams implementation overview](https://learn.microsoft.com/dotnet/orleans/implementation/streams-implementation/) - [Azure Queue streams implementation](https://learn.microsoft.com/dotnet/orleans/implementation/streams-implementation/azure-queue-streams) - [Load balancing](https://learn.microsoft.com/dotnet/orleans/implementation/load-balancing) - [Unit testing](https://learn.microsoft.com/dotnet/orleans/implementation/testing) ## Resources - [Frequently asked questions](https://learn.microsoft.com/dotnet/orleans/resources/frequently-asked-questions) - [Design principles](https://learn.microsoft.com/dotnet/orleans/resources/orleans-architecture-principles-and-approach) - [Applicability](https://learn.microsoft.com/dotnet/orleans/resources/orleans-thinking-big-and-small) - [NuGet packages](https://learn.microsoft.com/dotnet/orleans/resources/nuget-packages) - [Best practices](https://learn.microsoft.com/dotnet/orleans/resources/best-practices) - [Student projects](https://learn.microsoft.com/dotnet/orleans/resources/student-projects) - [External links](https://learn.microsoft.com/dotnet/orleans/resources/links) ## Official Examples ### Microsoft Learn And Samples Browser - [Orleans samples browser](https://learn.microsoft.com/samples/browse/?expanded=dotnet&products=dotnet-orleans) - [dotnet/samples Orleans folder](https://github.com/dotnet/samples/tree/main/orleans) - [Orleans repo sample index](https://github.com/dotnet/orleans/blob/main/samples/README.md) ### Highlighted Official Samples - [Hello, World](https://learn.microsoft.com/samples/dotnet/samples/orleans-hello-world-sample-app) - [Adventure](https://learn.microsoft.com/samples/dotnet/samples/orleans-text-adventure-game) - [Chirper](https://learn.microsoft.com/samples/dotnet/samples/orleans-chirper-social-media-sample-app) - [GPS Tracker](https://learn.microsoft.com/samples/dotnet/samples/orleans-gps-device-tracker-sample) - [Presence Service](https://learn.microsoft.com/samples/dotnet/samples/orleans-gaming-presence-service-sample) - [Tic Tac Toe](https://learn.microsoft.com/samples/dotnet/samples/orleans-tictactoe-web-based-game) - [Voting](https://learn.microsoft.com/samples/dotnet/samples/orleans-voting-sample-app-on-kubernetes) - [Chat Room](https://learn.microsoft.com/samples/dotnet/samples/orleans-chat-room-sample) - [Bank Account / ACID transactions](https://learn.microsoft.com/samples/dotnet/samples/orleans-bank-account-acid-transactions) - [Blazor Server sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-aspnet-core-blazor-server-sample) - [Blazor WebAssembly sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-aspnet-core-blazor-wasm-sample) - [Stocks sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-stocks-sample-app) - [Transport Layer Security sample](https://learn.microsoft.com/samples/dotnet/samples/orleans-transport-layer-security-tls) - [Streaming with Azure Event Hubs](https://learn.microsoft.com/samples/dotnet/samples/orleans-streaming-pubsub-with-azure-event-hub) ## GitHub Source Entry Points - [Orleans repository README](https://github.com/dotnet/orleans) - [Repository releases](https://github.com/dotnet/orleans/releases) - [Repository samples README](https://github.com/dotnet/orleans/blob/main/samples/README.md) - [dotnet/samples Orleans source tree](https://github.com/dotnet/samples/tree/main/orleans) ## Usage Guidance - Start with the smallest relevant page instead of loading the whole tree into context. - Use the TOC snapshot to see whether a topic already has an official page before inventing guidance. - Use the hub snapshot to see the official top-level featured pages and resource entry points. - Prefer Learn pages for normative guidance and sample pages for concrete wiring patterns. - When a task needs exact package or provider naming, cross-check against the live docs page and the NuGet packages resource page. -
patterns.md 19.8 KB
# Orleans Patterns Detailed patterns for building robust Orleans applications. --- ## Grain Patterns ### Stateless Worker Grain Use for stateless operations that can be parallelized across silos. ```csharp [StatelessWorker(maxLocalWorkers: 4)] public class ImageProcessorGrain : Grain, IImageProcessorGrain { public Task<byte[]> ResizeImage(byte[] imageData, int width, int height) { // CPU-bound work distributed across workers return Task.FromResult(ImageLib.Resize(imageData, width, height)); } } ``` **When to use:** - CPU-bound stateless operations - Request distribution across cluster - No per-identity state needed ### Singleton Grain Ensure only one instance exists in the cluster. ```csharp public interface ILeaderboardGrain : IGrainWithIntegerKey { Task<List<LeaderboardEntry>> GetTop(int count); Task Submit(string playerId, int score); } // Usage: Always use key 0 by convention var leaderboard = grainFactory.GetGrain<ILeaderboardGrain>(0); ``` **When to use:** - Global coordination - Cluster-wide configuration - Rate limiting across cluster ### Observer Pattern Push notifications from grains to clients. ```csharp // Observer interface public interface IGameObserver : IGrainObserver { void OnGameStateChanged(GameState state); void OnPlayerJoined(string playerId); } // Observable grain public class GameGrain : Grain, IGameGrain { private readonly ObserverManager<IGameObserver> _observers; public GameGrain() { _observers = new ObserverManager<IGameObserver>( TimeSpan.FromMinutes(5), // Expiration this.GetLogger<GameGrain>()); } public Task Subscribe(IGameObserver observer) { _observers.Subscribe(observer, observer); return Task.CompletedTask; } public Task Unsubscribe(IGameObserver observer) { _observers.Unsubscribe(observer); return Task.CompletedTask; } private void NotifyStateChange(GameState state) { _observers.Notify(o => o.OnGameStateChanged(state)); } } ``` ### Grain Call Filter Intercept grain calls for cross-cutting concerns. ```csharp public class LoggingGrainCallFilter : IIncomingGrainCallFilter { private readonly ILogger<LoggingGrainCallFilter> _logger; public LoggingGrainCallFilter(ILogger<LoggingGrainCallFilter> logger) { _logger = logger; } public async Task Invoke(IIncomingGrainCallContext context) { var grainType = context.Grain.GetType().Name; var methodName = context.ImplementationMethod.Name; _logger.LogInformation("Entering {Grain}.{Method}", grainType, methodName); var sw = Stopwatch.StartNew(); try { await context.Invoke(); _logger.LogInformation("{Grain}.{Method} completed in {Elapsed}ms", grainType, methodName, sw.ElapsedMilliseconds); } catch (Exception ex) { _logger.LogError(ex, "{Grain}.{Method} failed after {Elapsed}ms", grainType, methodName, sw.ElapsedMilliseconds); throw; } } } // Registration silo.AddIncomingGrainCallFilter<LoggingGrainCallFilter>(); ``` --- ## Persistence Patterns ### Multiple Named States Store different aspects of grain state separately. ```csharp public class PlayerGrain : Grain, IPlayerGrain { private readonly IPersistentState<PlayerProfile> _profile; private readonly IPersistentState<PlayerInventory> _inventory; private readonly IPersistentState<PlayerProgress> _progress; public PlayerGrain( [PersistentState("profile", "profiles")] IPersistentState<PlayerProfile> profile, [PersistentState("inventory", "items")] IPersistentState<PlayerInventory> inventory, [PersistentState("progress", "progress")] IPersistentState<PlayerProgress> progress) { _profile = profile; _inventory = inventory; _progress = progress; } public async Task UpdateProfile(string displayName) { _profile.State.DisplayName = displayName; await _profile.WriteStateAsync(); // Only profile is persisted } } ``` ### Conditional Persistence Write state only when needed. ```csharp public class CounterGrain : Grain, ICounterGrain { private readonly IPersistentState<CounterState> _state; private int _transientCount; private const int PersistThreshold = 100; public async Task Increment() { _state.State.Count++; _transientCount++; // Batch persistence for performance if (_transientCount >= PersistThreshold) { await _state.WriteStateAsync(); _transientCount = 0; } } public override async Task OnDeactivateAsync( DeactivationReason reason, CancellationToken ct) { // Always persist on deactivation if (_transientCount > 0) { await _state.WriteStateAsync(); } await base.OnDeactivateAsync(reason, ct); } } ``` ### Event Sourcing Pattern Store events instead of current state. ```csharp [GenerateSerializer] public abstract record AccountEvent(DateTime Timestamp); [GenerateSerializer] public record DepositEvent(DateTime Timestamp, decimal Amount) : AccountEvent(Timestamp); [GenerateSerializer] public record WithdrawEvent(DateTime Timestamp, decimal Amount) : AccountEvent(Timestamp); [GenerateSerializer] public class AccountEventLog { [Id(0)] public List<AccountEvent> Events { get; set; } = []; } public class AccountGrain : Grain, IAccountGrain { private readonly IPersistentState<AccountEventLog> _log; private decimal _balance; // Computed from events public AccountGrain( [PersistentState("events", "eventStore")] IPersistentState<AccountEventLog> log) { _log = log; } public override Task OnActivateAsync(CancellationToken ct) { // Rebuild state from events _balance = _log.State.Events.Aggregate(0m, (bal, evt) => evt switch { DepositEvent d => bal + d.Amount, WithdrawEvent w => bal - w.Amount, _ => bal }); return base.OnActivateAsync(ct); } public async Task Deposit(decimal amount) { var evt = new DepositEvent(DateTime.UtcNow, amount); _log.State.Events.Add(evt); _balance += amount; await _log.WriteStateAsync(); } } ``` ### Snapshotting Combine event sourcing with periodic snapshots. ```csharp [GenerateSerializer] public class AccountSnapshot { [Id(0)] public decimal Balance { get; set; } [Id(1)] public int LastEventIndex { get; set; } [Id(2)] public DateTime SnapshotTime { get; set; } } public class AccountGrain : Grain, IAccountGrain { private readonly IPersistentState<AccountEventLog> _log; private readonly IPersistentState<AccountSnapshot> _snapshot; private decimal _balance; private const int SnapshotInterval = 100; public override async Task OnActivateAsync(CancellationToken ct) { // Start from snapshot _balance = _snapshot.State.Balance; // Apply events since snapshot var newEvents = _log.State.Events .Skip(_snapshot.State.LastEventIndex); foreach (var evt in newEvents) { ApplyEvent(evt); } await base.OnActivateAsync(ct); } private async Task AppendEvent(AccountEvent evt) { _log.State.Events.Add(evt); ApplyEvent(evt); await _log.WriteStateAsync(); // Create snapshot periodically if (_log.State.Events.Count % SnapshotInterval == 0) { _snapshot.State = new AccountSnapshot { Balance = _balance, LastEventIndex = _log.State.Events.Count, SnapshotTime = DateTime.UtcNow }; await _snapshot.WriteStateAsync(); } } } ``` --- ## Streaming Patterns ### Basic Stream Producer ```csharp public class SensorGrain : Grain, ISensorGrain { private IAsyncStream<SensorReading>? _stream; public override Task OnActivateAsync(CancellationToken ct) { var streamProvider = this.GetStreamProvider("StreamProvider"); _stream = streamProvider.GetStream<SensorReading>( StreamId.Create("Sensors", this.GetPrimaryKeyString())); return base.OnActivateAsync(ct); } public async Task ReportReading(double value) { var reading = new SensorReading { SensorId = this.GetPrimaryKeyString(), Value = value, Timestamp = DateTime.UtcNow }; await _stream!.OnNextAsync(reading); } } ``` ### Implicit Stream Subscription Automatic subscription based on grain identity. ```csharp [ImplicitStreamSubscription("Sensors")] public class SensorAggregatorGrain : Grain, ISensorAggregatorGrain { private double _lastValue; public override async Task OnActivateAsync(CancellationToken ct) { var streamProvider = this.GetStreamProvider("StreamProvider"); var stream = streamProvider.GetStream<SensorReading>( StreamId.Create("Sensors", this.GetPrimaryKeyString())); await stream.SubscribeAsync(OnReading); await base.OnActivateAsync(ct); } private Task OnReading(SensorReading reading, StreamSequenceToken? token) { _lastValue = reading.Value; // Process reading return Task.CompletedTask; } } ``` ### Stream Fan-Out Broadcast to multiple consumers. ```csharp public class NotificationGrain : Grain, INotificationGrain { public async Task BroadcastNotification(Notification notification) { var streamProvider = this.GetStreamProvider("Notifications"); // Broadcast to topic stream var globalStream = streamProvider.GetStream<Notification>( StreamId.Create("Notifications", "global")); await globalStream.OnNextAsync(notification); // Also send to user-specific streams foreach (var userId in notification.TargetUsers) { var userStream = streamProvider.GetStream<Notification>( StreamId.Create("Notifications", userId)); await userStream.OnNextAsync(notification); } } } ``` ### Reliable Stream Consumption Handle failures and resume from last position. ```csharp public class OrderProcessorGrain : Grain, IOrderProcessorGrain { private StreamSubscriptionHandle<Order>? _subscription; private readonly IPersistentState<StreamPosition> _position; public OrderProcessorGrain( [PersistentState("streamPos", "positions")] IPersistentState<StreamPosition> position) { _position = position; } public override async Task OnActivateAsync(CancellationToken ct) { var streamProvider = this.GetStreamProvider("Orders"); var stream = streamProvider.GetStream<Order>( StreamId.Create("Orders", "incoming")); // Resume from last known position _subscription = await stream.SubscribeAsync( OnOrderReceived, OnError, token: _position.State.LastToken); await base.OnActivateAsync(ct); } private async Task OnOrderReceived(Order order, StreamSequenceToken? token) { await ProcessOrder(order); // Persist position after successful processing _position.State.LastToken = token; await _position.WriteStateAsync(); } } ``` --- ## Coordination Patterns ### Distributed Lock Coordinate exclusive access across grains. ```csharp public interface ILockGrain : IGrainWithStringKey { Task<bool> TryAcquire(string owner, TimeSpan timeout); Task Release(string owner); } public class LockGrain : Grain, ILockGrain { private string? _currentOwner; private DateTime _expiresAt; public Task<bool> TryAcquire(string owner, TimeSpan timeout) { var now = DateTime.UtcNow; // Check if lock is available or expired if (_currentOwner == null || now >= _expiresAt) { _currentOwner = owner; _expiresAt = now + timeout; return Task.FromResult(true); } // Already owned if (_currentOwner == owner) { _expiresAt = now + timeout; // Extend return Task.FromResult(true); } return Task.FromResult(false); } public Task Release(string owner) { if (_currentOwner == owner) { _currentOwner = null; } return Task.CompletedTask; } } ``` ### Saga Pattern Coordinate multi-grain transactions with compensation. ```csharp public interface IOrderSagaGrain : IGrainWithGuidKey { Task<SagaResult> Execute(OrderRequest request); } public class OrderSagaGrain : Grain, IOrderSagaGrain { private readonly IPersistentState<SagaState> _state; public async Task<SagaResult> Execute(OrderRequest request) { _state.State.Status = SagaStatus.Started; _state.State.Request = request; await _state.WriteStateAsync(); try { // Step 1: Reserve inventory var inventory = GrainFactory.GetGrain<IInventoryGrain>(request.ProductId); await inventory.Reserve(request.Quantity); _state.State.InventoryReserved = true; await _state.WriteStateAsync(); // Step 2: Charge payment var payment = GrainFactory.GetGrain<IPaymentGrain>(request.CustomerId); await payment.Charge(request.Amount); _state.State.PaymentCharged = true; await _state.WriteStateAsync(); // Step 3: Create order var order = GrainFactory.GetGrain<IOrderGrain>(this.GetPrimaryKey()); await order.Create(request); _state.State.Status = SagaStatus.Completed; await _state.WriteStateAsync(); return SagaResult.Success(); } catch (Exception ex) { await Compensate(); return SagaResult.Failed(ex.Message); } } private async Task Compensate() { _state.State.Status = SagaStatus.Compensating; await _state.WriteStateAsync(); if (_state.State.PaymentCharged) { var payment = GrainFactory.GetGrain<IPaymentGrain>( _state.State.Request!.CustomerId); await payment.Refund(_state.State.Request.Amount); } if (_state.State.InventoryReserved) { var inventory = GrainFactory.GetGrain<IInventoryGrain>( _state.State.Request!.ProductId); await inventory.CancelReservation(_state.State.Request.Quantity); } _state.State.Status = SagaStatus.Compensated; await _state.WriteStateAsync(); } } ``` ### Scatter-Gather Parallel queries with result aggregation. ```csharp public class SearchGrain : Grain, ISearchGrain { public async Task<SearchResults> Search(SearchQuery query) { // Scatter: Query all index partitions in parallel var partitionCount = 10; var tasks = Enumerable.Range(0, partitionCount) .Select(i => GrainFactory .GetGrain<IIndexPartitionGrain>(i) .Search(query)) .ToList(); // Gather: Collect and merge results var partialResults = await Task.WhenAll(tasks); return new SearchResults { Items = partialResults .SelectMany(r => r.Items) .OrderByDescending(i => i.Score) .Take(query.Limit) .ToList(), TotalCount = partialResults.Sum(r => r.TotalCount) }; } } ``` --- ## Performance Patterns ### Grain Pooling with Reentrant Calls Allow interleaved calls for high throughput. ```csharp [Reentrant] public class CacheGrain : Grain, ICacheGrain { private readonly Dictionary<string, CacheEntry> _cache = new(); public Task<string?> Get(string key) { if (_cache.TryGetValue(key, out var entry) && !entry.IsExpired) { return Task.FromResult<string?>(entry.Value); } return Task.FromResult<string?>(null); } public Task Set(string key, string value, TimeSpan ttl) { _cache[key] = new CacheEntry(value, DateTime.UtcNow + ttl); return Task.CompletedTask; } } ``` ### Read-Through Cache Pattern ```csharp public class CachedDataGrain : Grain, ICachedDataGrain { private readonly IPersistentState<CachedDataState> _state; private readonly IExternalDataService _dataService; public async Task<Data> GetData(string key) { // Check cache if (_state.State.Cache.TryGetValue(key, out var cached)) { if (!cached.IsExpired) { return cached.Data; } } // Cache miss: fetch from external source var data = await _dataService.FetchAsync(key); // Update cache _state.State.Cache[key] = new CacheEntry<Data>(data, TimeSpan.FromMinutes(5)); await _state.WriteStateAsync(); return data; } } ``` ### Batch Processing Collect requests and process in batches. ```csharp public class BatchProcessorGrain : Grain, IBatchProcessorGrain { private readonly List<WorkItem> _pending = []; private IDisposable? _timer; private const int BatchSize = 100; private const int FlushIntervalMs = 1000; public override Task OnActivateAsync(CancellationToken ct) { _timer = RegisterGrainTimer( FlushBatch, default, TimeSpan.FromMilliseconds(FlushIntervalMs), TimeSpan.FromMilliseconds(FlushIntervalMs)); return base.OnActivateAsync(ct); } public async Task Submit(WorkItem item) { _pending.Add(item); if (_pending.Count >= BatchSize) { await FlushBatch(default); } } private async Task FlushBatch(object _) { if (_pending.Count == 0) return; var batch = _pending.ToList(); _pending.Clear(); // Process batch efficiently await ProcessBatchAsync(batch); } } ``` --- ## Testing Patterns ### Unit Testing with TestCluster ```csharp public class PlayerGrainTests : IClassFixture<TestClusterFixture> { private readonly TestCluster _cluster; public PlayerGrainTests(TestClusterFixture fixture) { _cluster = fixture.Cluster; } [Fact] public async Task UpdateScore_IncrementsScore() { // Arrange var player = _cluster.GrainFactory.GetGrain<IPlayerGrain>("test-player"); // Act await player.UpdateScore(100); var state = await player.GetState(); // Assert Assert.Equal(100, state.Score); } } public class TestClusterFixture : IDisposable { public TestCluster Cluster { get; } public TestClusterFixture() { var builder = new TestClusterBuilder(); builder.AddSiloBuilderConfigurator<TestSiloConfigurator>(); Cluster = builder.Build(); Cluster.Deploy(); } public void Dispose() => Cluster.StopAllSilos(); } public class TestSiloConfigurator : ISiloConfigurator { public void Configure(ISiloBuilder siloBuilder) { siloBuilder.AddMemoryGrainStorage("Default"); } } ``` ### Mocking Grain Dependencies ```csharp public class OrderGrainTests { [Fact] public async Task CreateOrder_CallsInventoryGrain() { // Arrange var mockInventory = new Mock<IInventoryGrain>(); var mockFactory = new Mock<IGrainFactory>(); mockFactory .Setup(f => f.GetGrain<IInventoryGrain>(It.IsAny<string>(), null)) .Returns(mockInventory.Object); var grain = new OrderGrain(mockFactory.Object); // Act await grain.CreateOrder(new OrderRequest { ProductId = "prod-1" }); // Assert mockInventory.Verify(i => i.Reserve(It.IsAny<int>()), Times.Once); } } ``` -
persistence-api.md 11.8 KB
# Persistence, Event Sourcing, and Transactions API Detailed state management API patterns from official Orleans documentation. ## IPersistentState API ```csharp public class PlayerGrain : Grain, IPlayerGrain { private readonly IPersistentState<PlayerProfile> _profile; private readonly IPersistentState<PlayerInventory> _inventory; public PlayerGrain( [PersistentState("profile", "profileStore")] IPersistentState<PlayerProfile> profile, [PersistentState("inventory", "itemStore")] IPersistentState<PlayerInventory> inventory) { _profile = profile; _inventory = inventory; } } ``` ### Operations | Operation | Behavior | |---|---| | `state.State` | Access/modify in-memory state | | `state.ReadStateAsync()` | Reload from storage (automatic on activation) | | `state.WriteStateAsync()` | Persist current state (must call explicitly) | | `state.ClearStateAsync()` | Clear/delete from storage | | `state.RecordExists` | Whether state exists in storage | | `state.Etag` | Provider-specific optimistic concurrency token | ### State Type Requirements ```csharp [GenerateSerializer] public class PlayerProfile { [Id(0)] public string Name { get; set; } = ""; [Id(1)] public int Level { get; set; } [Id(2)] public DateTime LastLogin { get; set; } } ``` - Use `[GenerateSerializer]` and `[Id(N)]` on all state types - Use JSON or version-tolerant format for stored state - Adding new `[Id]` members is safe; removing is safe if ID not reused; changing types is breaking ## Storage Provider Configuration ### Redis ```csharp siloBuilder.AddRedisGrainStorage("redis", options => { options.ConfigurationOptions = new ConfigurationOptions { EndPoints = { "localhost:6379" } }; options.DeleteStateOnClear = true; options.EntryExpiry = TimeSpan.FromDays(30); }); ``` ### Azure Cosmos DB ```csharp siloBuilder.AddCosmosGrainStorage("cosmos", options => { options.DatabaseName = "OrleansState"; options.ContainerName = "GrainState"; options.IsResourceCreationEnabled = true; options.DeleteStateOnClear = true; options.StateFieldsToIndex = new[] { "/State/Type" }; }); ``` ### Azure Table/Blob Storage ```csharp siloBuilder.AddAzureTableGrainStorage("table", options => options.ConfigureTableServiceClient(connectionString)); siloBuilder.AddAzureBlobGrainStorage("blob", options => options.ConfigureBlobServiceClient(connectionString)); ``` ### ADO.NET ```csharp siloBuilder.AddAdoNetGrainStorage("sql", options => { options.Invariant = "Microsoft.Data.SqlClient"; options.ConnectionString = connectionString; }); ``` ### Amazon DynamoDB ```csharp siloBuilder.AddDynamoDBGrainStorage("dynamo", options => { options.Service = "us-east-1"; options.AccessKey = accessKey; options.SecretKey = secretKey; options.TableName = "OrleansGrainState"; options.CreateIfNotExists = true; options.DeleteStateOnClear = true; }); ``` ### Storage Provider Summary | Provider | Package | Config Method | |---|---|---| | Redis | `Microsoft.Orleans.Persistence.Redis` | `AddRedisGrainStorage` | | Azure Table | `Microsoft.Orleans.Persistence.AzureStorage` | `AddAzureTableGrainStorage` | | Azure Blob | `Microsoft.Orleans.Persistence.AzureStorage` | `AddAzureBlobGrainStorage` | | Cosmos DB | `Microsoft.Orleans.Persistence.Cosmos` | `AddCosmosGrainStorage` | | ADO.NET | `Microsoft.Orleans.Persistence.AdoNet` | `AddAdoNetGrainStorage` | | DynamoDB | `Microsoft.Orleans.Persistence.DynamoDB` | `AddDynamoDBGrainStorage` | | Memory (dev) | built-in | `AddMemoryGrainStorage` | ### Custom Storage Provider ```csharp public class MyStorage : IGrainStorage { public Task ReadStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) { } public Task WriteStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) { } public Task ClearStateAsync<T>(string stateName, GrainId grainId, IGrainState<T> grainState) { } } ``` ## Event Sourcing with JournaledGrain ```csharp // State class [GenerateSerializer] public class BankAccountState { [Id(0)] public decimal Balance { get; set; } // Apply methods — called automatically by the runtime public void Apply(DepositEvent e) => Balance += e.Amount; public void Apply(WithdrawEvent e) => Balance -= e.Amount; } // Event base [GenerateSerializer] public abstract record AccountEvent; [GenerateSerializer] public record DepositEvent([property: Id(0)] decimal Amount) : AccountEvent; [GenerateSerializer] public record WithdrawEvent([property: Id(0)] decimal Amount) : AccountEvent; // Grain public class BankAccountGrain : JournaledGrain<BankAccountState, AccountEvent>, IBankAccountGrain { public Task Deposit(decimal amount) { RaiseEvent(new DepositEvent(amount)); return ConfirmEvents(); // immediate confirmation } public Task<decimal> GetBalance() => Task.FromResult(State.Balance); } ``` ### Confirmation Modes | Mode | Method | Tradeoff | |---|---|---| | Immediate | `ConfirmEvents()` after `RaiseEvent` | Consistent but slower | | Delayed | Just `RaiseEvent`, no confirm | Higher throughput, eventual consistency | | Conditional | Confirm at checkpoints | Balance of both | ### Log Consistency Providers Three built-in providers. Configure via silo builder: ```csharp siloBuilder.AddLogStorageBasedLogConsistencyProvider("LogStorage"); siloBuilder.AddStateStorageBasedLogConsistencyProvider("StateStorage"); siloBuilder.AddCustomStorageBasedLogConsistencyProvider("CustomStorage"); ``` ### Raising Multiple Events ```csharp // Individual raises — two storage accesses, risk of partial write RaiseEvent(e1); RaiseEvent(e2); await ConfirmEvents(); // Atomic batch — written atomically RaiseEvents(new[] { e1, e2 }); await ConfirmEvents(); ``` ### Retrieving Events ```csharp // Not all providers support this (throws NotSupportedException if unavailable) IReadOnlyList<AccountEvent> events = await RetrieveConfirmedEvents(0, Version); ``` ### Immediate vs Delayed Confirmation **Immediate**: call `ConfirmEvents()` after every `RaiseEvent`. Grain unavailable during storage issues. Safe, consistent. **Delayed**: raise events without confirming. Higher throughput but eventual consistency. ```csharp // Delayed — access tentative state IEnumerable<AccountEvent> UnconfirmedEvents { get; } AccountState TentativeState { get; } // State + all unconfirmed events applied ``` `TentativeState` is a best guess — events may be canceled or reordered in multi-instance scenarios. ### Conditional Events (Conflict Resolution) ```csharp // Returns false if race lost (version mismatch) bool success = await RaiseConditionalEvent(new WithdrawEvent { Amount = 100 }); // Awaiting the task is sufficient confirmation — no need for ConfirmEvents ``` Analogous to e-tag conditional updates. Use for operations that must not silently conflict (withdrawals). Unconditional events (deposits) always append. ### Notifications ```csharp // Called when confirmed state changes (load, write success, remote notification) protected override void OnStateChanged() { } // Called when tentative state changes (including during RaiseEvent) protected override void OnTentativeStateChanged() { } ``` ### Explicit Synchronization ```csharp await RefreshNow(); // confirms all unconfirmed events + loads latest from storage ``` ### Diagnostics ```csharp EnableStatsCollection(); LogConsistencyStatistics stats = GetStats(); DisableStatsCollection(); // Connection error monitoring protected override void OnConnectionIssue(ConnectionIssue issue) { } protected override void OnConnectionIssueResolved(ConnectionIssue issue) { } ``` ### Log Consistency Providers | Provider | Storage | RetrieveConfirmedEvents | Use Case | |---|---|---|---| | `StateStorage` | State snapshot via standard storage provider | No (events not persisted) | Production — snapshot-based | | `LogStorage` | Complete event list as single object | Yes (all events in memory) | Testing only — full read/write each time | | `CustomStorage` | Grain implements `ICustomStorageInterface<S,E>` | No (you control storage) | Production — custom backends | ```csharp // Register providers siloBuilder.AddStateStorageBasedLogConsistencyProvider("StateStorage"); siloBuilder.AddLogStorageBasedLogConsistencyProvider("LogStorage"); siloBuilder.AddCustomStorageBasedLogConsistencyProvider("CustomStorage"); // Attribute on grain [StorageProvider(ProviderName = "OrleansStorage")] [LogConsistencyProvider(ProviderName = "StateStorage")] public class MyGrain : JournaledGrain<MyState, MyEvent> { } ``` ### CustomStorage Interface ```csharp public interface ICustomStorageInterface<StateType, EventType> { Task<KeyValuePair<int, StateType>> ReadStateFromStorage(); // Return false on version mismatch (e-tag semantics) Task<bool> ApplyUpdatesToStorage(IReadOnlyList<EventType> updates, int expectedVersion); } ``` ### Replicated Instances Multi-cluster event sourcing guarantees: - Same version number = same state across instances - Racing events resolved into one agreed sequence - Notifications propagated after events raised ## ACID Transactions ```mermaid flowchart LR C["Client / Grain"] -->|"RunTransaction"| T["TransactionClient"] T -->|"Create tx context"| G1["Grain A<br/>[Reentrant]"] T -->|"Join tx context"| G2["Grain B<br/>[Reentrant]"] G1 -->|"PerformUpdate"| S1["ITransactionalState<T>"] G2 -->|"PerformUpdate"| S2["ITransactionalState<T>"] S1 & S2 -->|"Commit / Abort"| T ``` ### Setup ```csharp // Silo siloBuilder.UseTransactions(); // Client clientBuilder.UseTransactions(); // Storage siloBuilder.AddAzureTableTransactionalStateStorage("TransactionStore", options => options.ConfigureTableServiceClient(connectionString)); ``` ### Transaction Attributes | Attribute | Meaning | |---|---| | `[Transaction(TransactionOption.Create)]` | Always starts new transaction | | `[Transaction(TransactionOption.Join)]` | Must be within existing transaction | | `[Transaction(TransactionOption.CreateOrJoin)]` | Joins existing or creates new | | `[Transaction(TransactionOption.Suppress)]` | Not transactional | | `[Transaction(TransactionOption.Supported)]` | Not transactional but passes context | | `[Transaction(TransactionOption.NotAllowed)]` | Cannot be called in transaction | ### Grain Implementation ```csharp // Interface public interface IAccountGrain : IGrainWithStringKey { [Transaction(TransactionOption.Join)] Task Withdraw(decimal amount); [Transaction(TransactionOption.Join)] Task Deposit(decimal amount); [Transaction(TransactionOption.CreateOrJoin)] Task<decimal> GetBalance(); } // Grain — MUST be [Reentrant] [Reentrant] public class AccountGrain : Grain, IAccountGrain { private readonly ITransactionalState<AccountBalance> _balance; public AccountGrain( [TransactionalState("balance", "TransactionStore")] ITransactionalState<AccountBalance> balance) { _balance = balance; } public Task Withdraw(decimal amount) => _balance.PerformUpdate(state => { if (state.Value < amount) throw new InsufficientFundsException(); state.Value -= amount; }); public Task Deposit(decimal amount) => _balance.PerformUpdate(state => state.Value += amount); public Task<decimal> GetBalance() => _balance.PerformRead(state => state.Value); } ``` ### Client-Side Transactions ```csharp var transactionClient = serviceProvider.GetRequiredService<ITransactionClient>(); await transactionClient.RunTransaction(TransactionOption.Create, async () => { await fromAccount.Withdraw(100m); await toAccount.Deposit(100m); }); ``` ### Error Handling - `OrleansTransactionException` wraps app exceptions - `OrleansTransactionAbortedException` is retryable - Unknown-state: wait for `SystemResponseTimeout` before retrying - `OrleansTransactionsDisabledException` if `UseTransactions()` not called -
serialization-api.md 7.5 KB
# 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. ```csharp [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: ```csharp [GenerateSerializer] [Alias("player-state-v1")] public class PlayerState { } ``` Allows type to be renamed without breaking deserialization. ### `[Immutable]` Skip copy overhead for immutable types: ```csharp [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: ```csharp 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: ```csharp // 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. ```csharp // 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. ```csharp // 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 ```csharp [Immutable] [GenerateSerializer] public record SensorReading( [property: Id(0)] string SensorId, [property: Id(1)] double Value, [property: Id(2)] DateTime Timestamp); ``` ### Member-Level Immutability ```csharp [GenerateSerializer] public class MyGrainState { [Id(0), Immutable] public IReadOnlyList<string> Tags { get; set; } = []; [Id(1)] public int MutableCount { get; set; } } ``` ### Parameter-Level Immutability ```csharp 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: ```csharp // 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 ```csharp [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 ```csharp [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: ```csharp [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 ```csharp // 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 | -
streaming-api.md 10.5 KB
# Streaming, Broadcast Channels, and Observers API Detailed streaming and communication patterns from official Orleans documentation. ## Orleans Streams Streams are virtual, always exist, never fail, identified by `StreamId` (namespace + key). Decouple data generation from processing in time and space. ### Configuration ```csharp // Silo — memory streams (dev only) siloBuilder.AddMemoryStreams("StreamProvider") .AddMemoryGrainStorage("PubSubStore"); // Silo — Azure Event Hubs siloBuilder.AddEventHubStreams("EventHubProvider", options => { options.ConfigureEventHub(eh => eh.Configure(o => { o.ConnectionString = connectionString; o.ConsumerGroup = "$Default"; o.Path = "my-hub"; })); options.UseAzureTableCheckpointer(c => c.ConfigureTableServiceClient(storageConnection)); }); // Client clientBuilder.AddMemoryStreams("StreamProvider"); ``` ### Producing Events ```csharp var streamProvider = this.GetStreamProvider("StreamProvider"); var streamId = StreamId.Create("SensorData", this.GetPrimaryKeyString()); var stream = streamProvider.GetStream<SensorReading>(streamId); await stream.OnNextAsync(new SensorReading { Value = 42.0 }); ``` ### Consuming Events — Implicit Subscription ```csharp [ImplicitStreamSubscription("SensorData")] public class AggregatorGrain : Grain, IAggregatorGrain { public override async Task OnActivateAsync(CancellationToken ct) { var streamProvider = this.GetStreamProvider("StreamProvider"); var stream = streamProvider.GetStream<SensorReading>( StreamId.Create("SensorData", this.GetPrimaryKeyString())); await stream.SubscribeAsync( (reading, token) => { // Process reading return Task.CompletedTask; }); await base.OnActivateAsync(ct); } } ``` ### Consuming Events — Explicit Subscription ```csharp var stream = streamProvider.GetStream<Order>(StreamId.Create("Orders", "incoming")); StreamSubscriptionHandle<Order> handle = await stream.SubscribeAsync( onNextAsync: (item, token) => ProcessAsync(item), onErrorAsync: ex => HandleError(ex), onCompletedAsync: () => Task.CompletedTask, token: lastKnownToken); // resume from position // Unsubscribe await handle.UnsubscribeAsync(); ``` ### Delivery Semantics | Provider | Delivery | Ordering | |---|---|---| | Memory | Best-effort | FIFO per stream | | Azure Queue | At-least-once | FIFO per queue | | Event Hubs | At-least-once | Per partition | | Broadcast Channel | At-most-once | No guarantee | Rewindable streams support subscribing from arbitrary point using `StreamSequenceToken`. ### IAsyncEnumerable (Orleans 7+) Request-response scoped streaming, not pub-sub: ```csharp // Interface public interface IDataGrain : IGrainWithStringKey { IAsyncEnumerable<DataItem> GetItems( int count, [EnumeratorCancellation] CancellationToken ct = default); } // Implementation public class DataGrain : Grain, IDataGrain { public async IAsyncEnumerable<DataItem> GetItems( int count, [EnumeratorCancellation] CancellationToken ct = default) { for (int i = 0; i < count && !ct.IsCancellationRequested; i++) { yield return await FetchItem(i); } } } // Consumer await foreach (var item in grain.GetItems(100).WithCancellation(cts.Token)) { Process(item); } ``` Default batch size: 100. Configure with `.WithBatchSize(50)`. ## Broadcast Channels Fire-and-forget broadcast to all implicitly subscribed grains. No persistence, no delivery guarantees. ### Configuration ```csharp siloBuilder.AddBroadcastChannel("announcements"); ``` ### Consumer ```csharp [ImplicitChannelSubscription] public class NotificationGrain : Grain, INotificationGrain, IOnBroadcastChannelSubscribed { public Task OnSubscribed(IBroadcastChannelSubscription subscription) { subscription.Attach<Announcement>( item => OnAnnouncement(item), ex => OnError(ex)); return Task.CompletedTask; } private Task OnAnnouncement(Announcement item) { // Process broadcast message return Task.CompletedTask; } } ``` ### Producer ```csharp var provider = client.GetBroadcastChannelProvider("announcements"); var channelId = ChannelId.Create("system", Guid.Empty); var writer = provider.GetChannelWriter<Announcement>(channelId); await writer.Publish(new Announcement { Message = "System update" }); ``` ## Observers One-way async push notifications from grains to clients or other grains. ### Observer Interface ```csharp public interface IChatObserver : IGrainObserver { [OneWay] // fire-and-forget Task OnMessage(string user, string message); Task OnUserJoined(string user); } ``` ### Server-Side (ObserverManager) ```csharp public class ChatRoomGrain : Grain, IChatRoomGrain { private readonly ObserverManager<IChatObserver> _observers; public ChatRoomGrain(ILogger<ChatRoomGrain> logger) { _observers = new ObserverManager<IChatObserver>( TimeSpan.FromMinutes(5), logger); } public Task Subscribe(IChatObserver observer) { _observers.Subscribe(observer, observer); return Task.CompletedTask; } public Task Unsubscribe(IChatObserver observer) { _observers.Unsubscribe(observer); return Task.CompletedTask; } public Task SendMessage(string user, string message) { _observers.Notify(o => o.OnMessage(user, message)); return Task.CompletedTask; } } ``` ### Client-Side ```csharp // Create observer var observerInstance = new ChatObserver(); var observerRef = grainFactory.CreateObjectReference<IChatObserver>(observerInstance); // Subscribe var chatRoom = client.GetGrain<IChatRoomGrain>("lobby"); await chatRoom.Subscribe(observerRef); // Cleanup — must call to avoid memory leaks (held as WeakReference) grainFactory.DeleteObjectReference<IChatObserver>(observerRef); ``` ### Grain-as-Observer ```csharp public class MonitorGrain : Grain, IMonitorGrain, IChatObserver { public async Task StartMonitoring(string room) { var chatRoom = GrainFactory.GetGrain<IChatRoomGrain>(room); await chatRoom.Subscribe(this.AsReference<IChatObserver>()); } public Task OnMessage(string user, string message) { // Handle notification return Task.CompletedTask; } } ``` ### Observer Execution Model - Non-reentrant, single-threaded per observer reference - `[Reentrant]` / `[AlwaysInterleave]` attributes don't affect observers - `CancellationToken` support added in Orleans 9.0 ## Stream Providers ### Azure Event Hubs NuGet: `Microsoft.Orleans.Streaming.EventHubs`. Real-time ingestion, rewindable, supports replay from arbitrary point. ```csharp siloBuilder.AddEventHubStreams("EventHubProvider", options => { options.ConfigureEventHub(eh => eh.Configure(o => { o.ConnectionString = connectionString; o.ConsumerGroup = "$Default"; o.Path = "my-hub"; })); options.UseAzureTableCheckpointer(c => c.ConfigureTableServiceClient(storageConnection)); }); ``` ### Azure Queue (AQ) NuGet: `Microsoft.Orleans.Streaming.AzureStorage`. Uses pulling agents inside silos. Not rewindable. Does not guarantee FIFO on failures. ```csharp siloBuilder.AddAzureQueueStreams("AQProvider", optionsBuilder => optionsBuilder.ConfigureAzureQueue(options => options.Configure(opt => opt.QueueServiceClient = new QueueServiceClient(endpoint, credential)))); ``` ### Aspire Streaming Integration ```csharp // Azure Queue with Aspire var storage = builder.AddAzureStorage("storage"); var queues = storage.AddQueues("streaming"); var orleans = builder.AddOrleans("cluster") .WithStreaming("AQProvider", queues); // In-memory streaming orleans.WithMemoryStreaming("MemoryProvider"); // Broadcast channels orleans.WithBroadcastChannel("BroadcastChannel"); ``` ### Pub-Sub Management Managed by `PubSubRendezvousGrain`, persisted via `"PubSubStore"`: ```csharp siloBuilder.AddAzureTableGrainStorage("PubSubStore", options => options.TableServiceClient = new TableServiceClient(endpoint, credential)); ``` ## Streams Programming API Details ### Core Interfaces ```csharp public interface IAsyncObserver<in T> { Task OnNextAsync(T item, StreamSequenceToken token = null); Task OnCompletedAsync(); Task OnErrorAsync(Exception ex); } public interface IAsyncObservable<T> { Task<StreamSubscriptionHandle<T>> SubscribeAsync(IAsyncObserver<T> observer); } ``` ### Failure Recovery Consumer must re-attach processing logic in `OnActivateAsync`: ```csharp public override async Task OnActivateAsync(CancellationToken ct) { var stream = this.GetStreamProvider("SP").GetStream<string>(streamId); var handles = await stream.GetAllSubscriptionHandles(); foreach (var handle in handles) await handle.ResumeAsync(this); } ``` ### Implicit Subscriptions with IStreamSubscriptionObserver ```csharp [ImplicitStreamSubscription("MyNamespace")] public class MyGrain : Grain, IMyGrain, IStreamSubscriptionObserver { public async Task OnSubscribed(IStreamSubscriptionHandleFactory handleFactory) { var handle = handleFactory.Create<string>(); await handle.ResumeAsync(this); } } ``` ### Key Characteristics - Subscriptions are per-grain (not per-activation) — durable across activations - Multiple producers and consumers per stream - Subscribing X times delivers event X times - Rewindable streams (Event Hubs) support replay from `StreamSequenceToken` ### Why Orleans Streams? Existing stream systems (Kafka, Storm, Spark) are for uniform dataflow graphs. Orleans Streams target: 1. **Flexible processing logic** — imperative, functional, Rx, stateful, with side effects 2. **Dynamic topologies** — add/remove nodes at runtime 3. **Fine-grained granularity** — each stream link is an independent entity 4. **Distribution** — scalable, elastic, reliable Typical use: per-user streams with different processing logic per user, subscriptions changing dynamically. ## Streams vs Broadcast vs Observers vs IAsyncEnumerable | Feature | Streams | Broadcast | Observers | IAsyncEnumerable | |---|---|---|---|---| | Persistence | Provider-dependent | No | No | No | | Delivery | At-least-once (varies) | At-most-once | Best-effort | Request-scoped | | Subscription | Implicit + explicit | Implicit only | Manual | N/A | | Pattern | Pub/sub | Fan-out | Push notify | Pull streaming | | Survives restart | Yes (with provider) | No | No | No | | Use case | Event pipelines | Announcements | Client updates | Paginated data | -
testing-patterns.md 8 KB
# Orleans Testing Patterns Use this reference when the task is about testing Orleans grains together with real hosting, HTTP/SignalR surfaces, Aspire resources, or browser flows. These patterns are grounded in working test harnesses used in `AIBase` and `WA.Storied.Agents`: one shared AppHost fixture for the distributed topology, an optional `WebApplicationFactory` layer for direct Host DI/grain access, and separate browser contexts per UI test. ## Choose The Harness First | Need | Best Harness | Why | |---|---|---| | Grain runtime behavior only | `InProcessTestCluster` | Fastest Orleans-focused harness without HTTP/UI layers | | Real API, SignalR, SSE, or AppHost wiring | Shared Aspire/AppHost fixture | Exercises the actual distributed topology and resource graph | | Direct Host DI services or `IGrainFactory` from the hosted app | Shared Aspire/AppHost fixture + `WebApplicationFactory<TEntryPoint>` | Keeps real infra while letting tests resolve services from the Host container | | Browser automation against a co-hosted Orleans app | Shared Aspire/AppHost fixture + Playwright | Reuses one browser process and one AppHost boot, but isolates browser state per test | ## Runtime-Only Orleans Tests When the test is purely about grain scheduling, persistence, reentrancy, or state transitions, stay close to Orleans and use `InProcessTestCluster`: ```csharp var builder = new InProcessTestClusterBuilder(); builder.ConfigureSilo(siloBuilder => { siloBuilder.AddMemoryGrainStorageAsDefault(); }); await using var cluster = await builder.BuildAndStartAsync(); var grain = cluster.GrainFactory.GetGrain<IOrderGrain>(Guid.NewGuid()); await grain.SubmitAsync(new SubmitOrder("PO-42")); var state = await grain.GetStateAsync(); ``` Use this harness when HTTP, SignalR, and AppHost resources are irrelevant to the assertion. ## Shared AppHost Fixture For Real Topology Tests Use Aspire when the assertion depends on the actual Orleans host plus the real surrounding resources: ```csharp public sealed class AspireTestFixture : IAsyncDisposable { private DistributedApplication? _app; public DistributedApplication App => _app ?? throw new InvalidOperationException("App not initialized."); public string HostUrl { get; private set; } = string.Empty; public async Task InitializeAsync() { var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.My_AppHost>(); builder.Services.AddLogging(logging => { logging.ClearProviders(); logging.AddConsole(); logging.SetMinimumLevel(LogLevel.Warning); logging.AddFilter("Aspire.Hosting.Dcp", LogLevel.Warning); logging.AddFilter("Aspire.Hosting.Backchannel", LogLevel.Critical); }); _app = await builder.BuildAsync(); await _app.StartAsync(); using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); await _app.ResourceNotifications.WaitForResourceHealthyAsync("host", cts.Token); HostUrl = _app.CreateHttpClient("host", "http").BaseAddress?.ToString() ?? throw new InvalidOperationException("Host URL was not resolved."); } public HttpClient CreateApiClient() => App.CreateHttpClient("host", "http"); } ``` This is the right shape for: - HTTP endpoint tests - SignalR/SSE/OpenResponses tests - admin or operator UI tests - Orleans flows that require the real host pipeline, auth, or middleware ## Mix AppHost Infra With `WebApplicationFactory` This is the production-style pattern for co-hosted Orleans apps: boot infrastructure in Aspire once, then create a `WebApplicationFactory` over the Host/API assembly for direct DI and grain access. ```csharp public sealed class TestApplication : WebApplicationFactory<HostEntryPointMarker>, IAsyncDisposable { private static readonly AspireTestFixture SharedFixture = new(); private readonly Dictionary<string, string?> _overrides = new(StringComparer.OrdinalIgnoreCase); protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment(Environments.Development); builder.ConfigureAppConfiguration((_, config) => config.AddInMemoryCollection(_overrides)); } public async Task InitializeAsync() { await SharedFixture.InitializeAsync(); var tables = await SharedFixture.App.GetConnectionStringAsync("tables"); var blobs = await SharedFixture.App.GetConnectionStringAsync("blobs"); _overrides["ConnectionStrings:Tables"] = tables; _overrides["ConnectionStrings:Blobs"] = blobs; Environment.SetEnvironmentVariable("ConnectionStrings__Tables", tables); Environment.SetEnvironmentVariable("ConnectionStrings__Blobs", blobs); CreateClient(); } public AsyncServiceScope CreateScope() => Services.CreateAsyncScope(); public HttpClient CreateApiClient() { var client = CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false }); client.Timeout = TimeSpan.FromMinutes(5); return client; } } ``` Use this when the test needs: - `IGrainFactory` - repositories or managers from DI - direct access to runtime services - real HTTP clients and hub connections against the same host - whichever async fixture contract the test framework expects: xUnit, TUnit, or a repo-local wrapper ## TUnit Per-Session Sharing For TUnit-based Orleans suites, keep the AppHost boot outside individual tests: ```csharp [ClassDataSource<TestApplication>(Shared = SharedType.PerTestSession)] public sealed class OrderGrainIntegrationTests(TestApplication app) { [Test] public async Task Order_grain_persists_state_through_real_host() { await using var scope = app.CreateScope(); var grainFactory = scope.ServiceProvider.GetRequiredService<IGrainFactory>(); var grain = grainFactory.GetGrain<IOrderGrain>(Guid.NewGuid()); await grain.SubmitAsync(new SubmitOrder("PO-42")); var state = await grain.GetStateAsync(); await Assert.That(state.Number).IsEqualTo("PO-42"); } } ``` The same pattern works for API-only tests with `ClassDataSource<AspireTestFixture>(Shared = SharedType.PerTestSession)`. ## SignalR And Browser Flows For SignalR or UI flows, keep connection and browser helpers on the shared fixture: ```csharp public HubConnection CreateAgentHubConnection(string token) { var hubUrl = new Uri(new Uri(HostUrl), "/agenthub"); return new HubConnectionBuilder() .WithUrl(hubUrl, options => { options.AccessTokenProvider = () => Task.FromResult(token)!; options.Headers["Authorization"] = $"Bearer {token}"; options.Transports = HttpTransportType.LongPolling; }) .WithAutomaticReconnect() .Build(); } ``` For Playwright: - initialize Playwright once in the shared fixture - create a new browser context per test - set `BaseURL`, viewport, and `IgnoreHTTPSErrors` in the helper, not inline in every test ## Failure Diagnostics When a host-backed Orleans test fails, emit server-side logs before rethrowing: ```csharp var logStart = DateTimeOffset.UtcNow; try { var response = await app.CreateApiClient().GetAsync("/health"); response.EnsureSuccessStatusCode(); } catch { Console.WriteLine(app.GetErrorLogDump(logStart)); throw; } ``` Useful practices: - capture error/critical logs from the Host into a test log collector - print the log dump on HTTP 500 or startup failures - save Playwright screenshots and HTML on UI failures - keep AppHost resource logs available when resource-health waits fail ## Anti-Patterns - Creating `DistributedApplicationTestingBuilder.CreateAsync<...>()` inside each test method - Booting a second Orleans client inside a co-hosted Host test when the app already uses `UseOrleans` - Copy-pasting local connection strings into tests instead of resolving them from `SharedFixture.App` - Using in-memory substitutes for persistence-sensitive or streaming-sensitive integration tests - Sharing a Playwright page or browser context across tests instead of sharing only the browser process
-
-
SKILL.md 13 KB
--- name: dotnet-orleans version: "2.1.0" category: "Distributed" description: "Build or review distributed .NET applications with Orleans grains, silos, persistence, streaming, reminders, placement, transactions, serialization, event sourcing, testing, and cloud-native hosting." compatibility: "Prefer current Orleans releases (10.x / 9.x) with `UseOrleans`, `IPersistentState<TState>`, `RegisterGrainTimer`, `[GenerateSerializer]`, modern providers, and production-grade clustering." --- # Microsoft Orleans ## Trigger On - building or reviewing `.NET` code that uses `Microsoft.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 entities - `IGrainWithIntegerKey` — relational DB integration - `IGrainWithStringKey` — flexible string keys - `IGrainWithGuidCompoundKey` / `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 grains - `ITransactionalState<TState>` for ACID transactions across grains - `Grain<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]` - `RegisterGrainTimer` for activation-local periodic work (non-durable) - Reminders via `IRemindable` for durable low-frequency wakeups - Observers via `IGrainObserver` and `ObserverManager<T>` for one-way push notifications 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 methods - `RequestContext.AllowCallChainReentrancy()` for scoped reentrancy - Native `CancellationToken` support (last parameter, optional default) 8. **Choose hosting intentionally.** - `UseOrleans` for silos, `UseOrleansClient` for separate clients - Co-hosted client runs in same process (reduced latency, no extra serialization) - In Aspire, declare Orleans resource in AppHost, wire clustering/storage/reminders there, use `.AsClient()` for frontend-only consumers - In Aspire-backed tests, resolve Orleans backing-resource connection strings from the distributed app and feed them into the test host instead of duplicating local settings - Prefer `TokenCredential` with `DefaultAzureCredential` for Azure-backed providers 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.** - `ResourceOptimizedPlacement` is default since 9.2 (CPU, memory, activation count weighted) - `RandomPlacement`, `PreferLocalPlacement`, `HashBasedPlacement`, `ActivationCountBasedPlacement` - `SiloRoleBasedPlacement` for role-targeted placement - Custom placement via `IPlacementDirector` + `PlacementStrategy` + `PlacementAttribute` - Placement filtering (9.0+) for zone-aware and hardware-affinity placement - Activation repartitioning and rebalancing are experimental 11. **Make the cluster observable.** - Standard `Microsoft.Extensions.Logging` - `System.Diagnostics.Metrics` with meter `"Microsoft.Orleans"` - OpenTelemetry export via `AddOtlpExporter` + `AddMeter("Microsoft.Orleans")` - Distributed tracing via `AddActivityPropagation()` with sources `"Microsoft.Orleans.Runtime"` and `"Microsoft.Orleans.Application"` - Orleans Dashboard for operational visibility (secure with ASP.NET Core auth) - Health checks for cluster readiness 12. **Test the cluster behavior you actually depend on.** - `InProcessTestCluster` for 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 ```mermaid 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 `.NET` code - 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()` or `UseOrleansClient()` - reentrancy is handled deliberately — circular call patterns use `[Reentrant]`, `[AlwaysInterleave]`, or `AllowCallChainReentrancy` - transactional grains are marked `[Reentrant]` and use `PerformRead`/`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 - 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: - [GitHub Repository](https://github.com/dotnet/orleans) - [Overview](https://learn.microsoft.com/dotnet/orleans/overview) - [Best Practices](https://learn.microsoft.com/dotnet/orleans/resources/best-practices) - [Grain Persistence](https://learn.microsoft.com/dotnet/orleans/grains/grain-persistence) - [Grain Placement](https://learn.microsoft.com/dotnet/orleans/grains/grain-placement) - [Timers and Reminders](https://learn.microsoft.com/dotnet/orleans/grains/timers-and-reminders) - [Streaming](https://learn.microsoft.com/dotnet/orleans/streaming/) - [Transactions](https://learn.microsoft.com/dotnet/orleans/grains/transactions) - [Serialization](https://learn.microsoft.com/dotnet/orleans/host/configuration-guide/serialization) - [Testing](https://learn.microsoft.com/dotnet/orleans/implementation/testing) - [Orleans Dashboard](https://learn.microsoft.com/dotnet/orleans/dashboard/) - [Orleans and .NET Aspire Integration](https://learn.microsoft.com/dotnet/orleans/host/aspire-integration)
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.