Structr.Stateflows package provides functionality for modeling behavior of state-driven entities by creating and using state machines in your .NET application. It provides simple yet functional API via Stateflow object that encapsulate methods and properties to monitor available actions and change state of controlled entity.
AddStateflows() extension method performs registration of all configurator classes that implement IStateMachineConfigurator and stateflow providers that implement IStateflowProvider.
Param name
Param type
Description
assembliesToScan
params Assembly[]
List of assemblies to search configurator classes and stateflow providers.
configureOptions
Action<StateflowServiceOptions>
Options to be used by state machine provider.
Additionally configure IStateMachineProvider service by specifying it's type and lifetime used StateflowServiceOptions.
StateflowServiceOptions properties:
Property name
Property type
Description
ProviderServiceType
Type
Changes standard implementation of IStateMachineProvider to specified one. Default value is typeof(StateMachineProvider).
ProviderServiceLifetime
ServiceLifetime
Specifies the lifetime of an IStateMachineProvider service. Default value is Transient.
Usage
The basic usage is:
Let's say we have some application where user could create Issue objects containing feedback or comments and publish them:
Each of such states imply different set of actions, some of which lead to change of state (like Submitting or Solving) and some not (like Editing) Create an enum for all possible actions:
Now u should create configurator class that implements IStateMachineConfigurator for configuring state machine:
// StateMachineConfigurator is a abstract class allowing you to configure state machine synchronously.// For asynchronously configuring inherit from IStateMachineConfigurator interface instead of StateMachineConfigurator class.publicclassIssueStateMachineConfigurator:StateMachineConfigurator<Issue,IssueState,IssueAction>{ // As long as IssueStateMachineConfigurator is a normal .NET class with it's own constructor we could inject into it any dependencies needed. // Here we are adding some ICurrentUserService to be able to take info about current user while modeling Issue's behavior for its different states.privatereadonlyICurrentUserService _currentUserService;publicIssueStateMachineConfigurator(ICurrentUserService currentUserService)=> _currentUserService = currentUserService; // You should configure each issue state.protectedoverridevoidConfigure(Stateless.StateMachine<IssueState,IssueAction> stateMachine,Issue issue) { // Getting current user.ICurrentUser currentUser =_currentUserService.CurrentUser; // Getting info about current user for specified issue.bool isAdmin =currentUser.HasRole("Admin");bool isAuthor =issue.AuthorId==currentUser.Id;bool isAssignee =issue.AssigneeId==currentUser.Id;stateMachine.Configure(IssueState.Draft) .PermitIf(IssueAction.Submit,IssueState.Submitted, () => isAuthor) .InternalTransitionIf(IssueAction.Edit, () => isAuthor) .InternalTransitionIf(IssueAction.Delete, () => isAuthor);stateMachine.Configure(IssueState.Submitted) .PermitIf(IssueAction.AcceptToWork,IssueState.InProgress, () => isAdmin);stateMachine.Configure(IssueState.InProgress) .PermitIf(IssueAction.Cancel,IssueState.Canceled, () => isAssignee) .PermitIf(IssueAction.Solve,IssueState.Solved, () => isAssignee) .InternalTransitionIf(IssueAction.ChangeAssignee, () => isAssignee || isAdmin);stateMachine.Configure(IssueState.Canceled) .PermitIf(IssueAction.AcceptToWork,IssueState.InProgress, () => isAssignee) .InternalTransitionIf(IssueAction.ChangeAssignee, () => isAssignee || isAdmin); // IssueState.Solve don't be needed to configuring because this state doesn't have transitions. }}
Then create IssueStateflowProvider that inheriting from IStateflowProvider and provides access to configured state machine and issue object:
publicinterfaceIIssueStateflowProvider:IStateflowProvider<Issue,int,IssueState,IssueAction>{}publicclassIssueStateflowProvider:IIssueStateflowProvider{privatereadonlyIDbContext _dbContext; // This one is provided by Stateflow package and will give us an instance of the default state machine implementation.privatereadonlyIStateMachineProvider _stateMachineProvider;publicIssueStateflowProvider(IDbContext dbContext,IStateMachineProvider stateMachineProvider) { _dbContext = dbContext; _stateMachineProvider = stateMachineProvider; }publicasyncTask<Stateflow<Issue,IssueState,IssueAction>> GetStateflowAsync(int id,CancellationToken cancellationToken =default) { // Getting issue from storage.Issue? issue =await_dbContext.Issues.SingleOrDefaultAsync(x =>x.Id== id, cancellationToken); // Getting state machine.IStateMachine<IssueState,IssueAction> stateMachine =await_stateMachineProvider.GetStateMachineAsync<Issue,IssueState,IssueAction>(entity: issue, stateAccessor: x =>x.State, stateMutator: (x, state) =>x.State= state, cancellationToken: cancellationToken); // Creating stateflow instance for given issue.var stateflow =newStateflow<Issue,IssueState,IssueAction>(issue, stateMachine);return stateflow; }}
The last step is to inject IIssueStateflowProvider service and use it:
publicclassIssueService:IIssueService{privatereadonlyIDbContext _dbContext;privatereadonlyIIssueStateflowProvider _stateflowProvider;privatereadonlyICurrentUserService _currentUserService;publicIssueService(IDbContext dbContext,IIssueStateflowProvider stateflowProvider,ICurrentUserService currentUserService) { _dbContext = dbContext; _stateflowProvider = stateflowProvider; _currentUserService = currentUserService; }publicasyncTaskSubmitAsync(int issueId,CancellationToken cancellationToken =default) {Stateflow<Issue,IssueState,IssueAction> stateflow =await_stateflowProvider.GetStateflowAsync(issueId, cancellationToken);Issue issue =stateflow.Entity;IStateMachine<IssueState,IssueAction> stateMachine =stateflow.StateMachine;IssueAction action =IssueAction.Submit;if (stateMachine.CanFire(action) ==false) {thrownewStateflowException("Submit operation is not permitted."); } // Submitting issue while changing its state to "Submitted".stateMachine.Fire(action); // Save changes to database.await_dbContext.SaveChangesAsync(cancellationToken); }publicasyncTaskAcceptToWorkAsync(int issueId,CancellationToken cancellationToken =default) {Stateflow<Issue,IssueState,IssueAction> stateflow =await_stateflowProvider.GetStateflowAsync(issueId, cancellationToken);Issue issue =stateflow.Entity;IStateMachine<IssueState,IssueAction> stateMachine =stateflow.StateMachine;IssueAction action =IssueAction.AcceptToWork;if (stateMachine.CanFire(action) ==false) {thrownewStateflowException("AcceptToWork operation is not permitted."); } // Submitting issue while changing its state to "AcceptToWork".stateMachine.Fire(action); // Set assignee to issue.ICurrentUser currentUser =_currentUserService.CurrentUser;issue.AssigneeId=currentUser.Id; // Save changes to database.await_dbContext.SaveChangesAsync(cancellationToken); }/* Other actions implementations */}
To check available actions for issue on the presentation layer (WebUI for example):