Описание элементов перечислений в Swashbuckle

от автора

Swagger — замечательная вещь! Он позволяет легко посмотреть, каким API обладает ваш сервис, сгенерировать клиента для него на различных языках и даже попробовать поработать с сервисом через UI. В ASP.NET Core для поддержки Swagger существует пакет Swashbuckle.AspNetCore.

Но есть один недостаток, который мне не нравится. Swashbuckle способен строить описания методов, параметров и классов, основываясь на XML-комментариях в коде .NET. Но он не показывает те описания, которые применяются непосредственно к членам перечислений.

Позвольте мне показать, о чём идёт речь.

Создание сервиса

Я создал простой Web-сервис:

/// <summary> /// Contains endpoints that use different enums. /// </summary> [Route("api/data")] [ApiController] public class EnumsController : ControllerBase {     /// <summary>     /// Executes operation of requested type and returns result status.     /// </summary>     /// <param name="id">Operation id.</param>     /// <param name="type">Operation type.</param>     /// <returns>Result status.</returns>     [HttpGet]     public Task<Result> ExecuteOperation(int id, OperationType type)     {         return Task.FromResult(Result.Success);     }      /// <summary>     /// Changes data     /// </summary>     [HttpPost]     public Task<IActionResult> Change(DataChange change)     {         return Task.FromResult<IActionResult>(Ok());     } }

Этот контроллер использует перечисления во множестве мест: в качестве аргументов методов, в качестве результатов работы методов, в качестве типов свойств более сложных объектов:

/// <summary> /// Operation types. /// </summary> public enum OperationType {     /// <summary>     /// Do operation.     /// </summary>     Do,     /// <summary>     /// Undo operation.     /// </summary>     Undo }  /// <summary> /// Operation results. /// </summary> public enum Result {     /// <summary>     /// Operations was completed successfully.     /// </summary>     Success,     /// <summary>     /// Operation failed.     /// </summary>     Failure }  /// <summary> /// Data change information. /// </summary> public class DataChange {     /// <summary>     /// Data id.     /// </summary>     public int Id { get; set; }      /// <summary>     /// Source type.     /// </summary>     public Sources Source { get; set; }      /// <summary>     /// Operation type.     /// </summary>     public OperationType Operation { get; set; } }  /// <summary> /// Types of sources. /// </summary> public enum Sources {     /// <summary>     /// In-memory data source.     /// </summary>     Memory,     /// <summary>     /// Database data source.     /// </summary>     Database }

Для поддержки Swagger я установил в проект NuGet-пакет Swashbuckle.AspNetCore. Теперь его нужно подключить. Это делается в Startup-файле:

public class Startup {     // This method gets called by the runtime. Use this method to add services to the container.     public void ConfigureServices(IServiceCollection services)     {         services.AddControllers();          services.AddSwaggerGen(c => {              // Set the comments path for the Swagger JSON and UI.             var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";             var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);             c.IncludeXmlComments(xmlPath);         });     }      // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)     {         if (env.IsDevelopment())         {             app.UseDeveloperExceptionPage();         }          app.UseSwagger();          app.UseSwaggerUI();          app.UseRouting();          ...     } }

Теперь мы можем запустить наше приложение, и по адресу http://localhost:5000/swagger/index.html мы найдём описание нашего сервиса:

Swagger UI для приложения

Но пока наши перечисления представлены просто числами:

Представление перечислений числами

Лично мне было бы удобно, если бы перечисления представлялись строковыми значениями. Они хотя бы имеют некоторый смысл в отличии от безликих чисел.

Для этого нам нужно внести небольшие изменения в настройку нашего сервиса. Я установил NuGet-пакет Swashbuckle.AspNetCore.Newtonsoft. После этого, я чуть изменил настройки сервисов. Я заменил

services.AddControllers();

на

services.AddControllers().AddNewtonsoftJson(o => {     o.SerializerSettings.Converters.Add(new StringEnumConverter     {         CamelCaseText = true     }); });

Теперь наши перечисления представлены в виде строк:

Представление перечислений строками

Но и у этого представления есть, на мой взгляд, один недостаток. XML-комментарии, которые были назначены отдельным членам перечислений, не отображаются в Swagger UI.

Описание типов перечислений

Давайте посмотрим, как нам вернуть их. Большей частью поиск в интернете на эту тему ничего не дал мне. Но, в конце концов, я нашёл интересный код. К сожалению, он относится к старой версии Swashbuckle. Но он послужил мне хорошей отправной точкой.

Swashbuckle предоставляет возможность вмешиваться в процесс генерации документации. В частности, существует интерфейс ISchemaFilter, который позволяет изменять описания схем отдельных классов. Следующий код показывает, как изменить описание классов перечислений:

public class EnumTypesSchemaFilter : ISchemaFilter {     private readonly XDocument _xmlComments;      public EnumTypesSchemaFilter(string xmlPath)     {         if(File.Exists(xmlPath))         {             _xmlComments = XDocument.Load(xmlPath);         }     }      public void Apply(OpenApiSchema schema, SchemaFilterContext context)     {         if (_xmlComments == null) return;          if(schema.Enum != null && schema.Enum.Count > 0 &&             context.Type != null && context.Type.IsEnum)         {             schema.Description += "<p>Members:</p><ul>";              var fullTypeName = context.Type.FullName;              foreach (var enumMemberName in schema.Enum.OfType<OpenApiString>().Select(v => v.Value))             {                 var fullEnumMemberName = $"F:{fullTypeName}.{enumMemberName}";                  var enumMemberComments = _xmlComments.Descendants("member")                     .FirstOrDefault(m => m.Attribute("name").Value.Equals(fullEnumMemberName, StringComparison.OrdinalIgnoreCase));                 if (enumMemberComments == null) continue;                  var summary = enumMemberComments.Descendants("summary").FirstOrDefault();                 if (summary == null) continue;                  schema.Description += $"<li><i>{enumMemberName}</i> - {summary.Value.Trim()}</li>";              }              schema.Description += "</ul>";         }     } }

Конструктор этого класса принимает имя файла с XML-комментариями. Его содержимое для удобства работы читается в экземпляр XDocument. Затем в методе Apply мы проверяем, генерируется ли схема для перечисления или нет. Если это схема перечисления, то мы добавляем к описанию класса HTML-список, содержащий описание каждого используемого члена перечисления.

Теперь наш класс нужно подключить, чтобы Swashbuckle знал о нём:

services.AddSwaggerGen(c => {      // Set the comments path for the Swagger JSON and UI.     var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";     var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);     c.IncludeXmlComments(xmlPath);      c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath); });

Это делается с помощью метода SchemaFilter в настройках Swagger. Этому методу в качестве параметра передаётся имя файла с XML-комментариями. Именно оно будет передано в конструктор нашего класса EnumTypesSchemaFilter.

Теперь в Swagger UI описания классов-перечислений выглядят так:

XML-комментарии к членам перечисления

Описание перечислений в параметрах

Это уже лучше. Но всё же не достаточно хорошо. У нас в контроллере есть метод, который принимает перечисление в качестве параметра:

public Task<Result> ExecuteOperation(int id, OperationType type)

Давайте посмотрим, как выглядит его описание в Swagger UI:

Описание параметра

Как видите, здесь не присутствует никакого описания членов перечисления. Причина в том, что здесь мы видим описание параметра, а не его типа. Т.е. вы видите XML-комментарий, соответствующий параметру метода, а не типу этого параметра.

Но и эту проблему можно решить. Для этого воспользуемся другим интерфейсом Swashbuckle — IDocumentFilter. Вот его реализация:

public class EnumTypesDocumentFilter : IDocumentFilter {     public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)     {         foreach (var path in swaggerDoc.Paths.Values)         {             foreach(var operation in path.Operations.Values)             {                 foreach(var parameter in operation.Parameters)                 {                     var schemaReferenceId = parameter.Schema.Reference?.Id;                     if (string.IsNullOrEmpty(schemaReferenceId)) continue;                      var schema = context.SchemaRepository.Schemas[schemaReferenceId];                      if (schema.Enum == null || schema.Enum.Count == 0) continue;                      parameter.Description += "<p>Variants:</p>";                      int cutStart = schema.Description.IndexOf("<ul>");                     int cutEnd = schema.Description.IndexOf("</ul>") + 5;                      parameter.Description += schema.Description                         .Substring(cutStart, cutEnd - cutStart);                 }             }         }      } }

Здесь в методе Apply мы перебираем все параметры всех методов всех контроллеров. К сожалению, здесь Swashbuckle API не даёт доступа в типу параметра, а только к схеме его типа (ну или я не нашёл такой возможности). Поэтому мне пришлось вырезать описание членов перечисления из строки описания типа параметра.

Наш класс нужно зарегистрировать аналогичным образом, с помощью метода DocumentFilter:

services.AddSwaggerGen(c => {      // Set the comments path for the Swagger JSON and UI.     var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";     var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);     c.IncludeXmlComments(xmlPath);      c.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);     c.DocumentFilter<EnumTypesDocumentFilter>(); });

Вот как выглядит теперь описание параметра в Swagger UI:

Описание параметра с доступными вариантами

Заключение

Приведённый здесь код является скорее наброском для решения проблемы, чем окончательным вариантом. Надеюсь, он будет полезен вам и позволит добавить описание членов перечисления в ваш Swagger UI. Спасибо!

P.S. Вы можете найти код проекта на GitHub.

ссылка на оригинал статьи https://habr.com/ru/post/552624/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *