Налаштування Swashbuckle (Swagger) для WebAPI

Хто хоч раз тестував свій WebAPI знає такі инструемнты, як Postman або Advanced REST (экстеншены для Chrome). Ці инструемнты всім зручні, крім того, що не вміють самі дізнаватися які моделі приймає API, які віддає і не надає інформацію про всі можливі эндпоинтах. Це незручність вирішує пакет Swashbuckle, який вбудовується в проект генерацію Swagger специфікації та UI. Під катом коротко про те, як його прикрутити до проекту і деякі деталі щодо авторизації і роботи з «перевантаженим» эндпоинтами.

Прикручуємо до Проекту
Swashbuckle — це NuGet пакет, встраивающий в WebAPI автогенерацию інформації про вузлах у відповідності зі специфікацією OpenAPI. Ця специфікація є, дефакто, стандартом, як колись WSDL. Для установки буде потрібно чотири простих кроки.
  1. Встановлюємо з NuGet командою
    Install-Package Swashbuckle
  2. Включаємо XML документацію в настроюваннях проекту
  3. У файлі
    SwaggerConfig.cs
    , який створюється з установкою пакета, раскомментируем рядок
    c.IncludeXmlComments(GetXmlCommentsPath());
  4. В реалізації методу
    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 ендпонитов для клієнта.
Приклад розпізнання 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>();.
Приклад методу Apply для додавання деякого списку кодів
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, але запити не пройдуть. Є кілька шляхів надання цієї інформації:
  1. через вбудовану в браузер авторизацію
  2. через вбудовану форму авторизації в пакеті
  3. через параметри операцій
  4. через javascript


Вбудована в Браузер
Вбудована в браузер авторизація буде доступна «з коробки», якщо використовується атрибут і фільтр:
// Basic Authorization attributes
config.Filters.Add(new AuthorizeAttribute());
config.Filters.Add(new BasicAuthenticationFilter()); // реалізація IAuthenticationFilter

Додавши їх у конфігурації WebAPI, браузер запропонує ввести дані для аутентифікації в момент виконання запиту. Складність тут в тому, що скинути ці дані не так зручно і швидко, як ввести.

Вбудована Форма Авторизації в Swashbuckle
Інший спосіб зручніше в цьому плані, т. к. надає спеціальну форму. Щоб включити вбудовану форму аутентифікації в пакет необхідно зробити наступне:
  1. як і вище включити атрибут і фільтр для аутентифікації
  2. у налаштуваннях Swagger розкоментувати рядок
    c.BasicAuth("basic").Description("Basic HTTP Authentication");
  3. додати спеціальний IOperationFilter, додає інформацію про це у вузли
    c.OperationFilter<MarkSecuredMethodsOperationFilter>();
Реалізація методу Apply цього фільтра
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", Перечіслімого.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 теж можна відправляти дані в хедері запитів. Для цього необхідно зробити наступне:
  1. додати JS файл, як embedded ресурс
  2. у конфігурації Swagger разскомментировать рядок і вказати свій файл, ім'я ресурсу:
    c.InjectJavaScript(thisAssembly, "assembly.namesapce.swagger-basic-auth.js");
  3. у файлі написати так:
    swaggerUi.api.clientAuthorizations.add("basic", new SwaggerClient.ApiKeyAuthorization("Authorization", "Basic U3dhZ2dlcjpUZXN0", "header"));
Тепер ці дані будуть додаватися у вигляді хедера до кожного запитом. Взагалі, з допомогою цього JS-коду можна відправити будь хедеры, як я зрозумів. Параметр key, який дорівнює «basic» в прикладі, має бути унікальним, щоб не вискочила JS помилка в момент відправлення запиту.
Наприклад JS відправляє хедеры в Swagger
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 обхідний шлях.
Як і советуеся в повідомленні, слід самостійно вирішити ситуацію і є кілька варіантів:
  1. вибрати тільки один метод
  2. зробити один метод з усіма параметрами
  3. змінити генерацію документа
перший і другий способи реалізуються з допомогою налаштування
c.ResolveConflictingActions(Func<IEnumerable<ApiDescription>, ApiDescription> conflictingActionsResolver)
. Суть методу зводиться до того, щоб взяти кілька конфліктуючих методів і повернути один.
Приклад того, як об'єднати всі параметри
return apiDescriptions =>
{
var description = 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));
. Що б це зробити, можна вчинити так:
  1. створити свій class MySwaggerGenerator: ISwaggerProvider
  2. в репозиторії Swashbuckle на GitHub знайти SwaggerGenerator.cs (він тут
  3. скопіювати метод GetSwagger та інші пов'язані з ним методи в свій
  4. продублювати внутрішні змінні та ініціалізувати їх в конструкторі свого класу
  5. зареєструвати в конфігурації 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)
.

Література
  1. Swagger example
  2. RESTful Web API specification formats
  3. Customize Swashbuckle-generated API definitions
  4. Swagger object schema
  5. Authentication Filters in ASP.NET Web API 2
  6. A WebAPI Basic Authentication Authorization Filter
  7. Customize Authentication Header in SwaggerUI using Swashbuckle
  8. HTTP Message in Handlers ASP.NET Web API
  9. Managing Action Conflicts in ASP.Net 5 with Swashbuckle
  10. Tutorial Swagger project at GitHub
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.