PageSourceSearch

https://rcommon.com/assets/js/8e15bf8a.5a4f8052.js

js rcommon.com collected 2026-10-07 11:32:42 UTC 15,229 bytes, 1 lines download raw bytes

1"use strict";(globalThis.webpackChunkrcommon_website=globalThis.webpackChunkrcommon_website||[]).push([[23297],{39804(e,t,n){n.r(t),n.d(t,{assets:()=>o,contentTitle:()=>c,default:()=>g,frontMatter:()=>s,metadata:()=>d,toc:()=>l});var r=n(86070),i=n(81753),a=n(43292);const s={title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},c="Stateless",d={id:"state-machines/stateless",title:"Stateless",description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions.",source:"@site/versioned_docs/version-3.2.0/state-machines/stateless.mdx",sourceDirName:"state-machines",slug:"/state-machines/stateless",permalink:"/docs/3.2.0/state-machines/stateless",draft:!1,unlisted:!1,editUrl:"https://github.com/RCommon-Team/RCommon/tree/main/website/versioned_docs/version-3.2.0/state-machines/stateless.mdx",tags:[],version:"3.2.0",sidebarPosition:2,frontMatter:{title:"Stateless",sidebar_position:2,description:"RCommon.Stateless wraps the Stateless library behind IStateMachineConfigurator, supporting guarded transitions, parameterized triggers, and async entry/exit actions."},sidebar:"docsSidebar",previous:{title:"Overview",permalink:"/docs/3.2.0/state-machines/overview"},next:{title:"Caching",permalink:"/docs/3.2.0/category/caching"}},o={},l=[{value:"Installation",id:"installation",level:2},{value:"Configuration",id:"configuration",level:2},{value:"Defining states and triggers",id:"defining-states-and-triggers",level:2},{value:"Configuring a state machine",id:"configuring-a-state-machine",level:2},{value:"How deferred configuration works",id:"how-deferred-configuration-works",level:2},{value:"Guarded transitions",id:"guarded-transitions",level:2},{value:"Firing triggers",id:"firing-triggers",level:2}
1,{value:"Parameterized triggers",id:"parameterized-triggers",level:2},{value:"Entry and exit actions",id:"entry-and-exit-actions",level:2},{value:"Checking permitted triggers",id:"checking-permitted-triggers",level:2},{value:"API summary",id:"api-summary",level:2}];function h(e){const t={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,i.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"stateless",children:"Stateless"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"RCommon.Stateless"})," wraps the popular ",(0,r.jsx)(t.a,{href:"https://github.com/dotnet-state-machine/stateless",children:"Stateless"})," library behind the RCommon ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<TState, TTrigger>"})," abstraction. Application code works only with the RCommon interfaces; the Stateless library is an implementation detail."]}),"\n",(0,r.jsx)(t.h2,{id:"installation",children:"Installation"}),"\n",(0,r.jsx)(a.A,{packageName:"RCommon.Stateless"}),"\n",(0,r.jsx)(t.h2,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsxs)(t.p,{children:["Register the Stateless adapter with ",(0,r.jsx)(t.code,{children:"WithStatelessStateMachine"})," on the RCommon builder:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"using RCommon;\n\nbuilder.Services.AddRCommon()\n    .WithStatelessStateMachine();\n"})}),"\n",(0,r.jsxs)(t.p,{children:["This registers ",(0,r.jsx)(t.code,{children:"StatelessConfigurator<TState, TTrigger>"})," as the open-generic ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<TState, TTrigger>"})," implementation. Any combination of state and trigger enum types is automatically available for injection."]}),"\n",(0,r.jsx)(t.admonition,{title:"Runnable example",type:"tip",children:(0,r.jsxs)(t.p,{children:["See ",(0,r.jsx)(t.code,{children:"Examples/StateMachines/Examples.StateMachines.Stateless/"})," for a complete console app exercising guarded transitions, entry/exit actions, and parameterized triggers using this exact order-state-machine walkthrough."]})}),"\n",(0,r.jsx)(t.admonition,{title:"Modular composition",type:"tip",children:(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"WithStatelessStateMachine()"})," is ",(0,r.jsx)(t.code,{children:"TryAdd"}),"-hardened \u2014 repeat calls from multiple modules are idempotent. The open-generic ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<,>"})," registration is added exactly once regardless of how many modules invoke this verb. Modules can opt into state-machine support independently without coordinating. See ",(0,r.jsx)(t.a,{href:"/docs/3.2.0/core-concepts/modular-composition",children:"Modular Composition"})," for the full conflict matrix."]})}),"\n",(0,r.jsx)(t.h2,{id:"defining-states-and-triggers",children:"Defining states and triggers"}),"\n",(0,r.jsxs)(t.p,{children:["States and triggers are plain ",(0,r.jsx)(t.code,{children:"struct"})," enums:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"public enum OrderState\n{\n    Pending,\n    Approved,\n    Shipped,\n    Cancelled\n}\n\npublic enum OrderTrigger\n{\n    Approve,\n    Ship,\n    Cancel\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"configuring-a-state-machine",children:"Configuring a state machine"}),"\n",(0,r.jsxs)(t.p,{children:["Inject ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<TState, TTrigger>"})," and configure each state using ",(0,r.jsx)(t.code,{children:"ForState"}),". Call ",(0,r.jsx)(t.code,{children:"Build"})," to produce a running machine instance:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'public class OrderStateMachineService\n{\n    private readonly IStateMachineConfigurator<OrderState, OrderTrigger> _configurator;\n\n    public OrderStateMachineService(\n        IStateMachineConfigurator<OrderState, OrderTrigger> configurator)\n    {\n        _configurator = configurator;\n\n        _configurator.ForState(OrderState.Pending)\n            .Permit(OrderTrigger.Approve, OrderState.Approved)\n            .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\n\n        _configurator.ForState(OrderState.Approved)\n            .Permit(OrderTrigger.Ship, OrderState.Shipped)\n            .Permit(OrderTrigger.Cancel, OrderState.Cancelled)\n            .OnEntry(async ct =>\n            {\n                await NotifyWarehouseAsync(ct);\n            })\n            .OnExit(async ct =>\n            {\n                await LogStateChangeAsync("leaving Approved", ct);\n            });\n\n        _configurator.ForState(OrderState.Shipped)\n            .OnEntry(async ct =>\n            {\n                await SendShippingConfirmationAsync(ct);\n            });\n    }\n\n    public IStateMachine<OrderState, OrderTrigger> BuildFor(OrderState currentState)\n        => _configurator.Build(currentState);\n}\n'})}),"\n",(0,r.jsxs)(t.p,{children:["Each call to ",(0,r.jsx)(t.code,{children:"Build"})," produces a fully independent machine instance with its own current state. The configurator can be reused across the lifetime of the application without resetting."]}),"\n",(0,r.jsx)(t.h2,{id:"how-deferred-configuration-works",children:"How deferred configuration works"}),"\n",(0,r.jsxs)(t.p,{children:[(0,r.jsx)(t.code,{children:"StatelessConfigurator
1<TState, TTrigger>"})," uses a deferred pattern internally. Calls to ",(0,r.jsx)(t.code,{children:"ForState"})," record configuration actions rather than applying them immediately. When ",(0,r.jsx)(t.code,{children:"Build"})," is called:"]}),"\n",(0,r.jsxs)(t.ol,{children:["\n",(0,r.jsxs)(t.li,{children:["A new ",(0,r.jsx)(t.code,{children:"Stateless.StateMachine<TState, TTrigger>"})," is created with the given initial state."]}),"\n",(0,r.jsx)(t.li,{children:"All recorded configuration actions are replayed against the new machine."}),"\n",(0,r.jsxs)(t.li,{children:["The machine is wrapped in ",(0,r.jsx)(t.code,{children:"StatelessStateMachine<TState, TTrigger>"})," and returned as ",(0,r.jsx)(t.code,{children:"IStateMachine<TState, TTrigger>"}),"."]}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["This allows one ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"})," instance (registered as transient) to produce many independent machines."]}),"\n",(0,r.jsx)(t.h2,{id:"guarded-transitions",children:"Guarded transitions"}),"\n",(0,r.jsxs)(t.p,{children:["Use ",(0,r.jsx)(t.code,{children:"PermitIf"})," to add a condition to a transition. The guard is evaluated when ",(0,r.jsx)(t.code,{children:"CanFire"})," or ",(0,r.jsx)(t.code,{children:"FireAsync"})," is called:"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\n    .PermitIf(OrderTrigger.Ship, OrderState.Shipped, () => _inventoryService.HasStock())\n    .Permit(OrderTrigger.Cancel, OrderState.Cancelled);\n"})}),"\n",(0,r.jsx)(t.h2,{id:"firing-triggers",children:"Firing triggers"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"var machine = _service.BuildFor(order.State);\n\nif (machine.CanFire(OrderTrigger.Approve))\n{\n    await machine.FireAsync(OrderTrigger.Approve, cancellationToken);\n    order.State = machine.CurrentState;\n    await _repository.UpdateAsync(order, cancellationToken);\n}\n"})}),"\n",(0,r.jsx)(t.h2,{id:"parameterized-triggers",children:"Parameterized triggers"}),"\n",(0,r.jsxs)(t.p,{children:["The Stateless adapter fully supports parameterized triggers. The ",(0,r.jsx)(t.code,{children:"FireAsync<TData>"})," overload passes data to the underlying Stateless machine using ",(0,r.jsx)(t.code,{children:"SetTriggerParameters"}),":"]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'// Fire a trigger and pass data to the entry action\nawait machine.FireAsync(OrderTrigger.Ship, new ShipmentDetails { Carrier = "FedEx" }, cancellationToken);\n'})}),"\n",(0,r.jsxs)(t.p,{children:["The trigger parameter descriptor is cached by ",(0,r.jsx)(t.code,{children:"(trigger, dataType)"})," to prevent double-registration, which Stateless does not allow."]}),"\n",(0,r.jsx)(t.h2,{id:"entry-and-exit-actions",children:"Entry and exit actions"}),"\n",(0,r.jsxs)(t.p,{children:["Entry and exit actions are ",(0,r.jsx)(t.code,{children:"Func<CancellationToken, Task>"}),". Stateless's own ",(0,r.jsx)(t.code,{children:"OnEntryAsync"}),"/",(0,r.jsx)(t.code,{children:"OnExitAsync"})," do not accept a ",(0,r.jsx)(t.code,{children:"CancellationToken"}),"; the adapter passes ",(0,r.jsx)(t.code,{children:"CancellationToken.None"})," internally when forwarding to Stateless. The ",(0,r.jsx)(t.code,{children:"CancellationToken"})," you provide to ",(0,r.jsx)(t.code,{children:"FireAsync"})," is still checked before the trigger fires."]}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:"_configurator.ForState(OrderState.Approved)\n    .OnEntry(async ct =>\n    {\n        // ct is passed from FireAsync; Stateless itself receives CancellationToken.None\n        await _emailService.SendApprovalEmailAsync(ct);\n    });\n"})}),"\n",(0,r.jsx)(t.h2,{id:"checking-permitted-triggers",children:"Checking permitted triggers"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-csharp",children:'var machine = _service.BuildFor(order.State);\n\nforeach (var trigger in machine.PermittedTriggers)\n{\n    Console.WriteLine($"Can fire: {trigger}");\n}\n'})}
1),"\n",(0,r.jsx)(t.h2,{id:"api-summary",children:"API summary"}),"\n",(0,r.jsxs)(t.table,{children:[(0,r.jsx)(t.thead,{children:(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.th,{children:"Type"}),(0,r.jsx)(t.th,{children:"Package"}),(0,r.jsx)(t.th,{children:"Description"})]})}),(0,r.jsxs)(t.tbody,{children:[(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"StatelessConfigurator<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Implements ",(0,r.jsx)(t.code,{children:"IStateMachineConfigurator"}),"; records deferred config and builds machines"]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"StatelessStateMachine<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Wraps ",(0,r.jsx)(t.code,{children:"Stateless.StateMachine<TState, TTrigger>"})," as ",(0,r.jsx)(t.code,{children:"IStateMachine"})]})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"DeferredStateConfigurator<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsx)(t.td,{children:"Internal class that records state configuration for replay at build time"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateMachineConfigurator<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Abstraction; implement to swap the backing engine"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateMachine<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Running machine abstraction"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"IStateConfigurator<TState, TTrigger>"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Core"})}),(0,r.jsx)(t.td,{children:"Per-state transition and action configuration"})]}),(0,r.jsxs)(t.tr,{children:[(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"WithStatelessStateMachine()"})}),(0,r.jsx)(t.td,{children:(0,r.jsx)(t.code,{children:"RCommon.Stateless"})}),(0,r.jsxs)(t.td,{children:["Extension method on ",(0,r.jsx)(t.code,{children:"IRCommonBuilder"})]})]})]})]})]})}function g(e={}){const{wrapper:t}={...(0,i.R)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(h,{...e})}):h(e)}},43292(e,t,n){n.d(t,{A:()=>l});var r=n(30758);const i="container_xjrG",a="label_Y4p8",s="commandRow_FY5I",c="command_m7Qs",d="copyButton_u1GK";var o=n(86070);function l({packageName:e,version:t}){const[n,l]=(0,r.useState)(!1),h=t?`dotnet add package ${e} --version ${t}`:`dotnet add package ${e}`;return(0,o.jsxs)("div",{className:i,children:[(0,o.jsx)("div",{className:a,children:"NuGet Package"}),(0,o.jsxs)("div",{className:s,children:[(0,o.jsx)("code",{className:c,children:h}),(0,o.jsx)("button",{className:d,onClick:()=>{navigator.clipboard.writeText(h),l(!0),setTimeout(()=>l(!1),2e3)},title:"Copy to clipboard",children:n?"\u2713":"\ud83d\udccb"})]})]})}},81753(e,t,n){n.d(t,{R:()=>s,x:()=>c});var r=n(30758);const i={},a=r.createContext(i);function s(e){const t=r.useContext(a);return r.useMemo(function(){return"function"==typeof e?e(t):{...t,...e}},[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),r.createElement(a.Provider,{value:t},e.children)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.