Core Concepts
Understanding the fundamental concepts of Orleans.StateMachineES is essential for building robust state machines.
State Machines Fundamentals
A state machine models a system that can be in exactly one state at a time, with defined transitions between states triggered by events.
Components
- States - Possible conditions the system can be in
- Triggers - Events that cause transitions
- Transitions - Rules for moving between states
- Guards - Conditions that control whether transitions are allowed
- Callbacks - Actions executed during transitions
States
States represent distinct conditions or phases in your system's lifecycle.
Defining States
Use enums for type-safe state definitions:
public enum OrderState
{
Draft, // Initial state
Submitted, // Awaiting payment
Processing, // Being fulfilled
Shipped, // In transit
Delivered, // Complete
Cancelled // Terminated
}
State Properties
Mutually Exclusive: A state machine can only be in one state at a time (except with orthogonal regions).
Persistent: The current state persists across grain activations.
Observable: You can always query the current state with GetStateAsync().
State Configuration
Configure each state in BuildStateMachine():
StateMachine.Configure(OrderState.Processing)
.Permit(OrderTrigger.Ship, OrderState.Shipped)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled)
.OnEntry(() => StartFulfillmentProcess())
.OnExit(() => CleanupResources());
Triggers
Triggers are events that cause state transitions.
Defining Triggers
Use enums for type-safe trigger definitions:
public enum OrderTrigger
{
Submit,
ConfirmPayment,
Ship,
Deliver,
Cancel
}
Firing Triggers
Execute transitions with FireAsync():
await orderGrain.FireAsync(OrderTrigger.Submit);
Trigger Types
Simple Triggers: No additional data
await grain.FireAsync(OrderTrigger.Cancel);
Parameterized Triggers: Pass data with the transition
var trigger = StateMachine.SetTriggerParameters<string>(OrderTrigger.Ship);
await grain.FireAsync(trigger, "UPS123456789");
See Parameterized Triggers for details.
Transitions
Transitions define how the state machine moves from one state to another.
Permit Transitions
Use .Permit() to allow a transition:
StateMachine.Configure(OrderState.Submitted)
.Permit(OrderTrigger.ConfirmPayment, OrderState.Processing);
Conditional Transitions
Use .PermitIf() with guard conditions:
StateMachine.Configure(OrderState.Submitted)
.PermitIf(
OrderTrigger.ConfirmPayment,
OrderState.Processing,
() => _paymentReceived && _inventoryAvailable
);
Self-Transitions
Allow a state to transition to itself:
StateMachine.Configure(OrderState.Processing)
.PermitReentry(OrderTrigger.UpdateStatus);
Ignore Triggers
Ignore triggers without error:
StateMachine.Configure(OrderState.Cancelled)
.Ignore(OrderTrigger.Ship); // Silently ignored
Guard Conditions
Guards control whether a transition is allowed based on runtime conditions.
Simple Guards
StateMachine.Configure(OrderState.Draft)
.PermitIf(
OrderTrigger.Submit,
OrderState.Submitted,
() => _items.Count > 0 // Guard: must have items
);
Multiple Guards
All guards must pass for the transition to succeed:
StateMachine.Configure(OrderState.Submitted)
.PermitIf(
OrderTrigger.ConfirmPayment,
OrderState.Processing,
() => _paymentReceived,
() => _inventoryAvailable,
() => _shippingAddressValid
);
Unmet Guards
Check guard status before firing:
// Returns list of unmet guard descriptions
var unmetGuards = await grain.CanFireWithUnmetGuardsAsync(
OrderTrigger.ConfirmPayment
);
if (unmetGuards.UnmetGuards.Any())
{
Console.WriteLine("Cannot confirm payment:");
foreach (var guard in unmetGuards.UnmetGuards)
{
Console.WriteLine($" - {guard}");
}
}
See Guard Conditions for advanced patterns.
Callbacks
Callbacks execute custom logic during state transitions.
OnEntry Callbacks
Execute when entering a state:
StateMachine.Configure(OrderState.Processing)
.OnEntry(() =>
{
_processingStartTime = DateTime.UtcNow;
NotifyWarehouse();
});
OnExit Callbacks
Execute when leaving a state:
StateMachine.Configure(OrderState.Processing)
.OnExit(() =>
{
_processingDuration = DateTime.UtcNow - _processingStartTime;
RecordMetrics();
});
OnEntryFrom Callbacks
Execute when entering from a specific trigger:
StateMachine.Configure(OrderState.Cancelled)
.OnEntryFrom(OrderTrigger.CancelByCustomer, () =>
{
_cancellationReason = "Customer requested";
RefundPayment();
})
.OnEntryFrom(OrderTrigger.CancelBySystem, () =>
{
_cancellationReason = "Fraud detected";
BlockCustomer();
});
OnExitFrom Callbacks
Execute when leaving to a specific state:
StateMachine.Configure(OrderState.Processing)
.OnExitFrom(OrderState.Shipped, () =>
{
GenerateTrackingNumber();
});
Callback Constraints
Critical: Callbacks must be synchronous. Async operations are not supported.
// ❌ Wrong: Async not supported
.OnEntry(async () => await SendEmailAsync())
// ✅ Correct: Use timers for async work
.OnEntry(() => RegisterTimer(_ => SendEmailAsync(), null, TimeSpan.Zero, TimeSpan.MaxValue))
See Async Patterns for handling async operations correctly.
State Machine Lifecycle
Activation
When a grain activates, BuildStateMachine() is called to construct the state machine:
public override async Task OnActivateAsync(CancellationToken cancellationToken)
{
await base.OnActivateAsync(cancellationToken);
// State machine is now ready
}
State Persistence
The current state is persisted automatically:
- Automatic: State survives grain deactivation
- Consistent: State is part of grain state storage
- Event-Sourced: Optional full history with
EventSourcedStateMachineGrain
Deactivation
On deactivation, state is saved:
public override async Task OnDeactivateAsync(DeactivationReason reason, CancellationToken cancellationToken)
{
// State is automatically persisted
await base.OnDeactivateAsync(reason, cancellationToken);
}
State Machine Metadata
Query state machine information at runtime:
Current State
var state = await grain.GetStateAsync();
Permitted Triggers
var triggers = await grain.GetPermittedTriggersAsync();
// Returns: [OrderTrigger.Ship, OrderTrigger.Cancel]
Can Fire Check
bool canShip = await grain.CanFireAsync(OrderTrigger.Ship);
Complete Info
var info = await grain.GetStateMachineInfoAsync();
Console.WriteLine($"Current: {info.State}");
Console.WriteLine($"Initial: {info.InitialState}");
Console.WriteLine($"All states: {string.Join(", ", info.States)}");
Console.WriteLine($"All triggers: {string.Join(", ", info.Triggers)}");
State Diagrams
Visualize your state machine:
var dot = await grain.GetDotGraphAsync();
var mermaid = await grain.GetMermaidDiagramAsync();
var plantUml = await grain.GetPlantUmlDiagramAsync();
See Visualization Guide for details.
Best Practices
1. Use Descriptive Names
// ✅ Good: Clear intent
public enum OrderState { Draft, Submitted, Processing, Shipped }
public enum OrderTrigger { Submit, ConfirmPayment, Ship }
// ❌ Bad: Unclear meaning
public enum State { S1, S2, S3 }
public enum Event { E1, E2 }
2. Define All Transitions
// ✅ Good: All states configured
StateMachine.Configure(OrderState.Draft)
.Permit(OrderTrigger.Submit, OrderState.Submitted)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled);
// ❌ Bad: Unreachable states
StateMachine.Configure(OrderState.Draft)
.Permit(OrderTrigger.Submit, OrderState.Submitted);
// OrderState.Cancelled is unreachable!
The OSMES004 analyzer detects unreachable states.
3. Handle All Triggers
// ✅ Good: Define what happens with unexpected triggers
StateMachine.OnUnhandledTrigger((state, trigger) =>
{
_logger.LogWarning("Unexpected trigger {Trigger} in state {State}", trigger, state);
});
4. Keep Guards Simple
// ✅ Good: Simple, testable guard
.PermitIf(trigger, nextState, () => _isValid)
// ❌ Bad: Complex logic in guard
.PermitIf(trigger, nextState, () =>
{
// 50 lines of complex validation
// Cyclomatic complexity > 10
})
The OSMES008 analyzer warns about complex guards.
5. Avoid Side Effects in Guards
// ❌ Bad: Guard modifies state
.PermitIf(trigger, nextState, () =>
{
_counter++; // Side effect!
return _counter > 5;
})
// ✅ Good: Guard is pure
.PermitIf(trigger, nextState, () => _counter > 5)
Next Steps
- Parameterized Triggers - Pass data with transitions
- Guard Conditions - Advanced validation patterns
- Async Patterns - Handling async operations correctly
- Event Sourcing - Complete state history
Additional Resources
- Stateless Library Docs - Underlying state machine library
- API Reference
- Examples