0

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?

1 Answer 1

0

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:

scalar ui with only success response openapi

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.

update configureservices with custom open api extension

Then restart the Application --> go to the Scalar or Swagger UI page you should be able to see the Error Responses with their Schema

scalar UI with new error responses

Not the answer you're looking for? Browse other questions tagged or ask your own question.