Прикручиваем к Проекту
Swashbuckle — это NuGet пакет, встраивающий в WebAPI автогенерацию информации об узлах в соответствии со спецификацией OpenAPI. Эта спецификация является, дефакто, стандартом, как некогда WSDL. Для установки, потребуется четыре простых шага.
- Устанавливаем из NuGet командой
Install-Package Swashbuckle
- Включаем XML документацию в настройках проекта
- В файле
SwaggerConfig.cs
, который создаётся с установкой пакета, раскомментируем строкуc.IncludeXmlComments(GetXmlCommentsPath());
- В реализации метода
GetXmlCommentsPath()
пишемreturn string.Format(@"{0}\bin\BookStoreApiService.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
Комментарии (0)