Обработчики сообщений HTTP в веб-API ASP.NET
Обработчик сообщений — это класс, который получает HTTP-запрос и возвращает HTTP-ответ. Обработчики сообщений являются производными от абстрактного класса HttpMessageHandler .
Как правило, ряд обработчиков сообщений объединяются в цепочку. Первый обработчик получает HTTP-запрос, выполняет некоторую обработку и передает запрос следующему обработчику. В какой-то момент создается ответ и возвращается вверх по цепочке. Этот шаблон называется делегированным обработчиком.
Обработчики сообщений Server-Side
На стороне сервера конвейер веб-API использует некоторые встроенные обработчики сообщений:
- HttpServer получает запрос от узла.
- HttpRoutingDispatcher отправляет запрос на основе маршрута.
- HttpControllerDispatcher отправляет запрос контроллеру веб-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" для создания проекта, это можно сделать в классе 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 . Так как они являются вложенными, ответное сообщение перемещается в другом направлении. То есть последний обработчик является первым, кто получает ответные сообщения.
Обратите внимание, что вам не нужно задавать внутренние обработчики. платформа веб-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, используйте задачу<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
Некоторые веб-службы требуют, чтобы клиенты включали ключ 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);
}
}
Этот обработчик ищет ключ API в строке запроса URI. (В этом примере предполагается, что ключ является статической строкой. Реальная реализация, вероятно, будет использовать более сложную проверку.) Если строка запроса содержит ключ, обработчик передает запрос внутреннему обработчику.
Если у запроса нет допустимого ключа, обработчик создает ответное сообщение с состоянием 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, никогда не отправляются к контроллеру. Это позволяет заменить весь механизм контроллера веб-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
);