Описание элементов перечислений в 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/


Интересные статьи

Интересные статьи

Есть несколько способов добавить водяной знак в Битрикс. Рассмотрим два способа.
Всем привет. Когда я искал информацию о журналировании (аудите событий) в Bitrix, на Хабре не было ни чего, в остальном рунете кое что было, но кто же там найдёт? Для пополнения базы знаний...
Несмотря на то, что “в коробке” с Битриксом уже идут модули как для SOAP (модуль “Веб сервисы” в редакции “Бизнес” и старше), так и для REST (модуль “Rest API” во всех редакциях, начиная с...
Если вы последние лет десять следите за обновлениями «коробочной версии» Битрикса (не 24), то давно уже заметили, что обновляется только модуль магазина и его окружение. Все остальные модули как ...
Утро начинается тяжело, особенно если просыпаешься в пол шестого. За окном идет дождь, пряча под зонтами лица немногочисленных жаворонков, бегущих на работу, и сов, возвращающихся домой разме...