A .NET library for building Telegram bots with built-in state management, command routing, and middleware support. Designed for use in ASP.NET Core and other hosted applications.
- State Management: User and conversation-scoped state with pluggable storage
- Command Routing: Attribute-based command handlers
- Conversation Flows: Multi-step conversation support with step tracking
- Middleware Pipeline: Extensible middleware for logging, authentication, etc.
- Keyboard Builders: Fluent API for inline and reply keyboards
- Dependency Injection: Full integration with Microsoft.Extensions.DependencyInjection
Add the package reference to your project:
<PackageReference Include="GrammyNet.Core" Version="1.0.0" />If you don't need state management but want attribute-based routing:
using GrammyNet.Core.Commands;
using GrammyNet.Core.Handlers;
using GrammyNet.Core.Keyboard;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;
public class MyBotHandler : BaseStatelessUpdateHandler
{
public MyBotHandler(ILogger<MyBotHandler> logger) : base(logger) { }
[Command("start", Description = "Start the bot")]
public async Task HandleStart(ITelegramBotClient bot, Message message, CancellationToken ct)
{
var keyboard = InlineKeyboardBuilder.Create()
.Button("Help", "help")
.Button("About", "about")
.Build();
await bot.SendMessage(
message.Chat.Id,
"Welcome! Choose an option:",
replyMarkup: keyboard,
cancellationToken: ct);
}
[Command("help", Description = "Show help")]
public async Task HandleHelp(ITelegramBotClient bot, Message message, CancellationToken ct)
{
await bot.SendMessage(message.Chat.Id, "Available commands:\n/start\n/help", cancellationToken: ct);
}
[CallbackQuery("help")]
public async Task HandleHelpCallback(ITelegramBotClient bot, Update update, CancellationToken ct)
{
await bot.AnswerCallbackQuery(update.CallbackQuery!.Id);
await bot.SendMessage(update.CallbackQuery.Message!.Chat.Id, "Help info here!", cancellationToken: ct);
}
[CallbackQuery("about*")]
public async Task HandleAboutCallback(ITelegramBotClient bot, Update update, CancellationToken ct)
{
// Matches: about, about_v1, about_details, etc.
await bot.AnswerCallbackQuery(update.CallbackQuery!.Id, "GrammyNet Bot v1.0");
}
[TextMessage]
public async Task HandleText(ITelegramBotClient bot, Message message, CancellationToken ct)
{
await bot.SendMessage(message.Chat.Id, $"You said: {message.Text}", cancellationToken: ct);
}
}Configure services (Program.cs):
using GrammyNet.Core.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Stateless handler (inherits BaseStatelessUpdateHandler)
builder.Services.AddGrammyNet<MyBotHandler>(builder.Configuration);
var app = builder.Build();
app.Run();using GrammyNet.Core.State;
// User-scoped state (persists per user across all chats)
public class UserState
{
public string? Name { get; set; }
public string? Language { get; set; }
public int MessageCount { get; set; }
}
// Conversation-scoped state (persists per chat)
public class ChatState : ConversationFlowState
{
public DateTime? LastActivity { get; set; }
public List<string> RecentTopics { get; set; } = new();
}using GrammyNet.Core.Commands;
using GrammyNet.Core.Handlers;
using GrammyNet.Core.State;
using GrammyNet.Core.Keyboard;
using Microsoft.Extensions.Logging;
using Telegram.Bot;
using Telegram.Bot.Types;
public class MyBotHandler : BaseUpdateHandler<UserState, ChatState>
{
public MyBotHandler(ILogger<MyBotHandler> logger) : base(logger) { }
[Command("start", Description = "Start the bot")]
public async Task HandleStart(
ITelegramBotClient bot,
Message message,
StateContext<UserState, ChatState> state,
CancellationToken ct)
{
var userState = await state.GetUserStateAsync(ct);
userState.MessageCount++;
var keyboard = InlineKeyboardBuilder.Create()
.Button("Get Help", "help")
.Button("Settings", "settings")
.Row()
.Button("About", "about")
.Build();
await bot.SendMessage(
message.Chat.Id,
$"Welcome! You've sent {userState.MessageCount} messages.",
replyMarkup: keyboard,
cancellationToken: ct);
}
[Command("register", Description = "Start registration")]
public async Task HandleRegister(
ITelegramBotClient bot,
Message message,
StateContext<UserState, ChatState> state,
CancellationToken ct)
{
var chatState = await state.GetConversationStateAsync(ct);
chatState.StartFlow("register_name");
await bot.SendMessage(
message.Chat.Id,
"Let's get you registered! What's your name?",
cancellationToken: ct);
}
[ConversationStep("register_name")]
public async Task HandleRegisterName(
ITelegramBotClient bot,
Message message,
StateContext<UserState, ChatState> state,
CancellationToken ct)
{
var userState = await state.GetUserStateAsync(ct);
var chatState = await state.GetConversationStateAsync(ct);
userState.Name = message.Text;
chatState.GoToStep("register_language");
var keyboard = ReplyKeyboardBuilder.Create()
.Button("English").Button("Spanish")
.Row()
.Button("French").Button("German")
.OneTime()
.Build();
await bot.SendMessage(
message.Chat.Id,
$"Nice to meet you, {userState.Name}! What language do you prefer?",
replyMarkup: keyboard,
cancellationToken: ct);
}
[ConversationStep("register_language")]
public async Task HandleRegisterLanguage(
ITelegramBotClient bot,
Message message,
StateContext<UserState, ChatState> state,
CancellationToken ct)
{
var userState = await state.GetUserStateAsync(ct);
var chatState = await state.GetConversationStateAsync(ct);
userState.Language = message.Text;
chatState.EndFlow();
await bot.SendMessage(
message.Chat.Id,
$"Registration complete!\nName: {userState.Name}\nLanguage: {userState.Language}",
replyMarkup: new Telegram.Bot.Types.ReplyMarkups.ReplyKeyboardRemove(),
cancellationToken: ct);
}
[CallbackQuery("help")]
public async Task HandleHelpCallback(
ITelegramBotClient bot,
Update update,
CancellationToken ct)
{
await bot.AnswerCallbackQuery(update.CallbackQuery!.Id, "Help is on the way!");
await bot.SendMessage(
update.CallbackQuery.Message!.Chat.Id,
"Here's how to use this bot:\n/start - Start\n/register - Register",
cancellationToken: ct);
}
[CallbackQuery("settings*")]
public async Task HandleSettingsCallback(
ITelegramBotClient bot,
Update update,
CancellationToken ct)
{
// Matches: settings, settings_theme, settings_notifications, etc.
await bot.AnswerCallbackQuery(update.CallbackQuery!.Id);
await bot.SendMessage(
update.CallbackQuery.Message!.Chat.Id,
"Settings menu coming soon!",
cancellationToken: ct);
}
}// Program.cs or Startup.cs
using GrammyNet.Core.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Stateful handler (inherits BaseUpdateHandler<TUserState, TConversationState>)
builder.Services.AddGrammyNet<UserState, ChatState, MyBotHandler>(builder.Configuration);
var app = builder.Build();
app.Run();// appsettings.json
{
"GrammyNet": {
"BotToken": "YOUR_BOT_TOKEN_HERE"
}
}- User State: Scoped to a Telegram user ID. Persists across all chats.
- Conversation State: Scoped to a chat ID. Useful for group-specific data.
public async Task SomeHandler(StateContext<UserState, ChatState> state, CancellationToken ct)
{
// Get states (lazy loaded)
var userState = await state.GetUserStateAsync(ct);
var chatState = await state.GetConversationStateAsync(ct);
// Modify state
userState.MessageCount++;
// State is automatically saved after handler completes
// Or save manually:
await state.SaveUserStateAsync(ct);
// Clear state
await state.ClearUserStateAsync(ct);
}Use ConversationFlowState for multi-step conversations:
public class ChatState : ConversationFlowState
{
// Your additional properties
}
// Start a flow
chatState.StartFlow("step1");
// Move to next step
chatState.GoToStep("step2");
// Store temporary data
chatState.SetData("email", "user@example.com");
var email = chatState.GetData<string>("email");
// End the flow
chatState.EndFlow();
// Check if flow is active
if (chatState.IsFlowActive) { }// Command handler (e.g., /start, /help)
[Command("start", Description = "Start the bot")]
// Callback query handler
[CallbackQuery("action")] // Exact match
[CallbackQuery("prefix*")] // Prefix match
[CallbackQuery("*")] // Match all
// Text message handler
[TextMessage] // All text messages
[TextMessage(Pattern = @"\d+")] // Regex pattern
// Conversation step handler
[ConversationStep("step_name")]Handlers can accept any combination of these parameters (order doesn't matter):
ITelegramBotClient- The bot clientUpdate- The full update objectMessage- The message (for message updates)CallbackQuery- The callback query (for callback updates)StateContext<TUserState, TConversationState>- State contextCancellationToken- Cancellation token
var keyboard = InlineKeyboardBuilder.Create()
.Button("Option 1", "callback_1")
.Button("Option 2", "callback_2")
.Row()
.UrlButton("Visit Website", "https://example.com")
.Build();var keyboard = ReplyKeyboardBuilder.Create()
.Button("Yes").Button("No")
.Row()
.ContactButton("Share Contact")
.LocationButton("Share Location")
.OneTime()
.Resize()
.Placeholder("Choose an option...")
.Build();Implement IStateStorage<T> for custom storage (Redis, database, etc.):
public class RedisStateStorage<TState> : IStateStorage<TState>
where TState : class, new()
{
public Task<TState> GetStateAsync(string key, CancellationToken ct) { }
public Task SetStateAsync(string key, TState state, CancellationToken ct) { }
public Task DeleteStateAsync(string key, CancellationToken ct) { }
public Task<bool> ExistsAsync(string key, CancellationToken ct) { }
}
// Register with custom storage
services.AddGrammyNetWithCustomStorage<
UserState,
ChatState,
MyBotHandler,
RedisStateStorage<UserState>,
RedisStateStorage<ChatState>>(configuration);Create custom middleware for cross-cutting concerns:
public class AuthMiddleware<TUserState, TConversationState>
: IMiddleware<TUserState, TConversationState>
where TUserState : class, new()
where TConversationState : class, new()
{
public async Task InvokeAsync(
ITelegramBotClient botClient,
Update update,
StateContext<TUserState, TConversationState> stateContext,
UpdateDelegate next,
CancellationToken cancellationToken)
{
// Pre-processing
if (!IsAuthorized(stateContext.UserId))
{
await botClient.SendMessage(stateContext.ChatId, "Unauthorized");
return; // Don't call next()
}
// Call next middleware
await next();
// Post-processing
}
}GrammyNet.Core/
├── Commands/
│ ├── CommandAttribute.cs # Handler attributes
│ └── CommandRouter.cs # Attribute-based routing
├── Extensions/
│ ├── ServiceCollectionExtension.cs
│ └── TelegramBotClientExtensions.cs
├── Handlers/
│ ├── BaseUpdateHandler.cs # Base class for handlers
│ ├── IUpdateHandler.cs # Stateful handler interface
│ └── StatefulUpdateHandlerAdapter.cs
├── Keyboard/
│ └── InlineKeyboardBuilder.cs # Keyboard builders
├── Middleware/
│ ├── IMiddleware.cs
│ ├── LoggingMiddleware.cs
│ └── MiddlewarePipeline.cs
├── Services/
│ └── StatefulBotBackgroundService.cs
├── State/
│ ├── ConversationStep.cs # Flow state base
│ ├── InMemoryStateStorage.cs # Default storage
│ ├── IStateManager.cs
│ ├── IStateStorage.cs
│ └── StateContext.cs
└── README.md
MIT License