我正在将项目从 .NET 8 升级到 .NET 9。对 Swagger UI 和 Swagger Docs 的默认支持已被删除并替换为
AddOpenApi()
和 MapOpenApi()
。
在我的 Swagger json 文件中,我通常会使用自定义
IOperationProcessor
实现向所有端点添加常见错误响应类型。
我们如何从 .NET 8 升级到 .NET 9 并升级开放 API 文档实现?
所以我刚刚经历了升级到 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步:安装
Microsoft.AspNetCore.OpenApi
nuget包
第6步:安装
Scalar.AspNetCore
nuget包。
我们将使用 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()扩展
{
...
// Customise default API behaviour
services.AddEndpointsApiExplorer();
// Add the Open API document generation services
services.AddOpenApi();
...
}
以上内容应该足以运行 Open API json 文件。 因此,启动 Web API 并导航至:
https://localhost:PORT/openapi/v1.json
您应该会看到 Open API json 文件。
并导航至 https://localhost:PORT/scalar/v1
应显示标量 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()
{
// Register known exception types and handlers.
// Please note: add any new exceptions also the OpenApiGenerator.cs so they get included in the open api json document.
_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 = "The specified resource was not found.",
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 = "Unauthorized",
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 = "Forbidden",
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) =>
{
// foreach exception in `CustomExceptionHandler.cs` we need to add it to possible return types of an operation
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";
// Add the scheme to the document's components
doc.Components = doc.Components ?? new OpenApiComponents();
// foreach exception in `CustomExceptionHandler.cs` we need a response schema type
AddResponseSchema<ValidationException>(doc, typeof(ValidationProblemDetails));
AddResponseSchema<UnauthorizedAccessException>(doc);
AddResponseSchema<NotFoundException>(doc);
AddResponseSchema<ForbiddenAccessException>(doc);
return Task.CompletedTask;
});
});
}
// Helper method to add a response to an operation
private static void AddResponse<T>(OpenApiOperation operation, int statusCode) where T : class
{
var responseType = typeof(T);
var responseTypeName = responseType.Name;
// Check if the response for the status code already exists
if (operation.Responses.ContainsKey(statusCode.ToString()))
{
return;
}
// Create an OpenApiResponse and set the content to reference the exception schema
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
}
}
}
}
};
}
// Helper method to add a response schema to the OpenAPI document
private static void AddResponseSchema<T>(OpenApiDocument doc, Type? responseType = null)
{
var exceptionType = typeof(T);
var responseTypeName = exceptionType.Name;
// the default response type of errors / exceptions --> check: `CustomExceptionHandler.cs`
responseType = responseType ?? typeof(ProblemDetails);
// Define the schema for the exception type if it doesn't already exist
if (doc.Components.Schemas.ContainsKey(responseTypeName))
{
return;
}
// Dynamically build the schema based on the properties of T
var properties = responseType.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.ToDictionary(
prop => prop.Name,
prop => new OpenApiSchema
{
Type = GetOpenApiType(prop.PropertyType),
Description = $"Property of type {prop.PropertyType.Name}"
}
);
// Add the schema to the OpenAPI document components
doc.Components.Schemas[responseTypeName] = new OpenApiSchema
{
Type = "object",
Properties = properties
};
}
// Helper method to map .NET types to OpenAPI types
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"; // Fallback for complex types
}
}
更新
ConfigureServices.cs
以使用您的自定义添加开放 API 扩展方法。
然后重新启动应用程序 --> 转到 Scalar 或 Swagger UI 页面,您应该能够看到错误响应及其架构