Problem Details In .Net 7

Problem Details In .Net 7

  • avatar
    Name
    Meysam Hadeli
  • Problem Detail provide machine-readable details of errors in a HTTP response to avoid the need to define new error response formats for HTTP APIs based on https://tools.ietf.org/html/rfc7807. In the older version of .net doesn't produce a standardized error payload when an unhandled exception occurs and to handle this approach, documentation mention to use third-party library with name of Hellang Middleware ProblemDetails. It's a great library to map our exception to problem details payload.

    In .Net 7 eventually support this feature using the IProblemDetailsService, and we can map our exception to problem details format. In the next we point some aspect of this new feature in .net 7.

    Config problem details for all HTTP client and server error

    We need following code to register problem details to service collection for generate standard problem details response:

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers();
    
    builder.Services.AddProblemDetails();  // Register problem details
    
    var app = builder.Build();
    
    app.MapControllers();
    
    app.Run();
    

    Let's jump to create an example for test problem details with an unhandled exception:

    [Route("api/[controller]/[action]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        //api/values/SimpleTest/0
        [HttpGet("{number}")]
        public IActionResult SimpleTest(int number)
        {
            if (number <= 0)
            {
                throw new Exception("The number is less than or equal to 0!");
            }
    
            return Ok(number);
        }
    

    If we have some exception base on previous example, we will have a problem details response like below:

    {
        "type": "https://tools.ietf.org/html/rfc7231#section-6.6.1",
        "title": "System.Exception",
        "status": 500,
        "detail": "The number is less than or equal to 0!",
        "traceId": "00-1bb2c6bbfe99c8d6db6a49d7116259fc-1be4dd33a66ac5f3-00",
        "exception": { ... }
    }
    

    Custom problem details with ProblemDetailsOptions

    We can config problem details with ProblemDetailsOptions like below:

    Before config our custom problem details, we need to create following code that we need them in our example:

    public class ErrorFeature
    {
        public ErrorType Error  { get; set; }
    }
    
    public enum ErrorType
    {
        ArgumentException
    }
    

    Note: If ErrorFeature in the ProblemDetailsOptions had value, will be set these maps for custom errors.

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddControllers();
    builder.Services.AddSwaggerGen();
    
    builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = ctx => // Add custom problem details
        {
            var errorFeature = ctx.HttpContext.Features.Get<ErrorFeature>();
    
            if (errorFeature is not null)
            {
                (string Title, string Detail) details = errorFeature.Error switch
                {
                    ErrorType.ArgumentException =>
                    (
                        nameof(ArgumentException),
                        "We got argument-exception!"
                    ),
                    _ =>
                    (
                        nameof(Exception),
                        "We got default-exception!"
                    )
                };
    
                ctx.ProblemDetails.Title = details.Title;
                ctx.ProblemDetails.Detail = details.Detail;
            }
        }
    );
    
    var app = builder.Build();
    
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.MapControllers();
    
    app.Run();
    

    Let's check previous config with an example:

        // api/values/CustomErrorTest/0
        [HttpGet("{number}")]
        public IActionResult CustomErrorTest(int number)
        {
            if (number <= 0)
            {
                var errorType = new ErrorFeature
                {
                    Error = ErrorType.ArgumentException
                };
                HttpContext.Features.Set(errorType);
                return BadRequest();
            }
    
            return Ok(number);
        }
    

    If we have some exception base on previous example, we will have a response that handle with ProblemDetailsOptions like below:

    {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
      "title": "ArgumentException",
      "status": 400,
      "detail": "We got argument-exception!",
      "traceId": "00-dc886373dd7c1895785d110c301d9432-25e55729e418542e-00"
    }
    

    Note: We can also add additional parameter to problem details with ProblemDetailsOptions like below:

    builder.Services.AddProblemDetails(options =>
        options.CustomizeProblemDetails = ctx =>
                ctx.ProblemDetails.Extensions.Add("nodeId", Environment.MachineName));
    

    The result will be like this:

    {
      "type": "https://www.rfc-editor.org/rfc/rfc7231#section-6.5.1",
      "title": "ArgumentException",
      "status": 400,
      "detail": "We got argument-exception!",
      "traceId": "00-48fc51207f6996ba965ba1951c654ffa-d4d216d3756473fd-00",
      "nodeId": "machine-name"
    }
    

    Problem details middleware for map exceptions

    We can use problem details Middleware instead of using ProblemDetailsOptions and map our exceptions easily.

    The following code display how we can config problem details with middleware:

    var builder = WebApplication.CreateBuilder(args);
    var env = builder.Environment;
    
    builder.Services.AddControllers();
    builder.Services.AddSwaggerGen();
    builder.Services.AddProblemDetails();
    
    var app = builder.Build();
    
    app.UseStatusCodePages();
    
    app.UseExceptionHandler(exceptionHandlerApp =>
    {
        exceptionHandlerApp.Run(async context =>
        {
            context.Response.ContentType = "application/problem+json";
    
            if (context.RequestServices.GetService<IProblemDetailsService>() is { } problemDetailsService)
            {
                var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>();
                var exceptionType = exceptionHandlerFeature?.Error;
    
                if (exceptionType is not null)
                {
                    (string Title, string Detail, int StatusCode) details = exceptionType switch
                    {
                        CustomException customException =>
                        (
                            exceptionType.GetType().Name,
                            exceptionType.Message,
                            context.Response.StatusCode = (int)customException.StatusCode
                        ),
                        _ =>
                        (
                            exceptionType.GetType().Name,
                            exceptionType.Message,
                            context.Response.StatusCode = StatusCodes.Status500InternalServerError
                        )
                    };
    
                    var problem = new ProblemDetailsContext
                    {
                        HttpContext = context,
                        ProblemDetails =
                        {
                            Title = details.Title,
                            Detail = details.Detail,
                            Status = details.StatusCode
                        }
                    };
    
                    if (env.IsDevelopment())
                    {
                        problem.ProblemDetails.Extensions.Add("exception", exceptionHandlerFeature?.Error.ToString());
                    }
    
                    await problemDetailsService.WriteAsync(problem);
                }
            }
        });
    });
    
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.MapControllers();
    
    app.Run();
    

    Let's run an example for check previous configuration for handle exception with custom problem details middleware:

    Before run the example we need create a custom exception for use it in our test sample like the following:

    public class CustomException : Exception
    {
        public CustomException(
            string message,
            HttpStatusCode statusCode = HttpStatusCode.BadRequest
        ) : base(message)
        {
            StatusCode = statusCode;
        }
    
        public HttpStatusCode StatusCode { get; }
    }
    
        // api/values/CustomExceptionTest/0
        [HttpGet("{number}")]
        public IActionResult CustomExceptionTest(int number)
        {
            if (number <= 0)
            {
                throw new CustomException("Some custom exception!");
            }
    
            return Ok(number);
        }
    

    If we have some exception base on previous example, we will have a custom problem details response like below:

    {
      "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
      "title": "CustomException",
      "status": 400,
      "detail": "Some custom exception!",
      "exception": "Exception with more details..."
    }
    

    Note: For handling HttpStatusCode (404, 401, 405 and etc) error with format problem details. We can add this middleware:

    app.UseStatusCodePages();
    

    You can find the sample code in this repository:

    🔗 https://github.com/meysamhadeli/blog-samples/tree/main/src/problem-details-sample

    Conclusion

    In this post, I talk about new feature Problem Details that introduce in .Net 7 to handle exceptions with different way. Also, we can also use alternative way with using great library Hellang Middleware ProblemDetails.

    Reference

    https://learn.microsoft.com/en-us/aspnet/core/fundamentals/error-handling?view=aspnetcore-7.0#pds7 https://learn.microsoft.com/en-us/aspnet/core/web-api/handle-errors?view=aspnetcore-7.0#pds7