ASP.NET Web API中的 HTTP 消息处理程序

消息处理程序是接收 HTTP 请求并返回 HTTP 响应的类。 消息处理程序派生自抽象的 HttpMessageHandler 类。

通常,一系列消息处理程序链接在一起。 第一个处理程序接收 HTTP 请求,执行一些处理,并将请求提供给下一个处理程序。 在某个时候,会创建响应并返回链。 此模式称为 委派 处理程序。

链接在一起的消息处理程序示意图,说明了接收 H T T P 请求并返回 H T P 响应的过程。

Server-Side消息处理程序

在服务器端,Web API 管道使用一些内置消息处理程序:

  • HttpServer 从主机获取请求。
  • HttpRoutingDispatcher 根据路由调度请求。
  • HttpControllerDispatcher 将请求发送到 Web API 控制器。

可以将自定义处理程序添加到管道。 消息处理程序适用于在 HTTP 消息 (级别运行的交叉问题,而不是) 控制器操作。 例如,消息处理程序可能:

  • 读取或修改请求标头。
  • 向响应添加响应标头。
  • 在请求到达控制器之前对其进行验证。

此图显示了插入管道中的两个自定义处理程序:

服务器端消息处理程序的示意图,其中显示了插入到 Web A P I 管道中的两个自定义处理程序。

注意

在客户端,HttpClient 还使用消息处理程序。 有关详细信息,请参阅 HttpClient 消息处理程序

自定义消息处理程序

若要编写自定义消息处理程序,请从 System.Net.Http.DelegatingHandler 派生并重写 SendAsync 方法。 此方法具有以下签名:

Task<HttpResponseMessage> SendAsync(
    HttpRequestMessage request, CancellationToken cancellationToken);

方法采用 HttpRequestMessage 作为输入,并异步返回 HttpResponseMessage。 典型的实现将执行以下操作:

  1. 处理请求消息。
  2. 调用 base.SendAsync 以将请求发送到内部处理程序。
  3. 内部处理程序返回响应消息。 (此步骤是异步的。)
  4. 处理响应并将其返回到调用方。

下面是一个简单示例:

public class MessageHandler1 : DelegatingHandler
{
    protected async override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        Debug.WriteLine("Process request");
        // Call the inner handler.
        var response = await base.SendAsync(request, cancellationToken);
        Debug.WriteLine("Process response");
        return response;
    }
}

注意

base.SendAsync 的调用是异步的。 如果处理程序在此调用后执行了任何工作,请使用 await 关键字 (keyword) ,如下所示。

委托处理程序还可以跳过内部处理程序并直接创建响应:

public class MessageHandler2 : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Create the response.
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StringContent("Hello!")
        };

        // Note: TaskCompletionSource creates a task that does not contain a delegate.
        var tsc = new TaskCompletionSource<HttpResponseMessage>();
        tsc.SetResult(response);   // Also sets the task state to "RanToCompletion"
        return tsc.Task;
    }
}

如果委托处理程序在不调用 base.SendAsync的情况下创建响应,则请求将跳过管道的其余部分。 这对于验证请求 (创建错误响应) 的处理程序非常有用。

自定义消息处理程序示意图,说明在不调用基点 Send Async 的情况下创建响应的过程。

将处理程序添加到管道

若要在服务器端添加消息处理程序,请将处理程序添加到 HttpConfiguration.MessageHandlers 集合。 如果使用“ASP.NET MVC 4 Web 应用程序”模板创建项目,则可以在 WebApiConfig 类中执行此操作:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.MessageHandlers.Add(new MessageHandler1());
        config.MessageHandlers.Add(new MessageHandler2());

        // Other code not shown...
    }
}

消息处理程序的调用顺序与 MessageHandlers 集合中的出现顺序相同。 因为它们是嵌套的,因此响应消息会向另一个方向传播。 也就是说,最后一个处理程序是第一个获取响应消息的处理程序。

请注意,无需设置内部处理程序;Web API 框架会自动连接消息处理程序。

如果是 自承载,请创建 HttpSelfHostConfiguration 类的实例,并将处理程序添加到 MessageHandlers 集合。

var config = new HttpSelfHostConfiguration("http://localhost");
config.MessageHandlers.Add(new MessageHandler1());
config.MessageHandlers.Add(new MessageHandler2());

现在,我们来看看自定义消息处理程序的一些示例。

示例:X-HTTP-Method-Override

X-HTTP-Method-Override 是一个非标准 HTTP 标头。 它专为无法发送某些 HTTP 请求类型(如 PUT 或 DELETE)的客户端而设计。 相反,客户端会发送一个 POST 请求,并将 X-HTTP-Method-Override 标头设置为所需的方法。 例如:

X-HTTP-Method-Override: PUT

下面是一个消息处理程序,它添加了对 X-HTTP-Method-Override 的支持:

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)
    {
        // Check for HTTP POST with the X-HTTP-Method-Override header.
        if (request.Method == HttpMethod.Post && request.Headers.Contains(_header))
        {
            // Check if the header value is in our methods list.
            var method = request.Headers.GetValues(_header).FirstOrDefault();
            if (_methods.Contains(method, StringComparer.InvariantCultureIgnoreCase))
            {
                // Change the request method.
                request.Method = new HttpMethod(method);
            }
        }
        return base.SendAsync(request, cancellationToken);
    }
}

SendAsync 方法中,处理程序检查请求消息是否为 POST 请求,以及它是否包含 X-HTTP-Method-Override 标头。 如果是这样,它将验证标头值,然后修改请求方法。 最后,处理程序调用 base.SendAsync 将消息传递给下一个处理程序。

当请求到达 HttpControllerDispatcher 类时, HttpControllerDispatcher 将根据更新的请求方法路由请求。

示例:添加自定义响应标头

下面是一个消息处理程序,它将自定义标头添加到每个响应消息:

// .Net 4.5
public class CustomHeaderHandler : DelegatingHandler
{
    async protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
        response.Headers.Add("X-Custom-Header", "This is my custom header.");
        return response;
    }
}

首先,处理程序调用 base.SendAsync 以将请求传递给内部消息处理程序。 内部处理程序返回响应消息,但它使用 Task<T> 对象异步执行此操作。 响应消息在异步完成之前 base.SendAsync 不可用。

此示例使用 await 关键字 (keyword) 在完成后异步SendAsync执行工作。 如果面向 4.0 .NET Framework,请使用任务<T>。ContinueWith 方法:

public class CustomHeaderHandler : DelegatingHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken).ContinueWith(
            (task) =>
            {
                HttpResponseMessage response = task.Result;
                response.Headers.Add("X-Custom-Header", "This is my custom header.");
                return response;
            }
        );
    }
}

示例:检查 API 密钥

某些 Web 服务要求客户端在其请求中包含 API 密钥。 以下示例演示消息处理程序如何检查有效 API 密钥的请求:

public class ApiKeyHandler : DelegatingHandler
{
    public string Key { get; set; }

    public ApiKeyHandler(string key)
    {
        this.Key = key;
    }

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (!ValidateKey(request))
        {
            var response = new HttpResponseMessage(HttpStatusCode.Forbidden);
            var tsc = new TaskCompletionSource<HttpResponseMessage>();
            tsc.SetResult(response);    
            return tsc.Task;
        }
        return base.SendAsync(request, cancellationToken);
    }

    private bool ValidateKey(HttpRequestMessage message)
    {
        var query = message.RequestUri.ParseQueryString();
        string key = query["key"];
        return (key == Key);
    }
}

此处理程序在 URI 查询字符串中查找 API 密钥。 (对于此示例,我们假定密钥是静态字符串。实际实现可能会使用更复杂的 validation.) 如果查询字符串包含 键,处理程序会将请求传递给内部处理程序。

如果请求没有有效的密钥,处理程序会创建状态为 403(禁止)的响应消息。 在这种情况下,处理程序不调用 base.SendAsync,因此内部处理程序从不接收请求,控制器也不会接收请求。 因此,控制器可以假定所有传入请求都具有有效的 API 密钥。

注意

如果 API 密钥仅适用于某些控制器操作,请考虑使用操作筛选器而不是消息处理程序。 执行 URI 路由后运行操作筛选器。

Per-Route消息处理程序

HttpConfiguration.MessageHandlers 集合中的处理程序全局应用。

或者,在定义路由时,可以将消息处理程序添加到特定路由:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Routes.MapHttpRoute(
            name: "Route1",
            routeTemplate: "api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Routes.MapHttpRoute(
            name: "Route2",
            routeTemplate: "api2/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional },
            constraints: null,
            handler: new MessageHandler2()  // per-route message handler
        );

        config.MessageHandlers.Add(new MessageHandler1());  // global message handler
    }
}

在此示例中,如果请求 URI 与“Route2”匹配,则会将请求调度到 MessageHandler2。 下图显示了这两个路由的管道:

每个路由消息处理程序管道的示意图,说明通过定义路由将消息处理程序添加到特定路由的过程。

请注意, MessageHandler2 将替换默认的 HttpControllerDispatcher。 在此示例中, MessageHandler2 创建响应,并且与“Route2”匹配的请求永远不会转到控制器。 这样,便可以将整个 Web API 控制器机制替换为自己的自定义终结点。

或者,按路由的消息处理程序可以委托给 HttpControllerDispatcher,后者然后调度到控制器。

每个路由消息处理程序管道的示意图,其中显示了委托给 h t t p 控制器调度程序的进程,该调度程序随后调度到控制器。

以下代码演示如何配置此路由:

// List of delegating handlers.
DelegatingHandler[] handlers = new DelegatingHandler[] {
    new MessageHandler3()
};

// Create a message handler chain with an end-point.
var routeHandlers = HttpClientFactory.CreatePipeline(
    new HttpControllerDispatcher(config), handlers);

config.Routes.MapHttpRoute(
    name: "Route2",
    routeTemplate: "api2/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional },
    constraints: null,
    handler: routeHandlers
);