Skip to main content
PolyBot maps incoming Telegram commands to handler methods declaratively. Decorate any [MessageHandler] method with [Command] to restrict it to messages whose first entity is a bot command. The source generator emits all entity-checking, argument-parsing, and @botname-suffix verification code so you never write routing boilerplate by hand. Aliases can be declared positionally: [Command("start", "begin")] is shorthand for [Command(Aliases = ["start", "begin"])] — both forms are equivalent and can be combined.

The [Command] attribute

Properties at a glance

How command matching works

When Prefix is '/' (the default), PolyBot checks that the message’s first entity is of type BotCommand. If the command text includes an @botname suffix — such as /start@MyBot — the suffix is verified against PolyBotOptions.BotUsername, which is populated automatically by PolyBotClient.RunPollingAsync() via GetMe. A mismatched suffix falls through to the next handler; other handlers (like an echo fallback) continue routing normally.
For non-'/' prefixes, PolyBot matches by string.StartsWith (ordinal-ignore-case) against Message.Text directly — no entity required. A space (' ') or null character ('\0') prefix matches text starting with the alias alone.

Guest messages

Guest updates arrive with the bot’s mention first — @PolyBot /start — and Telegram marks that mention with a Mention entity at offset 0. On a [GuestMessageHandler], [Command] uses that entity to find where the command begins and matches from there, so the example below handles only /start:
The mention itself is not verified by name — Telegram only delivers guest messages for the bot that was mentioned, and matching is entity-driven: text that merely looks like @BotName /cmd without a Mention entity does not match. The @botsuffix check on the command entity itself (/start@MyBot) still applies. Non-'/' prefixes work the same way: the remainder after the mention entity is matched as if it were the whole text. Imperative code can use the same rule via CommandHelper.MatchGuestCommand.
Replies to guest messages never go through SendMessage — answer the guest query with AnswerGuestQuery and the message’s GuestQueryId. The generator reports a CUR046 warning when a [GuestMessageHandler] calls SendMessage, and a CUR047 warning when it answers the same guest query more than once. See the diagnostics reference.

CommandScope values

The Scope property maps to Telegram’s BotCommandScope and controls which chats see the command in the BotFather menu.

Argument extraction

Arguments are the whitespace-separated tokens that follow the command word in the message text. Three attributes let you bind handler parameters to those tokens instead of resolving them from the DI container.
[Arg], [Rest], and [Parse] are only meaningful on [Command]-restricted handlers. Placing them on a handler without [Command] produces warning CUR003 and the parameters are treated as normal DI dependencies.

[Arg] — single token

Binds one whitespace-separated token by position. The token is converted using TryParse (invariant culture for numeric types). Set IsOptional = true to yield the parameter’s default value instead of a parse error when the token is absent.
[Arg] parameter types must be the argument type, Nullable<T>, or object. Any other type produces warning CUR005 and falls back to DI injection.

[Rest] — remainder string

Binds everything that follows the positionally consumed [Arg] tokens as a single trimmed string. At most one [Rest] parameter is allowed per handler; more produce warning CUR006.

[Parse] — regex match

Binds a string parameter to the first match of a regular expression scanned (not anchored) against the remaining text. If there are capturing groups, Groups[1].Value is bound; otherwise the whole match is used. A non-match throws CommandArgsParseException.
[Parse] must be applied to a string parameter (CUR009) and cannot be declared after a [Rest] parameter on the same handler (CUR008).

BotFather auto-sync

Commands with a Description property are automatically collected into PolyBotBotFatherSync and synchronized with BotFather on startup. The generator groups commands by (Scope, LanguageCode) and sends one SetMyCommands call per unique pair.
Commands without a Description still route normally, but are skipped during synchronization — the Bot API rejects empty command descriptions, so pushing one would throw. CUR021 (warning) reminds you to either set a description or mark the command IsHidden = true.
Only '/'-prefixed commands are synchronized; commands with a custom Prefix are always hidden from BotFather regardless of IsHidden.
BotFather alias names must match ^[a-z0-9_]{1,32}$; violations are reported as compile-time error CUR020.

Diagnostics reference

Severity: Warning[Command] is placed on a handler whose update payload is not a Message. The command filter is ignored and the handler routes unconditionally.
Severity: WarningAn argument attribute appears on a handler that has no [Command]. The parameter is treated as a normal DI dependency.
Severity: WarningThe Prefix property is not a compile-time constant. Falls back to '/'.
Severity: WarningThe [Arg] parameter type is not the argument type, Nullable<T>, or object. The parameter is resolved from DI instead.
Severity: WarningMore than one [Rest] parameter is declared on a handler. Extra parameters beyond the first are treated as ordinary DI parameters.
Severity: Warning[Rest] is applied to a parameter whose type is not string. The parameter falls back to DI.
Severity: WarningA [Parse] parameter is declared after a [Rest] parameter. The [Parse] parameter falls back to DI since [Rest] absorbs all remaining text.
Severity: Warning[Parse] is applied to a parameter whose type is not string. Falls back to DI.
Severity: ErrorA command alias does not match ^[a-z0-9_]{1,32}$ and cannot be registered with BotFather.
Severity: WarningA '/'-prefixed command without IsHidden = true has no Description. The command is excluded from BotFather sync.
Severity: ErrorThe Description string exceeds BotFather’s 256-character limit.
Severity: WarningTwo commands with the same alias target the same (Scope, LanguageCode) pair. The second registration is discarded.
Severity: WarningAn alias starts with the same character as the command Prefix (e.g. Aliases = ["/start"] with the default / prefix), so the matcher looks for //start and the command can never match. Drop the leading prefix from the alias.
Severity: WarningA [GuestMessageHandler] calls SendMessage. Guest messages must be answered with AnswerGuestQuery using the message’s GuestQueryId.
Severity: WarningControl-flow analysis found a path where AnswerGuestQuery executes twice on one update (sequential calls, two calls in one loop body, or one before and one inside a loop). Mutually exclusive if/else branches with one call each are not flagged.