Chapter 12. Controllers and Routing
Knowing how home plumbing works is unnecessary much of the time—but when you need to know it, you really need to know it.
While ASP.NET Web API provides a litany of helpful high-level features ranging from serialization and model binding to support for OData-style queries, the core job of all Web APIs, and ASP.NET Web API as a result, is to process HTTP requests and provide appropriate responses. Therefore, it is critical to understand the core mechanics of how an HTTP request flows from a client through the various elements of the ASP.NET Web API infrastructure and programming model, ultimately resulting in an HTTP response that can be sent back to the client.
This chapter focuses on that message flow, exploring the fundamental mechanics and supporting the programming model behind request handling and response generation. In addition, this chapter will look at the key types and insertion points that enable the framework to be extended to support custom message flow and processing schemes.
HTTP Message Flow Overview
The precise message flow through ASP.NET Web API will vary somewhat depending on the choice of host, and hosting is discussed in much greater detail in Chapter 10. However, at a high level, the framework components that participate in the HTTP message flow fall into two categories (as illustrated in Figure 12-1):
- Components that rely only on the HTTP message for context
- Components that rely on the higher-level programming model for context
The components that rely only on the core HTTP message context form the lower-level “message handler” pipeline. These components receive an HttpRequestMessage
object from the hosting abstraction and are ultimately responsible for returning an HttpResponseMessage
object.
By contrast, components that rely on the higher-level programming model have visibility into and are able to take advantage of programming framework abstractions, such as the controller and action methods as well as the parameters that map to the various elements of the HTTP request.
As mentioned, the low-level mechanics for activities such as selecting URL routes varies depending on the host. For example, when a Web API is hosted as part of an MVC application hosted on IIS, an HTTP message flows through the core routing infrastructure provided by ASP.NET. Conversely, when a Web API is self-hosted, the message flows through a WCF channel stack built around an HttpListener
object. Regardless of the selected hosting option, a request will ultimately be converted to an instance of HttpRequestMessage
and will be passed to an instance of HttpServer
.
The Message Handler Pipeline
HttpServer
is the entry point into the message handler pipeline for host-specific components. It initializes the pipeline from the handlers supplied by both global and route configuration data using the HttpClientFactory
’s CreatePipeline
method, as shown in Example 12-1.
protected
virtual
void
Initialize
()
{
// Do final initialization of the configuration.
// It is considered immutable from this point forward.
_configuration
.
Initializer
(
_configuration
);
// Create pipeline
InnerHandler
=
HttpClientFactory
.
CreatePipeline
(
_dispatcher
,
_configuration
.
MessageHandlers
);
}
Finally, because HttpServer
itself derives from the DelegatingHandler
class, it acts as the first handler in a message handler pipeline. The complete pipeline consists of HttpServer
followed by any number of custom DelegatingHandler
objects that you register with HttpConfiguration
; followed by another special handler called HttpRoutingDispatcher
; and finally, either a custom route-specific message handler (or another message handler pipeline built up with HttpClientFactory.CreatePipeline
) supplied during route registration, or the default HttpControllerDispatcher
message handler. HttpControllerDispatcher
selects, creates, and dispatches the message to a controller instance. The pipeline is illustrated in Figure 12-2.
HttpServer
establishes itself as the first node in the pipeline by setting the value returned from HttpClientFactory.CreatePipeline
to its own InnerHandler
property. This enables HttpServer
to pass control to the next handler in the pipeline by calling the SendAsync
method on its base class. This approach is consistent with all message handlers in the pipeline:
return
base
.
SendAsync
(
request
,
cancellationToken
)
The DelegatingHandler
base class simply calls SendAsync
on the object’s InnerHandler
value. The inner handler processes the message in its SendAsync
method and then repeats the process by calling SendAsync
on its own InnerHandler
. This process of calling the inner handler’s SendAsync
method continues until the innermost handler is reached—which, in the case of a typical ASP.NET Web API, is the handler that dispatches the request to a controller instance. This style of pipeline, depicted in Figure 12-3, is sometimes referred to as a “Russian doll” because handlers are layered within one another, and request data flows from the outermost handler to the innermost handler (and then vice versa for response data) as a result of the outer handler directly calling its inner handler.[7]
Keep in mind that this entire data flow is asynchronous and therefore the value returned from SendAsync
is a Task
. In fact, its complete signature is as follows:
Task
<
HttpResponseMessage
>
SendAsync
(
HttpRequestMessage
request
,
CancellationToken
cancellationToken
)
Participating in a task-based async
pipeline can take some getting used to, and that topic is discussed in much more depth in Chapter 10. However, some basic guidelines are as follows for creating a task-based message handler.
-
To pass control on to the next, or inner, handler in the pipeline, simply return the value of calling
SendAsync
on the base class. -
To stop all further processing of the message and return a response (also known as “short-circuiting” the request processing), return a new
Task<HttpResponseMessage>
. -
To process the HTTP response as it flows back from the innermost handler to the outermost handler, append a continuation (implemented with the
ContinueWith
method) to the returned task. The continuation should take a single parameter to hold the task that is being continued and should return anHttpResponseMessage
object. With version 4.5 and later of the .NET Framework, you can simplify working with asynchronous code using theasync
andawait
keywords.
As an example, consider the message handler shown in Example 12-2, which examines an HTTP GET
request and determines whether the request is a conditional GET
request (that is, a request that contains an if-none-match
header). If the request is a conditional request, and the entity tag (ETag
) contained in the request cannot be found in a local cache, this indicates to the handler that the value of the underlying resource state has changed. Therefore, the handler lets the request continue to flow through the pipeline and to the appropriate controller by calling and returning the value of base.Send
Async
. This will ensure that the response to this GET
request will contain the most up-to-date representation of the resource.
protected
override
Task
<
HttpResponseMessage
>
SendAsync
(
HttpRequestMessage
request
,
CancellationToken
cancellationToken
)
{
if
(
request
.
Method
==
HttpMethod
.
Get
&&
request
.
Headers
.
IfNoneMatch
.
Count
>
0
&&
(!
IfNoneMatchContainsStoredEtagValue
(
request
)))
return
base
.
SendAsync
(
request
,
cancellationToken
).
ContinueWith
(
task
=>
{
var
resp
=
task
.
Result
;
resp
.
Headers
.
ETag
=
new
EntityTagHeaderValue
(
_eTagStore
.
Fetch
(
request
.
RequestUri
));
return
resp
;
});
}
...
//by default, let the request keep moving through the message handler pipeline
return
base
.
SendAsync
(
request
,
cancellationToken
);
}
The handler also adds a continuation to the returned task so that it can create and apply a new ETag
value to the response message. This new ETag
value can then be passed and validated for future requests to the resource.
Dispatcher
The final stage of the message handler pipeline is the dispatching stage. In earlier versions of ASP.NET Web API, this stage was predefined to select a controller from the information supplied by the route data, get an instance of the controller, and then pass the HTTP message and context to the controller for processing by the controller’s execution logic. You could still circumvent the controller programming model by simply adding a custom message handler that returned a new Task
object. However, that message handler needed to be added in the global HttpConfiguration
object, meaning that it needed to process every HTTP request sent to the Web API.
To enable message handlers to be configured on a per-route basis, as well as to enable different Web API frameworks that may use a different higher-level abstraction than IHttpController
, the team added a level of indirection to the dispatching process. HttpServer
composes an instance of HttpRoutingDispatcher
as the last node in the message handler pipeline. As the following excerpt from the product source code illustrates, HttpRoutingDispatcher
is responsible for invoking either a custom message handler supplied by the route or, alternately, the default HttpControllerDispatcher
. Because the dispatcher derives from HttpMessageHandler
, which cannot be directly invoked, the HttpRoutingDispatcher
code wraps the instance in an HttpMessageInvoker
object so that it can be executed:
var
invoker
=
(
routeData
.
Route
==
null
||
routeData
.
Route
.
Handler
==
null
)
?
_defaultInvoker
:
new
HttpMessageInvoker
(
routeData
.
Route
.
Handler
,
disposeHandler
:
false
);
return
invoker
.
SendAsync
(
request
,
cancellationToken
);
Route-specific message handlers are declared as a part of the route configuration itself. For example, consider the following route registration code:
public
static
void
Register
(
HttpConfiguration
config
)
{
config
.
Routes
.
MapHttpRoute
(
"customHandler"
,
"custom/{controller}/{id}"
,
defaults
:
new
{
id
=
RouteParameter
.
Optional
},
constraints
:
null
,
handler
:
HttpClientFactory
.
CreatePipeline
(
new
HttpControllerDispatcher
(
config
),
new
[]
{
new
MyHandler
()})
);
...
}
In addition to the standard route configuration and registration code, the customHandler
route provides a custom message handler as the last parameter to MapHttpRoute
. However, the code actually does more than simply register an instance of the custom MyHandler
message handler. It uses the HttpClientFactory.CreatePipeline
helper method to compose MyHandler
with the default HttpControllerDispatcher
message handler. This is an important point to keep in mind when you’re inserting route-specific message handlers. If a custom message handler is supplied to HttpRoutingDispatcher
, that message handler becomes responsible for any and all further processing of the HTTP message. The CreatePipeline
method accepts the desired “final destination” message handler as its first argument followed by a list of all the additional message handlers to be composed into the pipeline. The method then wires the message handlers together by setting the InnerHandler
property of the individual message handlers, and returns the first message handler in the chain. In this example, the chain consists of MyHandler
followed by HttpControllerDispatcher
. Keep in mind that for a message handler pipeline to be created like this, all message handlers except for the innermost handler must derive from DelegatingHandler
rather than directly from HttpMessageHandler
, since the DelegatingHandler
supports composition via its InnerHandler
property.
HttpControllerDispatcher
By default, the final stop in the message handler pipeline will be the HttpControllerDispatcher
. This handler is the glue that binds together the message handler pipeline with the higher-level programming model elements of controllers and actions (we will call this the controller pipeline). The HttpControllerDispatcher
has three responsibilities:
-
Select a controller using an object that implements
IHttpControllerSelector
. -
Get an instance of a controller using an object that implements
IHttpControllerActivator
. - Execute the controller instance, passing it a controller context object that is composed of the current configuration, route, and request context.
To fulfill these responsibilities, HttpControllerDispatcher
relies on two noteworthy types. These are types that implement the IHttpControllerSelector
interface and types that implement the IHttpControllerActivator
interface.
Controller Selection
As its title suggests, the responsibility of the IHttpControllerSelector
is to select the proper controller based on the HTTP request. ASP.NET Web API supplies a default implementation with the DefaultHttpControllerSelector
class. This class uses the following algorithm for choosing the controller:
- Determine whether the controller can be identified directly from the route data. This condition is true when attribute-based routing is used.
-
Check whether the controller name is valid. If it is either null or an empty string, throw a
404
response exception. -
Using the controller name, look for a matching
HttpControllerDescriptor
in its controller info cache and return it.
The controller info cache is a dictionary of controller names and HttpControllerDescriptor
objects that is initialized when the cache is first accessed. During initialization, the controller info cache uses an instance of the HttpControllerTypeCache
, which in turn uses an object that implements the IHttpControllerTypeResolver
for iterating assemblies and types and building up a list of all valid controller types. By default, Web API uses DefaultHttpControllerTypeResolver
, which selects as valid controllers any types that meet the following conditions:
- The type is a class.
- The class is public.
- The class is not abstract.
-
The class implements or derives from a class that implements the
IHttpController
interface. -
The class name ends with the string
"Controller"
.
Because controllers are discovered when the DefaultHttpControllerSelector
info cache is first accessed, any failure to find exactly one match for the requested controller name indicates an error in the default controller selection logic. For example, if no entries are found for the requested controller name, the framework returns an HTTP 404 Not Found
response to the client. If, on the other hand, more than one entry is found for the requested controller name, then the framework throws an InvalidOperationException
for the ambiguous match.
Assuming that the requested controller name matches a single entry in the default controller selector’s info cache, the controller selector returns the corresponding HttpControllerDescriptor
value to the calling HttpControllerDispatcher
.
One additional thing to note here is that the lifetime of the controller descriptor is the duration of the HttpConfiguration
object, which practically speaking means that the controller descriptor lifetime is the lifetime of the application.
Supporting attribute-based routes
Web API 2 added the ability to specify routes as attributes. These attributes can be applied to both controller classes and action methods, and this declarative approach to routing adds to the purely convention-based approach of matching controllers and actions by route parameters and naming conventions.
Mechanically, using attribute-based routing is a two-step process. The first step is decorating controllers and/or actions with RouteAttribute
and supplying the appropriate route template values. The second step is to have Web API map those attribute values to actual route data, which the framework can then use when processing requests.
For example, consider the following basic greeting Web API:
public
class
GreetingController
:
ApiController
{
// mapped to GET /api/greeting by default
public
string
GetGreeting
()
{
return
"Hello!"
;
}
}
Using attribute-based routing, we could map this controller and action to a completely different URL without requiring a change to the global route configuration rules:
public
class
GreetingController
:
ApiController
{
// mapped to GET /services/hello
[Route("services/hello")]
public
string
GetGreeting
()
{
return
"Hello!"
;
}
}
In order to ensure that attribute-based routes are correctly added to Web API’s route configuration, we must call the MapHttpAttributeRoutes
method on HttpConfiguration
as follows:
config
.
MapHttpAttributeRoutes
();
The approach of integrating attribute routes at configuration time enables fewer modifications to the other framework components. For example, because all of the complexity for parsing and managing attribute-based route values is managed at the RouteData
level, the impact to DefaultHttpControllerSelector
is limited to the following:
controllerDescriptor
=
routeData
.
GetDirectRouteController
();
if
(
controllerDescriptor
!=
null
)
{
return
controllerDescriptor
;
}
As the code indicates, if a controller and/or action is explicitly known from the route as a result of matching an attribute-based route, the HttpControllerDescriptor
is immediately selected and returned. Otherwise, the convention-based controller selection process attempts to find and select a controller based on the type name.
Plugging in a custom controller selector
While the default logic for selecting a controller will be sufficient for the majority of Web API development scenarios, there may be cases where it is beneficial to supply a custom selection strategy.
Overriding the default controller selection strategy requires creating a new controller selector service and then configuring it to be used by the framework. Creating a new controller selector is simply a matter of authoring a class that implements the IHttpControllerSelector
interface, which is defined as follows:
public
interface
IHttpControllerSelector
{
HttpControllerDescriptor
SelectController
(
HttpRequestMessage
request
);
IDictionary
<
string
,
HttpControllerDescriptor
>
GetControllerMapping
();
}
As the name indicates, the SelectController
method has the primary responsibility of choosing a controller type for a supplied HttpRequestMessage
and returning an HttpControllerDescriptor
object. The GetControllerMapping
method adds a secondary responsibility to the controller selector to return the entire set of controller names and their corresponding HttpControllerDescriptor
objects as a dictionary. To date, however, this responsibility is exercised only by ASP.NET Web API’s API explorer feature.
We configure a custom controller selector to be used by the framework through the HttpConfiguration
object’s Services
collection. For example, the following code snippet illustrates how to replace the default controller selector logic, which looks for the suffix Controller
in the class name, with a strategy that allows the developer to specify a custom suffix:[8]
const
string
controllerSuffix
=
"service"
;
config
.
Services
.
Replace
(
typeof
(
IHttpControllerSelector
),
new
CustomSuffixControllerSelector
(
config
,
controllerSuffix
));
Controller Activation
Once the controller selector finds and returns an HttpControllerDescriptor
object to the dispatcher, the dispatcher gets an instance of the controller by calling the CreateController
method on HttpControllerDescriptor
. That method, in turn, delegates the responsibility of creating or getting a controller instance to an object that implements the IHttpControllerActivator
interface.
The IHttpControllerActivator
has the single responsibility of creating controller instances, and this is reflected in its definition:
public
interface
IHttpControllerActivator
{
IHttpController
Create
(
HttpRequestMessage
request
,
HttpControllerDescriptor
controllerDescriptor
,
Type
controllerType
);
}
Similarly to controller selection, the default logic for controller activation is implemented in the class DefaultHttpControllerActivator
and registered with the framework in the DefaultServices
constructor.
The default controller activator creates controller objects via one of two methods. It first attempts to create an instance using the ASP.NET Web API dependency resolver. The dependency resolver is an implementation of the IDependencyResolver
interface, and provides a general mechanism for the framework to externalize tasks like creating objects and managing object lifetime. In ASP.NET Web API, this is also the mechanism used to plug in inversion-of-control (IOC) containers such as Ninject and Castle Windsor. An instance of a dependency resolver is registered with the framework through the DependencyResolver
property on the HttpConfiguration
object, and when the property is non-null
, the framework will call methods such as GetService(Type serviceType)
in order to create object instances rather than create instances of those types directly. This facility can promote more loosely coupled and extensible designs, both for the ASP.NET Web API Framework itself and for your own services.
In the event that a dependency resolver has not been registered with the framework or if the dependency resolver cannot create an instance for the requested controller type, the default controller activator attempts to create an instance of the supplied controller type by executing the type’s parameterless constructor.
After the instance of the controller is created by the controller activator, it is passed back to the controller dispatcher. The dispatcher then passes control flow into the controller pipeline by calling ExecuteAsync
on the controller object as follows:
return
httpController
.
ExecuteAsync
(
controllerContext
,
cancellationToken
);
Like the majority of the components discussed, the controller’s ExecuteAsync
method is asynchronous and returns an instance of Task
, thereby helping to improve the throughput of the Web API framework itself, as none of its components will block the thread of execution with I/O operations. This efficiency enables the framework to handle an increased number of requests given a finite number of computing resources.
The Controller Pipeline
While the message handler pipeline provides abstractions for the lower-level processing of HTTP requests and responses, the controller pipeline enables a developer to work with higher-level programming abstractions such as controllers, actions, models, and filters. Orchestrating all of the objects used in processing requests and responses is the controller instance itself—hence the term controller pipeline.
ApiController
At a foundational level, an ASP.NET Web API controller is any class that implements the IHttpController
interface. This interface consists of a single, asynchronous execute method, which by default is called by the underlying dispatcher:
public
interface
IHttpController
{
Task
<
HttpResponseMessage
>
ExecuteAsync
(
HttpControllerContext
controllerContext
,
CancellationToken
cancellationToken
);
}
While this simple interface provides a great deal of flexibility in its simplicity, it is devoid of much of the functionality that ASP.NET developers have grown accustomed to. This kind of functionality includes capabilities like authorization, model binding, and validation. In order to provide these capabilities while preserving the simplicity of the interface and reducing the amount of coupling between the message handler pipeline and the controller pipeline, the ASP.NET Web API team created the ApiController
base class. ApiController
extends the core controller abstraction and provides two types of services to the controller classes that derive from it:
- A processing model that includes filters, model binding, and action methods.
- Additional context objects and helpers. These include objects for the underlying configuration, request message, model state, and others.
ApiController Processing Model
The processing model orchestrated by ApiController
is made up of several different stages, and, like the lower-level message handler pipeline, provides many different points for extending the default data flow with custom logic.
In general, the controller pipeline enables an action method to be selected for processing a request, maps properties of the request to the parameters of the selected method, and allows for the execution of a variety of filter types. Request processing through ApiController
looks similar to Figure 12-4.
Similar to the message handler pipeline, the controller pipeline constructs a “Russian doll” structure wherein a request flows from an outermost scope through a series of nested scopes to the action method, which is the innermost scope. The action method generates a response, and that response flows back from the innermost scope to the outermost scope. Scopes in the controller pipeline are implemented via filters, and just as with the message handler pipeline, all components of the controller pipeline are implemented asynchronously via tasks. This is evident in pipeline interfaces such as IActionFilter
:
public
interface
IActionFilter
:
IFilter
{
Task
<
HttpResponseMessage
>
ExecuteActionFilterAsync
(
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
,
Func
<
Task
<
HttpResponseMessage
>>
continuation
);
}
}
Filters will be described in more detail later in the chapter. First, we will explore the process for selecting the correct action method on the controller based on aspects of the request.
Action selection
One of the first steps taken inside of the ApiController.ExecuteAsync
method is action selection. Action selection is the process of selecting a controller method based on the incoming HttpRequestMessage
. As in the case of controller selection, action selection is delegated to a type whose primary responsibility is action selection. This type can be any class that implements the IHttpActionSelector
interface. The signature for IHttp
ActionSelector
looks as follows:
public
interface
IHttpActionSelector
{
HttpActionDescriptor
SelectAction
(
HttpControllerContext
controllerContext
);
ILookup
<
string
,
HttpActionDescriptor
>
GetActionMapping
(
HttpControllerDescriptor
controllerDescriptor
);
}
As in the case of IHttpControllerSelector
, the IHttpActionSelector
technically has two responsibilities: selecting the action from the context and providing a list of action mappings. The latter responsibility enables the action selector to participate in ASP.NET Web API’s API explorer feature.
We can easily supply a custom action selector by either explicitly replacing the default action selector (discussed next) or using a dependency resolver—generally in concert with an IoC container. For example, the following uses the Ninject IoC container to replace the default action selector with a custom selector, appropriately named CustomActionSelector
:
var
kernel
=
new
StandardKernel
();
kernel
.
Bind
<
IHttpActionSelector
>().
To
<
CustomActionSelector
>();
config
.
DependencyResolver
=
new
NinjectResolver
(
kernel
);
In order to decide whether it makes sense to supply a custom action selector, you must first understand the logic implemented by the default action selector. The default action selector provided by Web API is the ApiControllerActionSelector
. Its implementation is effectively a series of filters that are expected to yield a single action from a list of candidate actions. The algorithm is implemented in ApiControllerActionSelector
’s FindMatchingActions
method and is illustrated in Figure 12-5.
The initial and pivotal decision point in the default action selection algorithm is whether or not the matched routes are standard routes (i.e., those declared in global Web API configuration code via methods such as MapHttpRoute
), or whether they are attribute-based routes created as a result of decorating an action with the RouteAttribute
attribute.
public
class
ValuesController
{
[ActionName("do")]
public
string
ExecuteSomething
()
{
...
}
}
If no action methods are found that match the value of the action route parameter, an HTTP 404 Not Found
response is returned. Otherwise, the list of matched actions is then filtered once more to remove actions that are not allowed for the specific method associated with the incoming request and returned as the initial action candidate list.
If the route data does not have an explicit entry for the action method, then the initial action candidate selection logic tries to infer the action method name from the HTTP method name. For example, for a GET
request, the selector will search for qualified action methods whose name begins with the string "Get"
.
If the request matches a route that has been declared via attribute-based routing, the initial candidate action list is provided by the route itself and then filtered to remove those candidate actions that are not appropriate for the HTTP method of the incoming request.
After establishing the initial list of candidate action methods, the default action selection logic executes a sequence of refinements to narrow down the list of candidate actions to exactly one. Those refinements are as follows:
- Filter out those methods that do not contain the set of parameters required for the matched route.
-
Narrow the list of candidate actions to the actions with the lowest evaluation order. You can control the order of candidate actions for attribute-based routing using the
RouteAttribute
’sOrder
property. By default,Order
is set to zero, resulting in the entire set of candidate actions being returned from this refinement stage. -
Narrow the list of candidate actions to those with the highest precedence. Precedence is used for attribute-based routes and is determined algorithmically by the
RoutePrecedence.Compute
function based on the matched route. - Group the remaining list of candidate actions by the number of parameters and then return the first candidate action from the group with the most parameters.
At this point, a single action method should be all that remains in the list of candidate actions. Hence, the default action selector performs a final check and takes one of the following three actions based on the number of candidate actions returned:
-
If 0, return an HTTP
405
message if a candidate action exists but is not allowed for the HTTP method of the request; return an HTTP404
message if there is no matching action. - If 1, return the matching action descriptor so that it can be invoked.
-
If > 1, throw an
InvalidOperationException
for an ambiguous match.
Filters
As depicted in Figure 12-4, filters provide a set of nested scopes that you can use to implement functionality that cuts across multiple controllers or actions. While conceptually similar, filters are broken down into four categories based on when they are run and what kind of data they have access to. The four categories are authentication filters, authorization filters, action filters, and exception filters. This factoring is illustrated in Figure 12-6.
At a fundamental level, a filter in Web API is any class that implements the IFilter
interface, which consists of a single method:
public
interface
IFilter
{
bool
AllowMultiple
{
get
;
}
}
In addition to providing the IFilter
interface, Web API provides a base class that implements the interface and also derives from the .NET Framework’s Attribute
class. This enables all derived filters to be added in one of two ways. First, they can be added directly to the global configuration object’s Filters
collection as follows:
config
.
Filters
.
Add
(
new
CustomActionFilter
());
Alternately, filters that derive from FilterAttribute
can be added as an attribute to either a Web API controller class or action method as follows:
[CustomActionFilter]
public
class
ValuesController
:
ApiController
{
...
}
Regardless of how filters are applied to a Web API project, they are stored in the HttpConfiguration.Filters
collection, which stores them as a collection of IFilter
objects. This generic design enables the HttpConfiguration
object to contain many different types of filters, including filter types that go beyond the four current categories of authentication filter, authorization filter, action filter, and exception filter. Such new filter types can be created and added to HttpConfiguration
without breaking the application or requiring changes to HttpConfiguration
. They can then be later discovered and run by a new, custom controller class.
The ordering and execution of filters is orchestrated by ApiController
based on the following criteria:
- Filter type
-
ApiController
groups filters by type and executes each group as a different nested scope, as shown in Figure 12-4. - Where applied
-
Filters added as a part of global configuration (
HttpConfiguration.Filters.Add(..)
) are added to theFilters
collection before filters added as attributes (ActionFilterAttribute
,AuthorizationFilterAttribute
,ExceptionFilterAttribute
) at the class or action method level. Filters added via attribute to the controller class are added before filters added to action methods. When filters are run, they execute in the reverse order of how they were added. Therefore, filters added globally are run first, followed by filters added via attribute to the controller, followed by filters added to the action method. - Order added
- After filters have been grouped, they are executed within the group based on the order in which they were added.
Authentication filters
Authentication filters have two responsibilities. First, they examine a request as it flows through the pipeline and validate a set of claims to establish an identity for the calling user. In the event that the identity cannot be established from the provided claims, the authentication filter may also be used to modify the response to provide further instructions to the user agent for establishing a user’s identity. This response is known as a challenge response. Authentication filters are discussed at greater length in Chapter 15.
Authorization filters
Authorization filters apply policy to enforce the level to which a user, client application, or other principal (in security terms) can access an HTTP resource or set of resources provided by Web API. The technical definition of an authorization filter is simply a class that implements the IAuthorizationFilter
interface. This interface contains a single method for running the filter asynchronously:
Task
<
HttpResponseMessage
>
ExecuteAuthorizationFilterAsync
(
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
,
Func
<
Task
<
HttpResponseMessage
>>
continuation
);
While this interface is the only requirement for running authorization filters, it is not the most developer-friendly programming model—largely because of the complexities associated with asynchronous programming and the .NET Framework’s task API. As a result, Web API provides the AuthorizationFilterAttribute
class. This class implements the IAuthorizationFilter
interface as well as the ExecuteAuthorizationFilterAsync
method. It then provides the following virtual method, which can be overridden by derived classes:
public
virtual
void
OnAuthorization
(
HttpActionContext
actionContext
);
The AuthorizationFilterAttribute
calls OnAuthorization
from its ExecuteAuthorizationFilterAsync
method, which enables derived authorization filters to be written in a more familiar, synchronous style. After calling OnAuthorization
, the base class then examines the state of the HttpActionContext
object and decides whether to let request processing continue or whether to return a new Task
with a new HttpResponseMessage
indicating an authorization failure.
When you are authoring a custom authorization filter that is derived from AuthorizationFilterAttribute
, the way to indicate an authorization failure is to set an HttpResponseMessage
object instance on actionContext.Response
, as follows:
public
class
CustomAuthFilter
:
AuthorizationFilterAttribute
{
public
override
void
OnAuthorization
(
HttpActionContext
actionContext
)
{
actionContext
.
Response
=
actionContext
.
Request
.
CreateErrorResponse
(
HttpStatusCode
.
Unauthorized
,
"denied"
);
}
}
When the call to OnAuthorize
is finished, the AuthorizationFilterAttribute
class uses the following code to analyze the state of the context and either continue processing or return a response immediately:
if
(
actionContext
.
Response
!=
null
)
{
return
TaskHelpers
.
FromResult
(
actionContext
.
Response
);
}
else
{
return
continuation
();
}
Additionally, if an exception is thrown from within OnAuthorize
, the AuthorizationFilterAttribute
class will catch the exception and halt processing, returning an HTTP response with the 500 Internal Server Error
response code:
try
{
OnAuthorization
(
actionContext
);
}
catch
(
Exception
e
)
{
return
TaskHelpers
.
FromError
<
HttpResponseMessage
>(
e
);
}
When you’re designing and implementing a new authorization filter, a good starting point is the existing AuthorizeAttribute
provided by Web API. This attribute uses Thread.CurrentPrincipal
to get the identity (and optionally role membership information) for an authenticated user and then compares it to policy information provided in the attribute’s constructor. Additionally, an interesting detail to note about the AuthorizeAttribute
is that it checks for the presence of the AllowAnonymousAttribute
on both the action method and the containing controller, and exits successfully if the attribute is present.
Action filters
Action filters are conceptually very similar to authorization filters. In fact, the signature of the IActionFilter
execute method looks identical to the corresponding method on IAuthorizationFilter
:
Task
<
HttpResponseMessage
>
IActionFilter
.
ExecuteActionFilterAsync
(
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
,
Func
<
Task
<
HttpResponseMessage
>>
continuation
)
Action filters differ from authorization filters in a couple of respects, however. The first difference is when action filters will be called. As we discussed earlier, filters are grouped by type and different groups are executed at different times. Authorization filters are run first, followed by action filters, followed by exception filters.
The second, and more notable, difference between action filters and the other two filter types is that action filters give the developer the ability to process a request on both sides of the call to the action method. This capability is exposed by the following two methods on the ActionFilterAttribute
class:
public
virtual
void
OnActionExecuting
(
HttpActionContext
actionContext
);
public
virtual
void
OnActionExecuted
(
HttpActionExecutedContext
actionExecutedContext
);
Just like in the case of the other filter types, ActionFilterAttribute
implements the IActionFilter
interface to abstract the complexities of working directly with the Task
API as well as to derive from Attribute
, thereby enabling action filters to be applied directly to controller classes and action methods.
As a developer, you can easily create action filters by simply deriving from ActionFilterAttribute
and overriding either or both the OnActionExecuting
and OnActionExecuted
methods. For example, Example 12-3 performs some basic auditing of action methods.
public
class
AuditActionFilter
:
ActionFilterAttribute
{
public
override
void
OnActionExecuting
(
HttpActionContext
c
)
{
Trace
.
TraceInformation
(
"Calling action {0}::{1} with {2} arguments"
,
c
.
ControllerContext
.
ControllerDescriptor
.
ControllerName
,
c
.
ActionDescriptor
.
ActionName
,
c
.
ActionArguments
.
Count
);
}
public
override
void
OnActionExecuted
(
HttpActionExecutedContext
c
)
{
object
returnVal
=
null
;
var
oc
=
c
.
Response
.
Content
as
ObjectContent
;
if
(
oc
!=
null
)
returnVal
=
oc
.
Value
;
Trace
.
TraceInformation
(
"Ran action {0}::{1} with result {2}"
,
c
.
ActionContext
.
ControllerContext
.
ControllerDescriptor
.
ControllerName
,
c
.
ActionContext
.
ActionDescriptor
.
ActionName
,
returnVal
??
string
.
Empty
);
}
}
If an exception is thrown from within an action filter, a Task<HttpResponseMessage>
containing the error is created and returned, thereby halting request processing by other components in the pipeline. This is consistent with the logic discussed for authorization filters. Because action filters enable processing both before and after the action method, ActionFilterAttribute
contains additional logic to nullify the context of the response message if an exception is thrown in the OnActionExecuted
side of the filter. It accomplishes this by simply wrapping the call to OnActionExecuted
in a try..catch
block and setting the response to null
in the catch
block:
try
{
OnActionExecuted
(
executedContext
);
...
}
catch
{
actionContext
.
Response
=
null
;
throw
;
}
Exception filters
Exception filters exist for the purpose of, as their name suggests, enabling the custom handling of exceptions that are thrown during the controller pipeline. As in the case of authorization and action filters, you define exception filters by implementing the IExceptionFilter
interface. Additionally, the framework provides the base class, ExceptionFilterAttribute
, which provides .NET Framework attribute capabilities and a more simplified programming model to classes that derive from it.
Exception filters can be extremely helpful in preventing the flow of potentially sensitive information outside of your service. As an example, database exceptions typically contain details identifying your database server or schema design. This kind of information could be used by an attacker to launch attacks against your database. The following is an example of an exception filter that logs exception details to the .NET Framework’s diagnostics system and then returns a generic error response:
public
class
CustomExceptionFilter
:
ExceptionFilterAttribute
{
public
override
void
OnException
(
HttpActionExecutedContext
actionExecutedContext
)
{
var
x
=
actionExecutedContext
.
Exception
;
Trace
.
TraceError
(
x
.
ToString
());
var
errorResponse
=
actionExecutedContext
.
Request
.
CreateErrorResponse
(
HttpStatusCode
.
InternalServerError
,
"Please contact your server administrator for more details."
);
actionExecutedContext
.
Response
=
errorResponse
;
}
}
When you apply this filter (globally, or at the controller or action level), action methods such as the following:
[CustomExceptionFilter]
public
IEnumerable
<
string
>
Get
()
{
...
throw
new
Exception
(
"Here are all of my users credit card numbers..."
);
}
will return a sanitized error message to the client:
$ curl http://localhost:54841/api/values {"Message":"Please contact your server administrator for more details."}
But what happens if your exception filter throws an exception? More broadly, what if you don’t have an exception filter? Where are unhandled exceptions ultimately caught? The answer is the HttpControllerDispatcher
. If you remember from earlier in the chapter, the HttpControllerDispatcher
is by default the last component in the message handler pipeline, and it is responsible for calling the ExcecuteAsync
method on a Web API controller. In addition, it wraps this call in a try..catch
block, as shown here:
protected
override
async
Task
<
HttpResponseMessage
>
SendAsync
(
HttpRequestMessage
request
,
CancellationToken
cancellationToken
)
{
try
{
return
await
SendAsyncCore
(
request
,
cancellationToken
);
}
catch
(
HttpResponseException
httpResponseException
)
{
return
httpResponseException
.
Response
;
}
catch
(
Exception
exception
)
{
return
request
.
CreateErrorResponse
(
HttpStatusCode
.
InternalServerError
,
exception
);
}
}
As you can see, the dispatcher is capable of returning the HttpResponseMessage
object attached to an HttpResponseException
. Additionally, in the case where an unhandled exception is caught, the dispatcher contains a generic exception handler that turns the exception into an HttpResponseMessage
containing details of the exception along with an HTTP 500 Internal Server Error
response code.
Model binding and validation
Chapter 13 will focus on model binding, so we will not spend a significant amount of time describing it here. However, the key point from the perspective of the controller pipeline is that model binding occurs just before the action filters are processed, as shown by the following fragment from the ApiController
class source code:
private
async
Task
<
HttpResponseMessage
>
ExecuteAction
(
HttpActionBinding
actionBinding
,
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
,
IEnumerable
<
IActionFilter
>
actionFilters
,
ServicesContainer
controllerServices
)
{
...
await
actionBinding
.
ExecuteBindingAsync
(
actionContext
,
cancellationToken
);
_modelState
=
actionContext
.
ModelState
;
...
}
The order is important here, as it means that the model state is available to action filters, making it trivial to, for example, build an action filter that automatically returns an HTTP 400 Bad Request
response in the event of an invalid model. This enables us to stop inserting code like the following into every PUT
and POST
action method:
public
void
Post
(
ModelValue
v
)
{
if
(!
ModelState
.
IsValid
)
{
var
e
=
Request
.
CreateErrorResponse
(
HttpStatusCode
.
BadRequest
,
ModelState
);
throw
new
HttpResponseException
(
e
);
}
}
Instead, this model state check can be pulled into a simple action filter, as shown in Example 12-4.
public
class
VerifyModelState
:
ActionFilterAttribute
{
public
override
void
OnActionExecuting
(
HttpActionContext
actionContext
)
{
if
(!
actionContext
.
ModelState
.
IsValid
)
{
var
e
=
actionContext
.
Request
.
CreateErrorResponse
(
HttpStatusCode
.
BadRequest
,
actionContext
.
ModelState
);
actionContext
.
Response
=
e
;
}
}
}
Action invocation
The final step in the controller pipeline is to invoke the selected action method on the controller. This responsibility falls to a specialized Web API component called the action invoker. An action invoker is any class that implements the IHttpActionInvoker
interface. This interface has the following signature:
public
interface
IHttpActionInvoker
{
Task
<
HttpResponseMessage
>
InvokeActionAsync
(
HttpActionContext
actionContext
,
CancellationToken
cancellationToken
);
}
ApiController
requests the action invoker from DefaultServices
, which means that you can replace it using either the Replace
method on DefaultServices
or using DependencyResolver
in conjunction with a dependency injection framework. However, the default implementation supplied by the framework, ApiControllerActionInvoker
, should be sufficient for most requirements.
The default invoker performs two primary functions, as illustrated here:
object
actionResult
=
await
actionDescriptor
.
ExecuteAsync
(
controllerContext
,
actionContext
.
ActionArguments
,
cancellationToken
);
return
actionDescriptor
.
ResultConverter
.
Convert
(
controllerContext
,
actionResult
);
The first responsibility is, as you would expect, to invoke the selected action method. The second responsibility is to convert the result of the action method call into an HttpResponseMessage
. For this task, the invoker uses a specialized object called an action result converter. An action result converter implements the IActionResultConverter
interface, which contains a single method accepting some context and returning an HttpResponseMessage
. Currently, Web API contains three action result converters:
-
ResponseMessageResultConverter
-
Used when an action method returns an
HttpResponseMessage
directly; passes the responses message through. -
ValueResultConverter<T>
-
Used when an action method returns a standard .NET Framework type; creates an
HttpResponseMessage
using the associatedHttpRequestMethod
’sCreateResponse<T>
method. -
VoidResultConverter
-
Used when an action method has a void return value; creates a new
HttpResponseMessage
with a status of204 No Content
.
Once the return value has been converted to an HttpResponseMessage
, that message can begin the flow back out of the controller and message handler pipelines, and then over the wire to the client.
Conclusion
In this chapter, we explored in depth the two pipelines that handle request processing in ASP.NET Web API: the low-level message handler pipeline and the controller pipeline. Each has associated benefits as well as trade-offs. For example, the message handler pipeline is executed early in the handling of requests, so it can be advantageous to leverage when your component could prevent the needless execution of more expensive code paths. However, this comes at the cost of having to work at the level of HttpRequestMessage
and HttpResponseMessage
. The controller pipeline, on the other hand, provides its components access to the higher-level programming model objects, such as those that describe the controller, action methods, and associated attributes. Both of these pipelines provide a complete set of default components as well as a flexible model for extensibility using either HttpConfiguration
or a custom DependencyResolver
.
In the next chapter, we will take a much deeper look at the core building blocks that make up the message handler pipeline, including the message handlers themselves as well as the HTTP primitives, HttpRequestMessage
and HttpResponseMessage
.
[7] This pipeline style can be contrasted with a style whereby the pipeline is external to its components and data flows as the result of the pipeline calling a component, obtaining its response, calling the next component, and so on.
[8] The full code for overriding the default controller suffix is more involved than simply providing a new controller selector.
Get Designing Evolvable Web APIs with ASP.NET now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.