Skip to content

Routing Contract

This page defines the observable routing rules of TeleFlow. It complements the handlers guide.

Selection Order

For a message update, TeleFlow tries command routes before message routes. A message that matches no command can still reach a [Message], [Text], template, or regex handler.

Within each route family, selection is deterministic:

Rule Earlier candidate wins
State A handler for the current state before a stateless handler
Command route kind Exact command, then command template, then command regex
Message route kind Exact text, then text template, then text regex, then a bare message handler
Route specificity The more specific route of the same kind
Final tie Registration order

Filters run after the route shape matches. A filter rejection continues search for another candidate; it is not an error. Typed callback routes run before raw [Callback] fallbacks. Chat-member routes require both update kind and declared transition to match before filters run.

Command Prefixes

CommandPrefixMode.Required accepts configured prefixes only. CommandPrefixMode.Optional tries a configured prefix and then prefix-less command text. CommandPrefixMode.NoPrefix accepts prefix-less text only.

An exact prefix-less command must consume the whole message. Thus [Command("help", PrefixMode = CommandPrefixMode.Optional)] matches help, but not help please. A prefixed exact command may have arguments, so /help please still reaches the help handler. AllowSpaceAfterPrefix controls whitespace after a configured prefix.

Overlapping Prefixes

The longest matching prefix wins, regardless of declaration order:

[Command("confirm", Prefixes = new[] { "!", "!!" })]
public Task Confirm(MessageContext ctx, CancellationToken ct)
{
    return ctx.Message.AnswerAsync("Confirmed.", ct);
}

!!confirm uses !!, even when ! is declared first. Prefixes are normalized once while route metadata is built, not for every update.

Slash Command Mentions

Telegram group commands can include a bot mention, for example /start@my_bot. TeleFlow accepts it only when my_bot is the current bot. /start@another_bot is never treated as /start.

Configure BotUsername when the username is known locally:

builder.Services.AddTelegramBot(options =>
{
    options.Token = token;
    options.BotUsername = "my_bot";
});

For standard AddLongPolling() and AddWebhook() applications, TeleFlow calls getMe once before the transport starts when BotUsername is absent. The username is cached for the process lifetime. Failure prevents the hosted transport from becoming ready. Route selection never performs Telegram I/O.

For a custom/direct update pipeline without a TeleFlow transport, configure BotUsername when mention-qualified commands must work. A bare /start never needs bot identity.

Templates, Regexes, And Text

[TextTemplate] and [CommandTemplate] are anchored by TeleFlow: the entire relevant text or command body must fit the template. Invalid optional or typed values simply make the route not match, allowing the next candidate.

[TextRegex] and [CommandRegex] preserve the application's expression. Add ^ and $ when it must cover the whole input.

Exact command comparisons and command-template matching normalize the configured pattern and incoming command body to Unicode NFC before applying the route's ordinal, culture-independent IgnoreCase setting. This lets canonically equivalent sequences such as composed ё and е plus a combining diaeresis match the same command. String values bound by [CommandTemplate] are NFC.

The original message text exposed through MessageContext is never changed. TeleFlow does not treat the distinct letters е and ё as aliases. Declare both routes explicitly when both spellings should be accepted.

Text routes and regex routes preserve the application's input contract and do not apply implicit Unicode normalization. Normalize explicitly in a regex when that route needs canonical-equivalence behavior.

Registration Parity

Generated registration through AddTelegramHandlersFromAssembly(...) and explicit AddTelegramHandler<T>() use the same runtime route table and selector. Generated registration is the recommended application default; explicit registration remains useful for focused modules and tests.