When the high-level API is not enough
Most controller methods should stay in the normal attribute-based model. Reach for the advanced tools when you need:- Manual control over how the request body is read
- Manual control over the exact response shape
- Low-level endpoint registration without controllers
- A custom JSON naming convention
- Constructor injection for controllers, middleware, and authorization handlers
Raw requests
[RawRequest] disables automatic binding and response serialization for a controller method. When you use it:
- The method must return
Task - The method must take exactly two parameters
- The second parameter must be
HttpResponse - For
GETandDELETE, the first parameter must beExtendedHttpRequest - For
POST,PUT, andPATCH, the first parameter must beHttpBodyRequest
Manual endpoint registration
The router also supports a low-level style without controllers:Parameter-name codecs
The router usesIParameterNameCodec to map CLR member names to JSON field names. Built-in codecs:
CamelCaseParameterNameCodecSnakeCaseParameterNameCodecTransparentParameterNameCodec
Service provider and constructor injection
The router exposes a built-inHttpServiceProvider through Router.ServiceProvider.
Constructor injection means the router creates your controller, middleware, or authorization class and supplies the constructor arguments for you.
Instead of this:
How to use it
The typical flow is:- Register shared dependencies on
Router.ServiceProvider. - Add constructor parameters to the controller, middleware, or authorization class.
- Register the controller or scan the assembly.
- Let the router resolve the constructor when it creates the instance.
What the router can resolve
Current constructor resolution supports:- Singletons registered with
RegisterSingleton(...) - Multi-registration services registered with
RegisterService(...) IEnumerable<T>andT[]constructor parameters for multi-registrations- Unregistered concrete dependencies when the provider can build their public constructor graph
- Use
RegisterSingleton(...)when one shared instance should be reused everywhere - Use
RegisterService(...)when you want to inject a collection of registered implementations - Use a concrete class parameter when the provider can recursively construct that class from other resolvable dependencies
[Middleware], and authorization middleware declared with [UseAuthorization].
Lifetime model
It helps to separate the lifetime of the object being created from the lifetime of the dependencies being injected:- Controllers are created per request
- Middleware attribute classes are created through the service provider when the router wires them up
- Authorization middleware declared with
[UseAuthorization]is also created through the service provider - Singleton dependencies are reused across every resolved instance
ControllerBase.Context, not inside shared singleton dependencies.
Constructor selection guidance
If a class has multiple public constructors, the provider picks the best resolvable one. That is useful, but in application code it is usually better to keep one clear public constructor for each controller or middleware class. That gives you:- More predictable wiring
- Fewer accidental constructor-selection surprises
- Cleaner registration failures when a dependency is missing
Common usage patterns
Constructor injection is most useful when:- A controller needs shared collaborators such as clocks, repositories, or API clients
- Middleware needs reusable services such as audit writers or feature flags
- Authorization handlers need access to permission stores or identity services
Context for request-specific transport state.
Practical rules
- Register dependencies before calling
RegisterController<T>()orRegisterControllers(...) - Prefer interfaces for shared services and concrete types for simple helper objects
- Use
IEnumerable<T>orT[]when multiple implementations should be injected together - Keep a single obvious public constructor when possible
- If a dependency cannot be resolved, simplify the constructor graph first before adding more registrations
Route helper
ControllerBase.GetRoute<T>() returns the [Route] prefix declared on a controller and caches the result.
A simple rule of thumb
Stay with normal attributed methods until you have a concrete reason not to. Use raw requests when:- The body shape is unusual
- You need direct access to parser output
- You want full control over the response lifecycle
- Controllers would add ceremony without helping
- Routes are generated or highly dynamic
- You want to build a focused low-level module