Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Minimal API: TypedResults, Problem, ValidationProblem #59560

Open
fdonnet opened this issue Dec 19, 2024 · 0 comments
Open

Minimal API: TypedResults, Problem, ValidationProblem #59560

fdonnet opened this issue Dec 19, 2024 · 0 comments
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc design-proposal This issue represents a design proposal for a different issue, linked in the description

Comments

@fdonnet
Copy link

fdonnet commented Dec 19, 2024

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.

@fdonnet fdonnet added the design-proposal This issue represents a design proposal for a different issue, linked in the description label Dec 19, 2024
@dotnet-issue-labeler dotnet-issue-labeler bot added the area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc label Dec 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc design-proposal This issue represents a design proposal for a different issue, linked in the description
Projects
None yet
Development

No branches or pull requests

1 participant