In the previous article, I wrote about API versioning and how to add Swagger to the sample project with support of API versioning. In this article, I show how to add custom middleware to handle exceptions globally and create a custom response when an error occurred.
Who can write bug-free codes? at least not me. While unhandled exceptions may occur in each system, it's really important to trap errors to log and fix them and showing proper response to the client. Exception handling middleware helps us to catch exceptions in a single place and avoid duplicate exception handling code through the application.
Step 1 - Implement exception handling middleware
First, add a new folder to the Infrastructure
folder and call it Middlewares
then add a new file ApiExceptionHandlingMiddleware.cs
. Add following codes:
public class ApiExceptionHandlingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiExceptionHandlingMiddleware> _logger;
public ApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<ApiExceptionHandlingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
_logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
var problemDetails = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "Internal Server Error",
Status = (int)HttpStatusCode.InternalServerError,
Instance = context.Request.Path,
Detail = "Internal server error occured!"
};
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
var result = JsonSerializer.Serialize(problemDetails);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
}
Alongside setting the status code of response to 500 context.Response.StatusCode = (int)HttpStatusCode.InternalServerError
, a message in format of ProblemDetails
exist in body of response.
Prior to ASP.NET Core 2.2 the default response type for an HTTP 400 (BadRequest(ModelState)
was this:
{
"": [
"A non-empty request body is required."
]
}
According to the Internet Engineering Task Force (IETF) RFC-7231 document, the ASP.NET Core team has implemented ProblemDetails, a machine-readable format for specifying errors in web API responses and complies with the RFC 7807 specification.
Step 2 - Register middleware
- Create an extension method to register middleware:
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
=> app.UseMiddleware<ApiExceptionHandlingMiddleware>();
}
- Open the
Startup.cs
class and in theConfigure
method add the middleware:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
...
}
app.UseApiExceptionHandling();
As you know the order of adding middleware components is important. If you are using UseDeveloperExceptionPage
for the development environment, then add ApiExceptionHandling
middleware after that.
Step 3 - Translate a business error into a domain exception
There are lots of arguments about when to throw an exception, however, when an exception should be thrown:
- The first and foremost reason is completing the process and giving result is impossible (Fail Fast):
private async Task AddProductToBasketAsync(Guid productId)
{
var product = await _repository.GetProductByIdAsync(productId);
if(product == null)
throw new DomainException($"Product with id {productId} could not be found.");
// Or simply return null?
// Or return an error code or warping response into another object that has `Succeeded` property like `IdentityResult`?
// Or return a tuple (false, "Product with id {productId} could not be found.")?
-
One of the disadvantages of throwing an exception is that exception has a performance cost. If you are writing a high-performance application, throwing an exception can hurt the performance.
To reiterate: exceptions should be truly exceptional.
-
What are the disadvantages of using error code or wrapper object?
- Well throwing an exception is safer because the caller code may forget to check the result for error code or null and go ahead with the execution. You have to check the result of method calls from bottom to up
- If you wrap the result into another object like IdentityResult, you should pay the extra heap allocation. For each call, an extra object should be initialized even for the successful operation. If you call an API 100 times with different inputs, how many times an exception may be thrown? So the rate of throwing an exception with the extra object initializing (and heap allocation) is not the same
Step 4 - Add DomainException class
- Create a new folder at the project root and name it
Domain
then add another folderException
- Add new file
DomainException.cs
toException
folder:
public class DomainException : Exception
{
public DomainException(string message)
: base(message)
{
}
}
- Catch
DomainException
and translate to bad request result in the exception handling middleware:
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
string result;
**if (ex is DomainException)
{
var problemDetails = new ValidationProblemDetails(new Dictionary<string, string[]> { { "Error", new[] { ex.Message } } })
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
Title = "One or more validation errors occurred.",
Status = (int)HttpStatusCode.BadRequest,
Instance = context.Request.Path,
};
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
result = JsonSerializer.Serialize(problemDetails);
}**
else
{
_logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
var problemDetails = new ProblemDetails
{
Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
Title = "Internal Server Error.",
Status = (int)HttpStatusCode.InternalServerError,
Instance = context.Request.Path,
Detail = "Internal Server Error!"
};
context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
result = JsonSerializer.Serialize(problemDetails);
}
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(result);
}
I translated the DomainException
to ValidationProblemDetails
just like unhandled exceptions. I will use DomainException
later on. Let's test to domain exception in action:
[HttpGet("throw-domain-exception")]
public IActionResult ThrowDomainError()
{
throw new DomainException("Product could not be found");
}
You can find the source code for this walkthrough on Github.
Top comments (2)
Awesome for .net core - for people on .net framework they can use nuget.org/packages/DeLoachAero.Web...
So useful article.