5. Customization - iwannabebot/SharpFsm GitHub Wiki
SharpFsm is designed for extensibility, allowing you to inject custom logic and integrate with external systems seamlessly. This is achieved through the use of generic context types, named conditions and side effects, and the flexible builder pattern.
Extending the FSM with Custom Logic
You can add custom logic to your FSM by:
- Defining custom conditions (predicates) that control when transitions are allowed.
- Defining custom side effects (actions) that execute when transitions occur.
- Using your own context type to pass any data or services needed for decision-making.
Example: Custom Condition and Side Effect
Suppose you want to allow a transition only if a user is an admin, and log every state change:
public enum MyState
{
Start,
Finished
}
public class UserContext
{
public bool IsAdmin { get; set; }
public string UserName { get; set; }
}
var registry = new TransitionRegistry<MyState, UserContext>();
registry.RegisterCondition("IsAdmin", ctx => ctx.IsAdmin);
registry.RegisterSideEffect("LogTransition", (ctx, _, _) =>
Console.WriteLine($"User {ctx.UserName} performed a transition."));
var builder = FiniteStateMachineBuilder<MyState, UserContext>.Create("UserFSM")
.WithInitialState(MyState.Start)
.WithRegistry(registry)
.AddTransition(MyState.Start, MyState.Finished)
.When("IsAdmin")
.WithSideEffect("LogTransition")
.Done();
var fsm = new FiniteStateMachine<MyState, UserContext>(builder.Build());
You can also use inline lambdas for one-off logic:
.AddTransition(MyState.Start, MyState.Finished)
.When(ctx => ctx.IsAdmin && ctx.UserName == "superuser")
.WithSideEffect((ctx, _, _) => AuditService.Log(ctx.UserName))
.Done()
Integrating with Other Systems
Your context object can hold references to services, repositories, or external APIs. You can call these from within your conditions or side effects.
Example: Calling an External Service
public enum OrderState
{
Packed,
Shipped
}
public class OrderContext
{
public INotificationService NotificationService { get; set; }
public string OrderId { get; set; }
}
var registry = new TransitionRegistry<OrderState, OrderContext>();
registry.RegisterSideEffect("NotifyExternal", ctx =>
ctx.NotificationService.NotifyOrderShipped(ctx.OrderId));
var builder = FiniteStateMachineBuilder<OrderState, OrderContext>.Create("Order")
.WithInitialState(OrderState.Packed)
.WithRegistry(registry)
.AddTransition(OrderState.Packed, OrderState.Shipped)
.WithSideEffect("NotifyExternal")
.Done();
Example: Database Integration
You can use your context to access a database or repository:
public class TicketContext
{
public ITicketRepository TicketRepository { get; set; }
public int TicketId { get; set; }
}
registry.RegisterSideEffect("UpdateStatus", (ctx, _, _) =>
ctx.TicketRepository.UpdateStatus(ctx.TicketId, "Closed"));
Mealy Machine
A Mealy machine produces outputs based on the current state and the input (i.e., the transition). In SharpFsm, this maps naturally to using side effects on transitions. How to implement:
- Define states and context as usual.
- Register side effects that produce output during transitions.
- The output/action is tied to the transition, not the state.
public enum MealyState { S0, S1 }
public class MealyContext
{
public string Input { get; set; }
public string Output { get; set; }
}
var registry = new TransitionRegistry<MealyState, MealyContext>();
registry.RegisterSideEffect("OutputA", (ctx, _, _) => ctx.Output = "A");
registry.RegisterSideEffect("OutputB", (ctx, _, _) => ctx.Output = "B");
var builder = FiniteStateMachineBuilder<MealyState, MealyContext>.Create("Mealy")
.WithInitialState(MealyState.S0)
.WithRegistry(registry)
.AddTransition(MealyState.S0, MealyState.S1)
.When(ctx => ctx.Input == "x")
.WithSideEffect("OutputA")
.Done()
.AddTransition(MealyState.S1, MealyState.S0)
.When(ctx => ctx.Input == "y")
.WithSideEffect("OutputB")
.Done();
var fsm = new FiniteStateMachine<MealyState, MealyContext>(builder.Build());
Moore Machine
A Moore machine produces outputs based solely on the current state. In SharpFsm, you can model this by associating output logic with state entry (not transition). How to implement:
- Define states and context.
- After each transition, execute logic based on the new state (e.g., in your application code or via a state entry handler).
- Alternatively, use side effects on all transitions to a given state, but this is less DRY.
public enum MooreState { S0, S1 }
public class MooreContext
{
public string Output { get; set; }
}
var registry = new TransitionRegistry<MooreState, MooreContext>();
registry.RegisterSideEffect("OutputSideEffect", (MooreContext ctx, MooreState oldState, MooreState newState) =>
{
switch (newState)
{
case MooreState.S0: ctx.Output = "A"; break;
case MooreState.S1: ctx.Output = "B"; break;
}
});
var builder = FiniteStateMachineBuilder<MooreState, MooreContext>.Create("Moore")
.WithInitialState(MooreState.S0)
.WithRegistry(registry)
.AddTransition(MooreState.S0, MooreState.S1)
.WithSideEffect("OutputSideEffect")
.Done()
.AddTransition(MooreState.S1, MooreState.S0)
.WithSideEffect("OutputSideEffect")
.Done();
var fsm = new FiniteStateMachine<MooreState, MooreContext>(builder.Build());
Summary
- Custom logic is injected via conditions and side effects, either registered in a
TransitionRegistry
or provided inline. - Integration is achieved by passing service references in your context and invoking them in your logic.
- This design keeps your FSM definitions clean, testable, and decoupled from infrastructure, while still allowing powerful customization.