Skip to content

Tags and conditional registration

Register or map only part of an assembly by tagging items and filtering at the call site.

3 min read

Four packages support tags: Immediate.Handlers, Immediate.Apis, Immediate.Injections and Immediate.Jobs. The mechanism is shared, and the semantics are easy to get backwards, so they are stated precisely here. Package-specific examples live on each package’s tagged-registration page.

The use case is one assembly, several hosts. A web host maps the HTTP endpoints; a worker host registers the background handlers; both share the same shared services.

Declaring tags

Tags are declared on the item, as a string array:

[Handler(Tags = ["worker"])]
public sealed partial class ProcessOutboxCommand { /* … */ }

[Handler(Tags = ["fulfillment"]), Job]
public sealed partial class ReserveInventoryJob { /* … */ }

[MapGet("/api/todos", Tags = ["web"])]
public sealed partial class GetTodosQuery { /* … */ }

[RouteGroup("/api/admin", Tags = ["admin"])]
public static partial class AdminGroup { }

[RegisterScoped(Tags = ["worker"])]
public sealed class OutboxProcessor { /* … */ }

Filtering at the call site

Filtering happens where you register, not where you declare:

Program.cs
// Worker host — background handlers and their services only
services.AddTodoHandlers(tags: "worker");
services.AddTodoServices("worker");
services.AddTodoJobs(tags: ["fulfillment"]);

// Web host — HTTP endpoints only
app.MapTodoEndpoints(tags: "web");

The four rules

Rules 1 and 2 together mean tags are additive: they let you pull in an extra slice of the assembly, not carve one out. If a handler must never be registered in the web host, tagging is the wrong tool — put it in a different assembly.

The tags parameter signature

The generated parameter adapts to your project’s C# language version:

Language versionSignature
C# 13 and laterparams ReadOnlySpan<string>
C# 12 and earlierparams string[]

Both are params, so the call site is identical either way — AddTodoServices("web", "admin"). The difference only shows if you pass a pre-built collection, where the span form will not accept a List<string> directly.

AddXxxHandlers takes a lifetime parameter before tags, so tags there are usually passed by name:

services.AddTodoHandlers(ServiceLifetime.Scoped, "worker");
services.AddTodoHandlers(tags: "worker");            // lifetime defaults to Scoped

MapXxxEndpoints takes an optional route prefix before tags:

app.MapTodoEndpoints(tags: ["web"]);
app.MapTodoEndpoints("/v1", "web");

AddXxxJobs takes its optional options delegate before tags. It reads the job’s Immediate.Handlers Tags value; use the same filter for Jobs and Handlers so every selected job has a generated handler at execution:

services.AddTodoHandlers(tags: ["fulfillment"]);
services.AddTodoJobs(options => options.UseInMemory(), tags: ["fulfillment"]);

AddXxxJobs does not replace AddXxxHandlers. Job queue definitions are assembly-wide and remain registered even when job tags filter which job definitions and schedulers are added.

Where to go next