Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - JamieG/stateless: A simple library for creating state machines in C# code · GitHub
Skip to content

Repository files navigation

Stateless Build statusNuGet Pre Release

Create state machines and lightweight state machine-based workflows directly in .NET code:

varphoneCall=newStateMachine<State,Trigger>(State.OffHook);phoneCall.Configure(State.OffHook).Permit(Trigger.CallDialed,State.Ringing);phoneCall.Configure(State.Ringing).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.CallConnected,State.Connected);phoneCall.Configure(State.Connected).OnEntry(()=>StartCallTimer()).OnExit(()=>StopCallTimer()).Permit(Trigger.LeftMessage,State.OffHook).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PlacedOnHold,State.OnHold);// ...phoneCall.Fire(Trigger.CallDialled);Assert.AreEqual(State.Ringing,phoneCall.State);

This project, as well as the example above, was inspired by Simple State Machine.

Features

Most standard state machine constructs are supported:

  • Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)
  • Hierarchical states
  • Entry/exit events for states
  • Guard clauses to support conditional transitions
  • Introspection

Some useful extensions are also provided:

  • Ability to store state externally (for example, in a property tracked by an ORM)
  • Parameterised triggers
  • Reentrant states
  • Export to DOT graph

Hierarchical States

In the example below, the OnHold state is a substate of the Connected state. This means that an OnHold call is still connected.

phoneCall.Configure(State.OnHold).SubstateOf(State.Connected).Permit(Trigger.TakenOffHold,State.Connected).Permit(Trigger.HungUp,State.OffHook).Permit(Trigger.PhoneHurledAgainstWall,State.PhoneDestroyed);

In addition to the StateMachine.State property, which will report the precise current state, an IsInState(State) method is provided. IsInState(State) will take substates into account, so that if the example above was in the OnHold state, IsInState(State.Connected) would also evaluate to true.

Entry/Exit Events

In the example, the StartCallTimer() method will be executed when a call is connected. The StopCallTimer() will be executed when call completes (by either hanging up or hurling the phone against the wall.)

The call can move between the Connected and OnHold states without the StartCallTimer() and StopCallTimer() methods being called repeatedly because the OnHold state is a substate of the Connected state.

Entry/Exit event handlers can be supplied with a parameter of type Transition that describes the trigger, source and destination states.

External State Storage

Stateless has been designed with encapsulation within an ORM-ed domain model in mind. Some ORMs place requirements upon where mapped data may be stored. To this end, the StateMachine constructor can accept function arguments that will be used to read and write the state values:

varstateMachine=newStateMachine<State,Trigger>(()=>myState.Value,
s =>myState.Value=s);

In this example the state machine will use the myState object for state storage.

Introspection

The state machine can provide a list of the triggers than can be successfully fired within the current state via the StateMachine.PermittedTriggers property.

Guard Clauses

The state machine will choose between multiple transitions based on guard clauses, e.g.:

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialled,State.Ringing,()=>IsValidNumber).PermitIf(Trigger.CallDialled,State.Beeping,()=>!IsValidNumber);

Guard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.

Parameterised Triggers

Strongly-typed parameters can be assigned to triggers:

varassignTrigger=stateMachine.SetTriggerParameters<string>(Trigger.Assign);stateMachine.Configure(State.Assigned).OnEntryFrom(assignTrigger, email =>OnAssigned(email));stateMachine.Fire(assignTrigger,"joe@example.com");

Trigger parameters can be used to dynamically select the destination state using the PermitDynamic() configuration method.

Ignored Transitions and Reentrant States

Firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown.

To ignore triggers within certain states, use the Ignore(TTrigger) directive:

phoneCall.Configure(State.Connected).Ignore(Trigger.CallDialled);

Alternatively, a state can be marked reentrant so its entry and exit events will fire even when transitioning from/to itself:

stateMachine.Configure(State.Assigned).PermitReentry(Trigger.Assigned).OnEntry(()=>SendEmailToAssignee());

By default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the OnUnhandledTrigger method:

stateMachine.OnUnhandledTrigger((state,trigger)=>{});

Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.

phoneCall.Configure(State.OffHook).PermitIf(Trigger.CallDialed,State.Ringing,IsValidNumber);stringgraph=phoneCall.ToDotGraph();

The StateMachine.ToDotGraph() method returns a string representation of the state machine in the DOT graph language, e.g.:

digraph {
OffHook -> Ringing [label="CallDialed [IsValidNumber]"];
}

This can then be rendered by tools that support the DOT graph language, such as the dot command line tool from graphviz.org or viz.js. See http://www.webgraphviz.com for instant gratification. Command line example: dot -T pdf -o phoneCall.pdf phoneCall.dot to generate a PDF file.

Building

Visual Studio 2015 is required to build this project.

Project Goals

This page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.

Please use the issue tracker or the if you'd like to report problems or discuss features.

(Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself.)

About

A simple library for creating state machines in C# code

Resources

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages