Skip to main content
The PolyBot source generator produces different output depending on the OutputType of the consuming project. Understanding this distinction lets you share filters and keyboards across multiple bot projects without duplicating logic or causing type collisions.

The OutputType switch

The generator reads the MSBuild OutputType property from the analyzer config:

Library projects

A library project contributes filters and keyboards that other PolyBot bots can consume. It emits:
  • One [XxxFilterAttribute] per discovered concrete IUpdateFilter class
  • One WithXxx() extension per discovered filter
  • Implementations for any partial keyboard methods/properties decorated with [CallbackButton]
No routing infrastructure (BotRouter, AddPolyBotRouter, hosting/webhook integrations) is emitted. This is intentional — if two library projects both emitted BotRouter, a bot that referenced both would get a type collision.

Handlers in libraries → CUR036

Declaring a handler method ([MessageHandler], [CallbackQueryHandler], etc.) in a library project produces CUR036 (warning):
The warning exists because a developer may accidentally put handlers in the wrong project. Move them to the executable.
CUR036 is a warning, not an error, to accommodate incremental refactoring. However, any handler decorated with [XxxHandler] in a library project is silently ignored at runtime — there is no router to call it.

Executable projects

An executable project (Exe or WinExe) emits the full routing infrastructure in addition to filters and keyboards. The output includes:
  • BotRouter — the IUpdateHandler implementation with the hard-coded switch routing
  • PolyBotExtensions.AddPolyBotRouter() — the DI registration extension method
  • PolyBotBotFatherSync — the generated IBotFatherSync implementation
  • Hosting/webhook integrations (when the matching packages are referenced)
All handlers declared in the executable are compiled into the router. Filters defined in the executable itself are also available as attributes and With* extensions, just like in a library.

Cross-assembly filter usage

A handler in an executable can use a filter that is defined (and whose [XxxFilter] attribute is emitted) in a referenced library. The generator resolves the pairing at build time:
When the generator for MyBot.exe encounters [NonEmptyTextFilter] on a handler method and does not find NonEmptyTextFilter in the executable’s own source, it looks in the referenced assembly (MyFilters.dll) for a concrete, non-generic class named NonEmptyTextFilter that implements IUpdateFilter. The fully qualified name from the library is then used in the generated BotRouter code. The same applies to With* extensions in await chains: WithNonEmptyTextFilter() resolves back to global::MyFilters.NonEmptyTextFilter in the emitted router branch. No assembly-wide pre-scan of MyFilters.dll is needed — resolution happens on demand, only for the names that actually appear in handler attributes.

Project structure example

Here is a concrete two-project setup: a shared filter library and a bot executable.

Filter library (MyBot.Filters.csproj)

Bot executable (MyBot.csproj)


Use cases for the library pattern

  • Shared filter libraries — write authentication, rate-check, or content-moderation filters once and reuse them across multiple bot executables.
  • Shared keyboard definitions — [CallbackButton] keyboards emitted in a library are available to any bot that references it.
  • Testing — a TestFilters.dll library can provide test-only filters (e.g. always-pass or always-fail) that are referenced by integration test projects.
Because filter wrapper attributes and With* extensions are generated into the PolyBot.Attributes namespace regardless of which assembly they originate from, handler code in the executable does not need to reference the library namespace explicitly — only the library project needs to be listed as a <ProjectReference>.