Skip to main content

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 GET and DELETE, the first parameter must be ExtendedHttpRequest
  • For POST, PUT, and PATCH, the first parameter must be HttpBodyRequest
In raw mode, you are responsible for reading all values and sending the response yourself.

Manual endpoint registration

The router also supports a low-level style without controllers:
This style is useful when you want a lightweight route table or need to build endpoints dynamically.

Parameter-name codecs

The router uses IParameterNameCodec to map CLR member names to JSON field names. Built-in codecs:
  • CamelCaseParameterNameCodec
  • SnakeCaseParameterNameCodec
  • TransparentParameterNameCodec
You can configure the codec directly on the router:
Use the codec that matches the JSON contract you want before controller registration:
The same codec is used for both request body decoding and response encoding.

Service provider and constructor injection

The router exposes a built-in HttpServiceProvider 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:
you let the router provide the dependency:
That keeps object creation in one place, makes the controller easier to test, and lets the same dependency graph be reused for controllers, middleware, and authorization handlers.

How to use it

The typical flow is:
  1. Register shared dependencies on Router.ServiceProvider.
  2. Add constructor parameters to the controller, middleware, or authorization class.
  3. Register the controller or scan the assembly.
  4. 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> and T[] constructor parameters for multi-registrations
  • Unregistered concrete dependencies when the provider can build their public constructor graph
In practice that means:
  • 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
The same provider is used for controller instances, middleware declared with [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
That means request-specific state should usually live on the controller instance or in 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
Keep constructor injection for long-lived collaborators. Use method parameters for route, query, and body data, and use Context for request-specific transport state.

Practical rules

  • Register dependencies before calling RegisterController<T>() or RegisterControllers(...)
  • Prefer interfaces for shared services and concrete types for simple helper objects
  • Use IEnumerable<T> or T[] 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
Use manual endpoint registration when:
  • Controllers would add ceremony without helping
  • Routes are generated or highly dynamic
  • You want to build a focused low-level module