From e39ea81e51825eadc8baf6961d6fb36d5a1eea83 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:02:18 +0000
Subject: [PATCH 1/9] Initial plan
From 486a7c3d8c243447ae31181dee06836a46600a68 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:07:44 +0000
Subject: [PATCH 2/9] Add State Pattern attributes and generator implementation
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../State/StateEntryAttribute.cs | 24 +
.../State/StateExitAttribute.cs | 24 +
.../State/StateGuardAttribute.cs | 21 +
.../State/StateMachineAttribute.cs | 111 ++
.../State/StateTransitionAttribute.cs | 25 +
.../StateMachineGenerator.cs | 952 ++++++++++++++++++
6 files changed, 1157 insertions(+)
create mode 100644 src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/State/StateGuardAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
create mode 100644 src/PatternKit.Generators.Abstractions/State/StateTransitionAttribute.cs
create mode 100644 src/PatternKit.Generators/StateMachineGenerator.cs
diff --git a/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
new file mode 100644
index 0000000..9744d13
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
@@ -0,0 +1,24 @@
+namespace PatternKit.Generators.State;
+
+///
+/// Marks a method to be invoked when entering a specific state.
+/// Entry hooks are executed after the State property is updated to the new state.
+/// The method can be synchronous (void) or asynchronous (ValueTask).
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class StateEntryAttribute : Attribute
+{
+ ///
+ /// The state for which this entry hook applies.
+ ///
+ public object State { get; set; } = null!;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The state for which this entry hook applies.
+ public StateEntryAttribute(object state)
+ {
+ State = state;
+ }
+}
diff --git a/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
new file mode 100644
index 0000000..a7ee72f
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
@@ -0,0 +1,24 @@
+namespace PatternKit.Generators.State;
+
+///
+/// Marks a method to be invoked when exiting a specific state.
+/// Exit hooks are executed before the State property is updated to the new state.
+/// The method can be synchronous (void) or asynchronous (ValueTask).
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class StateExitAttribute : Attribute
+{
+ ///
+ /// The state for which this exit hook applies.
+ ///
+ public object State { get; set; } = null!;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The state for which this exit hook applies.
+ public StateExitAttribute(object state)
+ {
+ State = state;
+ }
+}
diff --git a/src/PatternKit.Generators.Abstractions/State/StateGuardAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateGuardAttribute.cs
new file mode 100644
index 0000000..c5e558a
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/State/StateGuardAttribute.cs
@@ -0,0 +1,21 @@
+namespace PatternKit.Generators.State;
+
+///
+/// Marks a method as a guard condition for a state transition.
+/// The method must return bool or ValueTask<bool> and is evaluated
+/// before the transition occurs. If the guard returns false, the transition
+/// is prevented according to the GuardFailurePolicy.
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class StateGuardAttribute : Attribute
+{
+ ///
+ /// The state from which this guard applies.
+ ///
+ public object From { get; set; } = null!;
+
+ ///
+ /// The trigger for which this guard applies.
+ ///
+ public object Trigger { get; set; } = null!;
+}
diff --git a/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
new file mode 100644
index 0000000..8602734
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
@@ -0,0 +1,111 @@
+namespace PatternKit.Generators.State;
+
+///
+/// Marks a partial type as a state machine host that will generate Fire/FireAsync/CanFire methods
+/// for deterministic state transitions based on annotated transition, guard, and hook methods.
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
+public sealed class StateMachineAttribute : Attribute
+{
+ ///
+ /// The state enum type (must be an enum in v1).
+ ///
+ public Type StateType { get; }
+
+ ///
+ /// The trigger enum type (must be an enum in v1).
+ ///
+ public Type TriggerType { get; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The state enum type.
+ /// The trigger enum type.
+ public StateMachineAttribute(Type stateType, Type triggerType)
+ {
+ StateType = stateType;
+ TriggerType = triggerType;
+ }
+
+ ///
+ /// Gets or sets the name of the generated Fire method. Default: "Fire".
+ ///
+ public string FireMethodName { get; set; } = "Fire";
+
+ ///
+ /// Gets or sets the name of the generated FireAsync method. Default: "FireAsync".
+ ///
+ public string FireAsyncMethodName { get; set; } = "FireAsync";
+
+ ///
+ /// Gets or sets the name of the generated CanFire method. Default: "CanFire".
+ ///
+ public string CanFireMethodName { get; set; } = "CanFire";
+
+ ///
+ /// Gets or sets whether to generate async methods.
+ /// When null (default), async generation is inferred from the presence of async transitions/hooks.
+ ///
+ public bool? GenerateAsync { get; set; }
+
+ ///
+ /// Gets or sets whether to force async generation even if all transitions/hooks are synchronous.
+ /// Default is false.
+ ///
+ public bool ForceAsync { get; set; }
+
+ ///
+ /// Gets or sets the policy for handling invalid triggers.
+ /// Default is Throw.
+ ///
+ public StateMachineInvalidTriggerPolicy InvalidTrigger { get; set; } = StateMachineInvalidTriggerPolicy.Throw;
+
+ ///
+ /// Gets or sets the policy for handling guard failures.
+ /// Default is Throw.
+ ///
+ public StateMachineGuardFailurePolicy GuardFailure { get; set; } = StateMachineGuardFailurePolicy.Throw;
+}
+
+///
+/// Defines the policy for handling invalid triggers.
+///
+public enum StateMachineInvalidTriggerPolicy
+{
+ ///
+ /// Throw an InvalidOperationException when an invalid trigger is fired.
+ ///
+ Throw = 0,
+
+ ///
+ /// Ignore invalid triggers (no-op).
+ ///
+ Ignore = 1,
+
+ ///
+ /// Return false from CanFire and no-op in Fire for invalid triggers.
+ ///
+ ReturnFalse = 2
+}
+
+///
+/// Defines the policy for handling guard failures.
+///
+public enum StateMachineGuardFailurePolicy
+{
+ ///
+ /// Throw an InvalidOperationException when a guard returns false.
+ ///
+ Throw = 0,
+
+ ///
+ /// Ignore guard failures (no-op, state does not transition).
+ ///
+ Ignore = 1,
+
+ ///
+ /// Return false from CanFire and no-op in Fire when guard fails.
+ ///
+ ReturnFalse = 2
+}
diff --git a/src/PatternKit.Generators.Abstractions/State/StateTransitionAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateTransitionAttribute.cs
new file mode 100644
index 0000000..63bdc3c
--- /dev/null
+++ b/src/PatternKit.Generators.Abstractions/State/StateTransitionAttribute.cs
@@ -0,0 +1,25 @@
+namespace PatternKit.Generators.State;
+
+///
+/// Marks a method to be invoked during a specific state transition.
+/// The method can be synchronous (void/ValueTask) and is executed between
+/// exit hooks (old state) and entry hooks (new state).
+///
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class StateTransitionAttribute : Attribute
+{
+ ///
+ /// The state from which this transition originates.
+ ///
+ public object From { get; set; } = null!;
+
+ ///
+ /// The trigger that activates this transition.
+ ///
+ public object Trigger { get; set; } = null!;
+
+ ///
+ /// The state to which this transition leads.
+ ///
+ public object To { get; set; } = null!;
+}
diff --git a/src/PatternKit.Generators/StateMachineGenerator.cs b/src/PatternKit.Generators/StateMachineGenerator.cs
new file mode 100644
index 0000000..8fc84c8
--- /dev/null
+++ b/src/PatternKit.Generators/StateMachineGenerator.cs
@@ -0,0 +1,952 @@
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using System.Collections.Immutable;
+using System.Text;
+
+namespace PatternKit.Generators;
+
+///
+/// Source generator for the State pattern.
+/// Generates deterministic state machine implementations with explicit states, triggers,
+/// guards, entry/exit hooks, and sync/async support using ValueTask.
+///
+[Generator]
+public sealed class StateMachineGenerator : IIncrementalGenerator
+{
+ // Diagnostic IDs
+ private const string DiagIdTypeNotPartial = "PKST001";
+ private const string DiagIdStateNotEnum = "PKST002";
+ private const string DiagIdTriggerNotEnum = "PKST003";
+ private const string DiagIdDuplicateTransition = "PKST004";
+ private const string DiagIdInvalidTransitionSignature = "PKST005";
+ private const string DiagIdInvalidGuardSignature = "PKST006";
+ private const string DiagIdInvalidHookSignature = "PKST007";
+ private const string DiagIdAsyncMethodDetected = "PKST008";
+
+ private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
+ id: DiagIdTypeNotPartial,
+ title: "Type marked with [StateMachine] must be partial",
+ messageFormat: "Type '{0}' is marked with [StateMachine] but is not declared as partial. Add the 'partial' keyword to the type declaration.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor StateNotEnumDescriptor = new(
+ id: DiagIdStateNotEnum,
+ title: "State type must be an enum",
+ messageFormat: "State type '{0}' must be an enum type. Non-enum state types are not supported in v1.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor TriggerNotEnumDescriptor = new(
+ id: DiagIdTriggerNotEnum,
+ title: "Trigger type must be an enum",
+ messageFormat: "Trigger type '{0}' must be an enum type. Non-enum trigger types are not supported in v1.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor DuplicateTransitionDescriptor = new(
+ id: DiagIdDuplicateTransition,
+ title: "Duplicate transition detected",
+ messageFormat: "Duplicate transition detected for (From={0}, Trigger={1}). Each (From, Trigger) pair must be unique. Conflicting methods: {2}.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidTransitionSignatureDescriptor = new(
+ id: DiagIdInvalidTransitionSignature,
+ title: "Transition method signature invalid",
+ messageFormat: "Transition method '{0}' has an invalid signature. Transitions must return void or ValueTask, optionally accepting CancellationToken for async methods.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidGuardSignatureDescriptor = new(
+ id: DiagIdInvalidGuardSignature,
+ title: "Guard method signature invalid",
+ messageFormat: "Guard method '{0}' has an invalid signature. Guards must return bool or ValueTask, optionally accepting CancellationToken for async methods.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor InvalidHookSignatureDescriptor = new(
+ id: DiagIdInvalidHookSignature,
+ title: "Entry/Exit hook signature invalid",
+ messageFormat: "Entry/Exit hook method '{0}' has an invalid signature. Hooks must return void or ValueTask, optionally accepting CancellationToken for async methods.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor AsyncMethodDetectedDescriptor = new(
+ id: DiagIdAsyncMethodDetected,
+ title: "Async method detected but async generation disabled",
+ messageFormat: "Async method '{0}' detected but async generation is disabled. Enable GenerateAsync or ForceAsync on the [StateMachine] attribute.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Warning,
+ isEnabledByDefault: true);
+
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Find all type declarations with [StateMachine] attribute
+ var stateMachineTypes = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName: "PatternKit.Generators.State.StateMachineAttribute",
+ predicate: static (node, _) => node is TypeDeclarationSyntax,
+ transform: static (ctx, _) => ctx
+ );
+
+ // Generate for each type
+ context.RegisterSourceOutput(stateMachineTypes, (spc, typeContext) =>
+ {
+ if (typeContext.TargetSymbol is not INamedTypeSymbol typeSymbol)
+ return;
+
+ var attr = typeContext.Attributes.FirstOrDefault(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.State.StateMachineAttribute");
+ if (attr is null)
+ return;
+
+ GenerateStateMachineForType(spc, typeSymbol, attr, typeContext.TargetNode);
+ });
+ }
+
+ private void GenerateStateMachineForType(
+ SourceProductionContext context,
+ INamedTypeSymbol typeSymbol,
+ AttributeData attribute,
+ SyntaxNode node)
+ {
+ // Check if type is partial
+ if (!IsPartialType(node))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ TypeNotPartialDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Parse attribute configuration
+ var config = ParseStateMachineConfig(attribute, context, out var stateType, out var triggerType);
+ if (config is null || stateType is null || triggerType is null)
+ return;
+
+ // Validate state and trigger types are enums
+ if (stateType.TypeKind != TypeKind.Enum)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ StateNotEnumDescriptor,
+ node.GetLocation(),
+ stateType.ToDisplayString()));
+ return;
+ }
+
+ if (triggerType.TypeKind != TypeKind.Enum)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ TriggerNotEnumDescriptor,
+ node.GetLocation(),
+ triggerType.ToDisplayString()));
+ return;
+ }
+
+ // Collect transitions, guards, and hooks
+ var transitions = CollectTransitions(typeSymbol, stateType, triggerType, context);
+ var guards = CollectGuards(typeSymbol, stateType, triggerType, context);
+ var entryHooks = CollectEntryHooks(typeSymbol, stateType, context);
+ var exitHooks = CollectExitHooks(typeSymbol, stateType, context);
+
+ // Validate for duplicate transitions
+ if (!ValidateTransitions(transitions, typeSymbol, context))
+ return;
+
+ // Validate signatures
+ if (!ValidateSignatures(transitions, guards, entryHooks, exitHooks, context))
+ return;
+
+ // Determine if async generation is needed
+ var needsAsync = config.ForceAsync ||
+ (config.GenerateAsync ?? false) ||
+ DetermineIfAsync(transitions, guards, entryHooks, exitHooks);
+
+ // Generate the state machine implementation
+ var source = GenerateStateMachine(typeSymbol, config, stateType, triggerType,
+ transitions, guards, entryHooks, exitHooks, needsAsync);
+ var fileName = $"{typeSymbol.Name}.StateMachine.g.cs";
+ context.AddSource(fileName, source);
+ }
+
+ private static bool IsPartialType(SyntaxNode node)
+ {
+ return node switch
+ {
+ ClassDeclarationSyntax classDecl => classDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ StructDeclarationSyntax structDecl => structDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ RecordDeclarationSyntax recordDecl => recordDecl.Modifiers.Any(SyntaxKind.PartialKeyword),
+ _ => false
+ };
+ }
+
+ private StateMachineConfig? ParseStateMachineConfig(
+ AttributeData attribute,
+ SourceProductionContext context,
+ out ITypeSymbol? stateType,
+ out ITypeSymbol? triggerType)
+ {
+ stateType = null;
+ triggerType = null;
+
+ // Constructor arguments: stateType, triggerType
+ if (attribute.ConstructorArguments.Length < 2)
+ return null;
+
+ stateType = attribute.ConstructorArguments[0].Value as ITypeSymbol;
+ triggerType = attribute.ConstructorArguments[1].Value as ITypeSymbol;
+
+ if (stateType is null || triggerType is null)
+ return null;
+
+ var config = new StateMachineConfig
+ {
+ StateTypeName = stateType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat),
+ TriggerTypeName = triggerType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
+ };
+
+ foreach (var namedArg in attribute.NamedArguments)
+ {
+ switch (namedArg.Key)
+ {
+ case "FireMethodName":
+ config.FireMethodName = namedArg.Value.Value?.ToString() ?? "Fire";
+ break;
+ case "FireAsyncMethodName":
+ config.FireAsyncMethodName = namedArg.Value.Value?.ToString() ?? "FireAsync";
+ break;
+ case "CanFireMethodName":
+ config.CanFireMethodName = namedArg.Value.Value?.ToString() ?? "CanFire";
+ break;
+ case "GenerateAsync":
+ if (namedArg.Value.Value is bool ga)
+ config.GenerateAsync = ga;
+ break;
+ case "ForceAsync":
+ config.ForceAsync = namedArg.Value.Value is bool f && f;
+ break;
+ case "InvalidTrigger":
+ config.InvalidTriggerPolicy = namedArg.Value.Value is int itp ? itp : 0;
+ break;
+ case "GuardFailure":
+ config.GuardFailurePolicy = namedArg.Value.Value is int gfp ? gfp : 0;
+ break;
+ }
+ }
+
+ return config;
+ }
+
+ private ImmutableArray CollectTransitions(
+ INamedTypeSymbol typeSymbol,
+ ITypeSymbol stateType,
+ ITypeSymbol triggerType,
+ SourceProductionContext context)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var method in typeSymbol.GetMembers().OfType())
+ {
+ var transitionAttrs = method.GetAttributes().Where(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.State.StateTransitionAttribute");
+
+ foreach (var transitionAttr in transitionAttrs)
+ {
+ string? fromState = null;
+ string? trigger = null;
+ string? toState = null;
+
+ foreach (var namedArg in transitionAttr.NamedArguments)
+ {
+ if (namedArg.Key == "From")
+ fromState = GetEnumValueName(namedArg.Value, stateType);
+ else if (namedArg.Key == "Trigger")
+ trigger = GetEnumValueName(namedArg.Value, triggerType);
+ else if (namedArg.Key == "To")
+ toState = GetEnumValueName(namedArg.Value, stateType);
+ }
+
+ if (fromState is not null && trigger is not null && toState is not null)
+ {
+ builder.Add(new TransitionModel
+ {
+ Method = method,
+ FromState = fromState,
+ Trigger = trigger,
+ ToState = toState
+ });
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private ImmutableArray CollectGuards(
+ INamedTypeSymbol typeSymbol,
+ ITypeSymbol stateType,
+ ITypeSymbol triggerType,
+ SourceProductionContext context)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var method in typeSymbol.GetMembers().OfType())
+ {
+ var guardAttrs = method.GetAttributes().Where(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.State.StateGuardAttribute");
+
+ foreach (var guardAttr in guardAttrs)
+ {
+ string? fromState = null;
+ string? trigger = null;
+
+ foreach (var namedArg in guardAttr.NamedArguments)
+ {
+ if (namedArg.Key == "From")
+ fromState = GetEnumValueName(namedArg.Value, stateType);
+ else if (namedArg.Key == "Trigger")
+ trigger = GetEnumValueName(namedArg.Value, triggerType);
+ }
+
+ if (fromState is not null && trigger is not null)
+ {
+ builder.Add(new GuardModel
+ {
+ Method = method,
+ FromState = fromState,
+ Trigger = trigger
+ });
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private ImmutableArray CollectEntryHooks(
+ INamedTypeSymbol typeSymbol,
+ ITypeSymbol stateType,
+ SourceProductionContext context)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var method in typeSymbol.GetMembers().OfType())
+ {
+ var entryAttrs = method.GetAttributes().Where(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.State.StateEntryAttribute");
+
+ foreach (var entryAttr in entryAttrs)
+ {
+ string? state = null;
+
+ // Check constructor argument first
+ if (entryAttr.ConstructorArguments.Length > 0)
+ {
+ state = GetEnumValueName(entryAttr.ConstructorArguments[0], stateType);
+ }
+ else
+ {
+ // Check named argument
+ var stateArg = entryAttr.NamedArguments.FirstOrDefault(na => na.Key == "State");
+ if (stateArg.Key is not null)
+ state = GetEnumValueName(stateArg.Value, stateType);
+ }
+
+ if (state is not null)
+ {
+ builder.Add(new HookModel
+ {
+ Method = method,
+ State = state
+ });
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private ImmutableArray CollectExitHooks(
+ INamedTypeSymbol typeSymbol,
+ ITypeSymbol stateType,
+ SourceProductionContext context)
+ {
+ var builder = ImmutableArray.CreateBuilder();
+
+ foreach (var method in typeSymbol.GetMembers().OfType())
+ {
+ var exitAttrs = method.GetAttributes().Where(a =>
+ a.AttributeClass?.ToDisplayString() == "PatternKit.Generators.State.StateExitAttribute");
+
+ foreach (var exitAttr in exitAttrs)
+ {
+ string? state = null;
+
+ // Check constructor argument first
+ if (exitAttr.ConstructorArguments.Length > 0)
+ {
+ state = GetEnumValueName(exitAttr.ConstructorArguments[0], stateType);
+ }
+ else
+ {
+ // Check named argument
+ var stateArg = exitAttr.NamedArguments.FirstOrDefault(na => na.Key == "State");
+ if (stateArg.Key is not null)
+ state = GetEnumValueName(stateArg.Value, stateType);
+ }
+
+ if (state is not null)
+ {
+ builder.Add(new HookModel
+ {
+ Method = method,
+ State = state
+ });
+ }
+ }
+ }
+
+ return builder.ToImmutable();
+ }
+
+ private string? GetEnumValueName(TypedConstant constant, ITypeSymbol enumType)
+ {
+ if (constant.Value is int intValue)
+ {
+ // Get the enum member name from the value
+ var members = enumType.GetMembers().OfType()
+ .Where(f => f.IsConst && f.HasConstantValue && Equals(f.ConstantValue, intValue));
+ return members.FirstOrDefault()?.Name;
+ }
+ return null;
+ }
+
+ private bool ValidateTransitions(
+ ImmutableArray transitions,
+ INamedTypeSymbol typeSymbol,
+ SourceProductionContext context)
+ {
+ var transitionKeys = new Dictionary>();
+
+ foreach (var transition in transitions)
+ {
+ var key = $"{transition.FromState},{transition.Trigger}";
+ if (!transitionKeys.ContainsKey(key))
+ transitionKeys[key] = new List();
+ transitionKeys[key].Add(transition.Method.Name);
+ }
+
+ foreach (var kvp in transitionKeys.Where(kvp => kvp.Value.Count > 1))
+ {
+ var parts = kvp.Key.Split(',');
+ var methodNames = string.Join(", ", kvp.Value);
+ context.ReportDiagnostic(Diagnostic.Create(
+ DuplicateTransitionDescriptor,
+ Location.None,
+ parts[0],
+ parts[1],
+ methodNames));
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool ValidateSignatures(
+ ImmutableArray transitions,
+ ImmutableArray guards,
+ ImmutableArray entryHooks,
+ ImmutableArray exitHooks,
+ SourceProductionContext context)
+ {
+ foreach (var transition in transitions)
+ {
+ if (!ValidateTransitionSignature(transition.Method, context))
+ return false;
+ }
+
+ foreach (var guard in guards)
+ {
+ if (!ValidateGuardSignature(guard.Method, context))
+ return false;
+ }
+
+ foreach (var hook in entryHooks.Concat(exitHooks))
+ {
+ if (!ValidateHookSignature(hook.Method, context))
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool ValidateTransitionSignature(IMethodSymbol method, SourceProductionContext context)
+ {
+ var returnsVoid = method.ReturnsVoid;
+ var returnType = method.ReturnType;
+ var returnsValueTask = IsNonGenericValueTask(returnType);
+
+ if (!returnsVoid && !returnsValueTask)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidTransitionSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ // If parameters exist, they must be CancellationToken only
+ if (method.Parameters.Length > 1 ||
+ (method.Parameters.Length == 1 && !IsCancellationToken(method.Parameters[0].Type)))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidTransitionSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool ValidateGuardSignature(IMethodSymbol method, SourceProductionContext context)
+ {
+ var returnType = method.ReturnType;
+ var returnsBool = returnType.SpecialType == SpecialType.System_Boolean;
+ var returnsValueTaskBool = IsGenericValueTaskOfBool(returnType);
+
+ if (!returnsBool && !returnsValueTaskBool)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidGuardSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ // If parameters exist, they must be CancellationToken only
+ if (method.Parameters.Length > 1 ||
+ (method.Parameters.Length == 1 && !IsCancellationToken(method.Parameters[0].Type)))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidGuardSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool ValidateHookSignature(IMethodSymbol method, SourceProductionContext context)
+ {
+ var returnsVoid = method.ReturnsVoid;
+ var returnType = method.ReturnType;
+ var returnsValueTask = IsNonGenericValueTask(returnType);
+
+ if (!returnsVoid && !returnsValueTask)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidHookSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ // If parameters exist, they must be CancellationToken only
+ if (method.Parameters.Length > 1 ||
+ (method.Parameters.Length == 1 && !IsCancellationToken(method.Parameters[0].Type)))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ InvalidHookSignatureDescriptor,
+ method.Locations.FirstOrDefault(),
+ method.Name));
+ return false;
+ }
+
+ return true;
+ }
+
+ private bool DetermineIfAsync(
+ ImmutableArray transitions,
+ ImmutableArray guards,
+ ImmutableArray entryHooks,
+ ImmutableArray exitHooks)
+ {
+ foreach (var transition in transitions)
+ {
+ if (IsNonGenericValueTask(transition.Method.ReturnType))
+ return true;
+ }
+
+ foreach (var guard in guards)
+ {
+ if (IsGenericValueTaskOfBool(guard.Method.ReturnType))
+ return true;
+ }
+
+ foreach (var hook in entryHooks.Concat(exitHooks))
+ {
+ if (IsNonGenericValueTask(hook.Method.ReturnType))
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool IsNonGenericValueTask(ITypeSymbol type)
+ {
+ return type is INamedTypeSymbol namedType &&
+ namedType.Name == "ValueTask" &&
+ namedType.Arity == 0 &&
+ namedType.ContainingNamespace.ToDisplayString() == "System.Threading.Tasks";
+ }
+
+ private bool IsGenericValueTaskOfBool(ITypeSymbol type)
+ {
+ return type is INamedTypeSymbol namedType &&
+ namedType.Name == "ValueTask" &&
+ namedType.Arity == 1 &&
+ namedType.TypeArguments.Length == 1 &&
+ namedType.TypeArguments[0].SpecialType == SpecialType.System_Boolean &&
+ namedType.ContainingNamespace.ToDisplayString() == "System.Threading.Tasks";
+ }
+
+ private bool IsCancellationToken(ITypeSymbol type)
+ {
+ return type.ToDisplayString() == "System.Threading.CancellationToken";
+ }
+
+ private string GenerateStateMachine(
+ INamedTypeSymbol typeSymbol,
+ StateMachineConfig config,
+ ITypeSymbol stateType,
+ ITypeSymbol triggerType,
+ ImmutableArray transitions,
+ ImmutableArray guards,
+ ImmutableArray entryHooks,
+ ImmutableArray exitHooks,
+ bool needsAsync)
+ {
+ var sb = new StringBuilder();
+ var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
+ ? null
+ : typeSymbol.ContainingNamespace.ToDisplayString();
+
+ // File header
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine();
+
+ if (ns is not null)
+ {
+ sb.AppendLine($"namespace {ns};");
+ sb.AppendLine();
+ }
+
+ // Get type keyword (class, struct, record class, record struct)
+ var typeKeyword = GetTypeKeyword(typeSymbol);
+
+ sb.AppendLine($"partial {typeKeyword} {typeSymbol.Name}");
+ sb.AppendLine("{");
+
+ // State property
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// Gets the current state of the state machine.");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" public {config.StateTypeName} State {{ get; private set; }}");
+ sb.AppendLine();
+
+ // CanFire method
+ GenerateCanFireMethod(sb, config, transitions, guards, needsAsync);
+
+ // Fire method
+ GenerateFireMethod(sb, config, stateType, triggerType, transitions, guards, entryHooks, exitHooks, false);
+
+ // FireAsync method (if needed)
+ if (needsAsync)
+ {
+ GenerateFireMethod(sb, config, stateType, triggerType, transitions, guards, entryHooks, exitHooks, true);
+ }
+
+ sb.AppendLine("}");
+
+ return sb.ToString();
+ }
+
+ private string GetTypeKeyword(INamedTypeSymbol typeSymbol)
+ {
+ if (typeSymbol.IsRecord)
+ {
+ return typeSymbol.IsValueType ? "record struct" : "record class";
+ }
+ return typeSymbol.IsValueType ? "struct" : "class";
+ }
+
+ private void GenerateCanFireMethod(
+ StringBuilder sb,
+ StateMachineConfig config,
+ ImmutableArray transitions,
+ ImmutableArray guards,
+ bool needsAsync)
+ {
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// Determines whether the specified trigger can be fired from the current state.");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// The trigger to check.");
+ sb.AppendLine($" /// true if the trigger can be fired; otherwise, false.");
+ sb.AppendLine($" public bool {config.CanFireMethodName}({config.TriggerTypeName} trigger)");
+ sb.AppendLine($" {{");
+
+ // Group transitions by (from, trigger)
+ var transitionGroups = transitions
+ .GroupBy(t => (t.FromState, t.Trigger))
+ .OrderBy(g => g.Key.FromState)
+ .ThenBy(g => g.Key.Trigger);
+
+ if (transitionGroups.Any())
+ {
+ sb.AppendLine($" return (State, trigger) switch");
+ sb.AppendLine($" {{");
+
+ foreach (var group in transitionGroups)
+ {
+ var (fromState, trigger) = group.Key;
+
+ // Check if there's a guard for this transition
+ var guard = guards.FirstOrDefault(g => g.FromState == fromState && g.Trigger == trigger);
+
+ if (guard is not null)
+ {
+ // If guard is async and we're in sync context, we can't evaluate it
+ if (IsGenericValueTaskOfBool(guard.Method.ReturnType))
+ {
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false, // Guard is async, cannot evaluate synchronously");
+ }
+ else
+ {
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => {guard.Method.Name}(),");
+ }
+ }
+ else
+ {
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => true,");
+ }
+ }
+
+ sb.AppendLine($" _ => false");
+ sb.AppendLine($" }};");
+ }
+ else
+ {
+ sb.AppendLine($" return false;");
+ }
+
+ sb.AppendLine($" }}");
+ sb.AppendLine();
+ }
+
+ private void GenerateFireMethod(
+ StringBuilder sb,
+ StateMachineConfig config,
+ ITypeSymbol stateType,
+ ITypeSymbol triggerType,
+ ImmutableArray transitions,
+ ImmutableArray guards,
+ ImmutableArray entryHooks,
+ ImmutableArray exitHooks,
+ bool isAsync)
+ {
+ var methodName = isAsync ? config.FireAsyncMethodName : config.FireMethodName;
+ var returnType = isAsync ? "global::System.Threading.Tasks.ValueTask" : "void";
+ var ctParam = isAsync ? ", global::System.Threading.CancellationToken cancellationToken = default" : "";
+ var awaitKeyword = isAsync ? "await " : "";
+ var asyncModifier = isAsync ? "async " : "";
+ var configureAwait = isAsync ? ".ConfigureAwait(false)" : "";
+
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// Fires the specified trigger, potentially transitioning to a new state.");
+ sb.AppendLine($" /// ");
+ sb.AppendLine($" /// The trigger to fire.");
+ if (isAsync)
+ {
+ sb.AppendLine($" /// A cancellation token.");
+ }
+ sb.AppendLine($" public {asyncModifier}{returnType} {methodName}({config.TriggerTypeName} trigger{ctParam})");
+ sb.AppendLine($" {{");
+
+ // Group transitions by (from, trigger)
+ var transitionGroups = transitions
+ .GroupBy(t => (t.FromState, t.Trigger))
+ .OrderBy(g => g.Key.FromState)
+ .ThenBy(g => g.Key.Trigger)
+ .ToList();
+
+ if (transitionGroups.Count == 0)
+ {
+ // No transitions defined
+ if (config.InvalidTriggerPolicy == 0) // Throw
+ {
+ sb.AppendLine($" throw new global::System.InvalidOperationException($\"No transitions defined for state machine.\");");
+ }
+ sb.AppendLine($" }}");
+ sb.AppendLine();
+ return;
+ }
+
+ sb.AppendLine($" switch (State)");
+ sb.AppendLine($" {{");
+
+ // Group by from state
+ var stateGroups = transitionGroups.GroupBy(g => g.Key.FromState);
+ foreach (var stateGroup in stateGroups)
+ {
+ var fromState = stateGroup.Key;
+ sb.AppendLine($" case {config.StateTypeName}.{fromState}:");
+ sb.AppendLine($" switch (trigger)");
+ sb.AppendLine($" {{");
+
+ foreach (var triggerGroup in stateGroup)
+ {
+ var trigger = triggerGroup.Key.Trigger;
+ var transition = triggerGroup.First(); // Should only be one after validation
+ var guard = guards.FirstOrDefault(g => g.FromState == fromState && g.Trigger == trigger);
+
+ sb.AppendLine($" case {config.TriggerTypeName}.{trigger}:");
+
+ // Evaluate guard if exists
+ if (guard is not null)
+ {
+ var guardCall = IsGenericValueTaskOfBool(guard.Method.ReturnType)
+ ? (isAsync ? $"await {guard.Method.Name}(cancellationToken){configureAwait}" : $"{guard.Method.Name}(cancellationToken).GetAwaiter().GetResult()")
+ : $"{guard.Method.Name}()";
+
+ sb.AppendLine($" if (!{guardCall})");
+ sb.AppendLine($" {{");
+
+ if (config.GuardFailurePolicy == 0) // Throw
+ {
+ sb.AppendLine($" throw new global::System.InvalidOperationException($\"Guard failed for transition from {fromState} on trigger {trigger}.\");");
+ }
+ else // Ignore or ReturnFalse
+ {
+ if (isAsync)
+ sb.AppendLine($" return;");
+ else
+ sb.AppendLine($" return;");
+ }
+
+ sb.AppendLine($" }}");
+ }
+
+ // Execute exit hooks for fromState
+ var exitHooksForState = exitHooks.Where(h => h.State == fromState).ToList();
+ foreach (var exitHook in exitHooksForState)
+ {
+ var hookCall = IsNonGenericValueTask(exitHook.Method.ReturnType)
+ ? (isAsync ? $"await {exitHook.Method.Name}(cancellationToken){configureAwait};" : $"{exitHook.Method.Name}(cancellationToken).GetAwaiter().GetResult();")
+ : $"{exitHook.Method.Name}();";
+ sb.AppendLine($" {hookCall}");
+ }
+
+ // Execute transition action
+ if (IsNonGenericValueTask(transition.Method.ReturnType))
+ {
+ var transitionCall = isAsync
+ ? $"await {transition.Method.Name}(cancellationToken){configureAwait};"
+ : $"{transition.Method.Name}(cancellationToken).GetAwaiter().GetResult();";
+ sb.AppendLine($" {transitionCall}");
+ }
+ else
+ {
+ sb.AppendLine($" {transition.Method.Name}();");
+ }
+
+ // Update state
+ sb.AppendLine($" State = {config.StateTypeName}.{transition.ToState};");
+
+ // Execute entry hooks for toState
+ var entryHooksForState = entryHooks.Where(h => h.State == transition.ToState).ToList();
+ foreach (var entryHook in entryHooksForState)
+ {
+ var hookCall = IsNonGenericValueTask(entryHook.Method.ReturnType)
+ ? (isAsync ? $"await {entryHook.Method.Name}(cancellationToken){configureAwait};" : $"{entryHook.Method.Name}(cancellationToken).GetAwaiter().GetResult();")
+ : $"{entryHook.Method.Name}();";
+ sb.AppendLine($" {hookCall}");
+ }
+
+ sb.AppendLine($" return;");
+ }
+
+ // Default case for invalid trigger in this state
+ sb.AppendLine($" default:");
+ if (config.InvalidTriggerPolicy == 0) // Throw
+ {
+ sb.AppendLine($" throw new global::System.InvalidOperationException($\"Invalid trigger {{trigger}} for state {fromState}.\");");
+ }
+ else // Ignore or ReturnFalse
+ {
+ sb.AppendLine($" return;");
+ }
+ sb.AppendLine($" }}");
+ }
+
+ // Default case for states with no transitions
+ sb.AppendLine($" default:");
+ if (config.InvalidTriggerPolicy == 0) // Throw
+ {
+ sb.AppendLine($" throw new global::System.InvalidOperationException($\"No transitions defined for state {{State}}.\");");
+ }
+ else // Ignore or ReturnFalse
+ {
+ sb.AppendLine($" return;");
+ }
+
+ sb.AppendLine($" }}");
+ sb.AppendLine($" }}");
+ sb.AppendLine();
+ }
+
+ private sealed class StateMachineConfig
+ {
+ public string StateTypeName { get; set; } = null!;
+ public string TriggerTypeName { get; set; } = null!;
+ public string FireMethodName { get; set; } = "Fire";
+ public string FireAsyncMethodName { get; set; } = "FireAsync";
+ public string CanFireMethodName { get; set; } = "CanFire";
+ public bool? GenerateAsync { get; set; }
+ public bool ForceAsync { get; set; }
+ public int InvalidTriggerPolicy { get; set; } = 0; // 0=Throw, 1=Ignore, 2=ReturnFalse
+ public int GuardFailurePolicy { get; set; } = 0; // 0=Throw, 1=Ignore, 2=ReturnFalse
+ }
+
+ private sealed class TransitionModel
+ {
+ public IMethodSymbol Method { get; set; } = null!;
+ public string FromState { get; set; } = null!;
+ public string Trigger { get; set; } = null!;
+ public string ToState { get; set; } = null!;
+ }
+
+ private sealed class GuardModel
+ {
+ public IMethodSymbol Method { get; set; } = null!;
+ public string FromState { get; set; } = null!;
+ public string Trigger { get; set; } = null!;
+ }
+
+ private sealed class HookModel
+ {
+ public IMethodSymbol Method { get; set; } = null!;
+ public string State { get; set; } = null!;
+ }
+}
From 737cd8a9437e01fb7e2103619030c2e1a1d2a420 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:11:48 +0000
Subject: [PATCH 3/9] Add comprehensive unit tests for StateMachineGenerator -
all passing
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../StateMachineGenerator.cs | 20 +-
.../StateMachineGeneratorTests.cs | 839 ++++++++++++++++++
2 files changed, 854 insertions(+), 5 deletions(-)
create mode 100644 test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
diff --git a/src/PatternKit.Generators/StateMachineGenerator.cs b/src/PatternKit.Generators/StateMachineGenerator.cs
index 8fc84c8..1c87a65 100644
--- a/src/PatternKit.Generators/StateMachineGenerator.cs
+++ b/src/PatternKit.Generators/StateMachineGenerator.cs
@@ -826,8 +826,11 @@ private void GenerateFireMethod(
// Evaluate guard if exists
if (guard is not null)
{
+ var guardHasCt = guard.Method.Parameters.Length > 0 && IsCancellationToken(guard.Method.Parameters[0].Type);
var guardCall = IsGenericValueTaskOfBool(guard.Method.ReturnType)
- ? (isAsync ? $"await {guard.Method.Name}(cancellationToken){configureAwait}" : $"{guard.Method.Name}(cancellationToken).GetAwaiter().GetResult()")
+ ? (isAsync
+ ? (guardHasCt ? $"await {guard.Method.Name}(cancellationToken){configureAwait}" : $"await {guard.Method.Name}(){configureAwait}")
+ : (guardHasCt ? $"{guard.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult()" : $"{guard.Method.Name}().GetAwaiter().GetResult()"))
: $"{guard.Method.Name}()";
sb.AppendLine($" if (!{guardCall})");
@@ -852,18 +855,22 @@ private void GenerateFireMethod(
var exitHooksForState = exitHooks.Where(h => h.State == fromState).ToList();
foreach (var exitHook in exitHooksForState)
{
+ var hookHasCt = exitHook.Method.Parameters.Length > 0 && IsCancellationToken(exitHook.Method.Parameters[0].Type);
var hookCall = IsNonGenericValueTask(exitHook.Method.ReturnType)
- ? (isAsync ? $"await {exitHook.Method.Name}(cancellationToken){configureAwait};" : $"{exitHook.Method.Name}(cancellationToken).GetAwaiter().GetResult();")
+ ? (isAsync
+ ? (hookHasCt ? $"await {exitHook.Method.Name}(cancellationToken){configureAwait};" : $"await {exitHook.Method.Name}(){configureAwait};")
+ : (hookHasCt ? $"{exitHook.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult();" : $"{exitHook.Method.Name}().GetAwaiter().GetResult();"))
: $"{exitHook.Method.Name}();";
sb.AppendLine($" {hookCall}");
}
// Execute transition action
+ var transitionHasCt = transition.Method.Parameters.Length > 0 && IsCancellationToken(transition.Method.Parameters[0].Type);
if (IsNonGenericValueTask(transition.Method.ReturnType))
{
var transitionCall = isAsync
- ? $"await {transition.Method.Name}(cancellationToken){configureAwait};"
- : $"{transition.Method.Name}(cancellationToken).GetAwaiter().GetResult();";
+ ? (transitionHasCt ? $"await {transition.Method.Name}(cancellationToken){configureAwait};" : $"await {transition.Method.Name}(){configureAwait};")
+ : (transitionHasCt ? $"{transition.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult();" : $"{transition.Method.Name}().GetAwaiter().GetResult();");
sb.AppendLine($" {transitionCall}");
}
else
@@ -878,8 +885,11 @@ private void GenerateFireMethod(
var entryHooksForState = entryHooks.Where(h => h.State == transition.ToState).ToList();
foreach (var entryHook in entryHooksForState)
{
+ var entryHasCt = entryHook.Method.Parameters.Length > 0 && IsCancellationToken(entryHook.Method.Parameters[0].Type);
var hookCall = IsNonGenericValueTask(entryHook.Method.ReturnType)
- ? (isAsync ? $"await {entryHook.Method.Name}(cancellationToken){configureAwait};" : $"{entryHook.Method.Name}(cancellationToken).GetAwaiter().GetResult();")
+ ? (isAsync
+ ? (entryHasCt ? $"await {entryHook.Method.Name}(cancellationToken){configureAwait};" : $"await {entryHook.Method.Name}(){configureAwait};")
+ : (entryHasCt ? $"{entryHook.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult();" : $"{entryHook.Method.Name}().GetAwaiter().GetResult();"))
: $"{entryHook.Method.Name}();";
sb.AppendLine($" {hookCall}");
}
diff --git a/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs b/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
new file mode 100644
index 0000000..6d2338c
--- /dev/null
+++ b/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
@@ -0,0 +1,839 @@
+using Microsoft.CodeAnalysis;
+
+namespace PatternKit.Generators.Tests;
+
+public class StateMachineGeneratorTests
+{
+ [Fact]
+ public void BasicStateMachine_Class_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid, Shipped, Cancelled }
+ public enum OrderTrigger { Submit, Pay, Ship, Cancel }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private void OnPay() { }
+
+ [StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
+ private void OnShip() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(BasicStateMachine_Class_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Confirm we generated the expected file
+ var names = result.Results.SelectMany(r => r.GeneratedSources).Select(gs => gs.HintName).ToArray();
+ Assert.Contains("OrderFlow.StateMachine.g.cs", names);
+
+ // Verify the generated source contains expected members
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("public global::PatternKit.Examples.OrderState State { get; private set; }", generatedSource);
+ Assert.Contains("public bool CanFire(global::PatternKit.Examples.OrderTrigger trigger)", generatedSource);
+ Assert.Contains("public void Fire(global::PatternKit.Examples.OrderTrigger trigger)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void BasicStateMachine_Struct_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum LightState { Off, On }
+ public enum LightTrigger { Toggle }
+
+ [StateMachine(typeof(LightState), typeof(LightTrigger))]
+ public partial struct LightSwitch
+ {
+ [StateTransition(From = LightState.Off, Trigger = LightTrigger.Toggle, To = LightState.On)]
+ private void TurnOn() { }
+
+ [StateTransition(From = LightState.On, Trigger = LightTrigger.Toggle, To = LightState.Off)]
+ private void TurnOff() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(BasicStateMachine_Struct_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify struct keyword is used
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("partial struct LightSwitch", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void BasicStateMachine_RecordClass_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum DoorState { Closed, Open }
+ public enum DoorTrigger { OpenDoor, CloseDoor }
+
+ [StateMachine(typeof(DoorState), typeof(DoorTrigger))]
+ public partial record class Door
+ {
+ [StateTransition(From = DoorState.Closed, Trigger = DoorTrigger.OpenDoor, To = DoorState.Open)]
+ private void OnOpen() { }
+
+ [StateTransition(From = DoorState.Open, Trigger = DoorTrigger.CloseDoor, To = DoorState.Closed)]
+ private void OnClose() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(BasicStateMachine_RecordClass_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify record class keyword is used
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("partial record class Door", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void BasicStateMachine_RecordStruct_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum WindowState { Closed, Open }
+ public enum WindowTrigger { OpenWindow, CloseWindow }
+
+ [StateMachine(typeof(WindowState), typeof(WindowTrigger))]
+ public partial record struct Window
+ {
+ [StateTransition(From = WindowState.Closed, Trigger = WindowTrigger.OpenWindow, To = WindowState.Open)]
+ private void OnOpen() { }
+
+ [StateTransition(From = WindowState.Open, Trigger = WindowTrigger.CloseWindow, To = WindowState.Closed)]
+ private void OnClose() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(BasicStateMachine_RecordStruct_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify record struct keyword is used
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("partial record struct Window", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void AsyncStateMachine_WithValueTask_GeneratesCorrectly()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private async ValueTask OnSubmitAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private async ValueTask OnPayAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(AsyncStateMachine_WithValueTask_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify async methods are generated
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("public async global::System.Threading.Tasks.ValueTask FireAsync(global::PatternKit.Examples.OrderTrigger trigger, global::System.Threading.CancellationToken cancellationToken = default)", generatedSource);
+ Assert.Contains("await OnSubmitAsync(cancellationToken)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithGuards_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateGuard(From = OrderState.Draft, Trigger = OrderTrigger.Submit)]
+ private bool CanSubmit() => true;
+
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+ private bool CanPay() => true;
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private void OnPay() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithGuards_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify guards are called
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("CanSubmit()", generatedSource);
+ Assert.Contains("CanPay()", generatedSource);
+ Assert.Contains("if (!CanSubmit())", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithAsyncGuards_GeneratesCorrectly()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateGuard(From = OrderState.Draft, Trigger = OrderTrigger.Submit)]
+ private async ValueTask CanSubmitAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ return true;
+ }
+
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithAsyncGuards_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify async guards are called
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("await CanSubmitAsync(cancellationToken)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithEntryHooks_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateEntry(OrderState.Submitted)]
+ private void OnEnterSubmitted() { }
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private void OnPay() { }
+
+ [StateEntry(OrderState.Paid)]
+ private void OnEnterPaid() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithEntryHooks_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify entry hooks are called after state update
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("OnEnterSubmitted()", generatedSource);
+ Assert.Contains("OnEnterPaid()", generatedSource);
+
+ // Verify State is updated before entry hooks
+ var submitIndex = generatedSource.IndexOf("State = global::PatternKit.Examples.OrderState.Submitted");
+ var entrySubmittedIndex = generatedSource.IndexOf("OnEnterSubmitted()");
+ Assert.True(submitIndex < entrySubmittedIndex, "State should be updated before entry hook is called");
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithExitHooks_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateExit(OrderState.Draft)]
+ private void OnExitDraft() { }
+
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateExit(OrderState.Submitted)]
+ private void OnExitSubmitted() { }
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private void OnPay() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithExitHooks_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify exit hooks are called
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("OnExitDraft()", generatedSource);
+ Assert.Contains("OnExitSubmitted()", generatedSource);
+
+ // Verify exit hooks are called before transition action
+ var exitIndex = generatedSource.IndexOf("OnExitDraft()");
+ var transitionIndex = generatedSource.IndexOf("OnSubmit()");
+ Assert.True(exitIndex < transitionIndex, "Exit hook should be called before transition action");
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithAsyncEntryExitHooks_GeneratesCorrectly()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid }
+ public enum OrderTrigger { Submit, Pay }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateExit(OrderState.Draft)]
+ private async ValueTask OnExitDraftAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateEntry(OrderState.Submitted)]
+ private async ValueTask OnEnterSubmittedAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithAsyncEntryExitHooks_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify async entry/exit hooks are awaited
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("await OnExitDraftAsync(cancellationToken)", generatedSource);
+ Assert.Contains("await OnEnterSubmittedAsync(cancellationToken)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithInvalidTriggerPolicy_Ignore_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger), InvalidTrigger = StateMachineInvalidTriggerPolicy.Ignore)]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithInvalidTriggerPolicy_Ignore_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify no exception is thrown for invalid triggers
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.DoesNotContain("throw new global::System.InvalidOperationException", generatedSource);
+ Assert.Contains("return;", generatedSource); // Should just return instead
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void StateMachineWithGuardFailurePolicy_Ignore_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger), GuardFailure = StateMachineGuardFailurePolicy.Ignore)]
+ public partial class Machine
+ {
+ [StateGuard(From = State.A, Trigger = Trigger.T1)]
+ private bool CanTransition() => false;
+
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(StateMachineWithGuardFailurePolicy_Ignore_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify no exception is thrown for guard failures
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ var guardFailureIndex = generatedSource.IndexOf("if (!CanTransition())");
+ var throwIndex = generatedSource.IndexOf("throw new global::System.InvalidOperationException($\"Guard failed", guardFailureIndex);
+ Assert.True(throwIndex == -1, "Should not throw exception on guard failure with Ignore policy");
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void NonPartialType_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(NonPartialType_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST001
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST001");
+ }
+
+ [Fact]
+ public void NonEnumStateType_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public class State { }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(NonEnumStateType_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST002
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST002");
+ }
+
+ [Fact]
+ public void NonEnumTriggerType_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public class Trigger { }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(NonEnumTriggerType_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST003
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST003");
+ }
+
+ [Fact]
+ public void DuplicateTransition_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition1() { }
+
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition2() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(DuplicateTransition_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST004
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST004");
+ }
+
+ [Fact]
+ public void InvalidTransitionSignature_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private int OnTransition() => 42; // Invalid return type
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(InvalidTransitionSignature_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST005
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST005");
+ }
+
+ [Fact]
+ public void InvalidGuardSignature_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateGuard(From = State.A, Trigger = Trigger.T1)]
+ private void CanTransition() { } // Invalid return type (should be bool)
+
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(InvalidGuardSignature_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST006
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST006");
+ }
+
+ [Fact]
+ public void InvalidEntryHookSignature_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+
+ [StateEntry(State.B)]
+ private int OnEnterB() => 42; // Invalid return type
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(InvalidEntryHookSignature_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have diagnostic PKST007
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST007");
+ }
+
+ [Fact]
+ public void CompleteOrderFlowExample_GeneratesCorrectly()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum OrderState { Draft, Submitted, Paid, Shipped, Cancelled }
+ public enum OrderTrigger { Submit, Pay, Ship, Cancel }
+
+ [StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+ public partial class OrderFlow
+ {
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit() { }
+
+ [StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+ private bool CanPay() => true;
+
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private async ValueTask OnPayAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+
+ [StateExit(OrderState.Paid)]
+ private void OnExitPaid() { }
+
+ [StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
+ private void OnShip() { }
+
+ [StateEntry(OrderState.Shipped)]
+ private void OnEnterShipped() { }
+
+ [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() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(CompleteOrderFlowExample_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify all expected elements are generated
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("State { get; private set; }", generatedSource);
+ Assert.Contains("CanFire", generatedSource);
+ Assert.Contains("Fire(", generatedSource);
+ Assert.Contains("FireAsync(", generatedSource);
+ Assert.Contains("CanPay()", generatedSource);
+ Assert.Contains("OnExitPaid()", generatedSource);
+ Assert.Contains("OnEnterShipped()", generatedSource);
+ Assert.Contains("await OnPayAsync(cancellationToken)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void CustomMethodNames_GeneratesCorrectly()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger),
+ FireMethodName = "Transition",
+ FireAsyncMethodName = "TransitionAsync",
+ CanFireMethodName = "CanTransition")]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(CustomMethodNames_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify custom method names are used
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("public bool CanTransition", generatedSource);
+ Assert.Contains("public void Transition", generatedSource);
+ Assert.DoesNotContain("public void Fire(", generatedSource);
+ Assert.DoesNotContain("public bool CanFire", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+}
From ddece4b5a8f1b5acd4eccc41ec8cc31c8198fbcd Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:14:43 +0000
Subject: [PATCH 4/9] Add OrderFlow example for State Machine (generator not
yet running on Examples project)
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../Generators/State/OrderFlowDemo.cs | 210 ++++++++++++++++++
1 file changed, 210 insertions(+)
create mode 100644 src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
diff --git a/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs b/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
new file mode 100644
index 0000000..28afaad
--- /dev/null
+++ b/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
@@ -0,0 +1,210 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using PatternKit.Generators.State;
+
+namespace PatternKit.Examples.Generators.State;
+
+///
+/// Real-world example of a State Machine generator demonstrating an order processing workflow.
+/// This example shows:
+/// - Enum-based states and triggers
+/// - Synchronous and asynchronous transitions
+/// - Guards to control transitions
+/// - Entry and exit hooks for state changes
+/// - Proper cancellation token handling
+///
+public static class OrderFlowDemo
+{
+ public static void Run()
+ {
+ Console.WriteLine("=== Order Flow State Machine Demo ===\n");
+
+ // Create an order flow instance
+ var order = new OrderFlow("ORD-001", 299.99m);
+
+ Console.WriteLine($"Order: {order.Id}, Amount: ${order.Amount:F2}");
+ Console.WriteLine($"Initial State: {order.State}\n");
+
+ // Submit the order
+ Console.WriteLine("1. Submitting order...");
+ order.Fire(OrderTrigger.Submit);
+ Console.WriteLine($" State: {order.State}\n");
+
+ // Try to pay for the order (will check guard)
+ Console.WriteLine("2. Attempting to pay...");
+ if (order.CanFire(OrderTrigger.Pay))
+ {
+ // This is async, so we'll use RunAsync
+ RunAsync(order).GetAwaiter().GetResult();
+ }
+ else
+ {
+ Console.WriteLine(" Cannot pay for order (guard failed)\n");
+ }
+
+ // Ship the order
+ Console.WriteLine("4. Shipping order...");
+ order.Fire(OrderTrigger.Ship);
+ Console.WriteLine($" State: {order.State}\n");
+
+ Console.WriteLine("=== Order processing complete ===\n");
+ }
+
+ private static async Task RunAsync(OrderFlow order)
+ {
+ Console.WriteLine("3. Processing payment...");
+ await order.FireAsync(OrderTrigger.Pay, CancellationToken.None);
+ Console.WriteLine($" State: {order.State}\n");
+ }
+
+ public static void CancellationDemo()
+ {
+ Console.WriteLine("=== Cancellation Example ===\n");
+
+ var order = new OrderFlow("ORD-002", 599.99m);
+
+ Console.WriteLine($"Order: {order.Id}, Amount: ${order.Amount:F2}");
+ Console.WriteLine($"Initial State: {order.State}\n");
+
+ // Submit the order
+ Console.WriteLine("1. Submitting order...");
+ order.Fire(OrderTrigger.Submit);
+
+ // Cancel before processing
+ Console.WriteLine("2. Cancelling order before payment...");
+ order.Fire(OrderTrigger.Cancel);
+ Console.WriteLine($" State: {order.State}\n");
+
+ Console.WriteLine("=== Order was cancelled ===\n");
+ }
+
+ public static void GuardFailureDemo()
+ {
+ Console.WriteLine("=== Guard Failure Example ===\n");
+
+ var order = new OrderFlow("ORD-003", -50m); // Invalid amount
+
+ Console.WriteLine($"Order: {order.Id}, Amount: ${order.Amount:F2}");
+ Console.WriteLine($"Initial State: {order.State}\n");
+
+ // Submit the order
+ Console.WriteLine("1. Submitting order...");
+ order.Fire(OrderTrigger.Submit);
+
+ // Try to pay - guard will fail due to invalid amount
+ Console.WriteLine("2. Attempting to pay with invalid amount...");
+ if (order.CanFire(OrderTrigger.Pay))
+ {
+ Console.WriteLine(" Payment allowed");
+ }
+ else
+ {
+ Console.WriteLine(" Payment blocked by guard (invalid amount)\n");
+ }
+
+ Console.WriteLine("=== Guard prevented invalid payment ===\n");
+ }
+}
+
+///
+/// Enum defining the possible states of an order.
+///
+public enum OrderState
+{
+ Draft,
+ Submitted,
+ Paid,
+ Shipped,
+ Cancelled
+}
+
+///
+/// Enum defining the triggers that can transition an order between states.
+///
+public enum OrderTrigger
+{
+ Submit,
+ Pay,
+ Ship,
+ Cancel
+}
+
+///
+/// State machine for managing order lifecycle.
+/// Uses the StateMachine generator to create deterministic state 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
+ }
+
+ // Transition: Draft -> Submitted
+ [StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+ private void OnSubmit()
+ {
+ Console.WriteLine($" >> Transition: Submitting order {Id}");
+ }
+
+ // Guard: Can only pay if amount is valid
+ [StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+ private bool CanPay()
+ {
+ return Amount > 0;
+ }
+
+ // Transition: Submitted -> Paid (async)
+ [StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+ private async ValueTask OnPayAsync(CancellationToken ct)
+ {
+ Console.WriteLine($" >> Transition: Processing payment for {Id}...");
+ // Simulate payment processing
+ await Task.Delay(500, ct);
+ Console.WriteLine($" >> Payment of ${Amount:F2} processed");
+ }
+
+ // Exit hook: Called when leaving Paid state
+ [StateExit(OrderState.Paid)]
+ private void OnExitPaid()
+ {
+ Console.WriteLine($" >> Exit Hook: Finalizing payment for {Id}");
+ }
+
+ // Transition: Paid -> Shipped
+ [StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
+ private void OnShip()
+ {
+ Console.WriteLine($" >> Transition: Shipping order {Id}");
+ }
+
+ // Entry hook: Called when entering Shipped state
+ [StateEntry(OrderState.Shipped)]
+ private void OnEnterShipped()
+ {
+ Console.WriteLine($" >> Entry Hook: Order {Id} is now shipped, sending notification");
+ }
+
+ // Transitions for 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($" >> Transition: Cancelling order {Id}");
+ }
+
+ // Entry hook: Called when entering Cancelled state
+ [StateEntry(OrderState.Cancelled)]
+ private void OnEnterCancelled()
+ {
+ Console.WriteLine($" >> Entry Hook: Order {Id} is cancelled, processing refund if needed");
+ }
+}
From 514725e572fc3f65c750939798bbfafed45254d3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:15:37 +0000
Subject: [PATCH 5/9] Update AnalyzerReleases.Unshipped.md with State Pattern
diagnostics
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
src/PatternKit.Generators/AnalyzerReleases.Unshipped.md | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index ce3dcad..d4c5058 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -116,3 +116,11 @@ PKADP015 | PatternKit.Generators.Adapter | Error | Mapping method must be access
PKADP016 | PatternKit.Generators.Adapter | Error | Static members are not supported
PKADP017 | PatternKit.Generators.Adapter | Error | Ref-return members are not supported
PKADP018 | PatternKit.Generators.Adapter | Error | Indexers are not supported
+PKST001 | PatternKit.Generators.State | Error | Type marked with [StateMachine] must be partial
+PKST002 | PatternKit.Generators.State | Error | State type must be an enum
+PKST003 | PatternKit.Generators.State | Error | Trigger type must be an enum
+PKST004 | PatternKit.Generators.State | Error | Duplicate transition detected
+PKST005 | PatternKit.Generators.State | Error | Transition method signature invalid
+PKST006 | PatternKit.Generators.State | Error | Guard method signature invalid
+PKST007 | PatternKit.Generators.State | Error | Entry/Exit hook signature invalid
+PKST008 | PatternKit.Generators.State | Warning | Async method detected but async generation disabled
From 8caacf2cc498be18d99fa59265bffae7b061329a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:16:55 +0000
Subject: [PATCH 6/9] Improve comment clarity for async guard in CanFire method
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
src/PatternKit.Generators/StateMachineGenerator.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/PatternKit.Generators/StateMachineGenerator.cs b/src/PatternKit.Generators/StateMachineGenerator.cs
index 1c87a65..7cc7f3f 100644
--- a/src/PatternKit.Generators/StateMachineGenerator.cs
+++ b/src/PatternKit.Generators/StateMachineGenerator.cs
@@ -730,7 +730,7 @@ private void GenerateCanFireMethod(
// If guard is async and we're in sync context, we can't evaluate it
if (IsGenericValueTaskOfBool(guard.Method.ReturnType))
{
- sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false, // Guard is async, cannot evaluate synchronously");
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false, // Async guard cannot be evaluated in synchronous CanFire; use FireAsync if transition should be possible");
}
else
{
From 6f8c3216e334bbf2195032f4596b7a1494aed640 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 13 Feb 2026 20:19:04 +0000
Subject: [PATCH 7/9] Address code review feedback: improve documentation and
simplify code
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../State/StateEntryAttribute.cs | 3 ++-
.../State/StateExitAttribute.cs | 2 +-
src/PatternKit.Generators/StateMachineGenerator.cs | 11 +++++------
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
index 9744d13..1c0ef07 100644
--- a/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/State/StateEntryAttribute.cs
@@ -2,7 +2,8 @@ namespace PatternKit.Generators.State;
///
/// Marks a method to be invoked when entering a specific state.
-/// Entry hooks are executed after the State property is updated to the new state.
+/// Entry hooks are executed after the State property is updated to the new state
+/// and after the transition action method has been called.
/// The method can be synchronous (void) or asynchronous (ValueTask).
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
diff --git a/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
index a7ee72f..cda5ccb 100644
--- a/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/State/StateExitAttribute.cs
@@ -2,7 +2,7 @@ namespace PatternKit.Generators.State;
///
/// Marks a method to be invoked when exiting a specific state.
-/// Exit hooks are executed before the State property is updated to the new state.
+/// Exit hooks are executed before the transition action method and before the State property is updated.
/// The method can be synchronous (void) or asynchronous (ValueTask).
///
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
diff --git a/src/PatternKit.Generators/StateMachineGenerator.cs b/src/PatternKit.Generators/StateMachineGenerator.cs
index 7cc7f3f..f1dc93b 100644
--- a/src/PatternKit.Generators/StateMachineGenerator.cs
+++ b/src/PatternKit.Generators/StateMachineGenerator.cs
@@ -730,7 +730,9 @@ private void GenerateCanFireMethod(
// If guard is async and we're in sync context, we can't evaluate it
if (IsGenericValueTaskOfBool(guard.Method.ReturnType))
{
- sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false, // Async guard cannot be evaluated in synchronous CanFire; use FireAsync if transition should be possible");
+ // Async guard cannot be evaluated in synchronous CanFire;
+ // use FireAsync if transition should be possible
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false,");
}
else
{
@@ -840,12 +842,9 @@ private void GenerateFireMethod(
{
sb.AppendLine($" throw new global::System.InvalidOperationException($\"Guard failed for transition from {fromState} on trigger {trigger}.\");");
}
- else // Ignore or ReturnFalse
+ else // Ignore or ReturnFalse - both just return
{
- if (isAsync)
- sb.AppendLine($" return;");
- else
- sb.AppendLine($" return;");
+ sb.AppendLine($" return;");
}
sb.AppendLine($" }}");
From b1e396a289581205e0bae32b44e83e9af8e07f30 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 04:34:26 +0000
Subject: [PATCH 8/9] Address PR review feedback: Add diagnostics, fix guards,
improve enum handling, add tests
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
.../State/StateMachineAttribute.cs | 4 +-
.../AnalyzerReleases.Unshipped.md | 2 +
.../StateMachineGenerator.cs | 156 +++++++++++---
.../StateMachineGeneratorTests.cs | 197 ++++++++++++++++++
4 files changed, 333 insertions(+), 26 deletions(-)
diff --git a/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs b/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
index 8602734..e5e5465 100644
--- a/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
+++ b/src/PatternKit.Generators.Abstractions/State/StateMachineAttribute.cs
@@ -45,9 +45,9 @@ public StateMachineAttribute(Type stateType, Type triggerType)
///
/// Gets or sets whether to generate async methods.
- /// When null (default), async generation is inferred from the presence of async transitions/hooks.
+ /// When not specified, async generation is inferred from the presence of async transitions/hooks.
///
- public bool? GenerateAsync { get; set; }
+ public bool GenerateAsync { get; set; }
///
/// Gets or sets whether to force async generation even if all transitions/hooks are synchronous.
diff --git a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
index d4c5058..65ad3e8 100644
--- a/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
+++ b/src/PatternKit.Generators/AnalyzerReleases.Unshipped.md
@@ -124,3 +124,5 @@ PKST005 | PatternKit.Generators.State | Error | Transition method signature inva
PKST006 | PatternKit.Generators.State | Error | Guard method signature invalid
PKST007 | PatternKit.Generators.State | Error | Entry/Exit hook signature invalid
PKST008 | PatternKit.Generators.State | Warning | Async method detected but async generation disabled
+PKST009 | PatternKit.Generators.State | Error | Generic types not supported for State pattern
+PKST010 | PatternKit.Generators.State | Error | Nested types not supported for State pattern
diff --git a/src/PatternKit.Generators/StateMachineGenerator.cs b/src/PatternKit.Generators/StateMachineGenerator.cs
index f1dc93b..a35b208 100644
--- a/src/PatternKit.Generators/StateMachineGenerator.cs
+++ b/src/PatternKit.Generators/StateMachineGenerator.cs
@@ -23,6 +23,8 @@ public sealed class StateMachineGenerator : IIncrementalGenerator
private const string DiagIdInvalidGuardSignature = "PKST006";
private const string DiagIdInvalidHookSignature = "PKST007";
private const string DiagIdAsyncMethodDetected = "PKST008";
+ private const string DiagIdGenericTypeNotSupported = "PKST009";
+ private const string DiagIdNestedTypeNotSupported = "PKST010";
private static readonly DiagnosticDescriptor TypeNotPartialDescriptor = new(
id: DiagIdTypeNotPartial,
@@ -88,6 +90,22 @@ public sealed class StateMachineGenerator : IIncrementalGenerator
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);
+ private static readonly DiagnosticDescriptor GenericTypeNotSupportedDescriptor = new(
+ id: DiagIdGenericTypeNotSupported,
+ title: "Generic types not supported for State pattern",
+ messageFormat: "Type '{0}' is generic, which is not currently supported by the StateMachine generator. Remove the [StateMachine] attribute or use a non-generic type.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
+ private static readonly DiagnosticDescriptor NestedTypeNotSupportedDescriptor = new(
+ id: DiagIdNestedTypeNotSupported,
+ title: "Nested types not supported for State pattern",
+ messageFormat: "Type '{0}' is nested inside another type, which is not currently supported by the StateMachine generator. Remove the [StateMachine] attribute or move the type to the top level.",
+ category: "PatternKit.Generators.State",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true);
+
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Find all type declarations with [StateMachine] attribute
@@ -128,6 +146,26 @@ private void GenerateStateMachineForType(
return;
}
+ // Check for generic types
+ if (typeSymbol.IsGenericType)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ GenericTypeNotSupportedDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
+ // Check for nested types
+ if (typeSymbol.ContainingType is not null)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ NestedTypeNotSupportedDescriptor,
+ node.GetLocation(),
+ typeSymbol.Name));
+ return;
+ }
+
// Parse attribute configuration
var config = ParseStateMachineConfig(attribute, context, out var stateType, out var triggerType);
if (config is null || stateType is null || triggerType is null)
@@ -167,9 +205,33 @@ private void GenerateStateMachineForType(
return;
// Determine if async generation is needed
- var needsAsync = config.ForceAsync ||
- (config.GenerateAsync ?? false) ||
- DetermineIfAsync(transitions, guards, entryHooks, exitHooks);
+ var hasAsyncMembers = DetermineIfAsync(transitions, guards, entryHooks, exitHooks);
+ bool needsAsync;
+
+ // Check if GenerateAsync was explicitly set to false and async members exist
+ var explicitlyDisabled = config.GenerateAsyncExplicitlySet &&
+ config.GenerateAsync.HasValue &&
+ !config.GenerateAsync.Value;
+
+ if (explicitlyDisabled && hasAsyncMembers && !config.ForceAsync)
+ {
+ // Async members are present but async generation was explicitly disabled.
+ // Emit PKST008 and avoid generating FireAsync, while still allowing sync
+ // operations to block on async members as per the specification.
+ context.ReportDiagnostic(Diagnostic.Create(
+ AsyncMethodDetectedDescriptor,
+ node.GetLocation(),
+ "async transitions/guards/hooks"));
+ needsAsync = false;
+ }
+ else
+ {
+ // If ForceAsync is set, always generate async APIs.
+ // Otherwise, honor an explicit GenerateAsync value when present,
+ // falling back to generating async APIs only when async members exist.
+ needsAsync = config.ForceAsync ||
+ (config.GenerateAsync ?? hasAsyncMembers);
+ }
// Generate the state machine implementation
var source = GenerateStateMachine(typeSymbol, config, stateType, triggerType,
@@ -229,7 +291,10 @@ private static bool IsPartialType(SyntaxNode node)
break;
case "GenerateAsync":
if (namedArg.Value.Value is bool ga)
+ {
config.GenerateAsync = ga;
+ config.GenerateAsyncExplicitlySet = true;
+ }
break;
case "ForceAsync":
config.ForceAsync = namedArg.Value.Value is bool f && f;
@@ -420,13 +485,36 @@ private ImmutableArray CollectExitHooks(
private string? GetEnumValueName(TypedConstant constant, ITypeSymbol enumType)
{
- if (constant.Value is int intValue)
+ if (constant.Value is null)
+ return null;
+
+ ulong targetValue;
+ try
+ {
+ targetValue = Convert.ToUInt64(constant.Value);
+ }
+ catch
{
- // Get the enum member name from the value
- var members = enumType.GetMembers().OfType()
- .Where(f => f.IsConst && f.HasConstantValue && Equals(f.ConstantValue, intValue));
- return members.FirstOrDefault()?.Name;
+ return null;
}
+
+ foreach (var field in enumType.GetMembers().OfType())
+ {
+ if (!field.IsConst || !field.HasConstantValue || field.ConstantValue is null)
+ continue;
+
+ try
+ {
+ var fieldValue = Convert.ToUInt64(field.ConstantValue);
+ if (fieldValue == targetValue)
+ return field.Name;
+ }
+ catch
+ {
+ // Skip values that cannot be converted to UInt64
+ }
+ }
+
return null;
}
@@ -435,30 +523,41 @@ private bool ValidateTransitions(
INamedTypeSymbol typeSymbol,
SourceProductionContext context)
{
- var transitionKeys = new Dictionary>();
+ var transitionKeys = new Dictionary>();
foreach (var transition in transitions)
{
var key = $"{transition.FromState},{transition.Trigger}";
if (!transitionKeys.ContainsKey(key))
- transitionKeys[key] = new List();
- transitionKeys[key].Add(transition.Method.Name);
+ transitionKeys[key] = new List<(string MethodName, Location Location)>();
+
+ var methodLocation = transition.Method.Locations.FirstOrDefault() ?? Location.None;
+ transitionKeys[key].Add((transition.Method.Name, methodLocation));
}
+ var hasDuplicates = false;
+
foreach (var kvp in transitionKeys.Where(kvp => kvp.Value.Count > 1))
{
var parts = kvp.Key.Split(',');
- var methodNames = string.Join(", ", kvp.Value);
+ var methodNames = string.Join(", ", kvp.Value.Select(v => v.MethodName));
+
+ // Prefer a concrete source location from one of the conflicting methods.
+ var location = kvp.Value
+ .Select(v => v.Location)
+ .FirstOrDefault(loc => loc != Location.None) ?? Location.None;
+
context.ReportDiagnostic(Diagnostic.Create(
DuplicateTransitionDescriptor,
- Location.None,
+ location,
parts[0],
parts[1],
methodNames));
- return false;
+
+ hasDuplicates = true;
}
- return true;
+ return !hasDuplicates;
}
private bool ValidateSignatures(
@@ -710,8 +809,8 @@ private void GenerateCanFireMethod(
// Group transitions by (from, trigger)
var transitionGroups = transitions
.GroupBy(t => (t.FromState, t.Trigger))
- .OrderBy(g => g.Key.FromState)
- .ThenBy(g => g.Key.Trigger);
+ .OrderBy(g => g.Key.FromState, StringComparer.Ordinal)
+ .ThenBy(g => g.Key.Trigger, StringComparer.Ordinal);
if (transitionGroups.Any())
{
@@ -727,16 +826,22 @@ private void GenerateCanFireMethod(
if (guard is not null)
{
- // If guard is async and we're in sync context, we can't evaluate it
+ var guardHasCt = guard.Method.Parameters.Length > 0 && IsCancellationToken(guard.Method.Parameters[0].Type);
+
+ // If guard is async, evaluate it synchronously using GetAwaiter().GetResult()
if (IsGenericValueTaskOfBool(guard.Method.ReturnType))
{
- // Async guard cannot be evaluated in synchronous CanFire;
- // use FireAsync if transition should be possible
- sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => false,");
+ var guardCall = guardHasCt
+ ? $"{guard.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult()"
+ : $"{guard.Method.Name}().GetAwaiter().GetResult()";
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => {guardCall},");
}
else
{
- sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => {guard.Method.Name}(),");
+ var guardCall = guardHasCt
+ ? $"{guard.Method.Name}(global::System.Threading.CancellationToken.None)"
+ : $"{guard.Method.Name}()";
+ sb.AppendLine($" ({config.StateTypeName}.{fromState}, {config.TriggerTypeName}.{trigger}) => {guardCall},");
}
}
else
@@ -833,7 +938,9 @@ private void GenerateFireMethod(
? (isAsync
? (guardHasCt ? $"await {guard.Method.Name}(cancellationToken){configureAwait}" : $"await {guard.Method.Name}(){configureAwait}")
: (guardHasCt ? $"{guard.Method.Name}(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult()" : $"{guard.Method.Name}().GetAwaiter().GetResult()"))
- : $"{guard.Method.Name}()";
+ : (guardHasCt
+ ? (isAsync ? $"{guard.Method.Name}(cancellationToken)" : $"{guard.Method.Name}(global::System.Threading.CancellationToken.None)")
+ : $"{guard.Method.Name}()");
sb.AppendLine($" if (!{guardCall})");
sb.AppendLine($" {{");
@@ -932,7 +1039,8 @@ private sealed class StateMachineConfig
public string FireMethodName { get; set; } = "Fire";
public string FireAsyncMethodName { get; set; } = "FireAsync";
public string CanFireMethodName { get; set; } = "CanFire";
- public bool? GenerateAsync { get; set; }
+ public bool? GenerateAsync { get; set; } // Null = not set (infer), true = always generate, false = never generate
+ public bool GenerateAsyncExplicitlySet { get; set; } // Track if GenerateAsync was explicitly set
public bool ForceAsync { get; set; }
public int InvalidTriggerPolicy { get; set; } = 0; // 0=Throw, 1=Ignore, 2=ReturnFalse
public int GuardFailurePolicy { get; set; } = 0; // 0=Throw, 1=Ignore, 2=ReturnFalse
diff --git a/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs b/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
index 6d2338c..6aa6484 100644
--- a/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
+++ b/test/PatternKit.Generators.Tests/StateMachineGeneratorTests.cs
@@ -836,4 +836,201 @@ private void OnTransition() { }
var emit = updated.Emit(Stream.Null);
Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
}
+
+ [Fact]
+ public void GenerateAsyncFalse_WithAsyncMethods_ReportsDiagnostic()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger), GenerateAsync = false)]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private async ValueTask OnTransitionAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenerateAsyncFalse_WithAsyncMethods_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKST008 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+
+ // Debug: print all diagnostics
+ if (diagnostics.Length == 0)
+ {
+ // Check if code was even generated
+ var hasGeneratedCode = result.Results.Any(r => r.GeneratedSources.Length > 0);
+ Assert.True(hasGeneratedCode, "No code was generated");
+
+ // Check compilation diagnostics
+ var compDiags = updated.GetDiagnostics().Where(d => d.Id.StartsWith("PKST")).ToArray();
+ Assert.True(compDiags.Length > 0, $"No PKST diagnostics found. Generated code: {result.Results[0].GeneratedSources.Length} files");
+ }
+
+ Assert.Contains(diagnostics, d => d.Id == "PKST008");
+
+ // Verify FireAsync is NOT generated
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.DoesNotContain("FireAsync", generatedSource);
+ Assert.Contains("public void Fire", generatedSource);
+
+ // And the updated compilation actually compiles (sync Fire should block on async method)
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GuardWithCancellationToken_GeneratesCorrectly()
+ {
+ var source = """
+ using System.Threading;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateGuard(From = State.A, Trigger = Trigger.T1)]
+ private bool CanTransition(CancellationToken ct) => true;
+
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GuardWithCancellationToken_GeneratesCorrectly));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify guard is called with CancellationToken.None in CanFire
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("CanTransition(global::System.Threading.CancellationToken.None)", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void AsyncGuardInCanFire_EvaluatesSynchronously()
+ {
+ var source = """
+ using System.Threading;
+ using System.Threading.Tasks;
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateGuard(From = State.A, Trigger = Trigger.T1)]
+ private async ValueTask CanTransitionAsync(CancellationToken ct)
+ {
+ await Task.Delay(10, ct);
+ return true;
+ }
+
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(AsyncGuardInCanFire_EvaluatesSynchronously));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // No generator diagnostics
+ Assert.All(result.Results, r => Assert.Empty(r.Diagnostics));
+
+ // Verify async guard is evaluated synchronously with GetAwaiter().GetResult()
+ var generatedSource = result.Results[0].GeneratedSources[0].SourceText.ToString();
+ Assert.Contains("CanTransitionAsync(global::System.Threading.CancellationToken.None).GetAwaiter().GetResult()", generatedSource);
+
+ // And the updated compilation actually compiles
+ var emit = updated.Emit(Stream.Null);
+ Assert.True(emit.Success, string.Join("\n", emit.Diagnostics));
+ }
+
+ [Fact]
+ public void GenericType_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(GenericType_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKST009 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST009");
+ }
+
+ [Fact]
+ public void NestedType_ReportsDiagnostic()
+ {
+ var source = """
+ using PatternKit.Generators.State;
+
+ namespace PatternKit.Examples;
+
+ public enum State { A, B }
+ public enum Trigger { T1 }
+
+ public class Outer
+ {
+ [StateMachine(typeof(State), typeof(Trigger))]
+ public partial class Machine
+ {
+ [StateTransition(From = State.A, Trigger = Trigger.T1, To = State.B)]
+ private void OnTransition() { }
+ }
+ }
+ """;
+
+ var comp = RoslynTestHelpers.CreateCompilation(source, nameof(NestedType_ReportsDiagnostic));
+ var gen = new StateMachineGenerator();
+ _ = RoslynTestHelpers.Run(comp, gen, out var result, out var updated);
+
+ // Should have PKST010 diagnostic
+ var diagnostics = result.Results.SelectMany(r => r.Diagnostics).ToArray();
+ Assert.Contains(diagnostics, d => d.Id == "PKST010");
+ }
}
From abe2239840db28bac6bed53f62bdc34078174a73 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 14 Feb 2026 04:41:48 +0000
Subject: [PATCH 9/9] Add comprehensive documentation and expanded examples for
State Machine generator
Co-authored-by: JerrettDavis <2610199+JerrettDavis@users.noreply.github.com>
---
docs/generators/state-machine.md | 913 ++++++++++++++++++
docs/generators/toc.yml | 3 +
.../Generators/State/OrderFlowDemo.cs | 340 ++++++-
.../Generators/State/README.md | 559 +++++++++++
4 files changed, 1801 insertions(+), 14 deletions(-)
create mode 100644 docs/generators/state-machine.md
create mode 100644 src/PatternKit.Examples/Generators/State/README.md
diff --git a/docs/generators/state-machine.md b/docs/generators/state-machine.md
new file mode 100644
index 0000000..13f0293
--- /dev/null
+++ b/docs/generators/state-machine.md
@@ -0,0 +1,913 @@
+# 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:
+
+```csharp
+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:
+
+```csharp
+[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:
+
+```csharp
+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
+
+```csharp
+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:
+
+```csharp
+[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:**
+```csharp
+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:
+
+```csharp
+[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:**
+1. Exit hooks for `FromState` (if any)
+2. Transition action method (`[StateTransition]`) (if any)
+3. Update `State = ToState`
+4. Entry hooks for `ToState` (if any)
+
+### Async Support
+
+The generator automatically detects async methods and generates `FireAsync`:
+
+```csharp
+[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:**
+```csharp
+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:
+
+```csharp
+[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+public partial class OrderFlow
+{
+ [StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+ private async ValueTask 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:
+
+```csharp
+[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:
+
+```csharp
+[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:
+
+```csharp
+[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) - Throws `InvalidOperationException`
+- `Ignore` - Does nothing, returns silently
+
+#### Guard Failure Policy
+
+Controls what happens when a guard returns false:
+
+```csharp
+[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) - Throws `InvalidOperationException`
+- `Ignore` - Does nothing, returns silently
+
+### Async Generation Control
+
+Control async method generation explicitly:
+
+```csharp
+// 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**
+
+```csharp
+// 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
+
+```csharp
+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
+
+```csharp
+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 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:
+
+```csharp
+// 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:
+
+```csharp
+[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:
+
+```csharp
+[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:
+
+```csharp
+[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:
+
+```csharp
+///
+/// Manages the order fulfillment workflow from creation to delivery.
+/// States: Draft -> Submitted -> Paid -> Shipped -> Delivered
+///
+[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
+public partial class OrderWorkflow
+{
+ ///
+ /// Processes payment and transitions to Paid state.
+ /// Guard: Amount must be > 0 and < $100,000
+ ///
+ [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:**
+```csharp
+[StateMachine(typeof(State), typeof(Trigger))]
+public class OrderFlow // Missing 'partial'
+{
+}
+```
+
+**Solution:**
+```csharp
+[StateMachine(typeof(State), typeof(Trigger))]
+public partial class OrderFlow // Add 'partial'
+{
+}
+```
+
+#### PKST004: Duplicate transitions
+
+**Error:**
+```csharp
+[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:**
+```csharp
+[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+private int OnSubmit() // Must return void or ValueTask
+{
+ return 42;
+}
+```
+
+**Solution:**
+```csharp
+[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:
+
+```csharp
+// No boxing, no delegates, no allocations
+order.Fire(OrderTrigger.Submit);
+```
+
+### ValueTask for Async
+
+Async operations use `ValueTask` to minimize allocations:
+
+```csharp
+// ValueTask can complete synchronously without allocation
+await order.FireAsync(OrderTrigger.Pay, ct);
+```
+
+### Struct State Machines
+
+For ultra-high-performance scenarios, use struct:
+
+```csharp
+[StateMachine(typeof(State), typeof(Trigger))]
+public partial struct HighPerformanceStateMachine
+{
+ // Entire state machine on the stack
+}
+```
+
+## Migration Guide
+
+### From Manual Switch Statements
+
+**Before:**
+```csharp
+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:**
+```csharp
+[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:
+
+```csharp
+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:
+
+```csharp
+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:
+
+```csharp
+var json = JsonSerializer.Serialize(new { order.State, order.OrderId });
+// Save to database
+
+// Later, restore:
+var data = JsonSerializer.Deserialize(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:
+
+```csharp
+[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 Pattern Examples](../examples/state-machine-examples.md)
+- [Template Method Generator](template-method-generator.md) - For sequential workflows
+- [Builder Pattern](builder.md) - For object construction
+- [Visitor Pattern](visitor-generator.md) - For operation dispatch
diff --git a/docs/generators/toc.yml b/docs/generators/toc.yml
index bb2218a..ed0dbad 100644
--- a/docs/generators/toc.yml
+++ b/docs/generators/toc.yml
@@ -19,6 +19,9 @@
- name: Proxy
href: proxy.md
+- name: State Machine
+ href: state-machine.md
+
- name: Template Method
href: template-method-generator.md
diff --git a/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs b/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
index 28afaad..b748058 100644
--- a/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
+++ b/src/PatternKit.Examples/Generators/State/OrderFlowDemo.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using PatternKit.Generators.State;
@@ -7,15 +8,20 @@ namespace PatternKit.Examples.Generators.State;
///
/// Real-world example of a State Machine generator demonstrating an order processing workflow.
-/// This example shows:
+/// This comprehensive example shows:
/// - Enum-based states and triggers
/// - Synchronous and asynchronous transitions
-/// - Guards to control transitions
+/// - Guards to control transitions based on business rules
/// - Entry and exit hooks for state changes
/// - Proper cancellation token handling
+/// - Multiple scenarios (happy path, cancellation, guard failures)
+/// - Error handling with different policies
///
public static class OrderFlowDemo
{
+ ///
+ /// Runs the main order flow demonstration showing a complete happy-path workflow.
+ ///
public static void Run()
{
Console.WriteLine("=== Order Flow State Machine Demo ===\n");
@@ -58,6 +64,9 @@ private static async Task RunAsync(OrderFlow order)
Console.WriteLine($" State: {order.State}\n");
}
+ ///
+ /// Demonstrates order cancellation from different states.
+ ///
public static void CancellationDemo()
{
Console.WriteLine("=== Cancellation Example ===\n");
@@ -79,6 +88,9 @@ public static void CancellationDemo()
Console.WriteLine("=== Order was cancelled ===\n");
}
+ ///
+ /// Demonstrates guard failure when business rules prevent a transition.
+ ///
public static void GuardFailureDemo()
{
Console.WriteLine("=== Guard Failure Example ===\n");
@@ -105,41 +117,168 @@ public static void GuardFailureDemo()
Console.WriteLine("=== Guard prevented invalid payment ===\n");
}
+
+ ///
+ /// Demonstrates async cancellation token handling.
+ ///
+ public static async Task AsyncCancellationDemo()
+ {
+ Console.WriteLine("=== Async Cancellation Example ===\n");
+
+ var order = new OrderFlow("ORD-004", 450m);
+ var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
+
+ Console.WriteLine($"Order: {order.Id}, Amount: ${order.Amount:F2}");
+ Console.WriteLine("Timeout set to 100ms for 500ms operation\n");
+
+ order.Fire(OrderTrigger.Submit);
+
+ try
+ {
+ Console.WriteLine("Processing payment with timeout...");
+ await order.FireAsync(OrderTrigger.Pay, cts.Token);
+ Console.WriteLine("Payment completed successfully");
+ }
+ catch (OperationCanceledException)
+ {
+ Console.WriteLine("Payment operation was cancelled due to timeout");
+ order.Fire(OrderTrigger.Cancel);
+ Console.WriteLine($"Order cancelled. Final state: {order.State}\n");
+ }
+
+ Console.WriteLine("=== Async cancellation handled ===\n");
+ }
+
+ ///
+ /// Demonstrates state-based decision making.
+ ///
+ public static void StateBasedLogicDemo()
+ {
+ Console.WriteLine("=== State-Based Logic Example ===\n");
+
+ var order = new OrderFlow("ORD-005", 199.99m);
+
+ // Function to display available actions based on current state
+ void ShowAvailableActions(OrderFlow o)
+ {
+ Console.WriteLine($"Current state: {o.State}");
+ Console.WriteLine("Available actions:");
+
+ foreach (OrderTrigger trigger in Enum.GetValues(typeof(OrderTrigger)))
+ {
+ if (o.CanFire(trigger))
+ {
+ Console.WriteLine($" - {trigger}");
+ }
+ }
+ Console.WriteLine();
+ }
+
+ ShowAvailableActions(order);
+
+ order.Fire(OrderTrigger.Submit);
+ ShowAvailableActions(order);
+
+ order.FireAsync(OrderTrigger.Pay, CancellationToken.None).GetAwaiter().GetResult();
+ ShowAvailableActions(order);
+
+ Console.WriteLine("=== State-based logic complete ===\n");
+ }
+
+ ///
+ /// Runs all demonstration scenarios.
+ ///
+ public static async Task RunAllDemosAsync()
+ {
+ Console.WriteLine("╔════════════════════════════════════════════════╗");
+ Console.WriteLine("║ State Machine Pattern - Complete Demo Suite ║");
+ Console.WriteLine("╔════════════════════════════════════════════════╗\n");
+
+ Run();
+ await Task.Delay(500);
+
+ CancellationDemo();
+ await Task.Delay(500);
+
+ GuardFailureDemo();
+ await Task.Delay(500);
+
+ await AsyncCancellationDemo();
+ await Task.Delay(500);
+
+ StateBasedLogicDemo();
+
+ Console.WriteLine("╔════════════════════════════════════════════════╗");
+ Console.WriteLine("║ All Demonstrations Complete ║");
+ Console.WriteLine("╚════════════════════════════════════════════════╝\n");
+ }
}
///
-/// Enum defining the possible states of an order.
+/// Enum defining the possible states of an order in the fulfillment workflow.
///
public enum OrderState
{
+ /// Initial state - order is being prepared
Draft,
+
+ /// Order has been submitted for processing
Submitted,
+
+ /// Payment has been successfully processed
Paid,
+
+ /// Order has been shipped to the customer
Shipped,
+
+ /// Order was cancelled and will not be processed
Cancelled
}
///
-/// Enum defining the triggers that can transition an order between states.
+/// Enum defining the triggers that can cause state transitions.
///
public enum OrderTrigger
{
+ /// Submit the order for processing
Submit,
+
+ /// Process payment for the order
Pay,
+
+ /// Ship the order to the customer
Ship,
+
+ /// Cancel the order
Cancel
}
///
-/// State machine for managing order lifecycle.
-/// Uses the StateMachine generator to create deterministic state transitions.
+/// State machine for managing order lifecycle using the State Pattern Generator.
+/// Demonstrates deterministic state transitions with guards, hooks, and async support.
+///
+/// State Flow:
+/// Draft -> Submit -> Submitted -> Pay -> Paid -> Ship -> Shipped
+/// Draft/Submitted/Paid can -> Cancel -> Cancelled
///
[StateMachine(typeof(OrderState), typeof(OrderTrigger))]
public partial class OrderFlow
{
+ ///
+ /// Gets the unique identifier for this order.
+ ///
public string Id { get; }
+
+ ///
+ /// Gets the order amount.
+ ///
public decimal Amount { get; }
+ ///
+ /// Initializes a new instance of the OrderFlow state machine.
+ ///
+ /// Unique order identifier
+ /// Order amount in dollars
public OrderFlow(string id, decimal amount)
{
Id = id;
@@ -147,64 +286,237 @@ public OrderFlow(string id, decimal amount)
State = OrderState.Draft; // Set initial state
}
- // Transition: Draft -> Submitted
+ #region Transitions
+
+ ///
+ /// Handles the submission of an order from Draft to Submitted state.
+ ///
[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
private void OnSubmit()
{
Console.WriteLine($" >> Transition: Submitting order {Id}");
+ // In a real system: Validate order data, reserve inventory, etc.
}
- // Guard: Can only pay if amount is valid
+ ///
+ /// Guard that validates whether payment can be processed.
+ /// Checks that the amount is valid (greater than 0).
+ ///
[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
private bool CanPay()
{
+ // Business rule: Amount must be positive
return Amount > 0;
}
- // Transition: Submitted -> Paid (async)
+ ///
+ /// Processes payment asynchronously and transitions from Submitted to Paid state.
+ /// Simulates payment processing with a delay.
+ ///
[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
private async ValueTask OnPayAsync(CancellationToken ct)
{
Console.WriteLine($" >> Transition: Processing payment for {Id}...");
+
// Simulate payment processing
await Task.Delay(500, ct);
+
+ // In a real system:
+ // - Call payment gateway
+ // - Update payment records
+ // - Generate receipt
+
Console.WriteLine($" >> Payment of ${Amount:F2} processed");
}
- // Exit hook: Called when leaving Paid state
+ ///
+ /// Exit hook executed when leaving the Paid state.
+ /// Performs cleanup and finalization tasks.
+ ///
[StateExit(OrderState.Paid)]
private void OnExitPaid()
{
Console.WriteLine($" >> Exit Hook: Finalizing payment for {Id}");
+ // In a real system:
+ // - Send payment confirmation email
+ // - Update accounting system
+ // - Notify warehouse
}
- // Transition: Paid -> Shipped
+ ///
+ /// Handles the shipping of an order from Paid to Shipped state.
+ ///
[StateTransition(From = OrderState.Paid, Trigger = OrderTrigger.Ship, To = OrderState.Shipped)]
private void OnShip()
{
Console.WriteLine($" >> Transition: Shipping order {Id}");
+ // In a real system:
+ // - Generate shipping label
+ // - Notify shipping carrier
+ // - Update inventory
}
- // Entry hook: Called when entering Shipped state
+ ///
+ /// Entry hook executed when entering the Shipped state.
+ /// Sends notifications and updates tracking.
+ ///
[StateEntry(OrderState.Shipped)]
private void OnEnterShipped()
{
Console.WriteLine($" >> Entry Hook: Order {Id} is now shipped, sending notification");
+ // In a real system:
+ // - Send shipping notification email with tracking
+ // - Update customer portal
+ // - Start delivery monitoring
}
- // Transitions for cancellation from multiple states
+ ///
+ /// Handles order cancellation from multiple states.
+ /// Can be triggered from Draft, Submitted, or Paid 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($" >> Transition: Cancelling order {Id}");
+ // In a real system:
+ // - Release reserved inventory
+ // - Cancel payment authorization
+ // - Log cancellation reason
}
- // Entry hook: Called when entering Cancelled state
+ ///
+ /// Entry hook executed when entering the Cancelled state.
+ /// Processes refunds and cleanup.
+ ///
[StateEntry(OrderState.Cancelled)]
private void OnEnterCancelled()
{
Console.WriteLine($" >> Entry Hook: Order {Id} is cancelled, processing refund if needed");
+ // In a real system:
+ // - Issue refund if payment was processed
+ // - Send cancellation confirmation
+ // - Update analytics
+ }
+
+ #endregion
+
+ ///
+ /// Gets a human-readable description of the current state.
+ ///
+ public string GetStateDescription()
+ {
+ return State switch
+ {
+ OrderState.Draft => "Order is being prepared",
+ OrderState.Submitted => "Order is waiting for payment",
+ OrderState.Paid => "Payment received, preparing for shipment",
+ OrderState.Shipped => "Order is on its way to you",
+ OrderState.Cancelled => "Order has been cancelled",
+ _ => "Unknown state"
+ };
+ }
+
+ ///
+ /// Gets all triggers that are valid for the current state.
+ ///
+ public IEnumerable GetAvailableTriggers()
+ {
+ foreach (OrderTrigger trigger in Enum.GetValues(typeof(OrderTrigger)))
+ {
+ if (CanFire(trigger))
+ {
+ yield return trigger;
+ }
+ }
+ }
+}
+
+///
+/// Example of a more complex state machine with additional business logic.
+/// Demonstrates a document approval workflow.
+///
+public enum DocumentState
+{
+ Draft,
+ PendingReview,
+ Approved,
+ Rejected,
+ Published,
+ Archived
+}
+
+public enum DocumentAction
+{
+ SubmitForReview,
+ Approve,
+ Reject,
+ Revise,
+ Publish,
+ Archive
+}
+
+///
+/// Document approval workflow state machine.
+///
+[StateMachine(typeof(DocumentState), typeof(DocumentAction))]
+public partial class DocumentWorkflow
+{
+ public string DocumentId { get; }
+ public string Author { get; }
+ public List ReviewComments { get; } = new();
+
+ public DocumentWorkflow(string documentId, string author)
+ {
+ DocumentId = documentId;
+ Author = author;
+ State = DocumentState.Draft;
+ }
+
+ [StateTransition(From = DocumentState.Draft, Trigger = DocumentAction.SubmitForReview, To = DocumentState.PendingReview)]
+ private void OnSubmitForReview()
+ {
+ Console.WriteLine($"Document {DocumentId} submitted for review by {Author}");
+ }
+
+ [StateGuard(From = DocumentState.PendingReview, Trigger = DocumentAction.Approve)]
+ private bool CanApprove()
+ {
+ // Business rule: Must have at least one review comment
+ return ReviewComments.Count > 0;
+ }
+
+ [StateTransition(From = DocumentState.PendingReview, Trigger = DocumentAction.Approve, To = DocumentState.Approved)]
+ private void OnApprove()
+ {
+ Console.WriteLine($"Document {DocumentId} approved");
+ }
+
+ [StateTransition(From = DocumentState.PendingReview, Trigger = DocumentAction.Reject, To = DocumentState.Rejected)]
+ private void OnReject()
+ {
+ Console.WriteLine($"Document {DocumentId} rejected");
+ }
+
+ [StateTransition(From = DocumentState.Rejected, Trigger = DocumentAction.Revise, To = DocumentState.Draft)]
+ private void OnRevise()
+ {
+ ReviewComments.Clear();
+ 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)
+ {
+ Console.WriteLine($"Publishing document {DocumentId}...");
+ await Task.Delay(300, ct); // Simulate publishing
+ Console.WriteLine($"Document {DocumentId} published");
+ }
+
+ [StateTransition(From = DocumentState.Published, Trigger = DocumentAction.Archive, To = DocumentState.Archived)]
+ private void OnArchive()
+ {
+ Console.WriteLine($"Document {DocumentId} archived");
}
}
diff --git a/src/PatternKit.Examples/Generators/State/README.md b/src/PatternKit.Examples/Generators/State/README.md
new file mode 100644
index 0000000..8f4a8a8
--- /dev/null
+++ b/src/PatternKit.Examples/Generators/State/README.md
@@ -0,0 +1,559 @@
+# State Machine Pattern Examples
+
+This directory contains real-world examples demonstrating the State Machine Pattern Source Generator in action.
+
+## Overview
+
+The State Machine Pattern Generator creates deterministic finite state machines with:
+- ✅ Explicit states and triggers (enum-based)
+- ✅ Compile-time validation
+- ✅ Guards for conditional transitions
+- ✅ Entry/Exit lifecycle hooks
+- ✅ Sync and async support (ValueTask)
+- ✅ Zero runtime dependencies
+
+## Examples in This Directory
+
+### OrderFlowDemo.cs
+
+A comprehensive order processing workflow demonstrating:
+
+1. **Basic State Transitions** - Order lifecycle from Draft to Delivered
+2. **Guards** - Payment validation based on amount
+3. **Async Transitions** - Payment processing with async/await
+4. **Entry/Exit Hooks** - Notifications and cleanup actions
+5. **Multiple Transition Sources** - Cancellation from multiple states
+6. **Error Handling** - Guard failures and invalid triggers
+
+#### States
+- `Draft` - Initial state when order is created
+- `Submitted` - Order submitted for processing
+- `Paid` - Payment successfully processed
+- `Shipped` - Order shipped to customer
+- `Cancelled` - Order cancelled
+
+#### Triggers
+- `Submit` - Submit order for processing
+- `Pay` - Process payment
+- `Ship` - Ship the order
+- `Cancel` - Cancel the order
+
+#### Running the Demo
+
+```csharp
+using PatternKit.Examples.Generators.State;
+
+// Happy path: Draft -> Submitted -> Paid -> Shipped
+OrderFlowDemo.Run();
+
+// Cancellation scenario
+OrderFlowDemo.CancellationDemo();
+
+// Guard failure scenario
+OrderFlowDemo.GuardFailureDemo();
+```
+
+#### Sample Output
+
+```
+=== Order Flow State Machine Demo ===
+
+Order: ORD-001, Amount: $299.99
+Initial State: Draft
+
+1. Submitting order...
+ >> Transition: Submitting order ORD-001
+ State: Submitted
+
+2. Attempting to pay...
+3. Processing payment...
+ >> Transition: Processing payment for ORD-001...
+ >> Payment of $299.99 processed
+ State: Paid
+
+4. Shipping order...
+ >> Exit Hook: Finalizing payment for ORD-001
+ >> Transition: Shipping order ORD-001
+ >> Entry Hook: Order ORD-001 is now shipped, sending notification
+ State: Shipped
+
+=== Order processing complete ===
+```
+
+## Key Concepts Demonstrated
+
+### 1. Synchronous Transitions
+
+Simple state changes with synchronous actions:
+
+```csharp
+[StateTransition(From = OrderState.Draft, Trigger = OrderTrigger.Submit, To = OrderState.Submitted)]
+private void OnSubmit()
+{
+ Console.WriteLine($" >> Transition: Submitting order {Id}");
+}
+```
+
+### 2. Asynchronous Transitions
+
+Async operations with proper cancellation token handling:
+
+```csharp
+[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+private async ValueTask OnPayAsync(CancellationToken ct)
+{
+ Console.WriteLine($" >> Transition: Processing payment for {Id}...");
+ await Task.Delay(500, ct); // Simulate payment processing
+ Console.WriteLine($" >> Payment of ${Amount:F2} processed");
+}
+```
+
+### 3. Guards for Validation
+
+Prevent invalid transitions based on business rules:
+
+```csharp
+[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+private bool CanPay()
+{
+ return Amount > 0; // Only allow payment for valid amounts
+}
+```
+
+**Usage:**
+```csharp
+if (order.CanFire(OrderTrigger.Pay))
+{
+ await order.FireAsync(OrderTrigger.Pay, CancellationToken.None);
+}
+else
+{
+ Console.WriteLine(" Payment blocked by guard (invalid amount)");
+}
+```
+
+### 4. Entry and Exit Hooks
+
+Execute side effects when entering or leaving states:
+
+```csharp
+// Exit hook - runs before leaving Paid state
+[StateExit(OrderState.Paid)]
+private void OnExitPaid()
+{
+ Console.WriteLine($" >> Exit Hook: Finalizing payment for {Id}");
+}
+
+// Entry hook - runs after entering Shipped state
+[StateEntry(OrderState.Shipped)]
+private void OnEnterShipped()
+{
+ Console.WriteLine($" >> Entry Hook: Order {Id} is now shipped, sending notification");
+}
+```
+
+**Execution Order (when transitioning Paid -> Shipped):**
+1. `OnExitPaid()` - Exit hook
+2. `OnShip()` - Transition action
+3. `State = Shipped` - State update
+4. `OnEnterShipped()` - Entry hook
+
+### 5. Multiple Transitions from Same Source
+
+Handle common actions like cancellation from multiple states:
+
+```csharp
+[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($" >> Transition: Cancelling order {Id}");
+}
+```
+
+## Usage Patterns
+
+### Pattern 1: Check Before Fire
+
+Use `CanFire` to check if a trigger is valid:
+
+```csharp
+if (order.CanFire(OrderTrigger.Pay))
+{
+ order.Fire(OrderTrigger.Pay);
+}
+else
+{
+ Console.WriteLine("Cannot process payment at this time");
+}
+```
+
+### Pattern 2: Async Workflows
+
+Use `FireAsync` for async transitions:
+
+```csharp
+try
+{
+ await order.FireAsync(OrderTrigger.Pay, cancellationToken);
+ Console.WriteLine("Payment processed successfully");
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Payment cancelled");
+}
+catch (InvalidOperationException ex)
+{
+ Console.WriteLine($"Invalid transition: {ex.Message}");
+}
+```
+
+### Pattern 3: State-Based Logic
+
+Make decisions based on current state:
+
+```csharp
+switch (order.State)
+{
+ case OrderState.Draft:
+ Console.WriteLine("Order is still being prepared");
+ break;
+ case OrderState.Submitted:
+ Console.WriteLine("Waiting for payment");
+ break;
+ case OrderState.Shipped:
+ Console.WriteLine("Order is on its way!");
+ break;
+}
+```
+
+### Pattern 4: Transition History
+
+Track state changes by wrapping Fire methods:
+
+```csharp
+public class TrackedOrderFlow
+{
+ private readonly OrderFlow _flow;
+ private readonly List<(OrderState From, OrderTrigger Trigger, OrderState To)> _history = new();
+
+ public void Fire(OrderTrigger trigger)
+ {
+ var from = _flow.State;
+ _flow.Fire(trigger);
+ var to = _flow.State;
+ _history.Add((from, trigger, to));
+ }
+
+ public IReadOnlyList<(OrderState From, OrderTrigger Trigger, OrderState To)> History => _history;
+}
+```
+
+## Common Scenarios
+
+### Scenario 1: Happy Path Processing
+
+```csharp
+var order = new OrderFlow("ORD-001", 299.99m);
+
+// Draft -> Submitted
+order.Fire(OrderTrigger.Submit);
+
+// Submitted -> Paid
+await order.FireAsync(OrderTrigger.Pay, ct);
+
+// Paid -> Shipped
+order.Fire(OrderTrigger.Ship);
+
+Console.WriteLine($"Order completed in state: {order.State}");
+```
+
+### Scenario 2: Validation Failure
+
+```csharp
+var order = new OrderFlow("ORD-002", -50m); // Invalid amount
+order.Fire(OrderTrigger.Submit);
+
+// Guard will prevent payment
+if (!order.CanFire(OrderTrigger.Pay))
+{
+ Console.WriteLine("Cannot process payment - validation failed");
+ order.Fire(OrderTrigger.Cancel);
+}
+```
+
+### Scenario 3: Cancellation
+
+```csharp
+var order = new OrderFlow("ORD-003", 100m);
+order.Fire(OrderTrigger.Submit);
+
+// Customer changes mind before payment
+order.Fire(OrderTrigger.Cancel);
+
+Console.WriteLine($"Order cancelled in state: {order.State}");
+```
+
+### Scenario 4: Async with Cancellation
+
+```csharp
+var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+var order = new OrderFlow("ORD-004", 500m);
+
+try
+{
+ order.Fire(OrderTrigger.Submit);
+ await order.FireAsync(OrderTrigger.Pay, cts.Token);
+ Console.WriteLine("Payment processed before timeout");
+}
+catch (OperationCanceledException)
+{
+ Console.WriteLine("Payment processing timed out");
+ order.Fire(OrderTrigger.Cancel);
+}
+```
+
+## Testing Your State Machines
+
+### Unit Test Example
+
+```csharp
+using Xunit;
+
+public class OrderFlowTests
+{
+ [Fact]
+ public void Submit_TransitionsFromDraftToSubmitted()
+ {
+ // Arrange
+ var order = new OrderFlow("TEST-001", 100m);
+ Assert.Equal(OrderState.Draft, order.State);
+
+ // Act
+ order.Fire(OrderTrigger.Submit);
+
+ // Assert
+ Assert.Equal(OrderState.Submitted, order.State);
+ }
+
+ [Fact]
+ public void Pay_WithZeroAmount_BlockedByGuard()
+ {
+ // Arrange
+ var order = new OrderFlow("TEST-002", 0m);
+ order.Fire(OrderTrigger.Submit);
+
+ // Assert
+ Assert.False(order.CanFire(OrderTrigger.Pay));
+ }
+
+ [Fact]
+ public async Task PayAsync_ProcessesPaymentAndTransitionsToPaid()
+ {
+ // Arrange
+ var order = new OrderFlow("TEST-003", 250m);
+ order.Fire(OrderTrigger.Submit);
+
+ // Act
+ await order.FireAsync(OrderTrigger.Pay, CancellationToken.None);
+
+ // Assert
+ Assert.Equal(OrderState.Paid, order.State);
+ }
+
+ [Fact]
+ public void Ship_FromDraft_ThrowsInvalidOperationException()
+ {
+ // Arrange
+ var order = new OrderFlow("TEST-004", 100m);
+
+ // Act & Assert
+ Assert.Throws(() =>
+ order.Fire(OrderTrigger.Ship));
+ }
+}
+```
+
+### Integration Test Example
+
+```csharp
+public class OrderFlowIntegrationTests
+{
+ [Fact]
+ public async Task CompleteOrderWorkflow_ProcessesSuccessfully()
+ {
+ // Arrange
+ var order = new OrderFlow("INT-001", 199.99m);
+ var states = new List();
+
+ // Act
+ states.Add(order.State); // Draft
+
+ order.Fire(OrderTrigger.Submit);
+ states.Add(order.State); // Submitted
+
+ await order.FireAsync(OrderTrigger.Pay, CancellationToken.None);
+ states.Add(order.State); // Paid
+
+ order.Fire(OrderTrigger.Ship);
+ states.Add(order.State); // Shipped
+
+ // Assert
+ Assert.Equal(new[]
+ {
+ OrderState.Draft,
+ OrderState.Submitted,
+ OrderState.Paid,
+ OrderState.Shipped
+ }, states);
+ }
+}
+```
+
+## Best Practices
+
+### 1. Initialize State in Constructor
+
+Always set the initial state explicitly:
+
+```csharp
+public OrderFlow(string id, decimal amount)
+{
+ Id = id;
+ Amount = amount;
+ State = OrderState.Draft; // Explicit initial state
+}
+```
+
+### 2. Use Meaningful Names
+
+Choose clear, business-oriented names:
+
+```csharp
+// Good
+public enum OrderState { Draft, Submitted, Paid, Shipped }
+public enum OrderTrigger { Submit, Pay, Ship }
+
+// Avoid
+public enum State { S1, S2, S3, S4 }
+public enum Action { A1, A2, A3 }
+```
+
+### 3. Keep Transition Methods Focused
+
+Each method should have a single responsibility:
+
+```csharp
+// Good - focused on payment
+[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+private async ValueTask OnPayAsync(CancellationToken ct)
+{
+ await ProcessPaymentAsync(ct);
+}
+
+// Bad - mixing concerns
+[StateTransition(From = OrderState.Submitted, Trigger = OrderTrigger.Pay, To = OrderState.Paid)]
+private async ValueTask OnPayAsync(CancellationToken ct)
+{
+ await ProcessPaymentAsync(ct);
+ await SendEmailAsync(ct); // Should be in entry hook
+ await UpdateInventoryAsync(ct); // Should be separate
+}
+```
+
+### 4. Use Entry/Exit Hooks for Side Effects
+
+Separate concerns using hooks:
+
+```csharp
+[StateExit(OrderState.Paid)]
+private void OnExitPaid()
+{
+ // Cleanup, finalization
+ FinalizePaymentRecords();
+}
+
+[StateEntry(OrderState.Shipped)]
+private async ValueTask OnEnterShippedAsync(CancellationToken ct)
+{
+ // Side effects when entering state
+ await SendShippingNotificationAsync(ct);
+ await UpdateInventoryAsync(ct);
+}
+```
+
+### 5. Document Complex Workflows
+
+Add comments explaining business logic:
+
+```csharp
+///
+/// Processes payment and transitions to Paid state.
+/// Business Rules:
+/// - Amount must be greater than 0
+/// - Amount must be less than $10,000 (daily limit)
+/// - Payment method must be valid
+///
+[StateGuard(From = OrderState.Submitted, Trigger = OrderTrigger.Pay)]
+private bool CanPay()
+{
+ return Amount > 0 && Amount < 10000 && IsPaymentMethodValid();
+}
+```
+
+## Troubleshooting
+
+### Issue: Guard always returns false in CanFire
+
+**Problem:** Async guard evaluated synchronously
+
+**Solution:** Async guards use `GetAwaiter().GetResult()` in `CanFire`. Use `FireAsync` for proper async evaluation.
+
+### Issue: State doesn't change after Fire
+
+**Possible causes:**
+1. Guard returned false
+2. Invalid trigger for current state
+3. Check error handling policy
+
+**Debug:**
+```csharp
+if (order.CanFire(trigger))
+{
+ try
+ {
+ order.Fire(trigger);
+ }
+ catch (InvalidOperationException ex)
+ {
+ Console.WriteLine($"Transition failed: {ex.Message}");
+ }
+}
+```
+
+### Issue: Compilation error about missing partial keyword
+
+**Solution:** Ensure your class is marked as `partial`:
+
+```csharp
+[StateMachine(typeof(State), typeof(Trigger))]
+public partial class MyStateMachine // Add 'partial'
+{
+}
+```
+
+## Further Reading
+
+- [State Machine Generator Documentation](../../docs/generators/state-machine.md)
+- [Generator Diagnostics](../../docs/generators/troubleshooting.md)
+- [Pattern Overview](../../docs/patterns/behavioral/state/index.md)
+
+## Contributing
+
+Have an interesting state machine example? Submit a PR with:
+1. Clear business scenario description
+2. State and trigger definitions
+3. Complete, runnable example
+4. Expected output
+5. Key concepts demonstrated