Description
Summary
Enhance the way we return TypedResult
for minimal api and ProblemDetails
when the system raise an error.
Motivation and goals
I believe the newly added features related to this topic are great, but there is room for improvement.
In scope
- TypedResults
- ProblemDetails options
- Minimal Api
- Exceptions versus "business" errors.
Out of scope
N/A
Risks / unknowns
None
Examples
The way we can now use the following code is great:
services.AddProblemDetails(options =>
{
options.CustomizeProblemDetails = context =>
{
context.ProblemDetails.Instance =
$"{context.HttpContext.Request.Method} {context.HttpContext.Request.Path}";
context.ProblemDetails.Extensions.TryAdd("requestId", context.HttpContext.TraceIdentifier);
Activity? activity = context.HttpContext.Features.Get<IHttpActivityFeature>()?.Activity;
context.ProblemDetails.Extensions.TryAdd("traceId", activity?.Id);
};
});
app.UseStatusCodePages();
Enriching our problem details in this manner is simply ❤
For exception handling in the case of a real exception, the ability to inject the IProblemDetailsService
service is cool:
public class CustomExceptionHandler(IProblemDetailsService problemDetailsService, ILogger<CustomExceptionHandler> log) : IExceptionHandler
{
private readonly ILogger<CustomExceptionHandler> _log = log;
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_log.LogError(exception, "An error occurred: {message}",exception.Message);
var problemDetails = new ProblemDetails
{
Status = StatusCodes.Status500InternalServerError,
Title = "An error occurred",
Type = exception.GetType().Name,
Detail = exception.Message,
};
return await problemDetailsService.TryWriteAsync(new ProblemDetailsContext
{
Exception = exception,
HttpContext = httpContext,
ProblemDetails = problemDetails
});
}
}
However, I believe there is a need for more clarity regarding when we are serious about error handling and do not want to raise an exception in the case of a normal error. An exception should indicate an unplanned error.
Now that we have the TypedResults extension and a way to enforce our Minimal API to return certain types, ValidationProblem seems unnecessary because ultimately, we want to return either our normal typed result or a typed ProblemDetails result.
Something like that:
Task<Results<Created<TenantStandardResult>, ProblemHttpResult >>
Not something like that
Task<Results<Created<TenantStandardResult>, ProblemHttpResult, ValidationProblem >>
On my end, to always be able to return ProblemDetails
in the case of "business" errors, validation errors, or exceptions, I created a new extension called CustomTypedResults
as follows:
public static class CustomTypedResults
{
public static ProblemHttpResult Problem(IDictionary<string, string[]> validationErrors)
{
ArgumentNullException.ThrowIfNull(validationErrors);
var problemDetails = new ProblemDetails()
{
Detail = "Validation error",
Status = 400,
Title = "Validation error",
Extensions = { { "validationErrors", validationErrors } }
};
return TypedResults.Problem(problemDetails);
}
public static ProblemHttpResult Problem(IFeatureError featureError)
{
ArgumentNullException.ThrowIfNull(featureError);
var problemDetails = new ProblemDetails()
{
Detail = featureError.Details,
Status = (int)featureError.ErrorType,
Title = "Service - feature error",
Extensions = { { "errors", featureError.CustomErrors } }
};
return TypedResults.Problem(problemDetails);
}
}
With that, I m able to pass the result of a fluent validation (Dic) (like before with TypedResults.ValidationProblem():
CustomTypedResults.Problem()
Or pass my error interface (IFeatureError), the same way:
CustomTypedResults.Problem()
And maintain my typed API returns simple:
private async Task<Results<Created<TenantStandardResult>, ProblemHttpResult >>
Detailed design
I am unsure if this approach will be interesting for others, but I believe it simplifies the process and allows us to consistently return ProblemDetails
without further consideration.
Mainly when you don't use exception handler to trap normal errors...
I think it's not good and I prefer very much the Either<IFeatureError,RightResult>
pattern and be able to transform a IFeatureError
to ProblemDetails
.
I am curious if it would be beneficial to explore this topic further or if anyone has a better approach already.