Skip to content

Tagged registration

Tag endpoints and route groups, then register only the ones you want at startup.

2 min read

Tags let one assembly serve several hosts: a public API process registers the public endpoints, an admin process registers the admin ones, and integration tests register whichever slice they exercise. Declare tags on the attribute, and filter at the call site.

See Tags and conditional registration for the semantics shared across Immediate.Handlers, Immediate.Apis and Immediate.Injections.

Tagging an endpoint

Every Map attribute has a Tags init-only property:

GetUsers.cs
[Handler]
[MapGet("/users", Tags = ["Public"])]
public static partial class GetUsers
{
	// ...
}

An endpoint may carry several tags:

[MapDelete("/users/{id:int}", Tags = ["Admin", "Internal"])]

Tagging a route group

[RouteGroup] takes the same property. Tagging a group filters the group as a whole — including every endpoint and child group beneath it:

Admin.cs
[RouteGroup("api/admin", Tags = ["Admin"])]
public sealed partial class Admin
{
	private static void CustomizeGroup(RouteGroupBuilder group)
		=> group.RequireAuthorization("Admin");
}

Filtering at registration

Pass the tags you want after the prefix:

Program.cs
// everything
app.MapUsersApiEndpoints();

// only untagged endpoints plus those tagged "Public"
app.MapUsersApiEndpoints("", "Public");

// mounted under /api, "Public" or "Internal"
app.MapUsersApiEndpoints("/api", "Public", "Internal");

The rules, which are easy to get backwards:

  • Passing no tags registers everything, including endpoints and groups that are tagged.
  • Untagged endpoints and groups are always registered, whatever tags you pass. Tags are an opt-in filter on tagged items, not an allow-list for all of them.
  • An item matches if it shares at least one tag with the arguments.
  • Matching is ordinal string comparison — case-sensitive, no wildcards.

The generated code makes this literal:

if (tags is [] || Intersects(tags, ["Public"]))
{
	Users.GetUsers.MapEndpoint(group);
}

// untagged endpoints are emitted with no guard at all
Users.GetHealth.MapEndpoint(group);

Tags flow down through groups

MapXxxEndpoints passes its tags argument into every group’s generated MapGroup method, and each group passes it on to its children. A tag check therefore happens at each level:

  • If a group is tagged and does not match, the whole subtree is skipped — its endpoints are never considered.
  • If a group matches (or is untagged), each endpoint inside it is still checked against its own tags.

So an endpoint tagged "Admin" inside a group tagged "Public" is registered only when you pass both "Public" and "Admin" — or no tags at all.