Orleans.StateMachineES Developer Cheat Sheet
Quick Start Guide
1. Basic State Machine Grain
public enum OrderState { Pending, Processing, Shipped, Delivered }
public enum OrderTrigger { Process, Ship, Deliver, Cancel }
[StorageProvider(ProviderName = "Default")]
public class OrderGrain : StateMachineGrain<OrderState, OrderTrigger>, IOrderGrain
{
protected override StateMachine<OrderState, OrderTrigger> BuildStateMachine()
{
var machine = new StateMachine<OrderState, OrderTrigger>(OrderState.Pending);
machine.Configure(OrderState.Pending)
.Permit(OrderTrigger.Process, OrderState.Processing)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);
machine.Configure(OrderState.Processing)
.Permit(OrderTrigger.Ship, OrderState.Shipped);
return machine;
}
public Task ProcessAsync() => FireAsync(OrderTrigger.Process);
public Task ShipAsync() => FireAsync(OrderTrigger.Ship);
}
2. Event Sourcing Enabled
[LogConsistencyProvider(ProviderName = "LogStorage")]
[StorageProvider(ProviderName = "Default")]
public class OrderGrain : EventSourcedStateMachineGrain<OrderState, OrderTrigger, OrderGrainState>, IOrderGrain
{
protected override void ConfigureEventSourcing(EventSourcingOptions options)
{
options.AutoConfirmEvents = true;
options.EnableSnapshots = true;
options.SnapshotInterval = 100;
options.EnableIdempotency = true;
}
}
[GenerateSerializer]
public class OrderGrainState : EventSourcedStateMachineState<OrderState>
{
[Id(0)] public string? CustomerId { get; set; }
[Id(1)] public decimal Amount { get; set; }
}
3. Timer-Enabled States
public class OrderGrain : TimerEnabledStateMachineGrain<OrderState, OrderTrigger, OrderGrainState>
{
protected override void ConfigureTimeouts()
{
// Auto-cancel orders after 24 hours
RegisterStateTimeout(OrderState.Pending,
ConfigureTimeout(OrderState.Pending)
.After(TimeSpan.FromHours(24))
.TransitionTo(OrderTrigger.Cancel)
.UseReminder()
.WithName("OrderExpiry")
.Build());
// Process orders within 2 hours
RegisterStateTimeout(OrderState.Processing,
ConfigureTimeout(OrderState.Processing)
.After(TimeSpan.FromHours(2))
.TransitionTo(OrderTrigger.Ship)
.UseTimer()
.WithName("ProcessingTimeout")
.Build());
}
}
4. Hierarchical States
public enum DeviceState { Offline, Online, Idle, Active, Processing, Monitoring }
public enum DeviceTrigger { PowerOn, PowerOff, StartProcessing, StartMonitoring, Stop }
public class DeviceGrain : HierarchicalStateMachineGrain<DeviceState, DeviceTrigger, DeviceGrainState>
{
protected override StateMachine<DeviceState, DeviceTrigger> BuildStateMachine()
{
var machine = new StateMachine<DeviceState, DeviceTrigger>(DeviceState.Offline);
// Configure parent states
machine.Configure(DeviceState.Online)
.Permit(DeviceTrigger.PowerOff, DeviceState.Offline);
// Configure substates
machine.Configure(DeviceState.Idle)
.SubstateOf(DeviceState.Online)
.Permit(DeviceTrigger.StartProcessing, DeviceState.Processing);
machine.Configure(DeviceState.Processing)
.SubstateOf(DeviceState.Active)
.Permit(DeviceTrigger.Stop, DeviceState.Idle);
return machine;
}
protected override void ConfigureHierarchy()
{
DefineSubstate(DeviceState.Idle, DeviceState.Online);
DefineSubstate(DeviceState.Active, DeviceState.Online);
DefineSubstate(DeviceState.Processing, DeviceState.Active);
}
// Query hierarchy
public async Task<bool> IsOnlineAsync()
=> await IsInStateOrSubstateAsync(DeviceState.Online);
public async Task<IReadOnlyList<DeviceState>> GetCurrentPathAsync()
=> await GetCurrentStatePathAsync();
}
5. Source-Generated State Machines (Roslyn Generator)
YAML Specification (SmartLight.statemachine.yaml)
name: SmartLight
namespace: SmartHome.Devices
states: [Off, On, Dimmed, ColorMode, NightMode]
triggers: [TurnOn, TurnOff, Dim, SetColor, ActivateNightMode]
initialState: Off
transitions:
- { from: Off, to: On, trigger: TurnOn }
- { from: On, to: Dimmed, trigger: Dim }
- { from: On, to: ColorMode, trigger: SetColor }
JSON Specification (Thermostat.statemachine.json)
{
"name": "Thermostat",
"namespace": "SmartHome.Climate",
"states": ["Idle", "Heating", "Cooling", "Auto"],
"triggers": ["Heat", "Cool", "AutoMode", "Stop"],
"transitions": [
{ "from": "Idle", "to": "Heating", "trigger": "Heat" },
{ "from": "Heating", "to": "Idle", "trigger": "Stop" }
]
}
Generated Code Usage
// Auto-generated interfaces and implementations
ISmartLightGrain light = grainFactory.GetGrain<ISmartLightGrain>("living-room");
IThermostatGrain thermostat = grainFactory.GetGrain<IThermostatGrain>("main");
// Strongly-typed methods
await light.FireTurnOnAsync();
await light.FireDimAsync();
bool isOn = await light.IsOnAsync();
// Generated extension methods
SmartLightState.Off.IsTerminal();
SmartLightTrigger.TurnOn.GetDescription();
Project Configuration
<ItemGroup>
<AdditionalFiles Include="**\*.statemachine.yaml" />
<AdditionalFiles Include="**\*.statemachine.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Orleans.StateMachineES.Generators" />
</ItemGroup>
6. Orthogonal Regions (Parallel State Machines)
public class SmartHomeSystemGrain : OrthogonalStateMachineGrain<SmartHomeState, SmartHomeTrigger>
{
protected override void ConfigureOrthogonalRegions()
{
// Define independent regions
DefineOrthogonalRegion("Security", SmartHomeState.SecurityDisarmed, machine =>
{
machine.Configure(SmartHomeState.SecurityDisarmed)
.Permit(SmartHomeTrigger.ArmHome, SmartHomeState.SecurityArmedHome)
.Permit(SmartHomeTrigger.ArmAway, SmartHomeState.SecurityArmedAway);
machine.Configure(SmartHomeState.SecurityAlarm)
.OnEntry(() => _logger.LogWarning("ALARM TRIGGERED!"));
});
DefineOrthogonalRegion("Climate", SmartHomeState.ClimateOff, machine =>
{
machine.Configure(SmartHomeState.ClimateOff)
.Permit(SmartHomeTrigger.StartHeating, SmartHomeState.ClimateHeating)
.Permit(SmartHomeTrigger.StartCooling, SmartHomeState.ClimateCooling);
});
DefineOrthogonalRegion("Energy", SmartHomeState.EnergyNormal, machine =>
{
machine.Configure(SmartHomeState.EnergyNormal)
.Permit(SmartHomeTrigger.EnterPeakDemand, SmartHomeState.EnergyPeakDemand)
.Permit(SmartHomeTrigger.EnableSaving, SmartHomeState.EnergySaving);
});
// Map triggers to regions
MapTriggerToRegions(SmartHomeTrigger.VacationMode, "Security", "Climate", "Energy");
}
// Cross-region synchronization
protected override async Task OnRegionStateChangedAsync(
string regionName, SmartHomeState prev, SmartHomeState next, SmartHomeTrigger trigger)
{
if (regionName == "Presence" && next == SmartHomeState.PresenceAway)
{
// Auto-adjust when leaving home
await FireInRegionAsync("Security", SmartHomeTrigger.ArmAway);
await FireInRegionAsync("Climate", SmartHomeTrigger.SetEco);
await FireInRegionAsync("Energy", SmartHomeTrigger.EnableSaving);
}
}
// Usage
public async Task ActivateVacationModeAsync()
{
await FireInRegionAsync("Presence", SmartHomeTrigger.StartVacation);
await FireInRegionAsync("Security", SmartHomeTrigger.ArmAway);
await FireInRegionAsync("Climate", SmartHomeTrigger.SetEco);
await FireInRegionAsync("Energy", SmartHomeTrigger.EnableSaving);
}
}
// Client usage
var smartHome = grainFactory.GetGrain<ISmartHomeSystemGrain>("my-home");
await smartHome.FireInRegionAsync("Security", SmartHomeTrigger.ArmHome);
await smartHome.FireInRegionAsync("Climate", SmartHomeTrigger.StartHeating);
var status = await smartHome.GetStateSummary();
Console.WriteLine($"Security: {status.RegionStates["Security"]}");
Console.WriteLine($"Climate: {status.RegionStates["Climate"]}");
Core Interfaces & Methods
IStateMachineGrain<TState, TTrigger>
// State queries
Task<TState> GetStateAsync();
Task<bool> IsInStateAsync(TState state);
Task<IEnumerable<TTrigger>> GetPermittedTriggersAsync();
// Trigger execution
Task FireAsync(TTrigger trigger);
Task FireAsync<TArg0>(TTrigger trigger, TArg0 arg0);
Task FireAsync<TArg0, TArg1>(TTrigger trigger, TArg0 arg0, TArg1 arg1);
// Guard validation
Task<bool> CanFireAsync(TTrigger trigger);
Task<(bool, ICollection<string>)> CanFireWithUnmetGuardsAsync(TTrigger trigger);
// Lifecycle
Task ActivateAsync();
Task DeactivateAsync();
Timer Configuration
protected TimeoutConfiguration<TState, TTrigger> ConfigureTimeout(TState state)
{
return new TimeoutConfiguration<TState, TTrigger>(state)
.After(TimeSpan.FromMinutes(5)) // Duration
.TransitionTo(OrderTrigger.Timeout) // Target trigger
.UseTimer() // or .UseReminder()
.WithName("MyTimeout") // Optional name
.WithMetadata(new { reason = "expired" }) // Optional metadata
.Build();
}
Event Sourcing Options
🚀 PERFORMANCE BREAKTHROUGH: Event sourcing is 30.4% FASTER than regular state machines!
- Event-sourced: 5,923 transitions/sec (0.17ms latency)
- Regular: 4,123 transitions/sec (0.24ms latency)
protected override void ConfigureEventSourcing(EventSourcingOptions options)
{
options.AutoConfirmEvents = true; // Essential for optimal performance
// Performance optimizations
options.EnableSnapshots = true; // Enable periodic snapshots
options.SnapshotInterval = 100; // Events between snapshots
options.EnableIdempotency = true; // Deduplicate triggers
options.MaxDedupeKeysInMemory = 1000; // LRU cache size
// Optional stream publishing
options.PublishToStream = true; // Publish to Orleans Streams
options.StreamProvider = "SMS"; // Stream provider name
options.StreamNamespace = "Events"; // Stream namespace
}
⚠️ CRITICAL: AutoConfirmEvents = true is essential for:
- Maximum performance
- Proper state recovery after grain deactivation
- Reliable event persistence
Hierarchical State Queries
// Parent-child relationships
Task<TState?> GetParentStateAsync(TState state);
Task<IReadOnlyList<TState>> GetSubstatesAsync(TState parentState);
Task<IReadOnlyList<TState>> GetAncestorStatesAsync(TState state);
Task<IReadOnlyList<TState>> GetDescendantStatesAsync(TState parentState);
// Hierarchy navigation
Task<bool> IsInStateOrSubstateAsync(TState state);
Task<IReadOnlyList<TState>> GetCurrentStatePathAsync();
Task<TState?> GetActiveSubstateAsync(TState parentState);
Task<HierarchicalStateInfo<TState>> GetHierarchicalInfoAsync();
Common Patterns
Guards & Conditional Transitions
machine.Configure(OrderState.Processing)
.PermitIf(OrderTrigger.Ship, OrderState.Shipped,
() => HasInventory() && IsPaymentConfirmed())
.OnEntry(() => Logger.LogInformation("Started processing order"));
Parameterized Triggers
public async Task ProcessWithPriorityAsync(int priority)
{
await FireAsync(OrderTrigger.ProcessWithPriority, priority);
}
// In BuildStateMachine:
var priorityTrigger = machine.SetTriggerParameters<int>(OrderTrigger.ProcessWithPriority);
machine.Configure(OrderState.Pending)
.Permit(priorityTrigger, OrderState.Processing);
Custom State Classes
[GenerateSerializer]
public class OrderGrainState : EventSourcedStateMachineState<OrderState>
{
[Id(0)] public string CustomerId { get; set; } = "";
[Id(1)] public List<OrderItem> Items { get; set; } = new();
[Id(2)] public decimal Total { get; set; }
[Id(3)] public DateTime OrderDate { get; set; }
}
Stream Integration
// Configure in silo
siloBuilder.AddStreams(StreamConfigurator.StreamProvider)
.AddMemoryStreams("SMS");
// In grain
protected override void ConfigureEventSourcing(EventSourcingOptions options)
{
options.PublishToStream = true;
options.StreamProvider = "SMS";
options.StreamNamespace = "OrderEvents";
}
Testing Patterns
Basic Testing
[Fact]
public async Task Should_Process_Order_Successfully()
{
var grain = _cluster.Client.GetGrain<IOrderGrain>("order-123");
await grain.ProcessAsync();
var state = await grain.GetStateAsync();
state.Should().Be(OrderState.Processing);
}
Timer Testing
[Fact]
public async Task Should_Timeout_After_Configured_Duration()
{
var grain = _cluster.Client.GetGrain<IOrderGrain>("order-timeout");
await grain.ProcessAsync();
await Task.Delay(TimeSpan.FromSeconds(3)); // Wait for timeout
var state = await grain.GetStateAsync();
state.Should().Be(OrderState.Cancelled);
}
Hierarchical Testing
[Fact]
public async Task Should_Navigate_Hierarchy_Correctly()
{
var grain = _cluster.Client.GetGrain<IDeviceGrain>("device-123");
await grain.PowerOnAsync();
await grain.StartProcessingAsync();
(await grain.IsInStateOrSubstateAsync(DeviceState.Online)).Should().BeTrue();
(await grain.IsInStateAsync(DeviceState.Processing)).Should().BeTrue();
var path = await grain.GetCurrentStatePathAsync();
path.Should().ContainInOrder(DeviceState.Online, DeviceState.Active, DeviceState.Processing);
}
5. Distributed Sagas
using Orleans.StateMachineES.Sagas;
public class InvoiceProcessingSaga : SagaOrchestratorGrain<InvoiceData>, IInvoiceProcessingSagaGrain
{
protected override void ConfigureSagaSteps()
{
AddStep(new PostInvoiceStep())
.WithTimeout(TimeSpan.FromSeconds(30))
.WithRetry(3)
.WithMetadata("Description", "Posts invoice to accounting system");
AddStep(new CreateJournalEntryStep())
.WithTimeout(TimeSpan.FromSeconds(45))
.WithRetry(2)
.WithMetadata("Description", "Creates journal entries");
AddStep(new RunControlCheckStep())
.WithTimeout(TimeSpan.FromSeconds(60))
.WithRetry(1)
.WithMetadata("Description", "Runs compliance control checks");
}
protected override string GenerateBusinessTransactionId(InvoiceData sagaData)
{
return $"INV-TXN-{sagaData.InvoiceId}-{DateTime.UtcNow:yyyyMMddHHmmss}";
}
}
// Example saga step implementation
public class PostInvoiceStep : ISagaStep<InvoiceData>
{
public string StepName => "PostInvoice";
public TimeSpan Timeout => TimeSpan.FromSeconds(30);
public bool CanRetry => true;
public int MaxRetryAttempts => 3;
public async Task<SagaStepResult> ExecuteAsync(InvoiceData sagaData, SagaContext context)
{
try
{
var invoiceGrain = GrainFactory.GetGrain<IInvoiceGrain>(sagaData.InvoiceId);
var result = await invoiceGrain.PostAsync(sagaData, context.CorrelationId);
return SagaStepResult.Success(result);
}
catch (BusinessRuleException ex)
{
return SagaStepResult.BusinessFailure(ex.Message);
}
catch (Exception ex)
{
return SagaStepResult.TechnicalFailure(ex.Message, ex);
}
}
public async Task<CompensationResult> CompensateAsync(
InvoiceData sagaData,
SagaStepResult? stepResult,
SagaContext context)
{
try
{
var invoiceGrain = GrainFactory.GetGrain<IInvoiceGrain>(sagaData.InvoiceId);
await invoiceGrain.CancelAsync(context.CorrelationId);
return CompensationResult.Success();
}
catch (Exception ex)
{
return CompensationResult.Failure($"Failed to compensate invoice: {ex.Message}", ex);
}
}
}
Usage Examples
// Execute saga
var sagaGrain = grainFactory.GetGrain<IInvoiceProcessingSagaGrain>("saga-123");
var correlationId = Guid.NewGuid().ToString("N");
var invoiceData = new InvoiceData
{
InvoiceId = "INV-001",
CustomerId = "CUST-123",
Amount = 1500.00m
};
var result = await sagaGrain.ExecuteAsync(invoiceData, correlationId);
if (result.IsSuccess)
{
Console.WriteLine("Saga completed successfully");
}
else if (result.IsCompensated)
{
Console.WriteLine("Saga failed but was compensated successfully");
}
// Check saga status
var status = await sagaGrain.GetStatusAsync();
Console.WriteLine($"Saga status: {status.Status}");
Console.WriteLine($"Current step: {status.CurrentStepName}");
Console.WriteLine($"Progress: {status.CurrentStepIndex + 1}/{status.TotalSteps}");
// Get detailed execution history
var history = await sagaGrain.GetHistoryAsync();
foreach (var step in history.StepExecutions)
{
Console.WriteLine($"Step {step.StepName}: {step.IsSuccess} ({step.Duration.TotalMilliseconds}ms)");
}
Saga Features
- Orchestration Pattern: Central coordinator manages business process flow
- Automatic Compensation: Failed steps trigger rollback of completed steps in reverse order
- Retry Logic: Configurable retry attempts with exponential backoff for technical failures
- Correlation Tracking: Full correlation ID propagation across all distributed operations
- Event Sourcing Integration: Complete audit trail of saga execution and compensation
- Timeout Handling: Per-step timeouts with graceful failure handling
- Hierarchical State Management: Extends hierarchical state machine capabilities
- Business vs Technical Errors: Different handling strategies for different error types
6. State Machine Versioning
using Orleans.StateMachineES.Versioning;
public class VersionedOrderGrain :
VersionedStateMachineGrain<OrderState, OrderTrigger, VersionedOrderState>,
IVersionedOrderGrain
{
protected override async Task RegisterBuiltInVersionsAsync()
{
if (DefinitionRegistry != null)
{
// Register version 1.0.0
await DefinitionRegistry.RegisterDefinitionAsync<OrderState, OrderTrigger>(
GetType().Name,
new StateMachineVersion(1, 0, 0),
() => BuildOrderWorkflowV1(),
new StateMachineDefinitionMetadata
{
Description = "Initial order workflow",
Features = { "Basic order processing" }
});
// Register version 1.1.0 (backward compatible)
await DefinitionRegistry.RegisterDefinitionAsync<OrderState, OrderTrigger>(
GetType().Name,
new StateMachineVersion(1, 1, 0),
() => BuildOrderWorkflowV11(),
new StateMachineDefinitionMetadata
{
Description = "Enhanced order workflow",
Features = { "Enhanced validation", "Better error handling" }
});
// Register version 2.0.0 (breaking changes)
await DefinitionRegistry.RegisterDefinitionAsync<OrderState, OrderTrigger>(
GetType().Name,
new StateMachineVersion(2, 0, 0),
() => BuildOrderWorkflowV2(),
new StateMachineDefinitionMetadata
{
Description = "Major refactor",
Features = { "New approval workflow", "Multi-step processing" },
BreakingChanges = { "Added approval states", "Changed validation rules" }
});
}
}
protected override Task<StateMachine<OrderState, OrderTrigger>?> BuildVersionedStateMachineAsync(
StateMachineVersion version)
{
return Task.FromResult(version switch
{
{ Major: 1, Minor: 0, Patch: 0 } => BuildOrderWorkflowV1(),
{ Major: 1, Minor: 1, Patch: 0 } => BuildOrderWorkflowV11(),
{ Major: 2, Minor: 0, Patch: 0 } => BuildOrderWorkflowV2(),
_ => (StateMachine<OrderState, OrderTrigger>?)null
});
}
}
Usage Examples
// Check current version
var grain = grainFactory.GetGrain<IVersionedOrderGrain>("order-123");
var version = await grain.GetVersionAsync();
Console.WriteLine($"Current version: {version}");
// Get version compatibility info
var compatibility = await grain.GetVersionCompatibilityAsync();
Console.WriteLine($"Available versions: {string.Join(", ", compatibility.AvailableVersions)}");
// Upgrade to new version
var upgradeResult = await grain.UpgradeToVersionAsync(
new StateMachineVersion(1, 1, 0),
MigrationStrategy.Automatic);
if (upgradeResult.IsSuccess)
{
Console.WriteLine($"Upgraded to {upgradeResult.NewVersion} in {upgradeResult.UpgradeDuration.TotalMilliseconds}ms");
}
// Shadow evaluation - test without committing
var shadowResult = await grain.RunShadowEvaluationAsync(
new StateMachineVersion(2, 0, 0),
OrderTrigger.Submit);
if (shadowResult.WouldSucceed)
{
Console.WriteLine($"Shadow: {shadowResult.CurrentState} -> {shadowResult.PredictedState}");
// Safe to upgrade
}
// Blue-green deployment
var blueGreenResult = await grain.UpgradeToVersionAsync(
new StateMachineVersion(2, 0, 0),
MigrationStrategy.BlueGreen);
// Custom migration with hooks
var customResult = await grain.UpgradeToVersionAsync(
new StateMachineVersion(2, 0, 0),
MigrationStrategy.Custom);
Migration Hooks
public class CustomMigrationHook : IMigrationHook
{
public string HookName => "CustomDataMigration";
public int Priority => 50;
public async Task<bool> BeforeMigrationAsync(MigrationContext context)
{
// Pre-migration validation and data transformation
if (context.ToVersion.Major > context.FromVersion.Major)
{
// Handle breaking changes
var data = context.GetStateValue<OrderData>("OrderData");
if (data != null)
{
var transformed = TransformForNewVersion(data);
context.SetStateValue("OrderData", transformed);
}
}
return true;
}
public async Task AfterMigrationAsync(MigrationContext context)
{
// Post-migration verification
Console.WriteLine($"Migration completed for {context.GrainId}");
}
public async Task OnMigrationRollbackAsync(MigrationContext context, Exception error)
{
// Rollback custom changes
Console.WriteLine($"Rolling back migration: {error.Message}");
}
}
Version Compatibility Checking
var checker = serviceProvider.GetRequiredService<IVersionCompatibilityChecker>();
// Check upgrade compatibility
var result = await checker.CheckCompatibilityAsync(
"OrderGrain",
new StateMachineVersion(1, 0, 0),
new StateMachineVersion(2, 0, 0));
if (!result.IsCompatible)
{
foreach (var change in result.BreakingChanges)
{
Console.WriteLine($"Breaking change: {change.Description}");
Console.WriteLine($"Impact: {change.Impact}, Mitigation: {change.Mitigation}");
}
}
// Get upgrade recommendations
var recommendations = await checker.GetUpgradeRecommendationsAsync(
"OrderGrain",
new StateMachineVersion(1, 0, 0));
foreach (var rec in recommendations)
{
Console.WriteLine($"Upgrade to {rec.ToVersion}: {rec.RecommendationType}");
Console.WriteLine($"Risk: {rec.RiskLevel}, Effort: {rec.EstimatedEffort}");
}
Versioning Features
- Semantic Versioning: Full major.minor.patch version support with pre-release and build metadata
- Backward Compatibility: Automatic compatibility checking for minor version upgrades
- Breaking Change Detection: Identifies and documents breaking changes in major versions
- Migration Strategies: Automatic, custom, blue-green, and dry-run migration options
- Shadow Evaluation: Test new versions without affecting live state
- Migration Hooks: Extensible system for custom migration logic with priorities
- Rollback Support: Automatic state backup and rollback on migration failure
- Deployment Validation: Check compatibility with existing deployed versions
- Audit Trail: Complete history of version upgrades and migrations
Best Practices
1. State Machine Design
- Keep states focused and meaningful
- Use hierarchical states for related behaviors
- Design for testability with clear state transitions
- Enable nullable reference types (
<Nullable>enable</Nullable>) for better null safety
2. Performance
- Use timers for short durations (< 5 minutes)
- Use reminders for long durations (> 5 minutes)
- Enable snapshots for high-frequency state machines
- Configure appropriate dedupe key limits
- Optimize async methods by removing unnecessary
asynckeywords where no await is needed
3. Error Handling
try
{
await FireAsync(OrderTrigger.Process);
}
catch (InvalidStateTransitionException ex)
{
Logger.LogWarning("Invalid transition: {Message}", ex.Message);
// Handle invalid transition gracefully
}
4. Orleans Configuration
// In Program.cs
siloBuilder
.AddMemoryGrainStorage("Default")
.AddLogStorageBasedLogConsistencyProvider("LogStorage")
.AddMemoryGrainStorage("PubSubStore")
.AddStreams("SMS")
.AddMemoryStreams("SMS");
5. Build Configuration
<!-- Recommended project settings for Orleans.StateMachineES -->
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Package references with verified compatibility -->
<ItemGroup>
<PackageReference Include="Orleans.StateMachineES" Version="1.0.1" />
<PackageReference Include="Microsoft.Orleans.Sdk" Version="9.1.2" />
<!-- For testing -->
<PackageReference Include="xunit" Version="2.8.0" />
<PackageReference Include="FluentAssertions" Version="7.0.0" />
</ItemGroup>
6. Monitoring & Observability
protected override async Task RecordTransitionEvent(/*...*/)
{
// Add custom telemetry
using var activity = ActivitySource.StartActivity("StateMachine.Transition");
activity?.SetTag("grain.id", GetPrimaryKeyString());
activity?.SetTag("from.state", fromState.ToString());
activity?.SetTag("to.state", toState.ToString());
await base.RecordTransitionEvent(/*...*/);
}
Troubleshooting
Common Issues
- InvalidStateTransitionException: Check permitted triggers with
GetPermittedTriggersAsync() - Timer not firing: Verify reminder/timer registration and Orleans configuration
- Events not persisting: Check log consistency provider configuration
- Hierarchy not working: Ensure both
SubstateOf()andDefineSubstate()are called
Debug Commands
// Check current state and permitted triggers
var state = await grain.GetStateAsync();
var triggers = await grain.GetPermittedTriggersAsync();
var info = await grain.GetInfoAsync();
// For hierarchical grains
var hierarchy = await grain.GetHierarchicalInfoAsync();
var path = await grain.GetCurrentStatePathAsync();
Complete Example Applications
The examples/ directory contains four applications:
- ECommerceWorkflow - Order processing with event sourcing, timers, and monitoring
- DocumentApproval - Hierarchical states with saga orchestration
- MonitoringDashboard - Health checks, metrics, and visualization
- SmartHome - Source generator and orthogonal regions demonstration
SmartHome Example Highlights
The SmartHome example demonstrates the newest features:
- State machines generated from YAML/JSON specifications
- Orthogonal regions with 4 independent subsystems (Security, Climate, Energy, Presence)
- Cross-region synchronization and reactions
- Integration between generated device grains and orthogonal system grain
See examples/README.md for complete documentation and usage instructions.
This cheat sheet covers all major features of Orleans.StateMachineES. For detailed examples, see the test projects and documentation.