27 KiB
Getting Started with Telegrator
This guide will walk you through the core concepts and advanced features of Telegrator — a modern, aspect-oriented, mediator-based framework for building powerful and maintainable Telegram bots in C#.
- 1. Installation
- 2. Framework Mechanics Overview
- 3. Step-by-Step Tutorials
- 4. Advanced Topics
- 5. FAQ & Best Practices
- 6. Links
1. Installation
Telegrator is distributed as a NuGet package. You can install it using the .NET CLI, the NuGet Package Manager Console, or by managing NuGet packages in Visual Studio.
Prerequisites
- .NET >= 5.0
or.NET Core >= 2.0orFramework >= 4.6.1 (.NET Standart 2.1 compatible) - A Telegram Bot Token from @BotFather.
.NET CLI
dotnet add package Telegrator
Package Manager Console
Install-Package Telegrator
Hosting Integrations
- .NET Core >= 8.0
Telegrator.Hosting: For console/background servicesTelegrator.Hosting.Web: For ASP.NET Core/Webhook (WIP)
2. Framework Mechanics Overview
2.1. Basic Concepts
Telegrator is built around several core ideas:
- Aspect-Oriented Handlers: Each handler is a focused, reusable class that reacts to a specific type of update (message, command, callback, etc.).
- Mediator Pattern: All updates are routed through a central
UpdateRouter, which dispatches them to the appropriate handlers based on filters and priorities. - Filters as Attributes: Handler classes are decorated with filter attributes that declaratively specify when the handler should run.
- State Management: Built-in mechanisms for managing user/chat state without external storage.
- Concurrency Control: Fine-grained control over how many handlers run in parallel, both globally and per-handler.
2.2. Practice: Minimal Bot
Here's how to create a minimal bot that replies to any private message containing "hello":
using Telegrator;
using Telegrator.Handlers;
using Telegrator.Annotations;
using Telegram.Bot.Types;
using Telegram.Bot.Types.Enums;
[MessageHandler]
[ChatType(ChatType.Private)]
[TextContains("hello", StringComparison.InvariantCultureIgnoreCase)]
public class HelloHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("Hello! Nice to meet you!", cancellationToken: cancellation);
return Ok;
}
}
class Program
{
static void Main(string[] args)
{
TelegratorClient bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
bot.Handlers.AddHandler<HelloHandler>();
bot.StartReceiving();
Console.ReadLine();
}
}
How is it working?
[MessageHandler]: This attribute marksHelloHandleras a handler forMessageupdates.[ChatType(ChatType.Private)]: This filter ensures the handler only runs for private chat messages.[TextContains("hello")]: This filter checks if the message contains "hello" (case-insensitive).TelegratorClient: The main bot client that manages the connection to Telegram and the update processing pipeline.bot.Handlers.AddHandler<HelloHandler>(): Registers the handler with the bot.bot.StartReceiving(): Starts the long-polling loop to fetch updates from Telegram.Reply(...): A helper method that sends a reply to the original message.
2.3. Working with Filters
Filters are the gatekeepers of your bot logic. They are applied as attributes to handler classes and determine when a handler should be executed.
Common Filters:
[CommandAlias("start")]— Only for the/startcommand[TextContains("hello")]— Message contains "hello"[ChatType(ChatType.Private)]— Only private chats[FromUserId(123456789)]— Only from a specific user[HasReply]— Only if the message is a reply
Combining Filters: You can combine filters using logical modifiers:
- Multiple filters on a handler, by default, are combined with logical AND
Modifiers = FilterModifier.OrNext- will combine this and next filter with OR logicModifiers = FilterModifier.Not- Inverts the filter- This flags can be combined using bit OR (
Modifiers = FilterModifier.Not | FilterModifier.OrNext)
Example:
[MessageHandler]
[ChatType(ChatType.Private)]
[TextContains("hello", Modifiers = FilterModifier.Not)]
public class NotHelloHandler : MessageHandler
{
// Runs for private messages that do NOT contain "hello"
}
[MessageHandler]
[TextContains("bot", Modifiers = FilterModifier.OrNext)]
[Mentioned()]
public class NotHelloHandler : MessageHandler
{
// Runs for messages that contains "bot" or if bot was mentioned using @
}
How is it working?
- Multiple Filters: The handler has two filters that work together with logical AND.
[ChatType(ChatType.Private)]: Ensures only private chat messages are processed.[TextContains("hello", Modifiers = FilterModifier.Not)]: TheNotmodifier inverts the filter, so it matches messages that do NOT contain "hello".- Combined Logic: The handler will only run for private messages that don't contain "hello".
2.4. State Management
Telegrator provides built-in state management for multi-step conversations (wizards, forms, quizzes) without a database.
Note
Each type of
StateKeeper's (EnumStateKeeper, NumericStateKeeper) is shared beetwen EVERY handler in project.
Types of State:
- NumericState: Integer-based steps
- StringState: Named steps
- EnumState: Enum-based scenarios
How to Use:
- Define your state (enum/int/string)
- Use a state filter attribute on your handler:
[EnumState<MyEnum>(MyEnum.Step1)][NumericState(1)]
- Change state inside the handler using extension methods:
container.ForwardEnumState<MyEnum>()container.ForwardNumericState()container.DeleteEnumState<MyEnum>()
Example:
public enum QuizState
{
Start = SpecialState.NoState, Q1, Q2
}
[CommandHandler]
[CommandAlias("quiz")]
[EnumState<QuizState>(QuizState.Start)]
public class StartQuizHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
container.ForwardEnumState<QuizState>();
await Reply("Quiz started! Question 1: What is the capital of France?");
return Ok;
}
}
[MessageHandler]
[EnumState<QuizState>(QuizState.Q1)]
public class Q1Handler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
if (Input.Text.Trim().Equals("Paris", StringComparison.InvariantCultureIgnoreCase))
await Reply("Correct!");
else
await Reply("Incorrect. The answer is Paris.");
container.ForwardEnumState<QuizState>();
await Reply("Question 2: What is 2 + 2?");
return Ok;
}
}
How is it working?
- Enum State Definition:
QuizStateenum defines the conversation flow withStart = SpecialState.NoStateindicating no initial state.- State Filter:
[EnumState<QuizState>(QuizState.Start)]ensures the handler only runs when the user is in the "Start" state.- State Transition:
container.ForwardEnumState<QuizState>()moves the user to the next state (Q1).- Next Handler: The
Q1Handlerwill only run when the user is in stateQuizState.Q1.- State Management: Each handler manages its own state transition, creating a clear conversation flow.
2.5. Concurrency & Awaiting
Concurrency Control:
- Limit the number of concurrent executions globally using
MaximumParallelWorkingHandlersinTelegramBotOptions
Awaiting Other Updates:
- Use
AwaitingProviderto wait for a user's next update (message or callback) inside a handler:
[CommandHandler]
[CommandAlias("ask")]
public class AskHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("What is your name?");
var nextMessage = await container.AwaitMessage().BySenderId(cancellation);
await Reply($"Hello, {nextMessage.Text}!");
return Ok;
}
}
How is it working?
- Awaiting Provider:
container.AwaitMessage()creates a temporary handler that waits for the next message.- Sender Filter:
.BySenderId(cancellation)ensures only messages from the same user are captured.- Async Flow: The handler pauses execution until the user responds, then continues with the conversation.
- Context Preservation: The original handler context is maintained during the awaiting process.
2.6. Extensibility
You can extend Telegrator by creating custom filters, attributes, and state keepers.
Custom Filter Attribute Example:
using Telegram.Bot.Types;
using Telgrator.Attributes;
using Telegrator.Handlers;
public class AdminOnlyAttribute() : FilterAnnotation<Message>
{
private readonly List<long> _adminIds = [];
public void AddAdmin(long id) => _adminIds.Add(id);
public void RemoveAdmin(long id) => _adminIds.Remove(id);
public override bool CanPass(FilterExecutionContext<Message> context)
=> _adminIds.Contains(context.Input.From?.Id);
}
[MessageHandler]
[AdminOnly]
public class AdminHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("Hello, admin!");
return Ok;
}
}
// ...
AdminOnlyAttribute.AddAdmin(123456789);
bot.StartReceiving();
How is it working?
- Custom Filter:
AdminOnlyAttributeinherits fromFilterAnnotation<Message>to create a reusable filter attribute.- Filter Logic:
CanPass()method checks if the message sender's ID matches the admin ID.- Usage: The filter is applied as an attribute
[AdminOnly]to restrict access users that not registered as admins.
2.7. Integration
Telegrator works in console, hosted applications, --and ASP.NET Core (webhook)-- (WIP) projects.
Console App Example:
var bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
bot.Handlers.CollectHandlersDomainWide();
bot.StartReceiving();
Hosting Example:
using Telegrator.Hosting;
var builder = TelegramBotHost.CreateBuilder();
builder.Handlers.AddHandler<StartHandler>();
var host = builder.Build();
await host.StartAsync();
How is it working?
- Console Integration:
TelegratorClientprovides a simple way to create bots in console applications.- Domain-Wide Collection:
CollectHandlersDomainWide()automatically discovers and registers all handlers in the current assembly.- ASP.NET Core Integration:
TelegramBotHost.CreateBuilder()provides a builder pattern for hosting bots in ASP.NET Core applications.- Dependency Injection: Handlers and their dependencies are automatically registered with the DI container.
3. Step-by-Step Tutorials
3.1. Minimal Bot Creation
3.2. Command Filtering
Message is considered command if is start with '/' and has not null or empty name.
Instead of using the MessageHandler for command (such as /start) you should use CommandHandler.
[CommandHandler]
[CommandAlias("start")]
public class StartHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("Welcome! Use /help to see available commands.");
return Ok;
}
}
[CommandHandler]
[CommandAlias("help")]
public class HelpHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("Available commands:\n/start - Start the bot\n/help - Show this help");
return Ok;
}
}
[MessageHandler]
[TextStartsWith("/", Modifiers = FilterModifier.Not)]
public class EchoHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply($"You said: \"{Input.Text}\"");
return Ok;
}
}
How is it working?
- Command Handlers:
[CommandHandler]and[CommandAlias]work together to handle specific commands like/startand/help.- Echo Handler:
[TextStartsWith("/", Modifiers = FilterModifier.Not)]catches all messages that don't start with "/" (non-commands).- Handler Separation: Each command has its own dedicated handler, making the code modular and maintainable.
- Filter Modifiers: The
Notmodifier inverts the filter logic to exclude command messages.
3.3. State Management Wizard
[CommandHandler]
[CommandAlias("wizard")]
[NumericState(SpecialState.NoState)]
public class StartWizardHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
container.CreateNumericState(); // This code is not necesary, as "Forward" method can automatically creates state if needed, but its recomended to use
container.ForwardNumericState();
await Reply("What is your name?");
return Ok;
}
}
[MessageHandler]
[NumericState(1)]
public class NameHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
container.ForwardNumericState();
await Reply($"Nice to meet you, {Input.Text}! How old are you?");
return Ok;
}
}
[MessageHandler]
[NumericState(2)]
public class AgeHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
if (int.TryParse(Input.Text, out int age))
{
await Reply($"Thank you! You are {age} years old. Wizard completed!");
container.DeleteNumericState();
}
else
{
await Reply("Please enter a valid age (number).");
}
return Ok;
}
}
How is it working?
- Numeric State:
[NumericState(SpecialState.NoState)]starts the wizard when no state exists.- State Creation:
container.CreateNumericState()initializes the numeric state for the user.- State Progression:
container.ForwardNumericState()moves to the next step (1, then 2).- State Cleanup:
container.DeleteNumericState()removes the state when the wizard completes.- Input Validation: The age handler validates numeric input and provides feedback.
3.4. Awaiting CallbackQuery
[CommandHandler]
[CommandAlias("menu")]
public class MenuHandler : CommandHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
var keyboard = new InlineKeyboardMarkup(new[]
{
InlineKeyboardButton.WithCallbackData("Option 1", "option1"),
InlineKeyboardButton.WithCallbackData("Option 2", "option2")
});
await Reply("Choose an option:", replyMarkup: keyboard, cancellationToken: cancellation);
return Ok;
}
}
[CallbackQueryHandler]
[CallbackData("option1")]
public class Option1Handler : CallbackQueryHandler
{
public override async Task<Result> Execute(IHandlerContainer<CallbackQuery> container, CancellationToken cancellation)
{
await AnswerCallbackQuery("You selected Option 1!", cancellationToken: cancellation);
await EditMessageText("You selected Option 1!");
return Ok;
}
}
[CallbackQueryHandler]
[CallbackData("option2")]
public class Option2Handler : CallbackQueryHandler
{
public override async Task<Result> Execute(IHandlerContainer<CallbackQuery> container, CancellationToken cancellation)
{
await AnswerCallbackQuery("You selected Option 2!", cancellationToken: cancellation);
await EditMessageText("You selected Option 2!");
return Ok;
}
}
How is it working?
- Inline Keyboard:
InlineKeyboardMarkupcreates interactive buttons withCallbackDataidentifiers.- CallbackQuery Handlers:
[CallbackQueryHandler]and[CallbackData]work together to handle button clicks.- Response Methods:
AnswerCallbackQuery()provides immediate feedback, whileEditMessageText()updates the message.- Handler Separation: Each button option has its own dedicated handler for clean code organization.
3.5. Adding a Custom Filter
public class PremiumUserAttribute : UpdateFilterAttribute<Message>
{
public override Message? GetFilterringTarget(Update update)
=> update.Message;
public override bool CanPass(FilterExecutionContext<Message> context)
=> context.Input.From?.IsPremium == true;
}
[MessageHandler]
[PremiumUser]
public class PremiumFeatureHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("This feature is only available for premium users!");
return Ok;
}
}
How is it working?
- Custom Filter:
PremiumUserAttributeinherits fromUpdateFilterAttribute<Message>to create a reusable filter.- Premium Check:
context.Input.From?.IsPremium == truechecks if the user has Telegram Premium.- Target Extraction:
GetFilterringTarget()extracts theMessagefrom theUpdateobject.- Usage: The filter is applied as
[PremiumUser]to restrict features to premium users only.
4. Advanced Topics
4.1. Handler Priority
By default, handlers are processed in the order they are added. However, you can control the execution order using the Priority property in the handler attribute. A greater number means higher priority.
[MessageHandler(Priority = 1)] // Runs before default priority (0)
public class HighPriorityHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("This handler runs first!");
return Ok;
}
}
[MessageHandler(Priority = 0)] // Default priority
public class NormalPriorityHandler : MessageHandler
{
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
await Reply("This handler runs second!");
return Ok;
}
}
How is it working?
- Priority System: The
Priorityproperty in handler attributes controls execution order.- Higher Priority First: Handlers with higher priority numbers (1) run before those with lower numbers (0).
- Default Priority: When not specified, handlers have priority 0.
- Execution Order: This ensures critical handlers (like admin commands) run before general handlers.
4.2. Dependency Injection (DI)
Telegrator is designed to work seamlessly with DI containers (e.g., ASP.NET Core). Handlers and their dependencies are automatically registered.
[MessageHandler]
public class MyHandler : MessageHandler
{
private readonly IMyService _myService;
private readonly ILogger<MyHandler> _logger;
public MyHandler(IMyService myService, ILogger<MyHandler> logger)
{
_myService = myService;
_logger = logger;
}
public override async Task<Result> Execute(IHandlerContainer<Message> container, CancellationToken cancellation)
{
_logger.LogInformation("MyHandler executed!");
var result = _myService.DoSomething();
await Reply(result);
return Ok;
}
}
How is it working?
- Constructor Injection: Dependencies (
IMyService,ILogger) are automatically injected by the DI container.- Service Registration: When using
Telegrator.Hosting, services are automatically registered with the DI container.- Handler Instantiation: Telegrator creates handler instances through the DI container, resolving all dependencies.
- Logging Integration: Built-in logging support allows for comprehensive debugging and monitoring.
4.3. Custom State Keepers
You can implement your own state keeper by inheriting from StateKeeperBase<TKey, TState>. This allows for advanced scenarios (e.g., per-message state, custom key resolution).
4.4. Automatic Handler Discovery
Telegrator provides automatic discovery and registration of handlers across your entire application domain using the CollectHandlersDomainWide() method.
How it works:
- Scans all loaded assemblies in the current domain
- Automatically discovers classes decorated with handler attributes
- Registers them with the bot without manual registration
Example:
var bot = new TelegratorClient("<YOUR_BOT_TOKEN>");
bot.Handlers.CollectHandlersDomainWide(); // Automatically finds and registers all handlers
bot.StartReceiving();
Benefits:
- No need to manually register each handler
- Reduces boilerplate code
- Ensures all handlers are discovered automatically
- Perfect for large applications with many handlers
How is it working?
- Domain Scanning:
CollectHandlersDomainWide()scans all assemblies loaded in the current AppDomain.- Reflection Discovery: Uses reflection to find all classes decorated with handler attributes.
- Automatic Registration: Each discovered handler is automatically registered with the
HandlersCollection.- Handler Types: Supports all handler types:
MessageHandler,CommandHandler,CallbackQueryHandler, etc.
4.5. Hosting Integration
Telegrator provides seamless integration with .NET's generic host through the Telegrator.Hosting package, making it easy to build production-ready bot applications.
Installation:
dotnet add package Telegrator.Hosting
Dependencies:
Microsoft.Extensions.Hosting- .NET Generic HostMicrosoft.Extensions.DependencyInjection- Dependency InjectionMicrosoft.Extensions.Configuration- Configuration managementMicrosoft.Extensions.Logging- Logging infrastructure
Core Components:
TelegramBotHost- The main hosted service that manages the bot lifecycleTelegramBotHostBuilder- Builder pattern for configuring the bot hostTelegramBotOptions- Configuration options for the bot
Basic Example:
var builder = TelegramBotHost.CreateBuilder();
// Configure services
builder.Services.AddSingleton<IMyService, MyService>();
// Configure handlers
builder.Handlers.CollectHandlersDomainWide();
// Building host
var host = builder.Build();
await host.Run();
How is it working?
- Generic Host Integration:
TelegramBotHostimplementsIHostand integrates with .NET's generic host.- Lifecycle Management: The host manages the bot's startup, shutdown, and graceful termination.
- Dependency Injection: All handlers and services are automatically registered with the DI container.
- Configuration: Supports standard .NET configuration patterns (appsettings.json, environment variables, etc.).
- Logging: Integrates with .NET's logging infrastructure for comprehensive monitoring.
- Health Checks: Can be integrated with .NET's health check system for production monitoring.
4.6. Error Handling and Logging
You can subscribe to error events or set a custom exception handler:
bot.UpdateRouter.ExceptionHandler = new DefaultRouterExceptionHandler((client, exception, source, cancellationToken) =>
{
Console.WriteLine($"An error occurred: {exception.Message}");
return Task.CompletedTask;
});
How is it working?
- Exception Handler:
ExceptionHandlerproperty allows you to set a custom exception handler for the entire bot.- Error Context: The handler receives the bot client, exception, source information, and cancellation token.
- Global Error Handling: This provides a centralized way to handle all exceptions that occur during update processing.
- Logging Integration: Perfect place to log errors or send notifications to administrators.
4.7. Performance Optimization
- Use appropriate concurrency limits for resource-intensive operations
- Avoid thread-blocking operations in handlers
- Use state management for multi-step processes
- Use
AwaitingProviderfor complex conversation flows
4.8. Best Practices
- Organize handlers, filters, and state keepers in separate folders
- Use feature modules for large bots
- Prefer declarative filters over manual
ifstatements - Keep handlers focused and single-responsibility
5. FAQ & Best Practices
5.1. Q: My handler is not being triggered. What should I do?
- Check handler registration (use
bot.Handlers.AddHandler<MyHandler>()or domain-wide collection) - Check filter attributes and update types
- Enable debug logging
5.2. Q: How can I access the ITelegramBotClient or the original Update object inside a handler?
- Use
Client,Update, andInputproperties in your handler
5.3. Q: How do I handle errors?
- Set a custom exception handler or subscribe to error events
5.4. Q: How can I organize my code for a large bot?
- Use folders, feature modules, and namespaces
- Keep handlers focused and modular
6. Links
Feel free to contribute, ask questions, or open issues!
дыкий сишарп