Skip to content

Minimal API: TypedResults, Problem, ValidationProblem #59560

Open
@fdonnet

Description

@fdonnet

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-minimalIncludes minimal APIs, endpoint filters, parameter binding, request delegate generator etcdesign-proposalThis issue represents a design proposal for a different issue, linked in the description

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions