Skip to main content

Managing State

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.

Understanding State Keepers

State keepers maintain state information for users or chats. Telegrator provides three built-in state keeper types:
  • NumericStateKeeper - Tracks integer-based states (0, 1, 2, …)
  • StringStateKeeper - Tracks string-based states (“start”, “waiting_input”, …)
  • EnumStateKeeper - Tracks enum-based states for type safety

Basic State Flow Example

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 state
public 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();
    }
}

State Keeper Attributes

Numeric State

// 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 { /* ... */ }

String State

// 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 { /* ... */ }

Enum State

For type-safe state management, use enums:
public enum OrderState
{
    SelectingProduct,
    EnteringQuantity,
    ConfirmingAddress,
    ProcessingPayment,
    Complete
}

[MessageHandler]
[EnumState(OrderState.SelectingProduct)]
public class ProductSelectionHandler : MessageHandler
{
    public override async Task<Result> Execute(
        IHandlerContainer<Message> container,
        CancellationToken cancellation)
    {
        // Process product selection
        Container.ForwardEnumState<OrderState>();
        
        await Reply("How many would you like?", cancellationToken: cancellation);
        return Result.Ok();
    }
}

State Management Methods

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();
    }
}

Custom State Key Resolvers

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();
    }
}

Creating Custom Key Resolvers

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 { /* ... */ }

Multi-Step Wizard Example

Here’s a complete pizza ordering wizard:
[CommandHandler]
[CommandAllias("order")]
[StringState(SpecialState.NoState)]
public class StartOrderHandler : CommandHandler
{
    public override async Task<Result> Execute(
        IHandlerContainer<Message> container,
        CancellationToken cancellation)
    {
        Container.CreateStringState();
        Container.SetStringState("select_size");
        
        var keyboard = new ReplyKeyboardMarkup(new[]
        {
            new KeyboardButton[] { "Small", "Medium", "Large" }
        })
        {
            ResizeKeyboard = true
        };
        
        await Reply(
            "Welcome to Pizza Bot! Choose your size:",
            replyMarkup: keyboard,
            cancellationToken: cancellation);
        
        return Result.Ok();
    }
}

Cancel Command Pattern

Always provide a way to exit flows:
[CommandHandler]
[CommandAllias("cancel")]
[NumericState(SpecialState.AnyState)] // Works in any state
public 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();
    }
}

State Persistence

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...
}

Best Practices

  1. Always provide a cancel command - Users need a way to exit flows
  2. Validate user input - Don’t advance state if input is invalid
  3. Use enum states for complex flows - Type safety prevents errors
  4. Clear state when flows complete - Don’t leave users in limbo
  5. Handle timeout scenarios - Consider clearing state after inactivity
  6. Store minimal data in ExtraData - Use a database for large data
  7. Document state flow - Create diagrams showing state transitions

Next Steps

Concurrency Control

Learn how to manage concurrent handler execution

Aspect-Oriented Programming

Add cross-cutting concerns with pre and post processors

Build docs developers (and LLMs) love