Skip to main content

Start with a controller

Use [Controller] when you want a class to be discovered by RegisterControllers(...), and use [Route] to define its base path.
The full routes become:
  • GET /api/users
  • GET /api/users/me
  • GET /api/users/[userId]

Two registration paths

There are two ways to register attributed controllers:
  • Scan an assembly with RegisterControllers(typeof(UsersController).Assembly)
  • Register a single controller type with RegisterController<UsersController>()
[Controller] is required for assembly scanning. Direct RegisterController<T>() registration already names the controller type explicitly.

Parent controllers

Use [Controller(Parent = typeof(...))] when a controller should live under another controller’s route prefix. Parent controllers are route containers. The router walks the parent chain, prepends each parent’s [Route], and then appends the child controller route and method route.
Those routes become:
  • GET /api/parking-areas
  • GET /api/parking-areas/[areaId]
  • GET /api/parking-areas/[areaId]/squares/[squareId]
Parent route parameters are part of the final route, so child actions can bind them like any other route parameter. In the example above, AreaId comes from the parent route segment and SquareId comes from the child method route. Each type in the parent chain must be a controller with its own [Controller] and [Route]. Parent controllers can have actions of their own, or they can exist mainly to give nested resources a shared route prefix.
Controller-level middleware and authorization are registered on the route path for the controller that declares them. If a parent controller is registered with middleware on /parking-areas, that middleware can run for deeper routes under /parking-areas/... because the request pipeline walks route-tree parents. Declare child-specific middleware or authorization on the child controller when it should only apply to that nested resource.

HTTP method attributes

Use one of these on controller methods:
  • [HttpGet("...")]
  • [HttpPost("...")]
  • [HttpPut("...")]
  • [HttpPatch("...")]
  • [HttpDelete("...")]
The controller route and method route are concatenated.
Attribute-routed controllers currently expose GET, POST, PUT, PATCH, and DELETE. If you need a manual OPTIONS endpoint, register it directly on the router with RegisterOptions(...).

Route template segments

  • users — literal segment
  • [userId] — named route parameter
  • * — any single segment

How parameter matching works

Route placeholder names are matched against method parameter names case-insensitively. [HttpGet("/[userId]")] can bind to string UserId. Path matching is case-sensitive; route placeholder name matching is case-insensitive.

Reusing controller route prefixes

ControllerBase.GetRoute<T>() returns the route prefix declared on a controller type and caches it for reuse.
That is useful when you want to compose links or manual subroutes around an attributed controller without hard-coding the same prefix twice.

Controller lifecycle

The router creates a new controller instance per request:
  • Request-specific state does not leak between requests
  • ControllerBase.Context always belongs to the current request
  • Do not access ControllerBase.Context in the constructor

When to omit [Route]

If you omit [Route], the controller base path defaults to /. Most real applications are easier to read when each controller owns a clear path prefix. Binding and responses shows how route values, query strings, and JSON bodies are turned into controller method arguments.