GrammyNet.Core 1.0.1

dotnet add package GrammyNet.Core --version 1.0.1
                    
NuGet\Install-Package GrammyNet.Core -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GrammyNet.Core" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GrammyNet.Core" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="GrammyNet.Core" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GrammyNet.Core --version 1.0.1
                    
#r "nuget: GrammyNet.Core, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GrammyNet.Core@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GrammyNet.Core&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=GrammyNet.Core&version=1.0.1
                    
Install as a Cake Tool

GrammyNet.Core

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.

Features

  • 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

Installation

Add the package reference to your project:

<PackageReference Include="GrammyNet.Core" Version="1.0.0" />

Quick Start

Option A: Stateless (No State Management)

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

Option B: With State Management

1. Define Your State Classes

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

2. Create Your Update Handler

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

3. Configure Services

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

4. Add Configuration

// appsettings.json
{
  "GrammyNet": {
    "BotToken": "YOUR_BOT_TOKEN_HERE"
  }
}

State Management

User State vs Conversation State

  • User State: Scoped to a Telegram user ID. Persists across all chats.
  • Conversation State: Scoped to a chat ID. Useful for group-specific data.

Accessing State

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

Conversation Flows

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 Routing

Available Attributes

// 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")]

Handler Parameters

Handlers can accept any combination of these parameters (order doesn't matter):

  • ITelegramBotClient - The bot client
  • Update - The full update object
  • Message - The message (for message updates)
  • CallbackQuery - The callback query (for callback updates)
  • StateContext<TUserState, TConversationState> - State context
  • CancellationToken - Cancellation token

Keyboard Builders

Inline Keyboard

var keyboard = InlineKeyboardBuilder.Create()
    .Button("Option 1", "callback_1")
    .Button("Option 2", "callback_2")
    .Row()
    .UrlButton("Visit Website", "https://example.com")
    .Build();

Reply Keyboard

var keyboard = ReplyKeyboardBuilder.Create()
    .Button("Yes").Button("No")
    .Row()
    .ContactButton("Share Contact")
    .LocationButton("Share Location")
    .OneTime()
    .Resize()
    .Placeholder("Choose an option...")
    .Build();

Custom State Storage

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

Middleware

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

Project Structure

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

License

MIT License

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 126 1/19/2026
1.0.0 114 1/19/2026