Skip to content

Tagged registration

Register a subset of an assembly's handlers per host by tagging them and filtering at the call site.

2 min read

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

RebuildSearchIndexCommand.cs
[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:

Worker/Program.cs
builder.Services.AddApplicationBehaviors();
builder.Services.AddApplicationHandlers(tags: "worker");
Api/Program.cs
builder.Services.AddApplicationBehaviors();
builder.Services.AddApplicationHandlers(tags: "web");
Tests/Fixture.cs
// 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

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.