Class State Machine

GitHub home: https://github.com/lionfire/class-state-machine

Overview

A convention-oriented state machine for typical C# classes.

Status

Pre-alpha preview ready. Give it a shot and give me feedback.

Working:
  • Transitions
  • States
  • Event handlers

See the unit tests for examples.

Design Goals

  • Make it easy to turn typical C# classes into state machines
  • Make it easy to enforce conventions and state machine logic
  • Augment existing common coding style, with minimal imposition
  • Unintrusive on target codebase - No dependencies (aside from .NET Standard) - No base class - Optional code generation (separate DLL)
  • Harness Roslyn to generate design-time code that offers Intellisense and reduces the need for reflection during run-time startup
  • AOT compatible in order to run on iOS: no Reflection.Emit
  • Harness design-time code generation to eliminate boilerplate and promote standardization.
  • Allow users to create comprehensive state machine models, and have them autommatically trimmed down to the used states and transitions.
  • Support hierarchical state machines (FUTURE)

Features

  • Efficent and conveniently flexible method handlers - OnEntering{State}() or On{State} - OnLeaving{State}() or After{State} - On{Transition}()
  • Flexible language - On{Transition}(): OnInitialize or OnInitializing
  • Flexible events: - StateMachine.StateChanged - StateMachine.StateChanging - {Transition}
  • Guards - Can{Transition} methods - CanLeave{State} / CanEnter{State} methods
  • Cancelation - StateChangingContext
  • Allow user to override all conventions (FUTURE)

Requirements and impositions

  • Generated state machine code must be on a partial class with the [GenerateStateMachine] attribute.
  • States and transitions are defined as enum flags. - FUTURE: Allow arbitrary objects as long as they are IEquatable?
  • Transition flags have attributes that define from and to states. - FUTURE: Allow specification of transition info provider via provider type provided to [GenerateStateMachine] attribute
  • [StartingState] attribute on a member of the state enum indicates initial state - FUTURE: Allow specification of starting state via [GenerateStateMachine] attribute

Roadmap (features under consideration)

  • Hierachical state machine support - Sub-state machine example: paused/unpaused under running - Sub-state machine example: limited exit states from substate machine

  • Replace more reflection with design-time generated code

  • Extension DLL: Tools for state machines: - IStateMachine and IHas<IStateMachine> - IStateMachine members:

    • CurrentState
    • StateChanging event
  • Optional extensibility / lightweightness features: - Optionally parameterized state changes - Optionally remember previous transition - Optional zero meomory allocation (can be helpful in games)
  • Wrapper/Adapter for goal-seeking, if the desired state change is only possible with multiple steps.

Quick Start Sample

Reference these 2 nuget packages:
  • LionFire.StateMachines.Class.Generation
  • CodeGeneration.Roslyn.BuildTime

Nuget.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
    <add key="corefxlab" value="https://dotnet.myget.org/F/dotnet-corefxlab/api/v3/index.json" />
  </packageSources>
</configuration>
[Flags]
enum MyState {
    [StartingState]
    Uninitialized = 1 << 0,
    Ready = 1 << 1,
    Started = 1 << 2,
    Finished = 1 << 3
};

enum MyTransition
{
    [Transition(MyState.Uninitialized, MyState.Ready)]
    Initialize,
    [Transition(MyState.Ready, MyState.Started)]
    Start,
    [Transition(MyState.Uninitialized, MyState.Finished)]
    Finish
};

// NOTE: Due to a limitation at the moment the State and Transition types have to be in a separate assembly. (Pull-requests welcome.)

[StateMachine(typeof(MyState), typeof(MyTransition)]
public class MyClass
{

  public void OnInitialize()
  {
  }

  private string mustNotBeNull;
  public bool CanLeaveUninitialized()
  {
          if(mustNotBeNull == null) return false;

          return true;
  }

  public void AfterReady(StateChangeContext context)
  {
          context.Cancel();
  }

  public void OnStart()
  {
  }

  public void OnFinished()
  {
  }

  public void Test()
  {
    Console.WriteLine(stateMachine.CurrentState); // Uninitialized

    mustNotBeNull = "hello";
    Initialize();
    Console.WriteLine(stateMachine.CurrentState); // Ready

    Start();
    Console.WriteLine(stateMachine.CurrentState); // Started

    Finish();
    Console.WriteLine(stateMachine.CurrentState); // Finished
  }

  // Members not defined here are generated in obj/.../MyClass.{hashcode}.generated.cs

}