This is the sixth post on a series about the new Preview 4 of WCF Web API. The previous posts were:
- Elementary programming model
- Self-hosting, HTTPS and HTTP Basic Authentication
- IIS Hosting
- HTTP Message Classes
This post aims to present the processing architecture: what happens since a HTTP request message is received by the WCF Web API runtime until the HTTP response bytes are written into the transport layer.
The following description reflects my understanding of the WCF Web API runtime, based on the source code, and may not be completely accurate. Comments and corrections are greatly appreciated.
0. The big picture
The following diagram aims to schematize the HTTP request processing architecture.
In the bottom layer is the interface between the WCF runtime and the transport layer, namely the transport channel and the message encoder/decoder. The resource class, composed by a set of operations that handle HTTP requests and produce HTTP responses (for more details, see the first post), is at the top layer.
1. From bytes to HttpRequestMessage
The journey begins at the bottom layer, where a sequence of bytes obtained from the transport protocol is transformed into a HttpRequestMessage instance by WCF’s transport channel and message encoder/decoder.
2. Message handlers
After creation, the HttpRequestMessage instance passes through a sequence of message handlers. Each one of this message handlers receives an HttpRequestMessage and returns a HttpResponseMessage asynchronously. Being more precise, the message handlers expose the following method
Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
Each message handler has a reference to the next handler in the sequence, called the inner handler. A typical usage scenario is for the handler to perform some processing over the request message (e.g. read an header and add a property to the message, change the request method) and then forward it to the inner handler. After the inner handler returns the response, then another processing can be done over this response message (e.g. add an header) before it is returned.
Another usage scenario is for the handler to produce the response message immediately and return it without forwarding the request to the inner handler. This means the the upper layers of the runtime will not be called, namely the resource class.
For a concrete example of these two scenarios, consider a channel that implements HTTP Basic Authentication:
- The SendAsync method inspects the request message to see if it contains an Authorization header with valid credentials. If so, it adds a property with the user’s identity to the message and lets the request flow to the inner handler. In this case, the handler’s return will be the inner handler’s return.
- If the Authorization header does not exists or has invalid credentials, then a 401 response message is created with a WWW-Authenticate header. This response message is returned immediately, that is, the inner handler is not called – the request processing is short-circuited.
The message handlers provide an asynchronous interface that, given a HttpRequestMessage, returns a Task<HttpResponseMessage>. This means that message handlers are an adequate place to perform lengthy I/O operations without blocking the processing thread. Using the above concrete example, implementing HTTP Basic Authentication, this I/O operation can be the retrieval of the user’s roles from an external service/resource/database.
For a concrete example of a message channel, see http://weblogs.asp.net/cibrax/archive/2011/04/15/http-message-channels-in-wcf-web-apis-preview-4.aspx. Note that currently message handlers are still called message channels.
If the request message survives all the message handlers, the next phase in the request processing is the operation dispatch. In this phase, the request’s method and URI are used to select the operation that will be called. This selection uses the operation’s WebGetAttribute or WebInvokeAttribute to obtain the operation’s method and URI template. The request is then forwarded to the selected operation, via the operation handlers.
If no operations matchs the request, then a 404 response is produced.
4. Operation Handlers
The next processing phase is responsible for producing the parameter set required by the selected operation, from the request message. This parameters are produced by operation handlers. These operation handlers may also be used to perform other type of operation specific message processing (e.g. operation caching).
Message handlers receive a request message and produce (asynchronously) a response message. Operation handlers, on the other hand, receive a set of parameters and produce (synchronously) another set of parameters. These parameters are described by the HttpParameter class, namely
- by a name;
- and by a type.
There is another difference between message handlers and operation handlers: message handlers apply to every request; operation handler apply only to the operation where they where added. This is illustrated in the first diagram: there is one message handler “stack” and multiple operation handlers “stacks”.
Each operation handler derives from the HttpOperationHandler class and must implement the following methods:
- OnGetInputParameters – returns an IEnumerable<HttpParameter> with the parameters that the handler wants to receive.
- OnGetOutputParameters – returns an IEnumerable<HttpParameter> with the parameters that the handler produces.
- OnHandle – receives an object with the input parameter’s values and returns another object with the output parameter’s values.
Each operation handler receives parameters from a parameter bag (input parameters) and contribute with parameters to that bag (output parameters).
Initially, this bag is populated with the request message itself. Afterwards, the OnHandle method of each handler is executed, resulting in more parameter values added to the bag.
The operation handlers are execution by an order that satisfies the handler’s dependencies: when a given handler executes, all of its input parameters must already be in the bag, produced by previously executed handlers. This execution order is determined during service startup, based on the input and output characterization of each handler (OnGetInputParameters and OnGetOutputParameters).
The execution order must also guarantee that, at the end of this phase, all the operation’s input parameters are available in the bag.
If this order does not exists, e.g. an operation handler or the operation uses a parameter that isn’t provided by any other handler, then a runtime error occurs.
An example operation handler was showed in the third post.
Another interesting operation handler to analyze is the UriTemplateHandler. This handler, added automatically by the WCF Web API runtime, receives the request messaged and matches the operation’s URI template with the request URI. Then, it outputs one parameter for each bound variable. This is the way how variables are extracted from the URI, both from the path and from the query string, and turned into operation’s parameters.
5. Instance creation
Before the operation is called, a resource class instance is obtained – operations are instance methods.
It is possible to plugin a custom instance provider for obtaining these instances. A typical scenario is the use of an IoC/DI container for that purpose.
6. Operation invoke
Finally, at the topmost layer, the operation is invoked using the parameters computed by the operation handlers.
7. Response operation handlers
After the operation’s invocation, the output parameters pass through a sequence of response operation handlers. One of the most interesting things happening in this phase is the content encoding, which will be the subject of another post.
Also, if the response message was not returned by the operation, it is created in this phase
Finally, the response message flows down through the message handlers until being turned into a byte sequence and written into the transport layer.
- This post does not describe how custom message or operation handlers are added to the runtime. This will be the subject of a future post about configuration.
- In the following posts I will also present in more detail some of the described phases.
- Once again: comments, corrections and suggestions are welcomed.