State management is essential for creating multi-step conversations, wizards, and context-aware bot interactions. Telegrator provides a built-in state keeping system that tracks user or chat state without requiring manual state machines.
Here’s a simple registration wizard using numeric states:
1
Define state constants
public static class RegistrationStates{ public const int WaitingName = 1; public const int WaitingAge = 2; public const int WaitingEmail = 3; public const int Complete = 4;}
2
Create initial handler
using Telegrator.Handlers;using Telegrator.Annotations;using Telegrator.Annotations.StateKeeping;[CommandHandler][CommandAllias("register")][NumericState(SpecialState.NoState)] // Only when user has no statepublic class StartRegistrationHandler : CommandHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { // Create state for this user Container.CreateNumericState(); // Move to first state Container.ForwardNumericState(); await Reply( "Welcome! Let's get started. What's your name?", cancellationToken: cancellation); return Result.Ok(); }}
3
Handle state 1: Collect name
[MessageHandler][NumericState(RegistrationStates.WaitingName)]public class CollectNameHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { string name = Input.Text ?? ""; // Store name in extra data ExtraData["userName"] = name; // Move to next state Container.ForwardNumericState(); await Reply( $"Nice to meet you, {name}! How old are you?", cancellationToken: cancellation); return Result.Ok(); }}
4
Handle state 2: Collect age
[MessageHandler][NumericState(RegistrationStates.WaitingAge)]public class CollectAgeHandler : 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 for your age.", cancellationToken: cancellation); return Result.Ok(); } ExtraData["userAge"] = age; Container.ForwardNumericState(); await Reply( "Great! What's your email address?", cancellationToken: cancellation); return Result.Ok(); }}
5
Complete the flow
[MessageHandler][NumericState(RegistrationStates.WaitingEmail)]public class CollectEmailHandler : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { string email = Input.Text ?? ""; // Retrieve stored data string name = ExtraData["userName"] as string ?? ""; int age = (int)(ExtraData["userAge"] ?? 0); // Save to database (pseudo-code) // await SaveUserRegistration(name, age, email); // Delete state - user is done Container.DeleteNumericState(); await Reply( $"Registration complete!\\nName: {name}\\nAge: {age}\\nEmail: {email}", cancellationToken: cancellation); return Result.Ok(); }}
// Match specific numeric state[NumericState(1)]public class State1Handler : MessageHandler { /* ... */ }// Match "no state" (user hasn't started a flow)[NumericState(SpecialState.NoState)]public class NoStateHandler : MessageHandler { /* ... */ }// Match "any state" (user has any state)[NumericState(SpecialState.AnyState)]public class AnyStateHandler : MessageHandler { /* ... */ }
// Match specific string state[StringState("awaiting_input")]public class AwaitingInputHandler : MessageHandler { /* ... */ }// Special states work with string states too[StringState(SpecialState.NoState)]public class NoStringStateHandler : MessageHandler { /* ... */ }
The container provides several methods for managing state:
public class StateManagementExample : MessageHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { // Create a new state (sets to default/0) Container.CreateNumericState(); // Move state forward (increment) Container.ForwardNumericState(); // Move state backward (decrement) Container.BackwardNumericState(); // Set specific state Container.SetNumericState(5); // Get current state int currentState = Container.GetNumericState(); // Check if state exists bool hasState = Container.HasNumericState(); // Delete state Container.DeleteNumericState(); return Result.Ok(); }}
By default, states are tracked per sender (user). You can track state per chat instead:
using Telegrator.StateKeeping;// Track state per chat, not per user[MessageHandler][NumericState(1, typeof(ChatIdResolver))]public class ChatStateHandler : MessageHandler{ // This state is shared by all users in the same chat public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { await Reply("This is a chat-level state", cancellationToken: cancellation); return Result.Ok(); }}
using Telegrator.StateKeeping.Components;using Telegram.Bot.Types;public class GroupAdminResolver : IStateKeyResolver<long>{ public long ResolveKey(Update update) { // Custom logic: use group ID for groups, user ID for private if (update.Message?.Chat.Type is ChatType.Group or ChatType.Supergroup) { return update.Message.Chat.Id; } return update.Message?.From?.Id ?? 0; }}// Usage[MessageHandler][NumericState(1, typeof(GroupAdminResolver))]public class CustomResolverHandler : MessageHandler { /* ... */ }
[CommandHandler][CommandAllias("cancel")][NumericState(SpecialState.AnyState)] // Works in any statepublic class CancelFlowHandler : CommandHandler{ public override async Task<Result> Execute( IHandlerContainer<Message> container, CancellationToken cancellation) { // Clear any active state Container.DeleteNumericState(); Container.DeleteStringState(); await Reply( "Operation cancelled. All data cleared.", replyMarkup: new ReplyKeyboardRemove(), cancellationToken: cancellation); return Result.Ok(); }}
By default, state is stored in memory and will be lost when the bot restarts.
For production bots, implement custom state keepers with persistent storage (database, Redis, etc.).
To implement persistent state, create a custom state keeper:
public class PersistentNumericStateKeeper : StateKeeperBase<long, int>{ private readonly IDatabase _database; public override int DefaultState => 0; public PersistentNumericStateKeeper(IDatabase database) { _database = database; } public override void SetState(Update keySource, int newState) { long key = KeyResolver.ResolveKey(keySource); _database.SaveState(key, newState); } public override int GetState(Update keySource) { long key = KeyResolver.ResolveKey(keySource); return _database.LoadState(key) ?? DefaultState; } // Implement other methods...}