BotRouter — no reflection, no runtime discovery.
What is a handler?
A handler is any method that:- Carries exactly one
[XxxHandler]attribute from thePolyBot.Attributesnamespace. - Returns
Task<Result>,ValueTask<Result>,Task, orValueTask.
Task or ValueTask (without Result) stops routing after the handler completes — the generator treats the absence of a result value as an implicit Result.StopRouting.
A handler whose return type does not match any of these four forms is skipped and reported as diagnostic
CUR001 (warning). Always use Task<Result>, ValueTask<Result>, Task, or ValueTask. Plain Task/ValueTask stop routing; use Task<Result>/ValueTask<Result> with Result.Continue when you need fall-through behavior.The HandlerAttribute base class
Every generated [XxxHandler] attribute inherits from HandlerAttribute in the PolyBot.Attributes namespace:
The
Priority property follows higher value = earlier execution. A handler with Priority = 0 runs before one with Priority = -1. This is confirmed by DETAILS.md and the test suite, and is the authoritative definition.Generated update-type attributes
The source generator emits one attribute perUpdateType into the PolyBot.Attributes namespace. These attributes do not exist until after the first build — the Roslyn source generator must run to produce them. You never write these by hand; once the PolyBot.SourceGenerators package is referenced and a build has completed, the full set is available. For Telegram.Bot 22.10 the full set includes:
Priority ordering
Handlers for the same update type are sorted byPriority — higher values run first. When two handlers share the same priority (including the default of 0), they keep their source declaration order.
Fall-through routing
Result.Continue tells the router to keep going; Result.StopRouting (or its alias Result.Handled()) ends the routing chain for that update. See Routing Result for a full explanation.
Raw update handlers ([UpdateHandler])
[UpdateHandler] marks a handler that receives the raw Telegram.Bot.Types.Update instead of a typed payload — the method must declare an Update parameter (other parameters are resolved as usual). Use it for catch-all / fallback handlers (logging, telemetry, audit trails), cross-update-type filtering, or update types that have no specialized helper attribute yet.
Types) runs outside the switch (update.Type): with Priority >= 0 it runs before the switch, with negative Priority it runs after it as a fallback for updates no case handled. Result.StopRouting from any of them halts the remaining pipeline, including specialized handlers.
Declare Types to restrict the handler to explicit update types. It is then emitted into each matching case branch and sorted among the specialized handlers by Priority (higher first):
Types contribute exactly those update types, while a universal handler requests all update types (CUR042 warns about this). Guards that need a typed payload — [Command], [Pattern], state conditions — are not supported on [UpdateHandler] (CUR043); combine it with filter attributes or Result returns instead.
Static vs instance handlers
Handlers may be static or instance methods. The generator treats them differently:- Static handlers are called directly with no object allocation.
- Instance handlers are activated per invocation: the generated router calls
GetRequiredService<ContainingType>()on the currentIServiceProviderbefore every invocation. Register the containing class in DI accordingly.