原文:https://odetocode.com/blogs/scott/archive/2013/04/08/webapi-tip-8-working-with-tasks.aspx
Message handlers in the WebAPI feature a single method to process an HTTP request and turn the request into a response. SendAsync is the method name, and following the TAP, the method returns a Task<HttpResponseMessage>.
There are several different techniques available to return the Task result from a message handler. The samples on the ASP.NET site demonstrate a few of these techniques, although there is one new trick for .NET 4.5.
What you'll notice in these samples is that there is never a need to call Task.Run, or Task.Factory.StartNew, or use a Task constructor.
First, some message handlers will only need to process or modify the incoming request and not touch the response. In these cases the code can just return the result of the base SendAsync method (from DelegatingHandler). A good example is the MethodOverrideHandler sample on the ASP.NET site, which uses the presence of a header to change the HTTP method and thereby support clients or environments where HTTP PUT and DELETE messages are otherwise impossible.
public class MethodOverrideHandler : DelegatingHandler { readonly string [] _methods = { "DELETE" , "HEAD" , "PUT" }; const string _header = "X-HTTP-Method-Override" ; protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (request.Method == HttpMethod.Post && request.Headers.Contains(_header)) { var method = request.Headers.GetValues(_header).FirstOrDefault(); if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase)) { request.Method = new HttpMethod(method); } } return base .SendAsync(request, cancellationToken); } } |
Notice the code to return base.SendAsync as the last line of the method.
In other cases, a message handler might only want to change the response and not touch the request. An example might be a message handler to add a custom header into the response message (again a sample from the ASP.NET site). Regardless of which framework version is in use, the code will need to wait for base.SendAsync to create the response. With the C# 4 compiler, a friendly way to do this is using a task's ContinueWith method to create a continuation.
protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { return base .SendAsync(request, cancellationToken).ContinueWith( (task) => { var response = task.Result; response.Headers.Add( "X-Custom-Header" , "This is my custom header." ); return response; } ); } |
With C# 5.0, the async and await magic removes the need for ContinueWith:
async protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var response = await base .SendAsync(request, cancellationToken); response.Headers.Add( "X-Custom-Header" , "This is my custom header." ); return response; } |
Another set of handlers will want to short-circuit the pipeline processing and send an immediate response. These types of handlers are generally validating an incoming message and returning an error if something is wrong (like checking for the presence of API required header or authorization token). There is no need to call into base.SendAsync, instead the handler will create a response and not allow the rest of the pipeline to execute. For example, a message handler to require an encrypted connection:
protected override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { if (!Uri.UriSchemeHttps.Equals( request.RequestUri.Scheme, StringComparison.OrdinalIgnoreCase)) { var error = new HttpError( "SSL required" ); var response = request.CreateErrorResponse(HttpStatusCode.Forbidden, error); var tsc = new TaskCompletionSource<HttpResponseMessage>(); tsc.SetResult(response); return tsc.Task; } return base .SendAsync(request, cancellationToken); } |
There is still a call to base.SendAsync for the case when the request is encrypted, but over a regular connection the code returns an error response using a TaskCompletionSource. I think of TaskCompletionSource as an adapter for Task, since I can take code that doesn't require asynch execution (or is based on asynch events, like a timer), and make the code appear task oriented.
In .NET 4.5, Task.FromResult can accomplish the same goal with less code:
var error = new HttpError( "SSL required" ); var response = request.CreateErrorResponse(HttpStatusCode.Forbidden, error); return Task.FromResult(response); |
This concludes another electrifying post on the ASP.NET WebAPI.
WebAPI Tip #7: Beautiful Message Handlers
The WebAPI framework is full of abstractions. There are controllers, filter providers, model validators, and many other components that form the plumbing of the framework. However, there are only three simple abstractions you need to know on a daily basis to build reasonable software with the framework.
- HttpRequestMessage
- HttpResponseMessage
- HttpMessageHandler
The names are self explanatory. I was tempted to say HttpRequestMessage represents an incoming HTTP message, but saying so would mislead someone into thinking the WebAPI is all about server-side programming. I think of HttpRequestMessage as an incoming message on the server, but the HttpClient uses all the same abstractions, and when writing a client the request message is an outgoing message. There is a symmetric beauty in how these 3 core abstractions work on both the server and the client.
There are 4 abstractions in the above class diagram because while an HttpMessageHandler takes an HTTP request and returns an HTTP response, it doesn't know how to work in a pipeline. Multiple handlers can form a pipeline, or a chain of responsibility, which is useful for processing a network request. When you host the WebAPI in IIS with ASP.NET, you'll have a pipeline in IIS feeding requests to a pipeline in ASP.NET, which in turn gives control to a WebAPI pipeline. It's pipelines all the way down (to the final handler and back). It's the WebAPI pipeline that ultimately calls an ApiController in a project. You might never need to write a custom message handler, but if you want to work in the pipeline to inspect requests, check cookies, enforce SSL connections, modify headers, or log responses, then those types of scenarios are great for message handlers.
The 4th abstraction, DelegatingHandler, is a message handler that already knows how to work in a pipeline, thanks to an intelligent base class and an InnerHandler property. The code in a DelegatingHandler derived class doesn't need to do anything special to work in a pipeline except call the base class implementation of the SendAsync method. Here is where things can get slightly confusing because of the name (SendAsync), and the traditional notion of a pipeline. First, let's take a look at a simple message handler built on top of DelegatingHandler (it doesn't do anything useful, except to demonstrate later points):
public class DateObsessedHandler : DelegatingHandler { protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var requestDate = request.Headers.Date; // do something with the date ... var response = await base .SendAsync(request, cancellationToken); var responseDate = response.Headers.Date; // do something with the date ... return response; } } |
First, there is nothing pipeline specific in the code except for deriving from the DelegatingHandler base class and calling base.SendAsync (in other words, you never have to worry about using InnerHandler).
Secondly, we tend to think of a message pipeline as unidirectional. A message arrives at one end of the pipeline and passes through various components until it reaches a terminal handler. The pipeline for WebAPI message handlers is is bidirectional. Request messages arrive and pass through the pipeline until some handler generates a response, at which point the response message flows back out of the pipeline.
The purpose of SendAsync then, isn't just about sending. The purpose is to take a request message, and send a response message.
All the code in the method before the call to base.SendAsync is code to process the request message. You can check for required cookies, enforce an SSL connection, or change properties of the request for handlers further down the pipeline (like changing the HTTP method when a particular HTTP header is present).
When the call to base.SendAsync happens, the message continues to flow through the pipeline until a handler generates a response, and the response comes back in the other direction. Symmetry.
All the code in the method after the call to base.SendAsync is code to process the response message. You can add additional headers to the response, change the status code, and perform other post processing activities.
Most message handlers will probably only inspect the request or the response, not both. My sample code wanted to show both to show the two part nature of SendAsync. Once I started thinking of SendAsync as being divided into two parts, and how the messages flow up and down the pipeline, working with the WebAPI became easier. In a future post we'll look at a more realistic message handler that can inspect the request and short circuit the response.
Finally, it's easy to add a DelegatingHandler into the global message pipeline (this is for server code hosted in ASP.NET):
GlobalConfiguration.Configuration .MessageHandlers .Add( new DateObsessedHandler()); |
But remember, you can also use the same message handlers on the client with HttpClient and a client pipeline, which is a another reason the WebAPI is symmetrically beautiful.