IStateMachine gives you a typed, update-scoped API to read and write state from handler code.
Core concepts
A state is an enum value associated with a user’s (or chat’s) identity. Two attributes control which states a handler matches:[State<TEnum>(value)]— restricts the handler to updates where the resolved identity currently holds the given enum value.[NoState<TEnum>]— restricts the handler to updates where the resolved identity has no stored state forTEnum. This is the typical entry point into a flow.- Handlers with neither attribute are fully stateless and always execute when reached.
StateKey: choosing the identity dimension
Both[State<TEnum>] and [NoState<TEnum>] accept an optional StateKey parameter that controls which identity from the incoming update is used as the storage key.
Key resolution table
Combining multiple state attributes
Within one handler, attributes sharing the same(TEnum, StateKey) pair are OR-ed — any listed state value matches. Attributes with different pairs are AND-ed — all conditions must hold simultaneously.
The IStateMachine interface
Inject IStateMachine into any handler to read and write state for the update currently being routed. Keys are resolved from the current update automatically.
GetAsync never throws — it returns null when there is no current update, the key is unresolvable, or no state has been stored. SetAsync and ResetAsync throw InvalidOperationException on unresolvable keys to surface silent state loss as a visible failure.
AdvanceAsync and RollbackAsync
For ordered state enums, StateMachineExtensions provides two convenience transitions that walk one step forward or backward through the enum’s declared values.
RegistrationStep { AwaitingName, AwaitingAge, Completed }:
State storage
MemoryStateStorage (default)
AddPolyBotDefaults() registers an in-memory MemoryStateStorage as the IStateStorage singleton. It is keyed by (EnumType, rawKey) pairs, so different enum types never collide for the same identity.
Custom IStateStorage
Implement IStateStorage to back state in Redis, a database, or any other store. Register it before AddPolyBotRouter() so PolyBot’s try-add semantics pick up your implementation instead of the default.
Complete registration flow example
1
User sends /register with no active state
[NoState<RegistrationStep>] matches. The handler sets state to AwaitingName and asks for the user’s name.2
User replies with their name
[State<RegistrationStep>(RegistrationStep.AwaitingName)] matches. The handler saves the name, advances state to AwaitingAge, and asks for their age.3
User replies with their age
[State<RegistrationStep>(RegistrationStep.AwaitingAge)] matches. The handler saves the age, sets state to Completed, and confirms registration.4
Subsequent messages
Neither
[NoState] nor either [State] matches because the state is Completed (and no handler declares that state). Other stateless handlers continue to route normally.Diagnostics reference
CUR010 – [State]/[NoState] type argument is not an enum
CUR010 – [State]/[NoState] type argument is not an enum
Severity: ErrorThe type argument of
[State<TEnum>] or [NoState<TEnum>] is not an enum type. The attribute is ignored and the handler routes unconditionally.CUR011 – [State] and [NoState] declared for the same enum and StateKey
CUR011 – [State] and [NoState] declared for the same enum and StateKey
Severity: WarningA handler declares both
[State<TEnum>] and [NoState<TEnum>] for the same enum type and StateKey. The conditions are AND-ed, which means the handler can never match any update. Remove one of the conflicting attributes.