Кто хоть раз тестировал свой WebAPI знает такие инструемнты, как Postman или Advanced REST (экстеншены для Chrome). Эти инструемнты всем удобны, кроме того, что не умеют сами узнавать какие модели принимает API, какие отдает и не предоставляет информацию обо всех возможных эндпоинтах. Это неудобство решает пакет Swashbuckle, который встраивает в проект генерацию Swagger спецификации и UI. Под катом коротко о том, как его прикрутить к проекту и некоторые детали относительно авторизации и работы с «перегруженными» эндпоинтами.
Прикручиваем к Проекту
Swashbuckle — это NuGet пакет, встраивающий в WebAPI автогенерацию информации об узлах в соответствии со спецификацией OpenAPI. Эта спецификация является, дефакто, стандартом, как некогда WSDL. Для установки, потребуется четыре простых шага.
- Устанавливаем из NuGet командой
Install-Package Swashbuckle
- Включаем XML документацию в настройках проекта
- В файле
SwaggerConfig.cs
, который создаётся с установкой пакета, раскомментируем строкуc.IncludeXmlComments(GetXmlCommentsPath());
- В реализации метода
GetXmlCommentsPath()
пишемreturn string.Format(@"{0}binBookStoreApiService.XML", AppDomain.CurrentDomain.BaseDirectory);
Всё. Дальше необходимо описать методы API, response codes и кастомизировать далее.
Нюансы при Деплое WebAPI
При деплое WebAPI в продакшн может возникнуть проблема с тем, что XML файл отсутствует. Релиз сборка не включает их по умолчанию, но можно это обойти, подредактировав csproj файл. Надо в PropertyGroup проекта добавить <ExcludeXmlAssemblyFiles>false</ExcludeXmlAssemblyFiles>
и файл останется в bin/
.
Другая проблема подстерегает тех, кто прячет свой API за прокси. Решение не является универсальным, но в моем случае работает. Прокси добавляет хедеры к реквесту, по которым мы узнаём, какой должен быть URL ендпонитов для клиента.
// в файле SwaggerConfig.cs
c.RootUrl(req => ComputeClientHost(req));
// ниже пишем реализацию метода
public static string ComputeClientHost(HttpRequestMessage req)
{
var authority = req.RequestUri.Authority;
var scheme = req.RequestUri.Scheme;
// получаем хост, который видит клиент
if (req.Headers.Contains("X-Forwarded-Host"))
{
// в случае с цепочкой прокси необходимо взять самый первый
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
var firstForwardedHost = xForwardedHost.Split(',')[0];
authority = firstForwardedHost;
}
// получаем протокл, который используется клиентом
if (req.Headers.Contains("X-Forwarded-Proto"))
{
var xForwardedProto = req.Headers.GetValues("X-Forwarded-Proto").First();
xForwardedProto = xForwardedProto.Split(',')[0];
scheme = xForwardedProto;
}
return scheme + "://" + authority;
}
Добавляем Response Codes
Возвращаемые HTTP Status Codes можно добавить двумя способами: с помощью XML комментариев и с помощью атрибутов.
/// <response code="404">Not Found</response>
[SwaggerResponse(HttpStatusCode.NotFound, Type = typeof(Model), Description = "Not Found: no such endpoint")]
При этом необхродимо помнить, что XML комментарии имеют приоритет перед атрибутами. Последние будут проигнорированы, если два способа одновременно будут использованы для одного и того же метода. Так же, если используются XML комментарии, то указывать необходимо все кода, включая 200 (OK), а возвращаемую модель указать невозможно. Поэтому использование SwaggerResponse предпочтительнее, т.к. он лишен этих недостатков. Когда эндпоинт возвращает другой код, например 201 (Created), вместо дефолтного 200, первый необходимо удалить атрибутом [SwaggerResponseRemoveDefaults]
.
Для ленивых есть возможность добавить общие кода (например 400 (BadRequest) или 401 (Unauthorized)) сразу ко всем методам. Для этого надо реализовать интерфейс IOperationFilter и зарегистрировать такой класс с помощью c.OperationFilter<T>();.
HttpStatusCode[] _codes; // коды для добавления
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
// не всегда эта пропертя инициализирована
if (operation.responses == null)
operation.responses = new Dictionary<string, Response>();
foreach (var code in _codes) {
var codeNum = ((int)code).ToString();
var codeName = code.ToString();
// добавляем описание
if (!operation.responses.ContainsKey(codeNum))
operation.responses.Add(codeNum, new Response { description = codeName });
}
}
Авторизация WebAPI и Swashbuckle
В тексте ниже рассматривается несколько вариантов реализации Basic авторизации. Но пакет поддерживает и другие.
Если используется AuthorizeAttribute то Swashbuckle построит UI, но запросы не пройдут. Есть несколько путей предоставления этой информации:
- через встроеную в браузер авторизацию
- через встроеную форму авторизации в пакете
- через параметры операций
- через javascript
Встроеная в Браузер
Встроеная в браузер авторизация будет доступна «из коробки», если используется атрибут и фильтр:
// Basic Authorization attributes
config.Filters.Add(new AuthorizeAttribute());
config.Filters.Add(new BasicAuthenticationFilter()); // реализация IAuthenticationFilter
Добавив их в конфигурации WebAPI, браузер предложит ввести данные для аутентификации в момент выполнения запроса. Сложность тут в том, что сбросить эти данные не так удобно и быстро, как ввести.
Встроеная Форма Авторизации в Swashbuckle
Другой способ удобнее в этом плане, т.к. предоставляет специальную форму. Чтобы включить встроеную форму аутентификации в пакет необходимо сделать следующее:
- как и выше включить атрибут и фильтр для аутентификации
- в настройках Swagger разкомментировать строку
c.BasicAuth("basic").Description("Basic HTTP Authentication");
- добавить специальный IOperationFilter, добавляющий информацию об этом в узлы
c.OperationFilter<MarkSecuredMethodsOperationFilter>();
public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
var filterPipeline = apiDescription.ActionDescriptor.GetFilterPipeline();
// check if authorization is required
var isAuthorized = filterPipeline
.Select(filterInfo => filterInfo.Instance)
.Any(filter => filter is IAuthorizationFilter);
// check if anonymous access is allowed
var allowAnonymous = apiDescription.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any();
if (isAuthorized && !allowAnonymous)
{
if (operation.security == null)
operation.security = new List<IDictionary<string, IEnumerable<string>>>();
var auth = new Dictionary<string, IEnumerable<string>>
{
{"basic", Enumerable.Empty<string>()}
};
operation.security.Add(auth);
}
}
После этого можно будет использовать такую форму авторизации, а введенные данные будут использоваться для всех запросов.
Авторизация Параметром и JS Кодом
Следующие два способа следует рассматривать, как примеры работы с IOperationFilter и инжектированием своего JavaScript.
Параметры могут отправлять данные не только в body и query, но и в header. В этом случае надо будет вводить хеш.
operation.parameters.Add(new Parameter
{
name = "Authorization",
@in = "header", // обозначим, что значение отправится в хедере
description = "Basic U3dhZ2dlcjpUZXN0", // Basic Swagger:Test
required = true, // обязательность параметра
type = "string"
});
С помощью инжектирования своего JavaScript тоже можно отправлять данные в хедере запросов. Для этого необходимо сделать следующее:
- добавить JS файл, как embedded ресурс
- в конфигурации Swagger разскомментировать строку и указать свой файл как имя ресурса:
c.InjectJavaScript(thisAssembly, "assembly.namesapce.swagger-basic-auth.js");
- в файле написать так:
swaggerUi.api.clientAuthorizations.add("basic", new SwaggerClient.ApiKeyAuthorization("Authorization", "Basic U3dhZ2dlcjpUZXN0", "header"));
Теперь эти данные будут добавляться в виде хедера к каждому запросу. Вообще, с помощью этого JS кода можно отправить любые хедеры, как я понял. Параметр key, который равен «basic» в примере, должен быть уникальным, чтобы не выскочила JS ошибка в момент отправки запроса.
swaggerUi.api.clientAuthorizations.add("custom1", new SwaggerClient.ApiKeyAuthorization("X-Header-1", "value1", "header"));
swaggerUi.api.clientAuthorizations.add("custom2", new SwaggerClient.ApiKeyAuthorization("X-Header-2", "value2", "header"));
swaggerUi.api.clientAuthorizations.add("custom3", new SwaggerClient.ApiKeyAuthorization("X-Header-3", "value3", "header"));
Работаем с Обязательными Хедерами
В некоторых случаях неавторизационные хедеры могут быть обязательными. Например, хедеры с информацией о клиенте. Обычно, в pipeline WebAPI встраивается message handler, а именно реализуется DelegatingHandler и регистрируется в конфигурации WebAPI config.MessageHandlers.Add(new MandatoryHeadersHandler());
. В таком случае Swagger перестанет показывать что-либо, т.к. запросы к нему не пройдут, т.к. хендлер их запретит. Из коробки это никак не решается, поэтому необходимо предусмотреть данный случай в своем хендлере. Т.е. в случае запроса к URL swagger пропускать его. А далее поможет добавление хедеров с помощью JS, как описывалось выше.
Эндпоинты с Перегруженными Методами
WebAPI позволяет создавать несколько экшн-методов для одного эндпоинта, вызов которых зависит от параметров запроса.
[ResponseType(typeof (IList<Model>))]
public IHttpActionResult Get() {...}
[ResponseType(typeof (IList<Model>))]
public IHttpActionResult Get(int count, bool descending) {...}
Такие методы не поддерживаются Swagger по умолчанию и UI выдаст ошибку 500: Not supported by Swagger 2.0: Multiple operations with path 'api/<URL>' and method '<METHOD>'. See the config setting — «ResolveConflictingActions» for a potential workaround.
Как и советуеся в сообщении, следует самостоятельно решить ситуацию и есть несколько вариантов:
- выбрать только один метод
- сделать один метод со всеми параметрами
- изменить генерацию документа
первый и второй способы реализуются с помощью настройки c.ResolveConflictingActions(Func<IEnumerable<ApiDescription>, ApiDescription> conflictingActionsResolver)
. Суть метода сводится к тому, чтобы взять несколько конфликтующих методов и вернуть один.
return apiDescriptions =>
{
var descriptions = apiDescriptions as ApiDescription[] ?? apiDescriptions.ToArray();
var first = descriptions.First(); // строим относительно первого метода
var parameters = descriptions.SelectMany(d => d.ParameterDescriptions).ToList();
first.ParameterDescriptions.Clear();
// добавляем все параметры и делаем их опциональными
foreach (var parameter in parameters)
if (first.ParameterDescriptions.All(x => x.Name != parameter.Name))
{
first.ParameterDescriptions.Add(new ApiParameterDescription
{
Documentation = parameter.Documentation,
Name = parameter.Name,
ParameterDescriptor = new OptionalHttpParameterDescriptor((ReflectedHttpParameterDescriptor) parameter.ParameterDescriptor),
Source = parameter.Source
});
}
return first;
};
// это наследование необходимо, т.к. IsOptional имеет только getter
public class OptionalHttpParameterDescriptor : ReflectedHttpParameterDescriptor
{
public OptionalHttpParameterDescriptor(ReflectedHttpParameterDescriptor parameterDescriptor)
: base(parameterDescriptor.ActionDescriptor, parameterDescriptor.ParameterInfo)
{
}
public override bool IsOptional => true;
}
Крадинальный Способ
Третий способ более кардинальный и является отхождением от OpenAPI спецификации. Можно вывести все эндпоинты с параметрами:
Для этого необходимо изменить способ генерации документа Swagger с помощью IDocumentFilter и сгенерировать описание самостоятельно.
В жизни такой способ редко когда понадобится, поэтому копнем еще глубже. Еще один способ, который я рекомендовал бы только тем, кому интересны внутренности Swashbuckle — это заменить SwaggerGenerator. Это делается в строчке c.CustomProvider(defaultProvider => new NewSwaggerProvider(defaultProvider));
. Что бы это сделать, можно поступить так:
- создать свой class MySwaggerGenerator: ISwaggerProvider
- в репозитории Swashbuckle на GitHub найти SwaggerGenerator.cs (он тут)
- скопировать метод GetSwagger и другие связанные с ним методы в свой
- продублировать внутренние переменные и инициализировать их в конструкторе своего класса
- зарегистрировать в конфигурации Swagger
private readonly IApiExplorer _apiExplorer;
private readonly IDictionary<string, Info> _apiVersions;
private readonly JsonSerializerSettings _jsonSerializerSettings;
private readonly SwaggerGeneratorOptions _options;
public MultiOperationSwaggerGenerator(ISwaggerProvider sp)
{
var sg = (SwaggerGenerator) sp;
var privateFields = sg.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic);
_apiExplorer = privateFields.First(pf => pf.Name == "_apiExplorer").GetValue(sg) as IApiExplorer;
_jsonSerializerSettings = privateFields.First(pf => pf.Name == "_jsonSerializerSettings").GetValue(sg) as JsonSerializerSettings;
_apiVersions = privateFields.First(pf => pf.Name == "_apiVersions").GetValue(sg) as IDictionary<string, Info>;
_options = privateFields.First(pf => pf.Name == "_options").GetValue(sg) as SwaggerGeneratorOptions;
}
После этого надо найти место var paths = GetApiDescriptionsFor(apiVersion)....
. Это то место, где создаются пути. Например, чтобы получить то, что в примере, необходимо GroupBy() заменить на .GroupBy(apiDesc => apiDesc.RelativePath)
.
Литература
- Swagger example
- RESTful Web API specification formats
- Customize Swashbuckle-generated API definitions
- Swagger object schema
- Authentication Filters in ASP.NET Web API 2
- A WebAPI Basic Authentication Authorization Filter
- Customize Authentication Header in SwaggerUI using Swashbuckle
- HTTP Message Handlers in ASP.NET Web API
- Managing Action Conflicts in ASP.Net 5 with Swashbuckle
- Tutorial Swagger project at GitHub
Автор: ETman