Skip to content

Handlers And Routing

Handlers are the main user-facing part of TeleFlow. A handler is a normal class with one or more methods decorated by routing attributes.

Command Handler

public sealed class StartHandler
{
    [Command("start")]
    public Task Handle(MessageContext ctx, CancellationToken ct)
    {
        return ctx.Message.AnswerAsync("Welcome.", ct);
    }
}

[Command("start")] matches /start by default. The command prefix can be changed through attribute properties.

Command prefix matching is explicit. The default is CommandPrefixMode.Required, which means the user must send /start or another configured prefix:

[Command("start", Prefixes = new[] { "/", "!" })]
public Task Start(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Welcome.", ct);
}

For slash commands in groups, /start@my_bot matches only the named bot. When BotUsername was not configured, TeleFlow resolves bot identity once before a standard long-polling or webhook transport starts. See the routing contract for mention behavior, overlapping prefixes, and precedence rules.

Use CommandPrefixMode.Optional when the same route should accept both a Telegram command and prefix-less command-like text:

[Command("help", PrefixMode = CommandPrefixMode.Optional)]
public Task Help(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Help.", ct);
}

Use CommandPrefixMode.NoPrefix when you want command routing semantics, but only for prefix-less text:

[Command("help", PrefixMode = CommandPrefixMode.NoPrefix)]
public Task HelpText(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Help.", ct);
}

Prefix-less exact [Command] routes, and [CommandTemplate] routes without placeholders, require the whole text to equal the command name. A short command such as [Command("i", PrefixMode = CommandPrefixMode.Optional)] will match i and /i details, but not the normal sentence I need help.

Use [CommandTemplate] or [CommandRegex] for prefix-less command-like input with arguments. For common natural language words, prefer a required prefix: [CommandTemplate("i {text}", PrefixMode = CommandPrefixMode.Optional)] is supposed to match I need help.

Text Handler

public sealed class MenuHandler
{
    [Text("Settings")]
    public Task Settings(MessageContext ctx, CancellationToken ct)
    {
        return ctx.Message.AnswerAsync("Opening settings.", ct);
    }
}

[Text] supports match modes through TextMatchMode.

[Text("support", TextMatchMode.Contains, ignoreCase: true)]
public Task ContainsSupport(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Support request detected.", ct);
}

Template Routes

Templates are useful when the message has a stable structure:

[CommandTemplate("ticket {id:long}")]
public Task ShowTicket(MessageContext ctx, long id, CancellationToken ct)
{
    return ctx.Message.AnswerAsync($"Ticket #{id}", ct);
}

The template describes the command body, not the prefix. Write [CommandTemplate("ticket {id:long}")], not [CommandTemplate("/ticket {id:long}")].

If you need to support both /ticket 42 and ticket 42, keep one handler and make the prefix optional:

[CommandTemplate(
    "ticket {id:long}",
    PrefixMode = CommandPrefixMode.Optional)]
public Task ShowTicket(MessageContext ctx, long id, CancellationToken ct)
{
    return ctx.Message.AnswerAsync($"Ticket #{id}", ct);
}

This replaces the old workaround where the same method had both [CommandTemplate] and [TextTemplate].

Command names and command-template bodies use Unicode NFC normalization during matching. The raw message text remains unchanged, while string values bound by [CommandTemplate] are NFC. This handles canonically equivalent input without treating different letters such as е and ё as automatic aliases.

Text templates work without a command prefix:

[TextTemplate("order {id:long}")]
public Task ShowOrder(MessageContext ctx, long id, CancellationToken ct)
{
    return ctx.Message.AnswerAsync($"Order #{id}", ct);
}

Regex Routes

Use regex routes when the input shape is not well represented by a template:

[TextRegex(@"^INV-(\d+)$")]
public Task Invoice(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Invoice code received.", ct);
}

Regex routes are powerful, but templates are usually easier to read and maintain. Regex input is not normalized implicitly; the expression owns that behavior.

Media Filters

TeleFlow includes marker attributes for common message content:

public sealed class UploadHandler
{
    [HasPhoto]
    public Task Photo(MessageContext ctx, CancellationToken ct)
    {
        return ctx.Message.AnswerAsync("Photo received.", ct);
    }

    [HasDocument]
    public Task Document(MessageContext ctx, CancellationToken ct)
    {
        return ctx.Message.AnswerAsync("Document received.", ct);
    }
}

Other media filters include audio, voice, video, video note, animation, contact, dice, location, poll, sticker, venue, caption, and message thread markers.

Routes and filters both use C# attribute syntax. Route attributes such as [CommandTemplate] select the handler and bind values. Filter attributes such as [HasPhoto], [ChatType], and [UseFilter<TFilter>] add conditions before invocation. See Filters for the full distinction.

Class-Level Metadata

Routing and filter attributes can be placed on the class when every method should share the same condition:

[ChatType(TelegramChatType.Private)]
public sealed class PrivateChatHandlers
{
    [Command("profile")]
    public Task Profile(MessageContext ctx, CancellationToken ct)
    {
        return ctx.Message.AnswerAsync("Private profile.", ct);
    }
}

Handler Parameters

Handler methods can receive:

  • the current context;
  • route values from templates;
  • typed callback payloads;
  • CancellationToken;
  • services from dependency injection.
public sealed class TicketHandler
{
    [CommandTemplate("ticket {id:long}")]
    public Task Handle(
        MessageContext ctx,
        long id,
        ITicketRepository tickets,
        CancellationToken ct)
    {
        return ctx.Message.AnswerAsync($"Ticket #{id}", ct);
    }
}

Keep method signatures readable. If the handler needs many services, move application logic into a service and inject that service.

Direct Registration

Direct registration is simple and explicit:

builder.Services.AddTelegramHandler<StartHandler>();
builder.Services.AddTelegramModule<AdminHandlers>();

Use it for small apps, tests, or narrow modules. Direct registration registers only the named handler or module type; it does not scan the assembly around it.

Assembly Registration

Generated assembly registration is the recommended default for larger applications:

builder.Services.AddTelegramHandlersFromAssembly(typeof(Program).Assembly);

This requires IWF.TeleFlow.Generators. Missing generated metadata is treated as a startup configuration error.