Skip to content

Repository files navigation

Carubbi.StateMachine

A state machine library for .NET 10 with compile-time source generation, fluent startup configuration and dependency injection integration. No attributes, no runtime weaving, no reflection.

How it works

  1. Declare your entity as a partial class and write its transition logic in <Method>Core partial method pairs.
  2. Register the machine at startup via services.AddStateMachine<TEntity>(...), binding transitions to methods with a fluent API.
  3. The bundled Roslyn source generator emits the public wrapper methods and the StateMachine property for every registered entity.
public partial class Order
{
    // Public API is generated for you: Ship(), Deliver(), Cancel()
    private partial void ShipCore()
    {
        // transition logic
    }

    private partial void DeliverCore() { }

    private partial void CancelCore() { }
}
var services = new ServiceCollection();

services.AddStateMachine<Order>(fsm => fsm
    .Initial("Draft")
    .Allow("Draft", "Placed").For(nameof(Order.Ship))
    .Allow("Placed", "Shipped").For(e => e.Deliver())
    .Allow("Draft", "Canceled").For(nameof(Order.Cancel)));

Usage:

var order = new Order();
order.Ship();                              // guarded by the configured transitions
Console.WriteLine(order.StateMachine.CurrentState);   // "Placed"

order.StateMachine.TransitionStarted += (_, e) => e.Cancel = /* ... */;

Rules

  • Transition logic lives in XxxCore partial method pairs (empty defining declaration + implementing declaration). The generator publishes the public Xxx wrapper.
  • Methods are only valid while the entity's current state matches one of the Allow(from, to) bindings registered for that method.
  • When no binding matches:
    • StateMachine.IgnoreInvalidOperations == false (default): an InvalidOperationException describing the current and allowed states is thrown.
    • IgnoreInvalidOperations == true: the body is skipped silently (non-void wrappers return default).
  • Handlers of TransitionStarted may set Cancel = true; the transition and method body are then skipped.
  • TransitionEnded fires after the state changed.
  • Accessing an entity whose type was never configured throws a descriptive InvalidOperationException.

Diagnostics

ID Severity Meaning
SM001 Error Registered entity is not declared partial.
SM003 Error Generated wrapper/property name collides with an existing member.
SM004 Error Core method declares out/ref parameters (cannot forward on skipped transitions).
SM005 Error Generic transition methods are not supported.

Ambiguous configurations (two transitions from the same state bound to one method, missing initial state) are rejected eagerly at configuration time.

Breaking changes from v1.x

  • Targets .NET 10 only.
  • [InitialState] / [Transition] attributes, IStatedEntity and StateMachine.Configure() are gone; configuration moved to the fluent builder at startup.
  • Entities are plain partial classes; the StateMachine property is generated (get-only, lazy) instead of user-declared.
  • Replaced NConcern/CNeptune AOP with a Roslyn incremental source generator (Carubbi.StateMachine.Generators) - fully AOT-safe.
  • Tests migrated from MSTest v1 to TUnit.

Packages

  • Carubbi.StateMachine - core library + DI integration (Microsoft.Extensions.DependencyInjection.Abstractions).
  • Carubbi.StateMachine.Generators - shipped automatically as an analyzer dependency of the main package.

Build & test

Requires the .NET 10 SDK.

dotnet build
dotnet test

About

Generic State Machine Aspect-oriented-programming based Implementation

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages