{"id":13431493,"url":"https://github.com/dotnet-state-machine/stateless","last_synced_at":"2025-05-16T09:00:20.272Z","repository":{"id":37579893,"uuid":"23001353","full_name":"dotnet-state-machine/stateless","owner":"dotnet-state-machine","description":"A simple library for creating state machines in C# code","archived":false,"fork":false,"pushed_at":"2025-01-01T18:35:20.000Z","size":7981,"stargazers_count":5818,"open_issues_count":71,"forks_count":773,"subscribers_count":226,"default_branch":"dev","last_synced_at":"2025-05-09T08:44:36.432Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dotnet-state-machine.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2014-08-15T19:57:39.000Z","updated_at":"2025-05-08T22:31:41.000Z","dependencies_parsed_at":"2023-02-15T11:46:58.454Z","dependency_job_id":"1c020f05-0e0d-497e-97bd-23de420f7158","html_url":"https://github.com/dotnet-state-machine/stateless","commit_stats":{"total_commits":483,"total_committers":85,"mean_commits":5.682352941176471,"dds":0.8115942028985508,"last_synced_commit":"7223486fbbc975b85e03fed0b762da3a70c5c515"},"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet-state-machine%2Fstateless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet-state-machine%2Fstateless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet-state-machine%2Fstateless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dotnet-state-machine%2Fstateless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dotnet-state-machine","download_url":"https://codeload.github.com/dotnet-state-machine/stateless/tar.gz/refs/heads/dev","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254501547,"owners_count":22081526,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-31T02:01:03.562Z","updated_at":"2025-05-16T09:00:18.532Z","avatar_url":"https://github.com/dotnet-state-machine.png","language":"C#","readme":"# Stateless [![Build status](https://github.com/dotnet-state-machine/stateless/actions/workflows/BuildAndTestOnPullRequests.yml/badge.svg)](https://github.com/dotnet-state-machine/stateless/actions/workflows/BuildAndTestOnPullRequests.yml) [![NuGet Pre Release](https://img.shields.io/nuget/vpre/Stateless.svg)](https://www.nuget.org/packages/stateless) [![Join the chat at https://gitter.im/dotnet-state-machine/stateless](https://badges.gitter.im/dotnet-state-machine/stateless.svg)](https://gitter.im/dotnet-state-machine/stateless?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge) [![Stack Overflow](https://img.shields.io/badge/stackoverflow-tag-orange.svg)](http://stackoverflow.com/questions/tagged/stateless-state-machine)\n\n**Create *state machines* and lightweight *state machine-based workflows* directly in .NET code:**\n\n```csharp\nvar phoneCall = new StateMachine\u003cState, Trigger\u003e(State.OffHook);\n\nphoneCall.Configure(State.OffHook)\n    .Permit(Trigger.CallDialled, State.Ringing);\n\nphoneCall.Configure(State.Connected)\n    .OnEntry(t =\u003e StartCallTimer())\n    .OnExit(t =\u003e StopCallTimer())\n    .InternalTransition(Trigger.MuteMicrophone, t =\u003e OnMute())\n    .InternalTransition(Trigger.UnmuteMicrophone, t =\u003e OnUnmute())\n    .InternalTransition\u003cint\u003e(_setVolumeTrigger, (volume, t) =\u003e OnSetVolume(volume))\n    .Permit(Trigger.LeftMessage, State.OffHook)\n    .Permit(Trigger.PlacedOnHold, State.OnHold);\n\n// ...\n\nphoneCall.Fire(Trigger.CallDialled);\nAssert.AreEqual(State.Ringing, phoneCall.State);\n```\n\nThis project, as well as the example above, was inspired by [Simple State Machine (Archived)](https://web.archive.org/web/20170814020207/http://simplestatemachine.codeplex.com/).\n\n## Features\n\nMost standard state machine constructs are supported:\n\n * Generic support for states and triggers of any .NET type (numbers, strings, enums, etc.)\n * Hierarchical states\n * Entry/exit actions for states\n * Guard clauses to support conditional transitions\n * Introspection\n\nSome useful extensions are also provided:\n\n * Ability to store state externally (for example, in a property tracked by an ORM)\n * Parameterised triggers\n * Reentrant states\n * Export to DOT graph\n * Export to mermaid graph\n\n### Hierarchical States\n\n\nIn the example below, the `OnHold` state is a substate of the `Connected` state. This means that an `OnHold` call is still connected.\n\n```csharp\nphoneCall.Configure(State.OnHold)\n    .SubstateOf(State.Connected)\n    .Permit(Trigger.TakenOffHold, State.Connected)\n    .Permit(Trigger.PhoneHurledAgainstWall, State.PhoneDestroyed);\n```\n\nIn addition to the `StateMachine.State` property, which will report the precise current state, an `IsInState(State)` method is provided. `IsInState(State)` will take substates into account, so that if the example above was in the `OnHold` state, `IsInState(State.Connected)` would also evaluate to `true`.\n\n### Entry/Exit actions\n\nIn the example, the `StartCallTimer()` method will be executed when a call is connected. The `StopCallTimer()` will be executed when call completes (by either hanging up or hurling the phone against the wall.)\n\nThe call can move between the `Connected` and `OnHold` states without the `StartCallTimer()` and `StopCallTimer()` methods being called repeatedly because the `OnHold` state is a substate of the `Connected` state.\n\nEntry/Exit action handlers can be supplied with a parameter of type `Transition` that describes the trigger, source and destination states.\n\n### Internal transitions\n\nSometimes a trigger needs to be handled, but the state shouldn't change. This is an internal transition. Use `InternalTransition` for this.\n\n### Initial state transitions\n\nA substate can be marked as initial state. When the state machine enters the super state it will also automatically enter the substate. This can be configured like this:\n\n```csharp\n    sm.Configure(State.B)\n        .InitialTransition(State.C);\n\n    sm.Configure(State.C)\n        .SubstateOf(State.B);\n```\n\nDue to Stateless' internal structure, it does not know when it is \"started\". This makes it impossible to handle an initial transition in the traditional way. It is possible to work around this limitation by adding a dummy initial state, and then use Activate() to \"start\" the state machine.\n\n```csharp\n    sm.Configure(InitialState)\n        .OnActivate(() =\u003e sm.Fire(LetsGo))\n        .Permit(LetsGo, StateA)\n```\n\n\n### External State Storage\n\nStateless is designed to be embedded in various application models. For example, some ORMs place requirements upon where mapped data may be stored, and UI frameworks often require state to be stored in special \"bindable\" properties. To this end, the `StateMachine` constructor can accept function arguments that will be used to read and write the state values:\n\n```csharp\nvar stateMachine = new StateMachine\u003cState, Trigger\u003e(\n    () =\u003e myState.Value,\n    s =\u003e myState.Value = s);\n```\n\nIn this example the state machine will use the `myState` object for state storage.\n\nAnother example can be found in the JsonExample solution, located in the example folder. \n\n\n### Activation / Deactivation\n\nIt might be necessary to perform some code before storing the object state, and likewise when restoring the object state. Use `Deactivate` and `Activate` for this. Activation should only be called once before normal operation starts, and once before state storage. \n\n### Introspection\n\nThe state machine can provide a list of the triggers that can be successfully fired within the current state via the `StateMachine.PermittedTriggers` property. Use `StateMachine.GetInfo()` to retrieve information about the state configuration.\n\n### Guard Clauses\n\nThe state machine will choose between multiple transitions based on guard clauses, e.g.:\n\n```csharp\nphoneCall.Configure(State.OffHook)\n    .PermitIf(Trigger.CallDialled, State.Ringing, () =\u003e IsValidNumber)\n    .PermitIf(Trigger.CallDialled, State.Beeping, () =\u003e !IsValidNumber);\n```\n\nGuard clauses within a state must be mutually exclusive (multiple guard clauses cannot be valid at the same time.) Substates can override transitions by respecifying them, however substates cannot disallow transitions that are allowed by the superstate.\n\nThe guard clauses will be evaluated whenever a trigger is fired. Guards should therefore be made side effect free.\n\n### Parameterised Triggers\n\nStrongly-typed parameters can be assigned to triggers:\n\n```csharp\nvar assignTrigger = stateMachine.SetTriggerParameters\u003cstring\u003e(Trigger.Assign);\n\nstateMachine.Configure(State.Assigned)\n    .OnEntryFrom(assignTrigger, email =\u003e OnAssigned(email));\n\nstateMachine.Fire(assignTrigger, \"joe@example.com\");\n```\n\nTrigger parameters can be used to dynamically select the destination state using the `PermitDynamic()` configuration method.\n\n### Ignored Transitions and Reentrant States\n\nIn Stateless, firing a trigger that does not have an allowed transition associated with it will cause an exception to be thrown. This ensures that all transitions are explicitly defined, preventing unintended state changes.\n\nTo ignore triggers within certain states, use the `Ignore(TTrigger)` directive:\n\n```csharp\nphoneCall.Configure(State.Connected)\n    .Ignore(Trigger.CallDialled);\n```\n\nAlternatively, a state can be marked reentrant. A reentrant state is one that can transition back into itself. In such cases, the state's exit and entry actions will be executed, providing a way to handle events that require the state to reset or reinitialize.\n\n```csharp\nstateMachine.Configure(State.Assigned)\n    .PermitReentry(Trigger.Assigned)\n    .OnEntry(() =\u003e SendEmailToAssignee());\n```\n\nBy default, triggers must be ignored explicitly. To override Stateless's default behaviour of throwing an exception when an unhandled trigger is fired, configure the state machine using the `OnUnhandledTrigger` method:\n\n```csharp\nstateMachine.OnUnhandledTrigger((state, trigger) =\u003e { });\n```\n\n### Dynamic State Transitions and State Re-entry\n\nDynamic state transitions allow the destination state to be determined at runtime based on trigger parameters or other logic.\n\n```csharp\nstateMachine.Configure(State.Start)\n    .PermitDynamic(Trigger.CheckScore, () =\u003e score \u003c 10 ? State.LowScore : State.HighScore);\n```\n\nWhen a dynamic transition results in the same state as the current state, it effectively becomes a reentrant transition, causing the state's exit and entry actions to execute. This can be useful for scenarios where the state needs to refresh or reset based on certain triggers.\n\n```csharp\nstateMachine.Configure(State.Waiting)\n    .OnEntry(() =\u003e Console.WriteLine($\"Elapsed time: {elapsed} seconds...\"))\n    .PermitDynamic(Trigger.CheckStatus, () =\u003e ready ? State.Done : State.Waiting);\n```\n\n### State change notifications (events)\n\nStateless supports 2 types of state machine events:\n * State transition\n * State machine transition completed\n\n#### State transition\n```csharp\nstateMachine.OnTransitioned((transition) =\u003e { });\n```\nThis event will be invoked every time the state machine changes state.\n\n#### State machine transition completed\n```csharp\nstateMachine.OnTransitionCompleted((transition) =\u003e { });\n```\nThis event will be invoked at the very end of the trigger handling, after the last entry action has been executed.\n\n### Export to DOT graph\n\nIt can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.\n \n```csharp\nphoneCall.Configure(State.OffHook)\n    .PermitIf(Trigger.CallDialled, State.Ringing, IsValidNumber);\n    \nstring graph = UmlDotGraph.Format(phoneCall.GetInfo());\n```\n\nThe `UmlDotGraph.Format()` method returns a string representation of the state machine in the [DOT graph language](https://en.wikipedia.org/wiki/DOT_(graph_description_language)), e.g.:\n\n```dot\ndigraph {\n  OffHook -\u003e Ringing [label=\"CallDialled [IsValidNumber]\"];\n}\n```\n\nThis can then be rendered by tools that support the DOT graph language, such as the [dot command line tool](http://www.graphviz.org/doc/info/command.html) from [graphviz.org](http://www.graphviz.org) or [viz.js](https://github.com/mdaines/viz.js). See http://www.webgraphviz.com for instant gratification.\nCommand line example: `dot -T pdf -o phoneCall.pdf phoneCall.dot` to generate a PDF file.\n\n### Export to Mermaid graph\n\nMermaid graphs can also be generated from state machines.\n \n```csharp\nphoneCall.Configure(State.OffHook)\n    .PermitIf(Trigger.CallDialled, State.Ringing);\n    \nstring graph = MermaidGraph.Format(phoneCall.GetInfo());\n```\n\nThe `MermaidGraph.Format()` method returns a string representation of the state machine in the [Mermaid](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-diagrams#creating-mermaid-diagrams), e.g.:\n\n```\nstateDiagram-v2\n     [*] --\u003e OffHook\n    OffHook --\u003e Ringing : CallDialled\n```\n\nThis can be rendered by GitHub markdown or an engine such as [Obsidian](https://github.com/obsidianmd).\n\n``` mermaid\nstateDiagram-v2\n     [*] --\u003e OffHook\n    OffHook --\u003e Ringing : CallDialled\n```\n\n### Async triggers\n\nOn platforms that provide `Task\u003cT\u003e`, the `StateMachine` supports `async` entry/exit actions and so on:\n\n```csharp\nstateMachine.Configure(State.Assigned)\n    .OnEntryAsync(async () =\u003e await SendEmailToAssignee());\n```\n\nAsynchronous handlers must be registered using the `*Async()` methods in these cases.\n\nTo fire a trigger that invokes asynchronous actions, the `FireAsync()` method must be used:\n\n```csharp\nawait stateMachine.FireAsync(Trigger.Assigned);\n```\n\n**Note:** while `StateMachine` may be used _asynchronously_, it remains single-threaded and may not be used _concurrently_ by multiple threads.\n\n## Advanced Features ##\n\n### Retaining the SynchronizationContext ###\nIn specific situations where all handler methods must be invoked with the consumer's `SynchronizationContext`, set the `RetainSynchronizationContext` property on creation:\n\n```csharp\nvar stateMachine = new StateMachine\u003cState, Trigger\u003e(initialState)\n{\n    RetainSynchronizationContext = true\n};\n```\n\nSetting this is vital within a Microsoft Orleans Grain for example, which requires the `SynchronizationContext` in order to make calls to other Grains.\n\n## Building\n\nStateless runs on .NET runtime version 4+ and practically all modern .NET platforms by targeting .NET Framework 4.6.2, .NET Standard 2.0 and .NET 8.0. Visual Studio 2017 or later is required to build the solution.\n\n\n## Contributing\n\nWe welcome contributions to this project. Check [CONTRIBUTING.md](CONTRIBUTING.md) for more info.\n\n\n## Project Goals\n\nThis page is an almost-complete description of Stateless, and its explicit aim is to remain minimal.\n\nPlease use the issue tracker or the Discussions page if you'd like to report problems or discuss features.\n\n(_Why the name? Stateless implements the set of rules regarding state transitions, but, at least when the delegate version of the constructor is used, doesn't maintain any internal state itself._)\n","funding_links":[],"categories":["Frameworks, Libraries and Tools","C\\#","others","C# #","Uncategorized","Architecture","框架, 库和工具","🗒️ Cheatsheets","State machines","状态机","C#","Identifiers","Application Frameworks"],"sub_categories":["Scheduler and Job","Uncategorized","任务计划","📦 Libraries","GUI - other","State Machine"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotnet-state-machine%2Fstateless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdotnet-state-machine%2Fstateless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdotnet-state-machine%2Fstateless/lists"}