I am upgrading a project from .NET 8 to .NET 9. The default support for Swagger UI and Swagger Docs has been removed and replaced with the AddOpenApi()
and MapOpenApi()
.
In my Swagger json file, I would normally add common error response types to all endpoints using a custom IOperationProcessor
implementation.
How do we go about upgrading from .NET 8 to .NET 9 and upgrading the open API documentation implementation?
So I just went through the process of upgrading to ASP.NET Core 9, and switching from Swagger to Open API and Scalar UI.
I am documenting it here and the part of adding Error Response codes across all endpoints for anyone else who comes across a similar issue.
Step 1 : Is to update the dotnet version from .NET 8 to .NET 9
Step 2 : Uninstall Swashbuckle and any other related projects.
Step 3: Remove UseSwaggerUi()
from the Program.cs
Step 4: Remove services.AddOpenApiDocument()...
from the ConfigureServices.cs
Step 5: Install Microsoft.AspNetCore.OpenApi
nuget package
Step 6: Intall Scalar.AspNetCore
nuget package.
We will be using Scalar UI instead of Swagger UI
Step 7: Update the Program.cs file to include the MapOpenApi
and MapScalarApiReference
code
...
app.MapStaticAssets();
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options
.WithTitle("TITLE_HERE")
.WithDownloadButton(true)
.WithTheme(ScalarTheme.Purple)
.WithDefaultHttpClient(ScalarTarget.JavaScript, ScalarClient.Axios);
});
app.UseRouting();
...
Step 8: Open ConfigureServices.cs and include the AddOpenApi() extension
{
...
// Customise default API behaviour
services.AddEndpointsApiExplorer();
// Add the Open API document generation services
services.AddOpenApi();
...
}
The above should be enough to get the Open API json file running.
So start the Web API and navigate to: https://localhost:PORT/openapi/v1.json
You should see the Open API json file.
and navigating to https://localhost:PORT/scalar/v1
should display the Scalar UI.
Adding default error responses to all operations.
So normally I define my API endpoints like below, as you can see I only define the success response and it's type.
[HttpGet]
[ProducesResponseType(typeof(List<GeofenceDto>), 200)]
public async Task<ActionResult<List<GeofenceDto>>> GetGeofences()
{
return await Mediator.Send(new GetGeofencesQuery());
}
So when generating the Open API files, it only contains the success response types:
Which is okay if your API never returns an Error Response, I like to handle my errors using a custom exception handler like below.
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"
});
}
}
As you can see I have certain Exceptions thrown and handled with a specific Status Code. One way to add these error responses is to add them to all the endpoints as error responses types.
but I want to do this using a common piece of code.
So to do this , Create a new file in your Web API project and give it a name of your chosing.
I named it OpenApiCustomGenerator
, and paste in the following code, modify according to your error response types and codes:
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
}
}
Update the ConfigureServices.cs
to use your Custom Add Open API extension method.
Then restart the Application --> go to the Scalar or Swagger UI page you should be able to see the Error Responses with their Schema
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With