https://github.com/drittich/state-machine
A simple convention-based finite state machine that lets you pass event data through to your transition actions.
https://github.com/drittich/state-machine
state-machine state-machine-cs state-machines statemachine statemachine-library statemachines
Last synced: 3 months ago
JSON representation
A simple convention-based finite state machine that lets you pass event data through to your transition actions.
- Host: GitHub
- URL: https://github.com/drittich/state-machine
- Owner: drittich
- Created: 2023-05-10T02:59:46.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2024-11-11T04:52:02.000Z (12 months ago)
- Last Synced: 2024-12-03T14:16:52.814Z (11 months ago)
- Topics: state-machine, state-machine-cs, state-machines, statemachine, statemachine-library, statemachines
- Language: C#
- Homepage:
- Size: 53.7 KB
- Stars: 1
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# State Machine
[](https://github.com/drittich/state-machine/actions/workflows/build.yml)
[](https://github.com/drittich/state-machine/actions/workflows/tests.yml)
A simple, extensible finite state machine that allows you to define states, events, transitions, and pass event data through to your transition actions.
## Installation
The `drittich.StateMachine` library is available on [NuGet](https://www.nuget.org/packages/drittich.StateMachine). You can install it using the Package Manager Console:
```bash
Install-Package drittich.StateMachine
```
Or using the .NET CLI:
```bash
dotnet add package drittich.StateMachine
```
This will install the library and its dependencies.
## Example Usage
The `StateMachine` class lets you define your own states, events, transitions, and a data transfer object (DTO) to pass data with events. This data can then be provided to the action that runs when a transition occurs.
You need to:
- Define enums for your states and events.
- Create a DTO class for event data.
- Initialize the state machine with an initial state and a logger.
- Add transitions that specify how the state machine moves from one state to another in response to events.
### Define States and Events
```csharp
enum MyStates
{
Initial,
SomeState,
SomeOtherState,
Complete
}
enum MyEvents
{
SomethingHappened,
SomethingElseHappened,
SomeOtherRandomEvent
}
```
### Create a DTO for Event Data
```csharp
public class MyDto
{
public int Prop1 { get; set; }
}
```
### Initialize the State Machine
```csharp
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
// Create a logger (use NullLogger if you don't need logging)
ILogger> logger = new NullLogger>();
// Initialize the state machine with the initial state
var sm = new StateMachine(MyStates.Initial, logger);
```
### Define the Transitions
With the simplified AddTransition method, you can now add transitions directly without needing to create Transition objects explicitly:
```csharp
// Add transitions to the state machine
sm.AddTransition(MyStates.Initial, MyEvents.SomethingHappened, MyStates.SomeState, SomeMethodToExecuteAsync);
sm.AddTransition(MyStates.SomeState, MyEvents.SomethingElseHappened, MyStates.Complete, SomeOtherMethodToExecuteAsync);
```
### Define the Action Methods
```csharp
using System.Threading;
using System.Threading.Tasks;
// Action method for the first transition
async Task SomeMethodToExecuteAsync(MyDto data, CancellationToken cancellationToken)
{
// Your action code here
await Task.Delay(100, cancellationToken);
Console.WriteLine("Executed SomeMethodToExecuteAsync");
}
// Action method for the second transition
async Task SomeOtherMethodToExecuteAsync(MyDto data, CancellationToken cancellationToken)
{
// Your action code here
await Task.Delay(100, cancellationToken);
Console.WriteLine("Executed SomeOtherMethodToExecuteAsync");
}
```
### Execute Transitions
```csharp
var data = new MyDto { Prop1 = 1 };
// Execute the first transition
var resultingState = await sm.GetNextAsync(MyEvents.SomethingHappened, data);
// resultingState is MyStates.SomeState
// Execute the second transition
var resultingState2 = await sm.GetNextAsync(MyEvents.SomethingElseHappened, data);
// resultingState2 is MyStates.Complete
```
### Handle Invalid Transitions
If an invalid transition is attempted (no transition is defined for the current state and event), an InvalidTransitionException is thrown.
```csharp
try
{
// Attempt an invalid transition
var resultingState3 = await sm.GetNextAsync(MyEvents.SomeOtherRandomEvent, data);
}
catch (InvalidTransitionException ex)
{
// Handle the exception
Console.WriteLine($"Invalid transition: {ex.Message}");
}
```
## Additional Features
### Guard Conditions
You can add guard conditions to transitions to control whether the transition should occur based on the event data.
```csharp
sm.AddTransition(
MyStates.SomeState,
MyEvents.SomeOtherRandomEvent,
MyStates.Complete,
SomeOtherMethodToExecuteAsync,
guard: data => data.Prop1 > 0
);
```
If the guard condition returns false, a GuardConditionFailedException is thrown, and the transition does not occur.
### Cancellation Support
The action methods accept a CancellationToken, allowing transitions to be canceled if needed.
```csharp
var cts = new CancellationTokenSource();
cts.CancelAfter(500); // Cancel after 500ms
try
{
await sm.GetNextAsync(MyEvents.SomethingHappened, data, cts.Token);
}
catch (TaskCanceledException)
{
Console.WriteLine("Transition was canceled.");
}
```
### Logging
The state machine uses ILogger to log information about transitions, warnings, and errors.
- **Information:** Successful transitions.
- **Warning:** Undefined transitions or guard condition failures.
- **Error:** Exceptions thrown during actions.
## Exception Handling
- **InvalidTransitionException:** Thrown when no transition is defined for the current state and event.
- **GuardConditionFailedException:** Thrown when a guard condition evaluates to false.
- **TaskCanceledException:** Thrown when a transition is canceled via a CancellationToken.
- **InvalidOperationException:** Thrown when attempting to add a duplicate transition.
## Thread Safety
The `StateMachine` class is thread-safe and can handle concurrent transition attempts appropriately. It ensures that only one transition occurs at a time, maintaining the integrity of the `CurrentState`.
## Complete Example
```csharp
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
// Define states and events
enum MyStates
{
Initial,
SomeState,
SomeOtherState,
Complete
}
enum MyEvents
{
SomethingHappened,
SomethingElseHappened,
SomeOtherRandomEvent
}
// DTO for event data
public class MyDto
{
public int Prop1 { get; set; }
}
class Program
{
static async Task Main(string[] args)
{
// Create a logger
ILogger> logger = new NullLogger>();
// Initialize the state machine
var sm = new StateMachine(MyStates.Initial, logger);
// Define transitions
sm.AddTransition(MyStates.Initial, MyEvents.SomethingHappened, MyStates.SomeState, SomeMethodToExecuteAsync);
sm.AddTransition(MyStates.SomeState, MyEvents.SomethingElseHappened, MyStates.Complete, SomeOtherMethodToExecuteAsync);
// Event data
var data = new MyDto { Prop1 = 1 };
// Execute transitions
var resultingState = await sm.GetNextAsync(MyEvents.SomethingHappened, data);
Console.WriteLine($"State after first transition: {resultingState}");
var resultingState2 = await sm.GetNextAsync(MyEvents.SomethingElseHappened, data);
Console.WriteLine($"State after second transition: {resultingState2}");
// Handle invalid transition
try
{
await sm.GetNextAsync(MyEvents.SomeOtherRandomEvent, data);
}
catch (InvalidTransitionException ex)
{
Console.WriteLine($"Invalid transition: {ex.Message}");
}
}
// Action methods
static async Task SomeMethodToExecuteAsync(MyDto data, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
Console.WriteLine("Executed SomeMethodToExecuteAsync");
}
static async Task SomeOtherMethodToExecuteAsync(MyDto data, CancellationToken cancellationToken)
{
await Task.Delay(100, cancellationToken);
Console.WriteLine("Executed SomeOtherMethodToExecuteAsync");
}
}
```
## Installation
To use the `StateMachine` class in your project, include the source code or compile it into a library that you can reference.
## Dependencies
- **.NET Standard 2.0** or higher.
- `Microsoft.Extensions.Logging.Abstractions` for logging interfaces.
Install via NuGet:
```bash
Install-Package Microsoft.Extensions.Logging.Abstractions
```
## License
This project is licensed under the MIT License.
## Contributing
Contributions are welcome! Please submit a pull request or open an issue to discuss improvements or features.
## Contact
For questions or support, please open an issue on the GitHub repository.
***
**Note:** Replace `SomeMethodToExecuteAsync` and `SomeOtherMethodToExecuteAsync` with your actual action methods. The DTO MyDto should contain the data relevant to your application.