То есть этот метод мог бы стать отличным средством для документирования наших REST-сервисов с одной стороны, и быть существенным дополнением к архитектурному ограничению HATEOAS с другой.
А теперь давайте отвлечёмся от страшных слов типа “HATEOAS” и зададимся вопросом: а есть ли какая-нибудь практическая польза от использования метода OPTIONS в веб-приложениях?
HTTP Message Handlers.
Унаследуем наш новый обработчик от класса DelegatingHandler, перегрузим метод SendAsync и добавим новую функциональность как продолжение таска. Это важно, поскольку мы хотим запустить вначале базовый механизм маршрутизации. В таком случае переменная request будет содержать все необходимые свойства.
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
return await base.SendAsync(request, cancellationToken).ContinueWith(
task =>
{
var response = task.Result;
if (request.Method == HttpMethod.Options)
{
var methods = new Actionselector(request).GetSupportedMethods();
if (methods != null)
{
response = new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(string.Empty)
};
response.Content.Headers.Add("Allow", methods);
response.Content.Headers.Add("Allow", "OPTIONS");
}
}
return response;
}, cancellationToken);
}
Класс Actionselector пытается найти подходящий контроллер для запроса в конструкторе. Если контроллер не найден, метод GetSupportedMethods вернёт null. Функция IsMethodSupported подменяет текущий запрос в контексте для того, чтобы найти action-метод по заданному имени. Блок finally восстанавливает RouteData, поскольку вызов _apiSelector.SelectAction может изменить это свойство контекста.
private class Actionselector
{
private readonly HttpRequestMessage _request;
private readonly HttpControllerContext _context;
private readonly ApiControllerActionselector _apiSelector;
private static readonly string[] Methods =
{ "GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "TRACE" };
public Actionselector(HttpRequestMessage request)
{
try
{
var configuration = request.GetConfiguration();
var requestContext = request.GetRequestContext();
var controllerDescriptor =
new DefaultHttpControllerSelector(configuration)
.SelectController(request);
_context = new HttpControllerContext
{
Request = request,
RequestContext = requestContext,
Configuration = configuration,
ControllerDescriptor = controllerDescriptor
};
}
catch
{
return;
}
_request = _context.Request;
_apiSelector = new ApiControllerActionselector();
}
public IEnumerable<string> GetSupportedMethods()
{
return _request == null ? null : Methods.Where(IsMethodSupported);
}
private bool IsMethodSupported(string method)
{
_context.Request = new HttpRequestMessage(
new HttpMethod(method), _request.RequestUri);
var routeData = _context.RouteData;
try
{
return _apiSelector.SelectAction(_context) != null;
}
catch
{
return false;
}
finally
{
_context.RouteData = routeData;
}
}
}
Финальный шаг – это добавление нашего обработчика в конфигурацию в startup-коде:
configuration.MessageHandlers.Add(new OptionsHandler());
Чтобы всё корректно работало, необходимо явно указывать типы параметров. Если используется атрибут Route, тип надо указывать прямо в нём:
[Route("Books/{id:long}", Name = "GetBook")]
Без явного указания типа путь Books/abcd будет рассматриваться как корректный.
Итак, теперь мы имеем реализацию OPTIONS для всех поддерживаемых Uri в сервисе. Однако, это всё ещё не идеальное решение. Описанный подход никак не учитывает авторизацию. Если мы используем маркеры доступа и каждый вызов сервиса предусматривает наличие текущего принципала, то его права никак не учитываются и ценность метода OPTIONS резко снижается. Для любого пользователя клиент всегда будет получать один и тот же список допустимых HTTP методов независимо от его прав.
Кроме того, сервис будет отвечать на запрос с кодом 200 OK даже если Uri содержит неверный идентификатор ресурса, например, /books/0.
Проблема возникает везде, где используются идентификаторы ресурсов. Выходом из этого может стать ручное добавление в контроллеры реализаций HttpOptions там, где это нужно. Эти action-методы должны проводить проверку прав пользователя при формировании списка допустимых методов. При этом наш OptionsHandler должен использовать ответ из контроллера, если он имеется.
Финальный код
Авторский перевод
Источник: Хабрахабр