Skip to content

Instantly share code, notes, and snippets.

@liammclennan
Last active January 23, 2017 05:18
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save liammclennan/8949046 to your computer and use it in GitHub Desktop.
Save liammclennan/8949046 to your computer and use it in GitHub Desktop.
Simple, re-useable state machine (c#)
// this script works in linqpad (https://www.linqpad.net/)
void Main()
{
var day = new DaysRevisions();
day.State.Dump();
day.State.To(LifecycleState.Draft, "me");
day.State.Dump();
day.State.To(LifecycleState.Historical, "me");
}
public class StateMachine<TEnum> where TEnum : struct, IConvertible
{
protected Dictionary<TEnum, Func<StateMachine<TEnum>, bool>> _rules;
[NotMapped]
public TEnum State {
get
{
if (string.IsNullOrEmpty(StateString))
{
return (TEnum)(Enum.GetValues(typeof (TEnum)).GetValue(0));
}
return (TEnum)Enum.Parse(typeof (TEnum), StateString);
}
set
{
StateString = value.ToString();
}
}
public string StateString { get; set; }
public string LastTransitionBy { get; protected set; }
public class StateTransitionPredicateException : Exception
{
public StateTransitionPredicateException(string message)
: base(message) {}
}
public class StateMachineRequiredStateException : Exception
{
public StateMachineRequiredStateException(string message)
: base(message) { }
}
public StateMachine(Dictionary<TEnum, Func<StateMachine<TEnum>, bool>> rules)
{
if (!typeof(TEnum).IsEnum)
{
throw new ArgumentException("TEnum must be an enumerated type");
}
_rules = rules;
LastTransitionBy = "";
StateString = "";
}
protected StateMachine()
{}
public StateMachine<TEnum> To(TEnum targetState, string approver)
{
var predicate = _rules.ContainsKey(targetState)
? _rules[targetState] : (sm) => true;
if (predicate(this))
{
State = targetState;
StateString = targetState.ToString();
LastTransitionBy = approver;
}
else
{
throw new StateTransitionPredicateException(string.Format("The transition from {0} to {1} was not allowed",State, targetState));
}
return this;
}
public bool Is(TEnum state)
{
return State.Equals(state);
}
public void Require(TEnum required)
{
if (!Is(required))
{
throw new StateMachineRequiredStateException("Expected state " + required + " but was " + State);
}
}
public static Func<StateMachine<TEnum>, bool> AllowTransitionFrom(params TEnum[] fromStates)
{
return (sm) => fromStates.Contains(sm.State);
}
}
public class DaysRevisions
{
public virtual StateMachine<LifecycleState> State { get; protected set; }
public DaysRevisions()
{
var capturedRandomCutoff = 690063227;
// this is the definition of the allowed transitions
State = new StateMachine<LifecycleState>(new Dictionary<LifecycleState, Func<StateMachine<LifecycleState>, bool>>
{
{LifecycleState.Draft, StateMachine<LifecycleState>.AllowTransitionFrom(LifecycleState.Unknown)},
{LifecycleState.Approved, StateMachine<LifecycleState>.AllowTransitionFrom(LifecycleState.Draft)},
{LifecycleState.Historical, StateMachine<LifecycleState>.AllowTransitionFrom(LifecycleState.Approved)},
{LifecycleState.Unknown, (sm) => new Random().Next() > capturedRandomCutoff}
});
}
}
public enum LifecycleState
{
Unknown,
Draft,
Approved,
Historical
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment