Curiosus.TelegramBot 3.0.1

dotnet add package Curiosus.TelegramBot --version 3.0.1
                    
NuGet\Install-Package Curiosus.TelegramBot -Version 3.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="Curiosus.TelegramBot" Version="3.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Curiosus.TelegramBot" Version="3.0.1" />
                    
Directory.Packages.props
<PackageReference Include="Curiosus.TelegramBot" />
                    
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 Curiosus.TelegramBot --version 3.0.1
                    
#r "nuget: Curiosus.TelegramBot, 3.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 Curiosus.TelegramBot@3.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=Curiosus.TelegramBot&version=3.0.1
                    
Install as a Cake Addin
#tool nuget:?package=Curiosus.TelegramBot&version=3.0.1
                    
Install as a Cake Tool

Curiosus.TelegramBot

Infrastructure library for building Telegram bots on .NET: command dispatching, multi-step commands as a state machine, a bounded update queue that survives restarts, chat authentication and HTTP proxy support.

Build License NuGet Downloads Coverage

Renamed: formerly Markeli.TelegramBot. Since 2.0.0 the package is published as Curiosus.TelegramBot by curiosus-dev. To migrate, update the package reference and replace the Markeli.TelegramBot namespace.

Why use it

Telegram.Bot gives you the Bot API; a real bot also needs the plumbing around it. Curiosus.TelegramBot provides it, so a bot is a set of command handlers and nothing else:

  • Commands, not update loops — implement ITelegramBotCommandHandler per command, routing by command text and message type is done for you.
  • Conversations out of the box — return a state and the next message of the chat comes back to the same handler.
  • Safe under load — bounded parallelism, optional per-key locks for commands that must not run concurrently, and pending updates survive a graceful restart when queue persistence is on: the bot drains in-flight work and saves the rest of the queue to disk before it stops.
  • Private bots in one line — allowed chat IDs and a password challenge for everyone else.
  • Zero startup code — the bot runs as an IHostedService registered by AddTelegramBotInfrastructure.

Features

  • Command dispatching — register handlers via ITelegramBotCommandHandler; an update goes to the handler whose CommandText the message starts with as a whole word (/ping, /ping now, /ping@MyBot, but not /pinger), the longest CommandText wins. Handlers also declare the update and message types they accept.
  • Multi-step commands (state machine) — a handler returns a state, and the next message of the chat comes back to the same handler with that state, so a command is a state machine over the conversation: questionnaires, wizards, confirmations. Custom states derive from TelegramBotCommandStateBase; a state lives in memory for an hour after the last step, and sending another /command leaves the flow. See Multi-step commands.
  • Update queue — updates are processed with bounded parallelism (MaxDegreeOfParallelism) and optional per-key locks (TryGetLockKey). With QueuePersistenceFilePath set, pending updates are saved to disk on graceful shutdown and processed after the next start.
  • Authentication — chats from AllowedChatIds are served right away, other chats must send the password first.
  • HTTP proxy — route all Bot API traffic through a proxy with optional credentials (HttpProxy).
  • Built-in /help command — opt-in handler that lists all registered commands via AddHelpCommand().
  • Rich message support — rich formatted messages (Bot API 10.1) are routed like plain text via Update.GetMessageText(), with structured blocks available through Update.GetRichBlocks().
  • Hosting and DI — AddTelegramBotInfrastructure / AddTelegramBotCommandHandler<T> register everything in IServiceCollection, the bot runs as an IHostedService; options are validated at registration time.

How it differs

Many .NET Telegram bot frameworks focus on routing and UI (menus, keyboards). Curiosus.TelegramBot focuses on running a bot reliably as a service:

  • Bounded, lock-aware processing instead of a task per update: a burst of updates can't exhaust the thread pool or the database, and commands that must not run concurrently are serialized by a key you choose.
  • No lost updates on deploy: graceful shutdown waits for in-flight handlers and persists the rest of the queue.
  • No unbounded memory growth: conversation states expire, nothing is kept per update.
  • Private bots without extra code: an allow-list plus a password challenge for everyone else.
  • Plain Telegram.Bot inside: handlers get ITelegramBotClient and Update as they are, nothing to relearn.

Not there yet: inline keyboards and callback queries, persistent conversation state, scoped handlers, middleware and webhooks are planned for 4.0 — see Roadmap.

Quick start

Prerequisites

Installation

dotnet add package Curiosus.TelegramBot

Usage

Register the infrastructure and command handlers in your DI container:

builder.Services.AddTelegramBotInfrastructure(new TelegramBotOptions
{
    ApiToken = "BOT_TOKEN",
    Password = "secret",
    AllowedChatIds = new[] { 123456L }
});

builder.Services.AddTelegramBotCommandHandler<PingCommandHandler>();
builder.Services.AddHelpCommand();

Implement a command handler:

public class PingCommandHandler : ITelegramBotCommandHandler
{
    public string CommandName => "Ping";
    public string CommandText => "/ping";
    public IReadOnlySet<UpdateType> SupportedUpdateTypes => new HashSet<UpdateType> { UpdateType.Message };
    public IReadOnlySet<MessageType> SupportedMessageTypes => TelegramBotMessageTypes.TextOrRich;

    public async Task<TelegramBotCommandProcessingResult> ProcessCommandAsync(
        ITelegramBotClient telegramBotClient, Update telegramUpdate,
        ITelegramBotCommandState? commandState, CancellationToken cancellationToken)
    {
        await telegramBotClient.SendMessage(
            telegramUpdate.Message!.Chat.Id, "pong", cancellationToken: cancellationToken);
        return TelegramBotCommandProcessingResult.WithoutState();
    }
}

The bot starts automatically as an IHostedService — no extra startup code required.

Architecture

Telegram API
    │ polling via Telegram.Bot
    ▼
TelegramBotUpdateDispatcher          (IHostedService — starts polling, runs dispatch loop)
    ├─ on receive ──► TelegramUpdateQueue.Enqueue()
    └─ dispatch loop
         ├─ TelegramUpdateQueue.Take()
         ├─ ResolveCommand()           (state-cache aware routing)
         ├─ TryAcquireLock()           (optional per-key exclusive lock)
         ├─ SemaphoreSlim              (MaxDegreeOfParallelism)
         └─► TelegramUpdateProcessor.ProcessAsync()
              ├─ Auth gate             (AllowedChatIds / password challenge)
              ├─ Message type guard
              ├─ State lookup          (TelegramBotCommandStateCache)
              ├─ ITelegramBotCommandHandler.ProcessCommandAsync()
              └─ State update/remove   (based on result.State)

Updates are polled, enqueued into a thread-safe BlockingCollection<Update>, and dispatched to handlers with configurable concurrency (MaxDegreeOfParallelism, default 10). If a handler returns state, the next message from that chat is routed to the same handler automatically.

Configuration

All settings are passed via TelegramBotOptions:

Property Type Default Description
ApiToken string required Telegram Bot API token.
Password string required Password for chat authentication (see below).
AllowedChatIds long[] [] Pre-authorized chat IDs that skip password verification.
MaxDegreeOfParallelism int 10 Maximum number of updates processed concurrently.
HttpProxy HttpProxyOptions? null HTTP proxy settings. When set, all bot API traffic is routed through this proxy. See below.
QueuePersistenceFilePath string? null File path for persisting pending updates on shutdown. If set, the queue is saved to disk during graceful shutdown and restored on next startup.

HTTP proxy

HttpProxyOptions fields:

Property Type Description
Url string Proxy URL (e.g. http://proxy.example.com:8080). Required.
Username string? Proxy authentication username.
Password string? Proxy authentication password.
services.AddTelegramBotInfrastructure(new TelegramBotOptions
{
    ApiToken = "BOT_TOKEN",
    Password = "secret",
    HttpProxy = new HttpProxyOptions
    {
        Url = "http://proxy.example.com:8080",
        Username = "user",
        Password = "pass"
    }
});

Authentication flow

Chats listed in AllowedChatIds are authorized automatically. When an unknown chat sends a message:

  1. The bot replies with "Hi! To use this bot, please, send a verification password."
  2. If the user sends the correct Password, the chat is added to the allowed set for the lifetime of the process. Authorization is stored in memory only and resets on application restart.
  3. If incorrect, the bot replies "Incorrect password! Please, try again."

Multi-step commands

A multi-step command is a state machine over the conversation: each step reads the current state, answers the user and returns the next state, or no state to finish. Return WithSimpleState() from ProcessCommandAsync to keep the conversation going — the next message from that chat will be routed to the same handler with the previous state:

public class GreetCommandHandler : ITelegramBotCommandHandler
{
    public string CommandName => "Greet";
    public string CommandText => "/greet";
    public IReadOnlySet<UpdateType> SupportedUpdateTypes => new HashSet<UpdateType> { UpdateType.Message };
    public IReadOnlySet<MessageType> SupportedMessageTypes => TelegramBotMessageTypes.TextOrRich;

    public async Task<TelegramBotCommandProcessingResult> ProcessCommandAsync(
        ITelegramBotClient telegramBotClient, Update telegramUpdate,
        ITelegramBotCommandState? commandState, CancellationToken cancellationToken)
    {
        var chatId = telegramUpdate.Message!.Chat.Id;

        if (commandState is null)
        {
            await telegramBotClient.SendMessage(
                chatId, "What is your name?", cancellationToken: cancellationToken);
            return TelegramBotCommandProcessingResult.WithSimpleState();
        }

        var name = telegramUpdate.GetMessageText();
        await telegramBotClient.SendMessage(
            chatId, $"Hello, {name}!", cancellationToken: cancellationToken);
        return TelegramBotCommandProcessingResult.WithoutState();
    }
}

For custom state data, implement ITelegramBotCommandState (or extend TelegramBotCommandStateBase for timestamps) and return it via new TelegramBotCommandProcessingResult { State = myState }.

The user can abort a multi-step flow at any time by sending another /command — it will be matched to the new handler instead.

States are kept per chat in memory and expire an hour after the last step; they don't survive a restart yet (persistent state storage is planned for 4.0).

Rich messages

A rich formatted message (Bot API 10.1) carries its content in Message.RichMessage and leaves Message.Text unset, so it arrives as MessageType.RichMessage rather than MessageType.Text.

Receiving. Declare TelegramBotMessageTypes.TextOrRich in SupportedMessageTypes to accept both, and read the text through Update.GetMessageText(), which falls back to flattening the rich blocks into plain text (blocks joined with newlines, inline formatting dropped). A handler that declares only MessageType.Text and reads Message.Text directly will reject rich messages.

For the structure itself, use Update.GetRichBlocks():

var blocks = telegramUpdate.GetRichBlocks();
if (blocks is not null)
{
    foreach (var table in blocks.OfType<RichBlockTable>())
    {
        // ...
    }
}

Sending. No library API is involved — handlers receive ITelegramBotClient directly and call Telegram.Bot themselves:

await telegramBotClient.SendRichMessage(chatId, new InputRichMessage
{
    Blocks =
    [
        new InputRichBlockSectionHeading { Text = new RichTextText { Text = "Daily report" } },
        new InputRichBlockParagraph { Text = new RichTextText { Text = "All systems nominal." } }
    ]
}, cancellationToken: cancellationToken);

InputRichMessage accepts exactly one of Blocks, Html, or Markdown. Use SendRichMessageDraft to stream a partial message while it is still being generated.

Concurrent lock keys

Override TryGetLockKey to prevent parallel execution of the same command for a specific context (e.g., per chat):

public bool TryGetLockKey(Update telegramUpdate, out string? lockKey)
{
    lockKey = $"my_command_{telegramUpdate.Message?.Chat.Id}";
    return true;
}

When a lock key is active, conflicting updates are re-enqueued and retried. This method has a default implementation that returns false (no locking), so most handlers don't need to override it.

Build

dotnet build
dotnet test

The project uses Cake for build automation, the same pipeline runs locally and on CI:

dotnet tool restore                   # once: Cake and ReportGenerator
dotnet cake                           # Clean + build + tests
dotnet cake --target=CoverageReport   # Tests with coverage + HTML report in ./artifacts/coverage-report/
dotnet cake --target=Pack             # NuGet package in ./artifacts/packages/

Build scripts and settings are shared with the other Curiosus libraries via curiosus-dev/dotnet-tools.

Packages are restored exclusively from nuget.org: the repository-level nuget.config clears any inherited source and maps every package pattern to nuget.org, so restore behaves identically on any machine.

Available packages

Package Version Downloads Coverage
Curiosus.TelegramBot NuGet Downloads Coverage

Roadmap

Version 4.0 is tracked in the v4 milestone: inline keyboards and callback queries, pluggable storage for the update queue, conversation state and authorized chats, configurable state key (chat, user or topic), scoped command handlers, a middleware pipeline, webhooks as a separate package and ready-made controls (date/time pickers, lists, yes/no).

License

MIT

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  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
3.0.1 40 10/3/2026
3.0.0 45 9/29/2026
2.0.0 53 9/28/2026