State Machine Pattern Generator
The State Machine Pattern Generator automatically creates deterministic finite state machines with explicit states, triggers, guards, and lifecycle hooks. It eliminates boilerplate code for state management while providing compile-time type safety, async/await support, and configurable error handling policies.
Overview
The generator produces:
- State property to track the current state
- Fire method for synchronous state transitions
- FireAsync method for asynchronous workflows with ValueTask and CancellationToken support
- CanFire method to check if a trigger is valid for the current state
- Deterministic transition resolution based on (FromState, Trigger) pairs
- Guard evaluation with configurable failure policies
- Entry/exit hooks for state lifecycle management
- Zero runtime overhead through source generation
Quick Start
1. Define Your States and Triggers
Define enums for your states and triggers:
using PatternKit.Generators.State;
public enum OrderState
{
Draft,
Submitted,
Paid,
Shipped,
Cancelled
}
public enum OrderTrigger
{
Submit,
Pay,
Ship,
Cancel
}
2. Create Your State Machine Host
Mark your class with [StateMachine] and define transitions:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
public string Id { get; }
public decimal Amount { get; }
public OrderFlow(string id, decimal amount)
{
Id = id;
Amount = amount;
State = OrderState.Draft; // Set initial state
}
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit()
{
Console.WriteLine($"Order {Id} submitted");
}
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private void OnPay()
{
Console.WriteLine($"Payment processed for {Id}");
}
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
private void OnShip()
{
Console.WriteLine($"Order {Id} shipped");
}
}
3. Build Your Project
The generator runs during compilation and produces the state machine implementation:
var order = new OrderFlow("ORD-001", 299.99m);
Console.WriteLine($"Current state: {order.State}"); // Draft
order.Fire(OrderTrigger.Submit);
Console.WriteLine($"Current state: {order.State}"); // Submitted
order.Fire(OrderTrigger.Pay);
Console.WriteLine($"Current state: {order.State}"); // Paid
order.Fire(OrderTrigger.Ship);
Console.WriteLine($"Current state: {order.State}"); // Shipped
4. Generated Code
partial class OrderFlow
{
public OrderState State { get; private set; }
public bool CanFire(OrderTrigger trigger)
{
return (State, trigger) switch
{
(OrderState.Draft, OrderTrigger.Submit) => true,
(OrderState.Submitted, OrderTrigger.Pay) => true,
(OrderState.Paid, OrderTrigger.Ship) => true,
_ => false
};
}
public void Fire(OrderTrigger trigger)
{
switch (State)
{
case OrderState.Draft:
switch (trigger)
{
case OrderTrigger.Submit:
OnSubmit();
State = OrderState.Submitted;
return;
}
break;
// ... more cases
}
throw new InvalidOperationException($"Invalid trigger {trigger} for state {State}");
}
}
Core Features
Guards
Guards control whether a transition is allowed based on runtime conditions:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
public decimal Amount { get; }
public bool IsPaymentAuthorized { get; set; }
[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
private bool CanPay()
{
return Amount > 0 && Amount < 10000 && IsPaymentAuthorized;
}
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private void OnPay()
{
Console.WriteLine($"Processing payment of ${Amount}");
}
}
Usage:
var order = new OrderFlow("ORD-001", 150.00m);
order.Fire(OrderTrigger.Submit);
if (order.CanFire(OrderTrigger.Pay))
{
order.Fire(OrderTrigger.Pay); // Only fires if guard passes
}
Entry and Exit Hooks
Execute code when entering or exiting specific states:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
[StateExit(OrderState.Paid)]
private void OnExitPaid()
{
Console.WriteLine("Finalizing payment transaction");
// Send payment confirmation email
// Update inventory
}
[StateEntry(OrderState.Shipped)]
private void OnEnterShipped()
{
Console.WriteLine("Order is being shipped");
// Send shipping notification
// Generate tracking number
// Update shipping status
}
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
private void OnShip()
{
Console.WriteLine("Preparing shipment");
}
}
Execution Order:
- Exit hooks for
FromState(if any) - Transition action method (
[StateTransition]) (if any) - Update
State = ToState - Entry hooks for
ToState(if any)
Async Support
The generator automatically detects async methods and generates FireAsync:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
Console.WriteLine("Processing payment...");
await ProcessPaymentAsync(ct);
await SendConfirmationEmailAsync(ct);
}
[StateEntry(OrderState.Shipped)]
private async ValueTask OnEnterShippedAsync(CancellationToken ct)
{
await NotifyShippingServiceAsync(ct);
await UpdateTrackingSystemAsync(ct);
}
}
Usage:
var order = new OrderFlow("ORD-001", 299.99m);
order.Fire(OrderTrigger.Submit);
// Use async method for async transitions
await order.FireAsync(OrderTrigger.Pay, cancellationToken);
await order.FireAsync(OrderTrigger.Ship, cancellationToken);
Async Guards
Guards can also be async to support I/O operations:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
private async ValueTask<bool> CanPayAsync(CancellationToken ct)
{
// Check with payment service
return await PaymentService.IsAuthorizedAsync(Id, Amount, ct);
}
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
await ProcessPaymentAsync(ct);
}
}
Note: Async guards are evaluated synchronously in CanFire() using GetAwaiter().GetResult(). Use FireAsync() for proper async execution.
Multiple Transitions from Same State
You can define multiple valid transitions from a single state:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
// Allow cancellation from multiple states
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Cancel, To = OrderState.Cancelled)]
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Cancel, To = OrderState.Cancelled)]
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Cancel, To = OrderState.Cancelled)]
private void OnCancel()
{
Console.WriteLine($"Order {Id} cancelled");
}
[StateEntry(OrderState.Cancelled)]
private void OnEnterCancelled()
{
// Process refund if applicable
// Send cancellation notification
}
}
Configuration Options
Custom Method Names
Customize the names of generated methods:
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
FireMethodName = "Transition",
FireAsyncMethodName = "TransitionAsync",
CanFireMethodName = "CanTransition")]
public partial class OrderFlow
{
// Will generate: Transition(), TransitionAsync(), CanTransition()
}
Error Handling Policies
Invalid Trigger Policy
Controls what happens when an invalid trigger is fired:
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
InvalidTrigger = StateMachineInvalidTriggerPolicy.Throw)] // Default
public partial class OrderFlow
{
// Throws InvalidOperationException on invalid trigger
}
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
InvalidTrigger = StateMachineInvalidTriggerPolicy.Ignore)]
public partial class OrderFlow
{
// Silently ignores invalid triggers
}
Available Policies:
Throw(default) - ThrowsInvalidOperationExceptionIgnore- Does nothing, returns silently
Guard Failure Policy
Controls what happens when a guard returns false:
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
GuardFailure = StateMachineGuardFailurePolicy.Throw)] // Default
public partial class OrderFlow
{
// Throws InvalidOperationException when guard fails
}
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
GuardFailure = StateMachineGuardFailurePolicy.Ignore)]
public partial class OrderFlow
{
// Silently ignores when guard fails
}
Available Policies:
Throw(default) - ThrowsInvalidOperationExceptionIgnore- Does nothing, returns silently
Async Generation Control
Control async method generation explicitly:
// Force async generation even without async methods
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
ForceAsync = true)]
public partial class OrderFlow
{
// Always generates FireAsync even for sync-only transitions
}
// Explicitly disable async generation (warning if async methods exist)
[StateMachine(
typeof(OrderState),
typeof(OrderTrigger),
GenerateAsync = false)]
public partial class OrderFlow
{
// Will emit PKST008 warning if async methods are present
}
Supported Target Types
The state machine generator supports:
- partial class
- partial struct
- partial record class
- partial record struct
// Class
[StateMachine(typeof(State), typeof(Trigger))]
public partial class OrderStateMachine { }
// Struct (for high-performance scenarios)
[StateMachine(typeof(State), typeof(Trigger))]
public partial struct LightweightStateMachine { }
// Record class (immutable by convention)
[StateMachine(typeof(State), typeof(Trigger))]
public partial record class OrderRecord { }
// Record struct
[StateMachine(typeof(State), typeof(Trigger))]
public partial record struct CompactStateMachine { }
Real-World Examples
Order Processing Workflow
public enum OrderState { Draft, Submitted, Paid, Shipped, Delivered, Cancelled, Refunded }
public enum OrderTrigger { Submit, Pay, Ship, Deliver, Cancel, Refund }
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderWorkflow
{
public string OrderId { get; }
public decimal Amount { get; }
public DateTime? PaidAt { get; private set; }
public DateTime? ShippedAt { get; private set; }
public OrderWorkflow(string orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
State = OrderState.Draft;
}
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit()
{
// Validate order
Console.WriteLine($"Order {OrderId} submitted");
}
[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
private bool CanPay() => Amount > 0 && Amount < 100000;
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
await ProcessPaymentAsync(ct);
PaidAt = DateTime.UtcNow;
Console.WriteLine($"Payment of ${Amount} processed for order {OrderId}");
}
[StateExit(OrderState.Paid)]
private void OnExitPaid()
{
Console.WriteLine("Finalizing payment records");
}
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
private async ValueTask OnShipAsync(CancellationToken ct)
{
await NotifyShippingServiceAsync(ct);
ShippedAt = DateTime.UtcNow;
Console.WriteLine($"Order {OrderId} shipped");
}
[StateEntry(OrderState.Shipped)]
private async ValueTask OnEnterShippedAsync(CancellationToken ct)
{
await SendTrackingNotificationAsync(ct);
Console.WriteLine("Tracking notification sent");
}
[StateTransition(From = OrderState.Shipped, Trigger = OrderTrigger.Deliver, To = OrderState.Delivered)]
private void OnDeliver()
{
Console.WriteLine($"Order {OrderId} delivered");
}
// Cancellation transitions
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Cancel, To = OrderState.Cancelled)]
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Cancel, To = OrderState.Cancelled)]
private void OnCancel()
{
Console.WriteLine($"Order {OrderId} cancelled");
}
// Refund transition
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Refund, To = OrderState.Refunded)]
[StateTransition(From = OrderState.Shipped, Trigger = OrderTrigger.Refund, To = OrderState.Refunded)]
[StateTransition(From = OrderState.Delivered, Trigger = OrderTrigger.Refund, To = OrderState.Refunded)]
private async ValueTask OnRefundAsync(CancellationToken ct)
{
await ProcessRefundAsync(ct);
Console.WriteLine($"Refund processed for order {OrderId}");
}
private Task ProcessPaymentAsync(CancellationToken ct) => Task.Delay(100, ct);
private Task NotifyShippingServiceAsync(CancellationToken ct) => Task.Delay(50, ct);
private Task SendTrackingNotificationAsync(CancellationToken ct) => Task.Delay(50, ct);
private Task ProcessRefundAsync(CancellationToken ct) => Task.Delay(100, ct);
}
Document Approval Workflow
public enum DocumentState { Draft, PendingReview, Approved, Rejected, Published, Archived }
public enum DocumentAction { SubmitForReview, Approve, Reject, Publish, Archive, Revise }
[StateMachine(typeof(DocumentState), typeof(DocumentAction))]
public partial class DocumentWorkflow
{
public string DocumentId { get; }
public string CurrentReviewer { get; private set; } = string.Empty;
public List<string> ApprovalHistory { get; } = new();
public DocumentWorkflow(string documentId)
{
DocumentId = documentId;
State = DocumentState.Draft;
}
[StateTransition(From = DocumentState.Draft, Trigger = DocumentAction.SubmitForReview, To = DocumentState.PendingReview)]
private void OnSubmitForReview()
{
CurrentReviewer = "reviewer@example.com";
Console.WriteLine($"Document {DocumentId} submitted for review to {CurrentReviewer}");
}
[StateGuard(From = DocumentState.PendingReview, Trigger = DocumentAction.Approve)]
private bool CanApprove()
{
return !string.IsNullOrEmpty(CurrentReviewer);
}
[StateTransition(From = DocumentState.PendingReview, Trigger = DocumentAction.Approve, To = DocumentState.Approved)]
private void OnApprove()
{
ApprovalHistory.Add($"{CurrentReviewer} approved at {DateTime.UtcNow}");
Console.WriteLine($"Document {DocumentId} approved by {CurrentReviewer}");
}
[StateTransition(From = DocumentState.PendingReview, Trigger = DocumentAction.Reject, To = DocumentState.Rejected)]
private void OnReject()
{
ApprovalHistory.Add($"{CurrentReviewer} rejected at {DateTime.UtcNow}");
Console.WriteLine($"Document {DocumentId} rejected by {CurrentReviewer}");
}
[StateTransition(From = DocumentState.Rejected, Trigger = DocumentAction.Revise, To = DocumentState.Draft)]
private void OnRevise()
{
CurrentReviewer = string.Empty;
Console.WriteLine($"Document {DocumentId} sent back to draft for revision");
}
[StateTransition(From = DocumentState.Approved, Trigger = DocumentAction.Publish, To = DocumentState.Published)]
private async ValueTask OnPublishAsync(CancellationToken ct)
{
await PublishToContentManagementSystemAsync(ct);
Console.WriteLine($"Document {DocumentId} published");
}
[StateEntry(DocumentState.Published)]
private void OnEnterPublished()
{
Console.WriteLine("Document is now publicly visible");
}
[StateTransition(From = DocumentState.Published, Trigger = DocumentAction.Archive, To = DocumentState.Archived)]
private void OnArchive()
{
Console.WriteLine($"Document {DocumentId} archived");
}
private Task PublishToContentManagementSystemAsync(CancellationToken ct) => Task.Delay(200, ct);
}
Best Practices
1. Define Clear State Enums
Use descriptive names that reflect business states:
// Good
public enum OrderState { Draft, Submitted, Paid, Shipped, Delivered }
// Avoid
public enum State { S1, S2, S3, S4, S5 }
2. Use Guards for Business Rules
Centralize validation logic in guards:
[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
private bool CanPay()
{
return Amount > 0 &&
Amount < MaxAllowedAmount &&
PaymentMethod != null &&
!IsBlacklisted;
}
3. Keep Transition Methods Focused
Each transition method should do one thing:
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
await ProcessPaymentAsync(ct);
// Don't mix concerns - handle notification in entry hook
}
[StateEntry(OrderState.Paid)]
private async ValueTask OnEnterPaidAsync(CancellationToken ct)
{
await SendPaymentConfirmationAsync(ct);
}
4. Use Async for I/O Operations
Prefer ValueTask for async operations:
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
private async ValueTask OnShipAsync(CancellationToken ct)
{
await ShippingService.CreateShipmentAsync(Id, ct);
}
5. Document Complex Workflows
Add XML documentation to help users understand the state machine:
/// <summary>
/// Manages the order fulfillment workflow from creation to delivery.
/// States: Draft -> Submitted -> Paid -> Shipped -> Delivered
/// </summary>
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderWorkflow
{
/// <summary>
/// Processes payment and transitions to Paid state.
/// Guard: Amount must be > 0 and < $100,000
/// </summary>
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
await ProcessPaymentAsync(ct);
}
}
Diagnostics
The generator provides comprehensive compile-time diagnostics:
| ID | Severity | Description |
|---|---|---|
| PKST001 | Error | Type marked with [StateMachine] must be partial |
| PKST002 | Error | State type must be an enum |
| PKST003 | Error | Trigger type must be an enum |
| PKST004 | Error | Duplicate transition detected for (From, Trigger) |
| PKST005 | Error | Transition method signature invalid |
| PKST006 | Error | Guard method signature invalid |
| PKST007 | Error | Entry/Exit hook signature invalid |
| PKST008 | Warning | Async method detected but async generation disabled |
| PKST009 | Error | Generic types not supported |
| PKST010 | Error | Nested types not supported |
Common Errors and Solutions
PKST001: Type not partial
Error:
[StateMachine(typeof(State), typeof(Trigger))]
public class OrderFlow // Missing 'partial'
{
}
Solution:
[StateMachine(typeof(State), typeof(Trigger))]
public partial class OrderFlow // Add 'partial'
{
}
PKST004: Duplicate transitions
Error:
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit1() { }
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Paid)]
private void OnSubmit2() { } // Duplicate!
Solution: Each (From, Trigger) pair must be unique. Consolidate or use guards.
PKST005: Invalid transition signature
Error:
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private int OnSubmit() // Must return void or ValueTask
{
return 42;
}
Solution:
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit() // Correct
{
}
Performance Considerations
Zero Allocation Path
The generator produces zero-allocation code for synchronous transitions:
// No boxing, no delegates, no allocations
order.Fire(OrderTrigger.Submit);
ValueTask for Async
Async operations use ValueTask to minimize allocations:
// ValueTask can complete synchronously without allocation
await order.FireAsync(OrderTrigger.Pay, ct);
Struct State Machines
For ultra-high-performance scenarios, use struct:
[StateMachine(typeof(State), typeof(Trigger))]
public partial struct HighPerformanceStateMachine
{
// Entire state machine on the stack
}
Migration Guide
From Manual Switch Statements
Before:
public class OrderFlow
{
public OrderState State { get; private set; }
public void Fire(OrderTrigger trigger)
{
switch (State)
{
case OrderState.Draft when trigger == OrderTrigger.Submit:
State = OrderState.Submitted;
OnSubmit();
break;
// ... many more cases
}
}
}
After:
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit() { }
// Compiler generates Fire(), CanFire(), etc.
}
From Other State Machine Libraries
Most state machine libraries use runtime configuration. This generator uses compile-time generation for:
- Better performance (no reflection)
- Better IntelliSense
- Compile-time validation
- Easier debugging
FAQ
Can I use custom types instead of enums?
Currently, only enums are supported for states and triggers (v1 limitation). This ensures:
- Compile-time validation
- Optimal performance
- Clear, unambiguous state representation
Can I have multiple state machines in one class?
No, each class can only have one [StateMachine] attribute. Consider composition:
public class Order
{
public OrderWorkflow Workflow { get; }
public PaymentProcessor Payment { get; }
}
Is it thread-safe?
No, the generated state machine is not thread-safe by default. Use external synchronization if needed:
private readonly object _lock = new();
public void SafeFire(OrderTrigger trigger)
{
lock (_lock)
{
_order.Fire(trigger);
}
}
Can I persist the state?
Yes, serialize the State property:
var json = JsonSerializer.Serialize(new { order.State, order.OrderId });
// Save to database
// Later, restore:
var data = JsonSerializer.Deserialize<OrderData>(json);
var order = new OrderFlow(data.OrderId, amount);
order.State = data.State; // Set via constructor or property
How do I test state machines?
Test transitions independently:
[Fact]
public void Submit_TransitionsToDraftToSubmitted()
{
var order = new OrderFlow("TEST-001", 100m);
Assert.Equal(OrderState.Draft, order.State);
order.Fire(OrderTrigger.Submit);
Assert.Equal(OrderState.Submitted, order.State);
}
[Fact]
public void CanPay_ReturnsFalse_WhenAmountIsZero()
{
var order = new OrderFlow("TEST-002", 0m);
order.Fire(OrderTrigger.Submit);
Assert.False(order.CanFire(OrderTrigger.Pay));
}
See Also
- State Machine Example
- Template Method Generator - For sequential workflows
- Builder Pattern - For object construction
- Visitor Pattern - For operation dispatch