SlimStateMachine 1.2.0
dotnet add package SlimStateMachine --version 1.2.0
NuGet\Install-Package SlimStateMachine -Version 1.2.0
<PackageReference Include="SlimStateMachine" Version="1.2.0" />
<PackageVersion Include="SlimStateMachine" Version="1.2.0" />
<PackageReference Include="SlimStateMachine" />
paket add SlimStateMachine --version 1.2.0
#r "nuget: SlimStateMachine, 1.2.0"
#:package SlimStateMachine@1.2.0
#addin nuget:?package=SlimStateMachine&version=1.2.0
#tool nuget:?package=SlimStateMachine&version=1.2.0
SlimStateMachine
A lightweight C# library for defining and managing state machines based on an entity class and an enum property representing its state.
Features
- Generic: Define state machines for any entity (
TEntity
) and its status enum (TEnum
). - Fluent Configuration: Use a builder pattern to define the initial state and allowed transitions.
- Static Access: Interact with the state machine using static methods (
StateMachine<TEntity, TEnum>.CanTransition(...)
, etc.). - Cached Configuration: State machine definitions are cached for performance after the initial configuration.
- Transition Information: Query possible transitions from the current state or any given state.
- Final State Detection: Check if a state is a final state (no outgoing transitions) or if an entity is currently in a final state.
- Pre-conditions: Define conditions (
Func<TEntity, bool>
) that must be met for a transition to occur. - Post-conditions (Actions): Define actions (
Action<TEntity>
) to be executed after a successful transition (before the state property is updated). - Automatic State Update: The
TryTransition
method automatically updates the entity's status property upon successful transition. - Mermaid Graph Generation: Generate a Mermaid.js graph definition string to visualize the state machine, including pre-condition descriptions.
- D2 Graph Generation: Generate a D2 graph definition string to visualize the state machine, including pre-condition descriptions.
- Thread-Safe: Configuration is thread-safe. Runtime access (checking/performing transitions) assumes the entity instance is handled appropriately by the calling code (e.g., not mutated concurrently during a transition check).
Installation
Install the package via NuGet Package Manager:
Install-Package SlimStateMachine
Or using .NET CLI:
dotnet add package SlimStateMachine
Supported Platforms
- .NET 9.0
- .NET 8.0
- .NET Standard 2.0
Usage
1. Define Your Entity and Enum
// Example: Invoice Management
public enum InvoiceStatus
{
Draft,
Sent,
Paid,
Cancelled
}
public class Invoice
{
public int Id { get; set; }
public InvoiceStatus Status { get; set; } // The state property
public decimal TotalAmount { get; set; }
public decimal AmountPaid { get; set; }
public decimal RemainingAmount => TotalAmount - AmountPaid;
public string? Notes { get; set; }
// You might initialize the status in the constructor or rely on the state machine's initial state
public Invoice()
{
// Status defaults to 'Draft' (enum default) which matches our example initial state
}
}
2. Configure the State Machine
This should typically be done once during application startup (e.g., in Program.cs
or a static constructor).
using SlimStateMachine;
// --- Configuration (Do this once at startup) ---
StateMachine<Invoice, InvoiceStatus>.Configure(
// 1. Specify the property holding the state
invoice => invoice.Status,
// 2. Use the builder to define the state machine rules
builder =>
{
// 2a. Set the initial state for new entities (if not set explicitly)
builder.SetInitialState(InvoiceStatus.Draft);
// 2b. Define allowed transitions
builder.AllowTransition(InvoiceStatus.Draft, InvoiceStatus.Sent);
// 2c. Transition with a Pre-condition
builder.AllowTransition(
InvoiceStatus.Sent,
InvoiceStatus.Paid,
preCondition: inv => inv.RemainingAmount <= 0, // Func<Invoice, bool>
preConditionExpression: "Remaining <= 0" // String for Mermaid graph
);
// 2d. Transition with a Post-condition (Action)
builder.AllowTransition(
InvoiceStatus.Draft,
InvoiceStatus.Cancelled,
postAction: inv => inv.Notes = "Cancelled while in Draft." // Action<Invoice>
);
// 2e. Transition with both Pre- and Post-conditions
builder.AllowTransition(
InvoiceStatus.Sent,
InvoiceStatus.Cancelled,
preCondition: inv => inv.RemainingAmount > 0, // Can only cancel if not fully paid
preConditionExpression: "Remaining > 0",
postAction: inv => inv.Notes = "Cancelled after sending (partially paid)."
);
}
);
// --- End Configuration ---
3. Interact with the State Machine
// Create an entity instance
var myInvoice = new Invoice { Id = 101, TotalAmount = 500, AmountPaid = 0 };
// Initial state is implicitly Draft (enum default), matching configured InitialState
// Check if a transition is possible
bool canSend = StateMachine<Invoice, InvoiceStatus>.CanTransition(myInvoice, InvoiceStatus.Sent); // true
bool canPay = StateMachine<Invoice, InvoiceStatus>.CanTransition(myInvoice, InvoiceStatus.Paid); // false (Remaining > 0)
Console.WriteLine($"Can send invoice {myInvoice.Id}? {canSend}");
Console.WriteLine($"Can pay invoice {myInvoice.Id}? {canPay}");
// Get possible next states
var possibleStates = StateMachine<Invoice, InvoiceStatus>.GetPossibleTransitions(myInvoice);
// possibleStates will contain [Sent, Cancelled] for the initial Draft state in this config
Console.WriteLine($"Possible next states for invoice {myInvoice.Id}: {string.Join(", ", possibleStates)}");
// Attempt a transition
bool transitionSucceeded = StateMachine<Invoice, InvoiceStatus>.TryTransition(myInvoice, InvoiceStatus.Sent);
if (transitionSucceeded)
{
Console.WriteLine($"Invoice {myInvoice.Id} transitioned to: {myInvoice.Status}"); // Status is now Sent
}
// Now try to pay - still fails precondition
transitionSucceeded = StateMachine<Invoice, InvoiceStatus>.TryTransition(myInvoice, InvoiceStatus.Paid);
Console.WriteLine($"Tried paying unpaid invoice. Succeeded? {transitionSucceeded}. Status: {myInvoice.Status}"); // false, Status remains Sent
// Simulate payment
myInvoice.AmountPaid = 500;
// Try paying again - now succeeds
canPay = StateMachine<Invoice, InvoiceStatus>.CanTransition(myInvoice, InvoiceStatus.Paid); // true
transitionSucceeded = StateMachine<Invoice, InvoiceStatus>.TryTransition(myInvoice, InvoiceStatus.Paid);
Console.WriteLine($"Tried paying fully paid invoice. Succeeded? {transitionSucceeded}. Status: {myInvoice.Status}"); // true, Status is now Paid
// Try cancelling - fails precondition (Remaining <= 0)
transitionSucceeded = StateMachine<Invoice, InvoiceStatus>.TryTransition(myInvoice, InvoiceStatus.Cancelled);
Console.WriteLine($"Tried cancelling paid invoice. Succeeded? {transitionSucceeded}. Status: {myInvoice.Status}"); // false, Status remains Paid
Console.WriteLine($"Notes: {myInvoice.Notes}"); // Post-action didn't run
4. Generate Mermaid Graph
Get a string representation of the state machine for visualization.
string mermaidGraph = StateMachine<Invoice, InvoiceStatus>.GenerateMermaidGraph();
Console.WriteLine("\n--- Mermaid Graph ---");
Console.WriteLine(mermaidGraph);
You can paste the output into tools or Markdown environments that support Mermaid (like GitLab, GitHub, Obsidian, online editors https://mermaid.live/):
graph TD
Start((⚪)) --> Draft
Draft --> Sent
Sent -- "Remaining <= 0" --> Paid
Draft --> Cancelled
Sent -- "Remaining > 0" --> Cancelled
5. Generate D2 Graph
Get a string representation of the state machine for visualization in D2 format.
string d2Graph = StateMachine<Invoice, InvoiceStatus>.GenerateD2Graph();
Console.WriteLine("\n--- D2 Graph ---");
Console.WriteLine(d2Graph);
You can paste the output into tools or Markdown environments that support D2 (like Obsidian, online editors https://play.d2lang.com/):
# State Machine: Invoice - InvoiceStatus
direction: down
# Styles
style {
fill: honeydew
stroke: limegreen
stroke-width: 2
font-size: 14
shadow: true
}
Start: {
shape: circle
style.fill: lightgreen
style.stroke: green
width: 40
height: 40
}
Start -> Draft
# Transitions
Draft -> Sent
Sent -> Paid: Remaining <= 0
Draft -> Cancelled
Sent -> Cancelled: Remaining > 0
6. Generate Diagram in Either Format
You can also use the generic diagram generator to create diagrams in either format:
string diagram = StateMachine<Invoice, InvoiceStatus>.GenerateDiagram(format: "Mermaid"); Console.WriteLine("\n--- Diagram ---"); Console.WriteLine(diagram);
Integration with ASP.NET Core and Domain-Driven Design
SlimStateMachine works well with ASP.NET Core applications and domain-driven design approaches:
// In your domain model
public class Order
{
public Guid Id { get; private set; }
public OrderStatus Status { get; private set; }
// Other domain properties...
// Encapsulated state transition methods
public bool ProcessOrder()
{
return StateMachine<Order, OrderStatus>.TryTransition(this, OrderStatus.Processing);
}
public bool ShipOrder()
{
return StateMachine<Order, OrderStatus>.TryTransition(this, OrderStatus.Shipped);
}
}
// In your startup code
StateMachine<Order, OrderStatus>.Configure(
order => order.Status,
builder => {
builder.SetInitialState(OrderStatus.Created);
builder.AllowTransition(OrderStatus.Created, OrderStatus.Processing,
preCondition: o => o.Items.Count > 0,
preConditionExpression: "Has items");
// More transitions...
}
);
Error Handling
InvalidOperationException
is thrown if you try to use the state machine before callingConfigure
or if you callConfigure
more than once for the sameTEntity
/TEnum
pair.StateMachineException
is thrown for configuration errors (e.g., missing initial state) or if aPostAction
throws an exception duringTryTransition
.ArgumentException
/ArgumentNullException
may be thrown during configuration if invalid parameters (like the property accessor) are provided.
Version History
1.1.0 (D2 Graph Support)
- Added support for generating D2 graph format for state machine visualization.
- Fixed minor bugs in Mermaid graph generation.
1.0.0 (Initial Release)
- Basic state machine functionality
- Pre-conditions and post-action support
- Mermaid graph generation
- Thread-safe configuration
License
This project is licensed under the MIT License - see the LICENSE file for details.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Collections.Immutable (>= 9.0.4)
-
net8.0
- System.Collections.Immutable (>= 9.0.4)
-
net9.0
- System.Collections.Immutable (>= 9.0.4)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.