Skip to main content
Filters are synchronous gates that run before a handler’s body. Implement IUpdateFilter, let the source generator discover your class, and decorate handlers with the generated [XxxFilter] attribute — no manual registration of the attribute itself required.

The IUpdateFilter interface

CanPass is synchronous and receives the raw Update envelope. Return true to allow the handler to run; return false to skip it and continue routing to the next handler.

How filter attributes are generated

The source generator scans every concrete (non-abstract), non-generic IUpdateFilter implementation reachable from your compilation — your project and all referenced assemblies, including nested classes. For each one it emits a wrapper attribute in PolyBot.Attributes with AllowMultiple = true. For example, a class named EchoBot.NonEmptyTextFilter becomes [NonEmptyTextFilter]. You never write these attributes by hand. Once a filter class exists, its attribute is available automatically.
Generic filter classes are skipped and reported as diagnostic CUR012. The generator cannot emit a type-safe attribute wrapper for an open generic — make your filter class non-generic and parameterise it through the constructor or through a DI-resolved service instead.

Generated abstract DTO bases

Working with the raw Update envelope is verbose. The generator emits an abstract base per payload DTO that extracts the payload before calling your override. These bases do not exist until after the first build — the source generator must run to produce them. They live in the PolyBot namespace:
For Telegram.Bot 22.10 the generator produces the following bases:

Writing a concrete filter

Subclass the appropriate generated base (from the PolyBot namespace) and override the payload-typed CanPass method:
The generator emits [NonEmptyTextFilter] automatically. Apply it to any handler:

A filter that always passes or always denies

Parameterless filters like the ones below need no DI registration — the router instantiates them with ActivatorUtilities.CreateInstance if they are not found in the container:
When DenyAllFilter vetoes the handler, routing falls through to the next matching handler normally — it does not throw or stop the pipeline.

Filters with DI dependencies

If your filter has constructor dependencies, register it in the service collection. The router calls GetService<T>() first and only falls back to ActivatorUtilities.CreateInstance when the filter is not registered:

Multiple filters — AND semantics

Stacking multiple filter attributes means every filter must pass, evaluated left-to-right (in attribute declaration order). The first false result short-circuits evaluation and skips the handler:
Evaluation order is attribute declaration order on the method. There is no way to configure evaluation order separately from attribute order.

Parameterized filters

When a filter’s constructor takes attribute-legal parameters (primitives, string, enums, Type, and arrays of those), the generator mirrors its public constructors onto the wrapper attribute and the With* extension. Arguments must be compile-time constants — anything else is a compile error (CUR044):
The same works in await chains — the generated With* overloads mirror the filter’s constructors:
Rules and caveats:
  • Arguments captured from the attribute or With* call are spliced into the generated code as new TFilter(name: literal, …). When arguments are present they bypass DI; parameterless usage keeps the GetService<T>() ?? ActivatorUtilities resolution, so DI-registered instances still win there.
  • Only attribute-legal parameter types are mirrored. Constructors with other required parameters (e.g. a DI service) are not mirrored — register such filters in DI and use the parameterless attribute.
  • Arguments that don’t match any mirrored constructor produce CUR045; non-constant expressions produce CUR044.
  • The await registration key includes only the filter type: two pending awaits using the same filter type with different arguments are treated as the same signature and the registration conflict throws.

Built-in filters (PolyBot.Filters)

PolyBot ships a set of ready-made filters (migrated from the classic Telegrator filter pack). All follow the rules above — parameterized ones accept their constructor arguments in the attribute or With* call: Bot-identity filters (MeRepliedFilter, MentionedFilter) optionally accept PolyBotOptions to compare against BotUsername; when it is not configured (or not resolvable from DI), they evaluate to false rather than throwing.

Runtime resolution

1

Router resolves filter from DI

The generated router calls GetService<DiGateFilter>() (or whichever filter type). If the service is registered, that instance is used.
2

Fallback to ActivatorUtilities

If GetService returns null, the router calls ActivatorUtilities.CreateInstance(provider, typeof(DiGateFilter)). This means parameterless filters and filters whose constructor dependencies are all resolvable from DI work without an explicit AddSingleton / AddTransient call.
3

CanPass is called

The resolved instance’s CanPass(Update update) is invoked. For DTO-based filters, the generated base extracts the payload before calling your protected override bool CanPass(TDto payload).
4

False result skips the handler

If any filter returns false, the handler’s body is not entered. The router continues to the next handler in priority order.

Filters in library projects

Filters can live in a separate class library and be referenced by a PolyBot application. The source generator runs in the library project and emits:
  • The [XxxFilter] attribute wrapper into PolyBot.Attributes.
  • The With{FilterName}() extension for inline-await chains into PolyBot.Attributes.
It does not emit routing infrastructure (BotRouter, PolyBotExtensions, etc.) — those are only generated for executable projects. This means filter libraries are fully portable and cause no type collisions when referenced.
The test suite confirms this behaviour — PolyBot.Attributes.FixtureFilterAttribute and PolyBot.Attributes.PolyBotAwaiterExtensions are present in the library assembly, while PolyBot.BotRouter and PolyBot.PolyBotExtensions are absent.
Organise shared filters into a dedicated library project and reference it from multiple bots. Each bot gets the filter’s generated attribute and await extension without duplicating the filter logic.