AddOpenApi – Добавление типов ошибок ответа ко всем операциям – .NET 9

Вопрос или проблема

Я обновляю проект с .NET 8 до .NET 9. Поддержка Swagger UI и Swagger Docs по умолчанию была удалена и заменена на AddOpenApi() и MapOpenApi().

В моем JSON-файле Swagger я обычно добавляю общие типы ответов с ошибками ко всем конечным точкам, используя пользовательскую реализацию IOperationProcessor.

Как мы можем обновить реализацию документации открытого API при переходе с .NET 8 на .NET 9?

Я только что прошел процесс обновления до dotnet core 9 и переключения с Swagger на Open API и Scalar UI.

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

Шаг 1: Обновить версию dotnet с dotnet 8 до dotnet 9.

Шаг 2: Удалить Swashbuckle и любые другие связанные проекты.

Шаг 3: Удалить UseSwaggerUi() из Program.cs.

Шаг 4: Удалить services.AddOpenApiDocument()... из ConfigureServices.cs.

Шаг 5: Установить пакет nuget Microsoft.AspNetCore.OpenApi.

Шаг 6: Установить пакет nuget Scalar.AspNetCore. Мы будем использовать Scalar UI вместо Swagger UI.

Шаг 7: Обновить файл Program.cs, чтобы включить код MapOpenApi и MapScalarApiReference.

...
app.MapStaticAssets();

app.MapOpenApi();
app.MapScalarApiReference(options =>
{
    options
        .WithTitle("TITLE_HERE")
        .WithDownloadButton(true)
        .WithTheme(ScalarTheme.Purple)
        .WithDefaultHttpClient(ScalarTarget.JavaScript, ScalarClient.Axios);
});

app.UseRouting();
...

Шаг 8: Открыть ConfigureServices.cs и включить расширение AddOpenApi().

{
...
// Настроить поведение API по умолчанию
services.AddEndpointsApiExplorer();

// Добавить службы генерации документа Open API
services.AddOpenApi();
...
}

Вышеуказанное должно быть достаточным, чтобы запустить JSON-файл Open API. Так что запустите Web API и перейдите по адресу: https://localhost:PORT/openapi/v1.json. Вы должны увидеть JSON-файл Open API. А переход по адресу https://localhost:PORT/scalar/v1 должен отобразить Scalar UI.

Добавление ответов по умолчанию на ошибки ко всем операциям.

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

[HttpGet]
[ProducesResponseType(typeof(List<GeofenceDto>), 200)]
public async Task<ActionResult<List<GeofenceDto>>> GetGeofences()
{
    return await Mediator.Send(new GetGeofencesQuery());
}

Таким образом, при генерации файлов Open API он содержит только типы успешных ответов:

scalar ui с только успешным ответом openapi

Что нормально, если ваш API никогда не возвращает ответ с ошибкой, я предпочитаю обрабатывать свои ошибки с помощью пользовательского обработчика исключений, как показано ниже.

public class CustomExceptionHandler : IExceptionHandler
{
    private readonly Dictionary<Type, Func<HttpContext, Exception, Task>> _exceptionHandlers;

    public CustomExceptionHandler()
    {
        // Регистрируем известные типы исключений и обработчики.
        // Обратите внимание: добавьте любые новые исключения также в OpenApiGenerator.cs, чтобы они были включены в документ open api json.
        _exceptionHandlers = new()
            {
                { typeof(ValidationException), HandleValidationException },
                { typeof(NotFoundException), HandleNotFoundException },
                { typeof(UnauthorizedAccessException), HandleUnauthorizedAccessException },
                { typeof(ForbiddenAccessException), HandleForbiddenAccessException },
            };
    }

    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        var exceptionType = exception.GetType();

        if (_exceptionHandlers.ContainsKey(exceptionType))
        {
            await _exceptionHandlers[exceptionType].Invoke(httpContext, exception);
            return true;
        }

        return false;
    }

    private async Task HandleValidationException(HttpContext httpContext, Exception ex)
    {
        var exception = (ValidationException)ex;

        httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;

        await httpContext.Response.WriteAsJsonAsync(new ValidationProblemDetails(exception.Errors)
        {
            Status = StatusCodes.Status400BadRequest,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1"
        });
    }

    private async Task HandleNotFoundException(HttpContext httpContext, Exception ex)
    {
        var exception = (NotFoundException)ex;

        httpContext.Response.StatusCode = StatusCodes.Status404NotFound;

        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails()
        {
            Status = StatusCodes.Status404NotFound,
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4",
            Title = "Указанный ресурс не найден.",
            Detail = exception.Message
        });
    }

    private async Task HandleUnauthorizedAccessException(HttpContext httpContext, Exception ex)
    {
        httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;

        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = StatusCodes.Status401Unauthorized,
            Title = "Неавторизованный",
            Type = "https://tools.ietf.org/html/rfc7235#section-3.1"
        });
    }

    private async Task HandleForbiddenAccessException(HttpContext httpContext, Exception ex)
    {
        httpContext.Response.StatusCode = StatusCodes.Status403Forbidden;

        await httpContext.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = StatusCodes.Status403Forbidden,
            Title = "Запрещено",
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.3"
        });
    }
}

Как видите, я обрабатываю определенные исключения и обрабатываю их с помощью конкретного кода состояния. Один способ добавить эти ответы об ошибках – это добавить их ко всем конечным точкам как типы ответов об ошибках.

Но я хочу сделать это с помощью общего кода. Для этого создайте новый файл в вашем проекте Web API и дайте ему название на ваше усмотрение. Я назвал его OpenApiCustomGenerator и вставил следующий код, модифицируйте в соответствии с вашими типами и кодами ответов на ошибки:

public static class OpenApiCustomGenerator
{
    public static void AddOpenApiCustom(this IServiceCollection services)
    {
        services.AddOpenApi(options =>
        {
            options.AddOperationTransformer((operation, context, ct) =>
            {
                // для каждого исключения в `CustomExceptionHandler.cs` мы должны добавить его к возможным типам возвращаемых значений операции
                AddResponse<ValidationException>(operation, StatusCodes.Status400BadRequest);
                AddResponse<UnauthorizedAccessException>(operation, StatusCodes.Status401Unauthorized);
                AddResponse<NotFoundException>(operation, StatusCodes.Status404NotFound);
                AddResponse<ForbiddenAccessException>(operation, StatusCodes.Status403Forbidden);

                return Task.CompletedTask;
            });

            options.AddDocumentTransformer((doc, context, cancellationToken) =>
            {
                doc.Info.Title = "TITLE_HERE";
                doc.Info.Description = "Описание API";

               // Добавить схему в составные части документа
               doc.Components = doc.Components ?? new OpenApiComponents();

                // для каждого исключения в `CustomExceptionHandler.cs` нам нужна схема типа ответа
                AddResponseSchema<ValidationException>(doc, typeof(ValidationProblemDetails));
                AddResponseSchema<UnauthorizedAccessException>(doc);
                AddResponseSchema<NotFoundException>(doc);
                AddResponseSchema<ForbiddenAccessException>(doc);

                return Task.CompletedTask;
            });
        });
    }

    // Вспомогательный метод для добавления ответа к операции
    private static void AddResponse<T>(OpenApiOperation operation, int statusCode) where T : class
    {
        var responseType = typeof(T);
        var responseTypeName = responseType.Name;

        // Проверьте, существует ли уже ответ для кода состояния
        if (operation.Responses.ContainsKey(statusCode.ToString()))
        {
            return;
        }

        // Создайте OpenApiResponse и установите содержимое для ссылки на схему исключения
        operation.Responses[statusCode.ToString()] = new OpenApiResponse
        {
            Description = $"{responseTypeName} - {statusCode}",
            Content = new Dictionary<string, OpenApiMediaType>
            {
                ["application/json"] = new OpenApiMediaType
                {
                    Schema = new OpenApiSchema
                    {
                        Reference = new OpenApiReference
                        {
                            Type = ReferenceType.Schema,
                            Id = responseTypeName
                        }
                    }
                }
            }
        };
    }

    // Вспомогательный метод для добавления схемы ответа в документ OpenAPI
    private static void AddResponseSchema<T>(OpenApiDocument doc, Type? responseType = null)
    {
        var exceptionType = typeof(T);
        var responseTypeName = exceptionType.Name;

        // тип ответа по умолчанию для ошибок / исключений --> см. `CustomExceptionHandler.cs`
        responseType = responseType ?? typeof(ProblemDetails);

        // Определите схему для типа исключения, если она еще не существует
        if (doc.Components.Schemas.ContainsKey(responseTypeName))
        {
            return;
        }

        // Динамически построить схему на основе свойств T
        var properties = responseType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
            .ToDictionary(
                prop => prop.Name,
                prop => new OpenApiSchema
                {
                    Type = GetOpenApiType(prop.PropertyType),
                    Description = $"Свойство типа {prop.PropertyType.Name}"
                }
            );

        // Добавить схему в компоненты документа OpenAPI
        doc.Components.Schemas[responseTypeName] = new OpenApiSchema
        {
            Type = "object",
            Properties = properties
        };
    }

    // Вспомогательный метод для отображения типов .NET в типы OpenAPI
    private static string GetOpenApiType(Type type)
    {
        return type == typeof(string) ? "string" :
               type == typeof(int) || type == typeof(long) ? "integer" :
               type == typeof(bool) ? "boolean" :
               type == typeof(float) || type == typeof(double) || type == typeof(decimal) ? "number" :
               "string"; // Резервный вариант для сложных типов
    }

}

Обновите ConfigureServices.cs, чтобы использовать ваш собственный метод расширения Add Open API.

обновить configureservices с пользовательским open api расширением

Затем перезапустите приложение –> перейдите на страницу Scalar или Swagger UI, вы должны увидеть ответы об ошибках с их схемами.

scalar UI с новыми ответами на ошибки

Ответ или решение

Добавление типов ошибок ответа к всем операциям при обновлении до .NET 9 с использованием Open API

При обновлении проекта с .NET 8 на .NET 9 необходимо учитывать несколько важных изменений, особенно в области документации и предоставления API. Ниже представлен пошаговый процесс, а также решение проблемы с добавлением стандартных типов ошибок ответов для всех операций.

Этапы обновления до .NET 9

Шаг 1: Обновление версии .NET

Перейдите на .NET 9, убедившись, что ваше окружение готово к адаптации новых возможностей и изменений в архитектуре.

Шаг 2: Удаление старых библиотек

Удалите все пакеты, связанные с Swashbuckle и Swagger, так как будет использоваться новый подход с Microsoft.AspNetCore.OpenApi.

Шаг 3: Устранение использования Swagger UI

В файле Program.cs уберите вызов UseSwaggerUi().

Шаг 4: Обновление конфигурации сервисов

Удалите services.AddOpenApiDocument() из ConfigureServices.cs.

Шаг 5: Установка необходимых пакетов

Установите следующие пакеты через NuGet:

  • Microsoft.AspNetCore.OpenApi
  • Scalar.AspNetCore (для использования Scalar UI вместо Swagger UI).

Шаг 6: Настройка MapOpenApi и MapScalarApiReference

Внесите изменения в Program.cs, добавив код для конфигурирования Open API и Scalar API:

app.MapStaticAssets();

app.MapOpenApi();
app.MapScalarApiReference(options =>
{
    options.WithTitle("TITLE_HERE")
           .WithDownloadButton(true)
           .WithTheme(ScalarTheme.Purple)
           .WithDefaultHttpClient(ScalarTarget.JavaScript, ScalarClient.Axios);
});

app.UseRouting();

Шаг 7: Добавление Open API в конфигурацию сервисов

Обновите ConfigureServices.cs, добавив метод AddOpenApi():

services.AddEndpointsApiExplorer();
services.AddOpenApi();

Шаг 8: Проверка работы API

Запустите приложение и убедитесь, что открыв URL https://localhost:PORT/openapi/v1.json вы видите корректный Open API json файл, а по адресу https://localhost:PORT/scalar/v1 – интерфейс Scalar UI.

Добавление стандартных ответов об ошибках

Чтобы ваши API могли генерировать и документировать ошибки, создайте класс, который добавит типы ошибок в ваши операции. Например, создайте файл OpenApiCustomGenerator.cs и добавьте следующий код:

Код для OpenApiCustomGenerator

public static class OpenApiCustomGenerator
{
    public static void AddOpenApiCustom(this IServiceCollection services)
    {
        services.AddOpenApi(options =>
        {
            options.AddOperationTransformer((operation, context, ct) =>
            {
                AddResponse<ValidationException>(operation, StatusCodes.Status400BadRequest);
                AddResponse<UnauthorizedAccessException>(operation, StatusCodes.Status401Unauthorized);
                AddResponse<NotFoundException>(operation, StatusCodes.Status404NotFound);
                AddResponse<ForbiddenAccessException>(operation, StatusCodes.Status403Forbidden);

                return Task.CompletedTask;
            });

            options.AddDocumentTransformer((doc, context, cancellationToken) =>
            {
                doc.Info.Title = "TITLE_HERE";
                doc.Info.Description = "API Description";

                doc.Components = doc.Components ?? new OpenApiComponents();

                AddResponseSchema<ValidationException>(doc, typeof(ValidationProblemDetails));
                AddResponseSchema<UnauthorizedAccessException>(doc);
                AddResponseSchema<NotFoundException>(doc);
                AddResponseSchema<ForbiddenAccessException>(doc);

                return Task.CompletedTask;
            });
        });
    }

    private static void AddResponse<T>(OpenApiOperation operation, int statusCode) where T : class
    {
        var responseTypeName = typeof(T).Name;

        if (!operation.Responses.ContainsKey(statusCode.ToString()))
        {
            operation.Responses[statusCode.ToString()] = new OpenApiResponse
            {
                Description = $"{responseTypeName} - {statusCode}",
                Content = new Dictionary<string, OpenApiMediaType>
                {
                    ["application/json"] = new OpenApiMediaType
                    {
                        Schema = new OpenApiSchema
                        {
                            Reference = new OpenApiReference
                            {
                                Type = ReferenceType.Schema,
                                Id = responseTypeName
                            }
                        }
                    }
                }
            };
        }
    }

    private static void AddResponseSchema<T>(OpenApiDocument doc, Type? responseType = null)
    {
        var responseTypeName = typeof(T).Name;
        responseType = responseType ?? typeof(ProblemDetails);

        if (!doc.Components.Schemas.ContainsKey(responseTypeName))
        {
            var properties = responseType.GetProperties().ToDictionary(
                prop => prop.Name,
                prop => new OpenApiSchema
                {
                    Type = GetOpenApiType(prop.PropertyType),
                    Description = $"Property of type {prop.PropertyType.Name}"
                }
            );

            doc.Components.Schemas[responseTypeName] = new OpenApiSchema
            {
                Type = "object",
                Properties = properties
            };
        }
    }

    private static string GetOpenApiType(Type type)
    {
        return type == typeof(string) ? "string" :
               type == typeof(int) || type == typeof(long) ? "integer" :
               type == typeof(bool) ? "boolean" :
               type == typeof(float) || type == typeof(double) || type == typeof(decimal) ? "number" :
               "string"; 
    }
}

Шаг 9: Включение пользовательского генератора в ConfigureServices.cs

Обновите метод ConfigureServices.cs, добавляя вызов вашего пользовательского метода:

services.AddOpenApiCustom();

Шаг 10: Перезапуск приложения и проверка результатов

Перезапустите приложение и повторно откройте интерфейс Scalar или Swagger UI. Вы должны увидеть новые типы ответов об ошибках с их схемами.

Заключение

Следуя приведённым выше шагам, вы сможете успешно обновить свой проект с .NET 8 до .NET 9, а также добавить обработку ошибок в документацию Open API. Таким образом, ваш проект станет более устойчивым и удобным для пользователей, предоставляя им необходимую информацию о возможных ошибках API.

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

Капча загружается...