[C#] New Thing: ProblemDetails

Before exploring ProblemDetails, let’s examine .NET Core’s robust exception handling mechanisms

[C#] New Thing: ProblemDetails

Before exploring ProblemDetails, let’s examine .NET Core’s robust exception handling mechanisms

Let’s embark on our journey by examining the mechanisms .NET Core offers for meticulously shaping HTTP responses to exceptions arising within API endpoint invocations

What Web API does when it hit an Exception?

To enhance the Web API development experience, .NET Core incorporates the DeveloperExceptionPage middleware within its development environment. This middleware renders comprehensive stack traces and exception details, enabling developers to swiftly grasp the nature of issues and expedite troubleshooting.

The response proffers an HTML page that meticulously unveils the particulars of the exception

In production environments or those involving end users, exposing internal system details through exceptions poses a security risk, potentially granting malicious actors unauthorized knowledge. Therefore, we typically log exceptions rather than directly responding to the user with such information

In .NET 8, to suppress the display of developer-specific exception details, a configuration adjustment within the launchSettings.json file is required. Specifically, the environment setting must be modified to a value other than Development

In older .NET version (.NET 5 in this case), we can comment out the line

//app.UseDeveloperExceptionPage();

Delving further into the intricacies of .NET 8, it becomes apparent that the UseDeveloperExceptionPage() function is no longer a viable mechanism for controlling the DeveloperExceptionPage middleware. Instead, one must pivot to the modification of the ASPNETCORE_ENVIRONMENT variable, ensuring its value deviates from Developmentto achieve the desired effect. With the DeveloperExceptionPageturned off, our Web API returns only 500 status without any further information.

Global Unhandled Exception Handler

While .NET Core’s default behavior of automatically returning a 500 status code offers a degree of tidiness and simplicity, it presents a significant drawback in its lack of informative feedback for users. This lack of clarity often proves frustrating for both front-end developers and end-users, as they are left with only vague and unhelpful messages such as “Something went wrong, please try again later!”, obscuring the nature of the issue and hindering effective troubleshooting.

To mitigate this drawback and enhance the user experience, we have the capability to craft middleware that enables the customization of responses when unanticipated exceptions arise.

ASP.NET Core presents two distinct avenues for achieving this customization: the construction of a custom action or the employment of a lambda expression. Of these approaches, I find the former to be of a more elegant and organized nature, thus aligning more closely with my personal preferences.

[ApiExplorerSettings(IgnoreApi =true)] 
[ApiController] 
public class ErrorController : ControllerBase 
{ 
    [Route("/error")] 
    public IActionResult HandleError() 
    { 
        var exceptionHandlerFeature = HttpContext.Features.Get<IExceptionHandlerFeature>()!; 
        var exception = exceptionHandlerFeature.Error; 
        return new ObjectResult(exception.Message) 
        { 
            StatusCode = 500 
        }; 
    } 
}

To implement a custom error handling action, define a bespoke method mapped to the /error route and register it within Startup.cs, as shown below.

app.UseExceptionHandler("/Error");

We can also archive this using lambda expression approach

app.UseExceptionHandler(exceptionHandlerApp => 
{ 
    exceptionHandlerApp.Run(async context => 
    { 
        context.Response.StatusCode = StatusCodes.Status500InternalServerError; 
 
        var exceptionHandlerFeature = context.Features.Get<IExceptionHandlerFeature>()!; 
        var exception = exceptionHandlerFeature.Error; 
 
        await context.Response.WriteAsync(exception.Message, Encoding.UTF8); 
    }); 
});

While both approaches afford developers a straightforward means of concealing exception details in favor of presenting more informative messages, they fall short of ensuring consistent responses to errors for API consumers

While individual API error handling can be facilitated by specific approaches, real-world scenarios often involve data aggregation from multiple providers. This introduces the significant challenge of disparate error response structures, making a universal handling strategy infeasible and, in practice, quickly becoming a cumbersome maintenance burden.

Don’t count on API error responses staying the same forever — they can change over time. Plus, each API has its own unique error model, so we need a separate way to handle errors for each one, even if they’re all written in C#.

ProblemDetails

/// <summary> 
/// A machine-readable format for specifying errors in HTTP API responses based on <see href="https://tools.ietf.org/html/rfc7807"/>. 
/// </summary> 
public class ProblemDetails 
{ 
    /// <summary> 
    /// A URI reference [RFC3986] that identifies the problem type. This specification encourages that, when 
    /// dereferenced, it provide human-readable documentation for the problem type 
    /// (e.g., using HTML [W3C.REC-html5-20141028]). When this member is not present, its value is assumed to be 
    /// "". 
    /// </summary> 
    public string? Type { get; set; } 
 
    /// <summary> 
    /// A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence 
    /// of the problem, except for purposes of localization(e.g., using proactive content negotiation; 
    /// see[RFC7231], Section 3.4). 
    /// </summary> 
    public string? Title { get; set; } 
 
    /// <summary> 
    /// The HTTP status code([RFC7231], Section 6) generated by the origin server for this occurrence of the problem. 
    /// </summary> 
    public int? Status { get; set; } 
 
    /// <summary> 
    /// A human-readable explanation specific to this occurrence of the problem. 
    /// </summary> 
    public string? Detail { get; set; } 
 
    /// <summary> 
    /// A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced. 
    /// </summary> 
    public string? Instance { get; set; } 
 
    /// <summary> 
    /// Gets the <see cref="IDictionary{TKey, TValue}"/> for extension members. 
    /// <para> 
    /// Problem type definitions MAY extend the problem details object with additional members. Extension members appear in the same namespace as 
    /// other members of a problem type. 
    /// </para> 
    /// </summary> 
    /// <remarks> 
    /// The round-tripping behavior for <see cref="Extensions"/> is determined by the implementation of the Input \ Output formatters. 
    /// In particular, complex types or collection types may not round-trip to the original type when using the built-in JSON or XML formatters. 
    /// </remarks> 
    public IDictionary<string, object?> Extensions { get; set; } = new Dictionary<string, object?>(StringComparer.Ordinal); 
}

The ProblemDetails is just a model, integral to the framework’s core, establishes a consistent and standardized mechanism for communicating errors to API clients, ensuring a predictable and informative error response structure.

We can enable ProblemDetails response format by adding this line to our Program.cs

builder.Services.AddProblemDetails();

This code line triggers the automatic conversion of unhandled exceptions and BadRequest() invocations into well-defined ProblemDetails responses

Suppose we built a custom exception and wanted to handle it in a specific way, like in the example below.

public class GreetingException : Exception 
    { 
        public string Greeting { set; get; } 
        public GreetingException(string greeting) 
        { 
            Greeting = greeting; 
        } 
    }

We can leverage the overload of the AddProblemDetails() method that accommodates an Action<ProblemDetailsOptions> argument to achieve greater customization.

builder.Services.AddProblemDetails(options => 
{ 
    options.CustomizeProblemDetails = ctx => 
    { 
        var exception = ctx.HttpContext.Features.Get<IExceptionHandlerPathFeature>()?.Error; 
        if (exception == null && exception is GreetingException greetingException) 
        { 
            ctx.ProblemDetails.Status = 500; 
            ctx.ProblemDetails.Title = greetingException.Greeting; 
        } 
    }; 
});

Consequently, upon implementation of the aforementioned modifications, a scenario in which our endpoint precipitates a GreetingException shall result in the return of a ProblemDetails object bearing a status code of 500, accompanied by a Title attribute whose value mirrors that of the GreetingException.Greeting field

Any other exceptions that we didn’t handle would response a ProblemDetails with generic message such as below

In conclusion, ProblemDetails offers a standardized and structured way to handle errors in ASP.NET Core APIs. By leveraging ProblemDetails, you can provide developers with clear information about any issues encountered during API requests. This not only improves the developer experience but also enhances the overall robustness and maintainability of your API.