Overview
Handlers are the core building blocks of your Telegram bot. Each handler processes a specific type of update and contains the business logic for responding to user interactions.
Handler Base Classes
Telegrator provides several specialized handler base classes for different update types:
MessageHandler Process text messages, photos, videos, and other message types
CommandHandler Handle bot commands (messages starting with /)
CallbackQueryHandler Respond to inline keyboard button presses
InlineQueryHandler Handle inline queries and chosen inline results
MessageHandler
The MessageHandler is used for processing message updates from users.
Basic Usage
using Telegram . Bot . Types ;
using Telegram . Bot . Types . Enums ;
using Telegrator . Handlers ;
using Telegrator . Annotations ;
[ MessageHandler ]
[ TextContains ( "hello" )]
public class HelloMessageHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
await Reply ( "Hello! How can I help you today?" );
return Result . Ok ();
}
}
Available Methods
The MessageHandler base class provides convenient methods for responding:
Reply Method
Replies directly to the received message:
protected async Task < Message > Reply (
string text ,
ParseMode parseMode = ParseMode . None ,
ReplyMarkup ? replyMarkup = null ,
LinkPreviewOptions ? linkPreviewOptions = null ,
int ? messageThreadId = null ,
IEnumerable < MessageEntity >? entities = null ,
bool disableNotification = false ,
bool protectContent = false ,
string ? messageEffectId = null ,
string ? businessConnectionId = null ,
bool allowPaidBroadcast = false ,
int ? directMessageTopicId = null ,
SuggestedPostParameters ? suggestedPostParameters = null ,
CancellationToken cancellationToken = default )
Use Reply() when you want the message to appear as a direct response to the user’s message (shown as “in reply to” in Telegram).
Response Method
Sends a message to the chat without replying to a specific message:
protected async Task < Message > Response (
string text ,
ParseMode parseMode = ParseMode . None ,
ReplyParameters ? replyParameters = null ,
ReplyMarkup ? replyMarkup = null ,
// ... other parameters
CancellationToken cancellationToken = default )
Example: Echo Bot
[ MessageHandler ]
[ HasText ] // Only messages with text
public class EchoHandler : MessageHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
string receivedText = Input . Text ?? "(no text)" ;
await Reply ( $"You said: { receivedText } " );
return Result . Ok ();
}
}
CommandHandler
The CommandHandler specializes in processing bot commands.
Basic Usage
using Telegrator . Handlers ;
using Telegrator . Annotations ;
[ CommandHandler ]
[ CommandAllias ( "start" , "begin" )]
public class StartCommandHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
await Reply (
"Welcome to the bot! Type /help for available commands." ,
parseMode : ParseMode . Html );
return Result . Ok ();
}
}
Command Properties
The CommandHandler provides access to command-specific data:
[ CommandHandler ]
[ CommandAllias ( "search" )]
public class SearchCommandHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// Get the command name (without /)
string command = ReceivedCommand ; // "search"
// Get all arguments as a string
string argsString = ArgumentsString ; // "cat videos funny"
// Get arguments as an array
string [] args = Arguments ; // ["cat", "videos", "funny"]
if ( args . Length == 0 )
{
await Reply ( "Please provide a search query." );
return Result . Ok ();
}
await Reply ( $"Searching for: { argsString } " );
return Result . Ok ();
}
}
Command Argument Parsing
The framework automatically splits command arguments:
// User sends: /setname John Doe
[ CommandHandler ]
[ CommandAllias ( "setname" )]
public class SetNameHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
// ReceivedCommand = "setname"
// ArgumentsString = "John Doe"
// Arguments = ["John", "Doe"]
if ( Arguments . Length < 2 )
{
await Reply ( "Usage: /setname <firstname> <lastname>" );
return Result . Ok ();
}
string firstName = Arguments [ 0 ]; // "John"
string lastName = Arguments [ 1 ]; // "Doe"
await Reply ( $"Name set to: { firstName } { lastName } " );
return Result . Ok ();
}
}
Arguments are split by spaces. If users need to include spaces in a single argument, you’ll need to implement your own parsing logic or use quotes.
CallbackQueryHandler
Handles button presses from inline keyboards.
Basic Usage
using Telegram . Bot . Types ;
using Telegram . Bot . Types . ReplyMarkups ;
using Telegrator . Handlers ;
using Telegrator . Annotations ;
[ CallbackQueryHandler ]
[ CallbackData ( "btn_confirm" )]
public class ConfirmButtonHandler : CallbackQueryHandler
{
public override async Task < Result > Execute (
IHandlerContainer < CallbackQuery > container ,
CancellationToken cancellation )
{
// Answer the callback query (removes loading state)
await Answer ( "Confirmed!" , showAlert : false );
// Edit the message that contained the button
await EditMessage ( "✅ Confirmed!" );
return Result . Ok ();
}
}
Available Methods
Answer Method
Answers the callback query (required to remove loading state):
protected async Task Answer (
string ? text = null ,
bool showAlert = false ,
string ? url = null ,
int cacheTime = 0 ,
CancellationToken cancellationToken = default )
Always call Answer() in your callback query handlers, even if you don’t want to show a message. Otherwise, the loading indicator will remain visible to the user.
EditMessage Method
Edits the message that triggered the callback:
protected async Task < Message > EditMessage (
string text ,
ParseMode parseMode = ParseMode . None ,
InlineKeyboardMarkup ? replyMarkup = null ,
IEnumerable < MessageEntity >? entities = null ,
LinkPreviewOptions ? linkPreviewOptions = null ,
CancellationToken cancellationToken = default )
Example: Confirmation Dialog
// Handler to send confirmation request
[ MessageHandler ]
[ CommandHandler ]
[ CommandAllias ( "delete" )]
public class DeleteCommandHandler : CommandHandler
{
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
var keyboard = new InlineKeyboardMarkup ( new []
{
new []
{
InlineKeyboardButton . WithCallbackData ( "✅ Confirm" , "delete_confirm" ),
InlineKeyboardButton . WithCallbackData ( "❌ Cancel" , "delete_cancel" )
}
});
await Reply (
"Are you sure you want to delete your account?" ,
replyMarkup : keyboard );
return Result . Ok ();
}
}
// Handler for confirmation button
[ CallbackQueryHandler ]
[ CallbackData ( "delete_confirm" )]
public class DeleteConfirmHandler : CallbackQueryHandler
{
public override async Task < Result > Execute (
IHandlerContainer < CallbackQuery > container ,
CancellationToken cancellation )
{
await Answer ( "Account deleted" , showAlert : true );
await EditMessage ( "✅ Your account has been deleted." );
// Perform actual deletion logic here
return Result . Ok ();
}
}
// Handler for cancel button
[ CallbackQueryHandler ]
[ CallbackData ( "delete_cancel" )]
public class DeleteCancelHandler : CallbackQueryHandler
{
public override async Task < Result > Execute (
IHandlerContainer < CallbackQuery > container ,
CancellationToken cancellation )
{
await Answer ();
await EditMessage ( "❌ Deletion cancelled." );
return Result . Ok ();
}
}
TypeData Property
Access callback data through the TypeData property:
[ CallbackQueryHandler ]
public class GenericCallbackHandler : CallbackQueryHandler
{
public override async Task < Result > Execute (
IHandlerContainer < CallbackQuery > container ,
CancellationToken cancellation )
{
// TypeData contains the callback_data string
string data = TypeData ;
if ( data . StartsWith ( "product_" ))
{
string productId = data . Substring ( 8 );
await Answer ( $"Selected product: { productId } " );
}
return Result . Ok ();
}
}
InlineQueryHandler
Handles inline queries and chosen inline results.
Basic Usage
using Telegram . Bot . Types ;
using Telegram . Bot . Types . InlineQueryResults ;
using Telegrator . Handlers ;
[ InlineQueryHandler ]
public class SearchInlineHandler : InlineQueryHandler
{
public override async Task < Result > Requested (
IHandlerContainer < InlineQuery > container ,
CancellationToken cancellation )
{
string query = InputQuery . Query ;
var results = new List < InlineQueryResult >
{
new InlineQueryResultArticle (
id : "1" ,
title : $"Search for: { query } " ,
inputMessageContent : new InputTextMessageContent ( query ))
};
await Answer ( results , cacheTime : 300 );
return Result . Ok ();
}
public override async Task < Result > Chosen (
IHandlerContainer < ChosenInlineResult > container ,
CancellationToken cancellation )
{
// Handle when user selects an inline result
string resultId = InputChosen . ResultId ;
// Log or track the selection
return Result . Ok ();
}
}
InlineQueryHandler requires implementing two methods: Requested() for handling the query, and Chosen() for when the user selects a result.
Answer Method
protected async Task Answer (
IEnumerable < InlineQueryResult > results ,
int ? cacheTime = null ,
bool isPersonal = false ,
string ? nextOffset = null ,
InlineQueryResultsButton ? button = null ,
CancellationToken cancellationToken = default )
Branching Handlers
Branching handlers allow multiple execution paths within a single handler.
BranchingMessageHandler
public abstract class BranchingMessageHandler : BranchingUpdateHandler < Message >
{
// Similar to MessageHandler but for branching scenarios
}
BranchingCommandHandler
public abstract class BranchingCommandHandler : BranchingMessageHandler
{
// Similar to CommandHandler but for branching scenarios
}
Use branching handlers when you need to implement complex conditional logic or multiple response paths within a single handler class.
Handler Lifecycle
Handlers follow a specific lifecycle managed by the framework:
LifetimeToken
Each handler has a LifetimeToken that tracks its lifecycle:
public HandlerLifetimeToken LifetimeToken { get ; }
Do not reuse handler instances. Each update should be processed by a fresh handler instance to avoid state contamination.
Best Practices
Each handler should do one thing well. If you need complex logic, split it into multiple handlers: // Good: Focused handler
[ CommandHandler ]
[ CommandAllias ( "start" )]
public class StartHandler : CommandHandler { }
// Bad: Handler trying to do too much
[ CommandHandler ]
[ CommandAllias ( "start" , "help" , "about" , "settings" )]
public class MultiPurposeHandler : CommandHandler { }
Inject services through constructors: [ CommandHandler ]
[ CommandAllias ( "stats" )]
public class StatsHandler : CommandHandler
{
private readonly IUserService _userService ;
public StatsHandler ( IUserService userService )
{
_userService = userService ;
}
public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
var stats = await _userService . GetStatsAsync ();
await Reply ( $"Total users: { stats . TotalUsers } " );
return Result . Ok ();
}
}
Use try-catch blocks to handle exceptions: public override async Task < Result > Execute (
IHandlerContainer < Message > container ,
CancellationToken cancellation )
{
try
{
await PerformOperation ();
return Result . Ok ();
}
catch ( Exception ex )
{
await Reply ( "Sorry, an error occurred. Please try again." );
// Log the exception
return Result . Fault ();
}
}
Always Answer Callback Queries
Even if you don’t want to show a message, call Answer(): // Good
await Answer (); // Removes loading indicator
// Bad - loading indicator stays visible
// (no Answer() call)
Filters Learn how to filter updates with attributes
Results Understand handler return values
State Management Track conversation state across updates
Architecture Understand the overall framework design