This article will focus on handling custom exceptions for flow control on .NET with a global exception handler and return a problem details object.
Having said that. There are, for the most part, two schools of thought. One where it is OK to throw a (custom) exception to control flow. i.e. Throw a when a user is not found. The second is implementing the Result Pattern where you return a Success or an Error to control the flow. i.e. UserNotFoundException.Result.UserNotFound
Problem Details
Problem details is the standard way of returning error messages from a REST API to the client.
Say your API is validating for a required field. i.e. . If the name is not present in the API request, instead of only returning a 400 Bad Request HTTP Status Code, you return a problem details response detailing what went wrong with the request so clients can take action on the invalid request.Name
The members of the problem details response are as follows:
- type – identifies the problem details type
- title – summary of the problem details
- status – HTTP Status code of the issue
- errors – details of what went wrong with the request
{
"type": "https://httpstatuses.com/400",
"title": "One or more validation errors occurred.",
"status": 400,
"errors": {
"Name": [
"'Name' must not be empty."
]
}
}
Global Exception Handler
You add the global exception handler to like soProgram.cs
var app = builder.Build(); app.UseExceptionHandler(GlobalExceptionHandler.Configure);
Then the implementation of the static classGlobalExceptionHandler
public static class GlobalExceptionHandler
{
public static void Configure(IApplicationBuilder builder)
{
builder.Run(async context =>
{
var exceptionHandlerPathFeature =
context.Features.Get<IExceptionHandlerPathFeature>();
// Declare the problem results
IResult problemResult;
// Switch statement to match the custom exceptions
switch (exceptionHandlerPathFeature?.Error)
{
case UserAlreadyExistsException:
{
var details = new ProblemDetails
{
Type = "https://httpstatuses.com/409",
Title = "User already exists.",
Status = StatusCodes.Status409Conflict,
};
problemResult = Results.Problem(details);
break;
}
// Other custom exceptions, say UnauthorizedException and return
// a 401 Unauthorized problem details
// This custom exception here contains validation errors from
// Fluent Validation
case ApiValidationException:
{
// Casting the exception to ApiValidationException to get the
// `Errors` property and send it back to the client
var exp = (ApiValidationException)exceptionHandlerPathFeature!.Error;
problemResult = Results.ValidationProblem
(
exp.Errors,
type: "https://httpstatuses.com/400",
statusCode: StatusCodes.Status400BadRequest
);
break;
}
// If no custom exception is matched, return generic 500 Internal Server
// error response
default:
{
var details = new ProblemDetails
{
Type = "https://httpstatuses.com/500",
Title = "An error occurred while processing your request.",
Status = StatusCodes.Status500InternalServerError
};
problemResult = Results.Problem(details);
break;
}
}
await problemResult.ExecuteAsync(context);
});
}
}
Bonus! Minimal APIs and Fluent Validation
With Minimal APIs, you have to invoke Fluent Validation inside the endpoint like so
var validationResult = await validator.ValidateAsync(userSignup, cancellationToken);
if (validationResult.IsValid is false)
throw new ApiValidationException(validationResult.ToDictionary());
In the example above, I map the validation results to custom exception. The global exception handler will handle this exception returning the appropriate error response with a 400 Bad Request status codeApiValidationException
And the implementation detailsApiValidationException
public class ApiValidationException : Exception
{
public IDictionary<string,string[]> Errors { get; }
public ApiValidationException(IDictionary<string,string[]> errors)
: base()
{
Errors = errors;
}
}
.NET 8
There is a new way of handling global exceptions in .NET 8. I will write about it at some point. Having said that, the code shown here will also work with .NET 8.