State management in Telegrator allows you to track where users are in multi-step conversations. Instead of processing every message the same way, you can maintain state and respond differently based on the conversation context.
Without state management, the bot can’t tell if “John” is a name (step 2) or an email address (step 6). State management solves this by tracking which step the user is on.
using Telegram.Bot.Types;using Telegrator.Handlers;using Telegrator.Annotations;using Telegrator.Annotations.StateKeeping;// Start the registration flow[CommandHandler][CommandAllias("register")]public class StartRegistrationHandler : CommandHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { // Create state for this user Container.CreateNumericState(); // State is now 1 (default) await Reply("Let's register you! What's your name?"); return Result.Ok(); }}// Handle step 1: Name input[MessageHandler][NumericState(1)][HasText]public class NameStepHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { string name = Input.Text; // Save name to database... // Move to next state Container.ForwardNumericState(); // Now in state 2 await Reply($"Nice to meet you, {name}! How old are you?"); return Result.Ok(); }}// Handle step 2: Age input[MessageHandler][NumericState(2)][HasText]public class AgeStepHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { if (!int.TryParse(Input.Text, out int age)) { await Reply("⚠️ Please enter a valid number."); return Result.Ok(); // Stay in state 2 } // Save age to database... // Move to next state Container.ForwardNumericState(); // Now in state 3 await Reply("Great! What's your email address?"); return Result.Ok(); }}// Handle step 3: Email input[MessageHandler][NumericState(3)][HasText]public class EmailStepHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { string email = Input.Text; // Save email to database... // Registration complete, delete state Container.DeleteNumericState(); await Reply("✅ Registration complete! Welcome aboard!"); return Result.Ok(); }}
// Create state (starts at 1 by default)Container.CreateNumericState();// Set specific stateContainer.SetNumericState(5);// Move forward (increment)Container.ForwardNumericState();// Move backward (decrement)Container.BackwardNumericState();// Delete stateContainer.DeleteNumericState();// Access the state keeper directlyNumericStateKeeper keeper = Container.NumericStateKeeper();
The default state is 1, not 0. This makes it more intuitive for step-based flows (“Step 1”, “Step 2”, etc.).
public class NumericStateKeeper : StateKeeperBase<long, int>{ /// <summary> /// Gets the default state value, which is 1. /// </summary> public override int DefaultState => 1; /// <summary> /// Moves the numeric state forward by incrementing. /// </summary> protected override int MoveForward(int currentState, long _) { return currentState + 1; } /// <summary> /// Moves the numeric state backward by decrementing. /// </summary> protected override int MoveBackward(int currentState, long _) { return currentState - 1; }}
// Create state (starts with empty string by default)Container.CreateStringState();// Set specific stateContainer.SetStringState("awaiting_name");// Delete stateContainer.DeleteStringState();// Access the state keeper directlyStringStateKeeper keeper = Container.StringStateKeeper();
String states don’t have Forward and Backward methods like numeric states, since there’s no inherent ordering to strings.
// Create state (starts at first enum value)Container.CreateEnumState<OrderStep>();// Set specific stateContainer.SetEnumState(OrderStep.EnterAddress);// Move to next enum valueContainer.ForwardEnumState<OrderStep>();// Move to previous enum valueContainer.BackwardEnumState<OrderStep>();// Delete stateContainer.DeleteEnumState<OrderStep>();// Access the state keeper directlyEnumStateKeeper<OrderStep> keeper = Container.EnumStateKeeper<OrderStep>();
Enum states provide compile-time safety. You can’t accidentally set an invalid state, and refactoring tools will update state references automatically.
public class EnumStateKeeper<TEnum> : ArrayStateKeeper<long, TEnum> where TEnum : Enum{ /// <summary> /// Gets the default state, which is the first value in the enum. /// </summary> public override TEnum DefaultState => ArrayStates.ElementAt(0);}
using Telegram.Bot.Types;using Telegrator.StateKeeping.Components;public class CustomKeyResolver : IStateKeyResolver<long>{ public long GetKey(Update update) { // Use chat ID for groups, user ID for private chats if (update.Message?.Chat.Type == ChatType.Private) return update.Message.From?.Id ?? 0; else return update.Message?.Chat.Id ?? 0; }}// Use it[MessageHandler][NumericState(1, new CustomKeyResolver())]public class SmartStateHandler : MessageHandler { }
using Telegrator.Annotations.StateKeeping;// Match any state (state exists but value doesn't matter)[MessageHandler][NumericState(SpecialState.Any)]public class AnyStateHandler : MessageHandler { }// Match when no state exists[MessageHandler][NumericState(SpecialState.NotSet)]public class NoStateHandler : MessageHandler { }
// Good: Clean up after completionContainer.DeleteNumericState();await Reply("✅ Registration complete!");// Bad: State persists foreverawait Reply("✅ Registration complete!");// State still exists, causing issues later
Provide Exit Commands
Let users escape a flow:
[MessageHandler][NumericState(SpecialState.Any)][TextEquals("/cancel")]public class CancelFlowHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { Container.DeleteNumericState(); await Reply("❌ Process cancelled."); return Result.Ok(); }}
Use Enums for Complex Flows
Enums provide better maintainability:
// Good: Self-documentingpublic enum CheckoutStep{ SelectProduct, EnterShipping, ConfirmPayment}// Bad: Magic numbers[NumericState(1)] // What does 1 mean?[NumericState(2)] // What does 2 mean?
Validate State Transitions
Add validation for invalid inputs:
[MessageHandler][NumericState(2)]public class AgeInputHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { if (!int.TryParse(Input.Text, out int age)) { await Reply("⚠️ Please enter a valid number."); return Result.Ok(); // Stay in same state } if (age < 0 || age > 150) { await Reply("⚠️ Please enter a realistic age."); return Result.Ok(); // Stay in same state } // Valid input, proceed Container.ForwardNumericState(); return Result.Ok(); }}
Handle State Expiration
Consider implementing state timeout:
// Store state creation timevar stateCreated = DateTime.UtcNow;// Check expirationif ((DateTime.UtcNow - stateCreated).TotalMinutes > 30){ Container.DeleteNumericState(); await Reply("⏰ Session expired. Please start again with /register."); return Result.Ok();}