ASP.NET Web API 中的 HTTP 訊息處理常式
訊息處理常式是接收 HTTP 要求的類別,並傳回 HTTP 回應。 訊息處理常式衍生自抽象 HttpMessageHandler 類別。
一般而言,一系列的訊息處理常式會鏈結在一起。 第一個處理常式會接收 HTTP 要求、執行一些處理,並將要求提供給下一個處理常式。 在某些時候,會建立回應並備份鏈結。 此模式稱為委派處理常式。
伺服器端訊息處理常式
在伺服器端,Web API 管線會使用一些內建訊息處理常式:
- HttpServer 從主機取得要求。
- HttpRoutingDispatcher 會根據路由分派要求。
- HttpControllerDispatcher 會將要求傳送至 Web API 控制器。
您可以將自訂處理常式新增至管線。 訊息處理常式適用於在 HTTP 訊息層級運作的跨領域關注 (而不是控制器動作)。 例如,訊息處理常式可能會:
- 讀取或修改要求標頭。
- 將回應標頭新增至回應。
- 在要求到達控制器之前先驗證要求。
此圖顯示插入管線的自訂處理常式:
注意
在用戶端上,HttpClient 也會使用訊息處理常式。 有關詳細資訊,請參閱 HttpClient 訊息處理常式。
自訂訊息處理常式
若要撰寫自訂訊息處理常式,請衍生自 System.Net.Http.DelegatingHandler 並覆寫 SendAsync 方法。 此方法具有下列簽章:
Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken);
方法會採用 HttpRequestMessage 作為輸入,並以非同步方式傳回 HttpResponseMessage。 一般實作會執行下列操作:
- 處理要求訊息。
- 呼叫
base.SendAsync
以將要求傳送至內部處理常式。 - 內部處理常式會傳回回應訊息。 (此步驟是非同步的。)
- 處理回應並將它傳回給呼叫端。
以下是一個簡單的範例:
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
的情況下建立回應,要求會略過管線的其餘部分。 這對於驗證要求的處理常式很實用 (建立錯誤回應)。
將處理常式新增至管線
若要在伺服器端新增訊息處理常式,請將處理常式新增至 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<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 金鑰。 (在此範例中,我們假設金鑰是靜態字串。實際的實作可能會使用更複雜的驗證。如果查詢字串包含金鑰,處理常式會將要求傳遞至內部處理常式。
如果要求沒有有效的金鑰,處理常式會建立狀態為 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
。 下圖顯示這兩個路由的管線:
請注意,MessageHandler2
取代了預設的 HttpControllerDispatcher。 在此範例中,MessageHandler2
建立回應,並要求符合「Route2」永遠不會移至控制器。 這可讓您將整個 Web API 控制器機制取代為您自己的自訂端點。
或者,個別路由訊息處理常式可以委派給 HttpControllerDispatcher,然後分派至控制器。
下列程式碼會示範如何設定此路由:
// 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
);