- Вопрос или проблема
- Ответ или решение
- Добавление типов ошибок ответа к всем операциям при обновлении до .NET 9 с использованием Open API
- Этапы обновления до .NET 9
- Шаг 1: Обновление версии .NET
- Шаг 2: Удаление старых библиотек
- Шаг 3: Устранение использования Swagger UI
- Шаг 4: Обновление конфигурации сервисов
- Шаг 5: Установка необходимых пакетов
- Шаг 6: Настройка MapOpenApi и MapScalarApiReference
- Шаг 7: Добавление Open API в конфигурацию сервисов
- Шаг 8: Проверка работы API
- Добавление стандартных ответов об ошибках
- Код для OpenApiCustomGenerator
- Шаг 9: Включение пользовательского генератора в ConfigureServices.cs
- Шаг 10: Перезапуск приложения и проверка результатов
- Заключение
Вопрос или проблема
Я обновляю проект с .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 он содержит только типы успешных ответов:
Что нормально, если ваш 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.
Затем перезапустите приложение –> перейдите на страницу Scalar или Swagger 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.