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

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

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

連結在一起的訊息處理常式圖表,說明接收 H T T P 要求並傳回 H T 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 關鍵字,如下所示。

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

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 ,回應訊息無法使用。

此範例會在完成之後 SendAsync ,使用await關鍵字以非同步方式執行工作。 如果您要將目標設為 .NET Framework 4.0,請使用工作< 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
);