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-genericIUpdateFilter 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.
Generated abstract DTO bases
Working with the rawUpdate 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:
Writing a concrete filter
Subclass the appropriate generated base (from thePolyBot namespace) and override the payload-typed CanPass method:
[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 withActivatorUtilities.CreateInstance if they are not found in the container:
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 callsGetService<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 firstfalse 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):
With* overloads mirror the filter’s constructors:
- Arguments captured from the attribute or
With*call are spliced into the generated code asnew TFilter(name: literal, …). When arguments are present they bypass DI; parameterless usage keeps theGetService<T>() ?? ActivatorUtilitiesresolution, 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 produceCUR044. - 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 intoPolyBot.Attributes. - The
With{FilterName}()extension for inline-await chains intoPolyBot.Attributes.
BotRouter, PolyBotExtensions, etc.) — those are only generated for executable projects. This means filter libraries are fully portable and cause no type collisions when referenced.
PolyBot.Attributes.FixtureFilterAttribute and PolyBot.Attributes.PolyBotAwaiterExtensions are present in the library assembly, while PolyBot.BotRouter and PolyBot.PolyBotExtensions are absent.