So I just went through the process of upgrading to dotnet 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 dotnet 8 to dotnet 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