Tagged registration
Register a subset of an assembly's handlers per host by tagging them and filtering at the call site.
When one assembly of handlers is consumed by more than one host — a web API and a background worker sharing an application project, say — you rarely want every host to register every handler. Tags let you declare which group a handler belongs to and filter at the registration call.
Tagging a handler
[Handler(Tags = ["worker"])]
public static partial class RebuildSearchIndexCommand
{
public sealed record Command;
private static ValueTask HandleAsync(Command command, CancellationToken token) =>
// ..
}A handler may carry several tags:
[Handler(Tags = ["worker", "background"])] Tags is an init-only string[]? property on [Handler], so it combines with the lifetime
constructor argument when you need both:
[Handler(ServiceLifetime.Singleton, Tags = ["worker"])] Filtering at registration
AddXxxHandlers takes the tags after the lifetime. Because the lifetime has a default, pass the tags by
name unless you are also setting a lifetime:
builder.Services.AddApplicationBehaviors();
builder.Services.AddApplicationHandlers(tags: "worker");builder.Services.AddApplicationBehaviors();
builder.Services.AddApplicationHandlers(tags: "web");// several tags, and a non-default lifetime
services.AddApplicationHandlers(ServiceLifetime.Transient, "worker", "web");The tags parameter is params ReadOnlySpan<string> when the project compiles with C# 13 or later, and params string[] on C# 12 and below. Either way you can pass loose arguments, an array, or a collection
expression.
The rules that catch people out
- Calling with no tags registers everything, tagged handlers included. Tags narrow a registration; they never opt a handler out of the default call.
- Untagged handlers are always registered, whatever tags you pass. A handler with no
Tagsis shared infrastructure and belongs to every host. - Matching is ordinal string equality. No wildcards, no prefixes, no case-insensitivity —
"Worker"and"worker"are different tags.
The consequence worth internalising: tagging is opt-in isolation for the host, not for the handler.
In the example above, the worker host gets RebuildSearchIndexCommand plus every untagged handler in
the assembly, and the API host gets everything untagged plus the "web" handlers.
If you want strict separation, tag every handler.
Behaviors are not filtered
AddXxxBehaviors() takes no tags and registers every behavior referenced anywhere in the assembly.
Behaviors are transient and only constructed when a handler that uses them is resolved, so an unused
registration costs nothing.
Tags behave the same way across Immediate.Handlers, Immediate.Apis and Immediate.Injections; the shared semantics are described on Tags and conditional registration.