How it works
InjectIUpdateAwaiter into a handler, call one of the generated WaitFor{UpdateType}Async() extension methods, chain any filter conditions, and call a terminal method that registers the await and returns Task<T?>. The handler suspends at that point; the generated router resumes it when a future update satisfies the declared conditions.
The
Result returned by a suspended-then-resumed handler does not affect routing. Once a handler suspends, the update that triggered the suspension is already considered handled.Builder chain
Entry point — WaitForMessageAsync() and friends
The source generator emits one WaitFor{UpdateType}Async() extension method per Telegram.Bot update type. The most common is WaitForMessageAsync(), which matches any message-shaped payload (Message, EditedMessage, ChannelPost, EditedChannelPost, BusinessMessage, EditedBusinessMessage, GuestMessage).
null on expiry:
TextMatches(pattern) — regex filter
Restricts the await to updates whose message Text (falling back to Caption) matches the regex. Multiple .TextMatches() calls are AND-ed.
With{FilterName}() — generated filter conditions
The generator emits one With{FilterName}() extension method per discovered concrete IUpdateFilter implementation. These are the same filters used with [XxxFilter] handler attributes.
Referencing an unknown filter name produces warning
CUR015. The filter check will not be emitted into the await branch.Where(static lambda) — inline predicate
Splices a static lambda directly into the generated router branch. The predicate is evaluated before the update is claimed by the awaiter.
Where lambdas (violation → CUR016, predicate not emitted):
- Must be declared
static(prevents captures; thestaticmodifier makes accidental captures a compile error) - Must be non-async
- Must be expression-bodied (no block body)
- Body may only reference the lambda parameter and literals — no external variables or members
Terminal methods — ByUserId, ByChatId, ByUserInChat
The terminal method registers the await and determines which identity dimension to key on.
Identity and lifetime
Each await is single-shot: it is removed once claimed or once the timeout expires. Two await sites with different declarative signatures (different payload types,TextMatches patterns, filter types, or Where flag) on the same identity can coexist — the router validates the signature before claiming.
Awaits are keyed by a numeric identity (user/chat id pair probed most-specific-first). The registration’s signature is held in per-site static constant arrays compared by reference and ordinal equality, so a different await site cannot accidentally claim another site’s pending await.
Non-matching updates fall through
While a handler is suspended, updates that do not satisfy the await’s conditions fall through to normal routing. Other handlers continue working for the same user during the wait.Complete multi-step conversation example
1
User sends /onboard
The handler sends a greeting and registers an await keyed by the user’s id, waiting for a text message.
2
User sends their name
The await claims the update, the handler resumes, stores the name, and registers a second await.
3
User sends their city
The second await is claimed, the handler finalizes the onboarding flow and returns.
Diagnostics reference
CUR013 – IUpdateAwaiter injected but no WaitFor* found
CUR013 – IUpdateAwaiter injected but no WaitFor* found
Severity: WarningA handler parameter is typed as
IUpdateAwaiter but the source generator found no WaitFor*Async call on it in the method body. The handler will run to completion inline. If the handler does call WaitFor*Async through a wrapper method, the await will deadlock the sequential poller — call WaitFor*Async directly on the IUpdateAwaiter parameter instead.CUR014 – TextMatches on a non-message await
CUR014 – TextMatches on a non-message await
Severity: Warning
TextMatches is chained onto an await whose target DTO is not message-shaped. The text-match condition can never be satisfied and no await branch is emitted for that site.CUR015 – Unknown filter in await chain
CUR015 – Unknown filter in await chain
Severity: WarningAn await chain references a filter type that could not be resolved during generation. The filter check will not be emitted into the await branch.
CUR016 – Where predicate fails static-lambda constraints
CUR016 – Where predicate fails static-lambda constraints
Severity: WarningA
Where predicate does not satisfy all static-lambda constraints (must be static, non-async, expression-bodied, and reference only the parameter). The predicate is not emitted into the await branch and is never evaluated.