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 concreteIUpdateFilterclass - One
WithXxx()extension per discovered filter - Implementations for any
partialkeyboard methods/properties decorated with[CallbackButton]
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):
Executable projects
An executable project (Exe or WinExe) emits the full routing infrastructure in addition to filters and keyboards. The output includes:BotRouter— theIUpdateHandlerimplementation with the hard-codedswitchroutingPolyBotExtensions.AddPolyBotRouter()— the DI registration extension methodPolyBotBotFatherSync— the generatedIBotFatherSyncimplementation- Hosting/webhook integrations (when the matching packages are referenced)
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:
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.dlllibrary can provide test-only filters (e.g. always-pass or always-fail) that are referenced by integration test projects.