ASP.NET Web API 中的 HTTP 訊息處理常式

訊息處理常式是接收 HTTP 要求的類別,並傳回 HTTP 回應。 訊息處理常式衍生自抽象 HttpMessageHandler 類別。

一般而言,一系列的訊息處理常式會鏈結在一起。 第一個處理常式會接收 HTTP 要求、執行一些處理,並將要求提供給下一個處理常式。 在某些時候,會建立回應並回到鏈條上游。 此模式稱為委派處理程序。

Diagram of message handlers chained together, illustrating process to receive an H T T P request and return an H T T P response.鏈結在一起的訊息處理常式圖表,說明接收 H T T P 要求並傳回 H T T P 回應的流程。

伺服器端訊息處理程序

在伺服器端,Web API 管線會使用一些內建訊息處理常式:

  • HttpServer 從主機取得請求。
  • HttpRoutingDispatcher 會根據路由分派請求。
  • HttpControllerDispatcher 會將要求傳送至 Web API 控制器。

您可以將自訂處理常式新增至管線。 訊息處理常式適用於在 HTTP 訊息層級運作的跨領域關注 (而不是控制器動作)。 例如,訊息處理常式可能會:

  • 讀取或修改要求標頭。
  • 將回應標頭加到回應中。
  • 在要求到達控制器之前先驗證要求。

此圖顯示插入管線中的兩個自訂處理常式:

伺服器端訊息處理常式的圖示,顯示在 Web API 管線中插入的兩個自訂處理常式。

注意

在用戶端上,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 關鍵字,如以下範例所示。

委派處理常式也可以略過內部處理常式,並直接建立回應:

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 關鍵字在 SendAsync 完成後非同步執行工作。 如果您的目標是 .NET Framework 4.0,請使用 Task.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 金鑰。 (在此範例中,我們假設金鑰是靜態字串。實際的實作可能會使用更複雜的驗證。如果查詢字串包含金鑰,處理常式會將要求傳遞至內部處理常式。

如果要求沒有有效的金鑰,處理常式會建立狀態為 403 禁止的回應訊息。 在此情況下,處理常式不會呼叫 base.SendAsync,因此內部處理常式永遠不會收到要求,且控制器也不會收到要求。 因此,控制器可以假設所有傳入要求都有有效的 API 金鑰。

注意

如果 API 金鑰僅適用於特定控制器動作,請考慮使用動作篩選,而不是訊息處理常式。 URI 路由執行後,將執行動作篩選器。

個別路由訊息處理常式

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。 下圖顯示這兩個路線的管線:

Diagram of per route message handlers pipeline, illustrating process to add a message handler to a specific route by defining the route.每個路由訊息處理常式管線的圖表,說明透過定義路由將訊息處理常式新增至特定路由的流程。

請注意,MessageHandler2 取代了預設的 HttpControllerDispatcher。 在此範例中,MessageHandler2 負責建立回應,並且符合「Route2」的請求永遠不會傳送到控制器。 這可讓您將整個 Web API 控制器機制取代為您自己的自訂端點。

或者,個別路由訊息處理常式可以委派給HttpControllerDispatcher,然後分派至控制器。

個別路由訊息處理常式的管線圖表,顯示將流程委派至 HTTP 控制器分派器,然後再分派到控制器。

下列程式碼會示範如何設定此路由:

// 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
);