[.NET] Описание элементов перечислений в Swashbuckle (перевод)

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
16-Апр-2021 17:30

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 мы найдём описание нашего сервиса:

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

Лично мне было бы удобно, если бы перечисления представлялись строковыми значениями. Они хотя бы имеют некоторый смысл в отличии от безликих чисел.
Для этого нам нужно внести небольшие изменения в настройку нашего сервиса. Я установил 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 описания классов-перечислений выглядят так:

Описание перечислений в параметрах
Это уже лучше. Но всё же не достаточно хорошо. У нас в контроллере есть метод, который принимает перечисление в качестве параметра:
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.
===========
Источник:
habr.com
===========

===========
Автор оригинала: Ivan Iakimov
===========
Похожие новости: Теги для поиска: #_.net, #_swagger, #_swaggerui, #_.net
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 21:03
Часовой пояс: UTC + 5