FluentResults icon indicating copy to clipboard operation
FluentResults copied to clipboard

[Question] Does ` FluentResults.Extensions.AspNetCore` include Problem Details?

Open jeffward01 opened this issue 2 years ago • 7 comments

It would be very nice if the Result HTTP Response was in a common 'standard' format such as: ProblemDetails

I did not see ProblemDetails mentioned, so I thought I would ask

Very cool library!! I really love it.

Also I am working on a way to integrate it with CSharpFunctionalExtensions - the author of CSharpFunctionalExtensions seems to have inspired you to write FluentResults

jeffward01 avatar Dec 19 '22 02:12 jeffward01

Thank your for the good feedback!

I had a very short look at the problem details concept last november but then I ignored it to save some time because I wanted to release a first mvp version of the FluentResults.Extensions.AspNetCore package.

If you see the need to use the ProblemDetails concept in this package you can try to integrate it and send an pr. Since November 2022 this asp.net core package have some thousands installs - so the interest is not that big in the community. I have to invest my time wisely. ;)

altmann avatar Feb 04 '23 17:02 altmann

@jeffward01 @altmann. I needed to add ProblemDetails too.

This is what I did following the documentation.

I'm using .Net7

On program.cs I added my custom Profile to handle the failing response

AspNetCoreResult.Setup(config => config.DefaultProfile = new CustomAspNetResultProblemDetailProfile());

This is the class CustomAspNetResultProblemDetailProfile

public class CustomAspNetResultProblemDetailProfile : DefaultAspNetCoreResultEndpointProfile
    {
        public override ActionResult TransformFailedResultToActionResult(FailedResultToActionResultTransformationContext context)
        {
            var result = context.Result;

            if (result.HasError<ApiProblemDetailsError>(out var domainErrors))
            {
                var problemDetail = domainErrors.First().ProblemDetails;

                return (HttpStatusCode)problemDetail.Status! switch 
                {
                    HttpStatusCode.NotFound => new NotFoundObjectResult(problemDetail),
                    HttpStatusCode.Unauthorized => new UnauthorizedObjectResult(problemDetail),
                    HttpStatusCode.BadRequest => new BadRequestObjectResult(problemDetail),
                    HttpStatusCode.Conflict => new ConflictObjectResult(problemDetail),
                    HttpStatusCode.UnprocessableEntity => new UnprocessableEntityResult(),
                    _ => new StatusCodeResult((int)problemDetail.Status)
                };
            }

            return new StatusCodeResult(500);
        }
    }

And here the ApiProblemDetailsError with some custom Errors

public abstract class ApiProblemDetailsError : Error
    {
        public ValidationProblemDetails ProblemDetails { get; }

        protected ApiProblemDetailsError(HttpContext httpContext, HttpStatusCode statusCode, string title, IDictionary<string, string[]> errors)
            : base(title)
        {
            ProblemDetails = new ValidationProblemDetails(errors)
            {
                Title = title,
                Status = (int)statusCode,                
                Type = $"https://httpstatuses.io/{(int)statusCode}",
                Instance = httpContext.Request.Path,
                Extensions =
                {
                    ["traceId"] = Activity.Current?.Id ?? httpContext.TraceIdentifier
                },
            };
        }
    }

    public class InvalidUserError : ApiProblemDetailsError
    {
        public InvalidUserError(HttpContext httpContext, IEnumerable<IdentityError> errors)
            : base(httpContext, HttpStatusCode.BadRequest, "Invalid User", CreateErrorDictionary(errors))
        { }

        private static IDictionary<string, string[]> CreateErrorDictionary(IEnumerable<IdentityError> identityErrors)
        {
            var errorDictionary = new Dictionary<string, string[]>(StringComparer.Ordinal);

            foreach (var identityError in identityErrors)
            {
                var key = identityError.Code;
                var error = identityError.Description;

                errorDictionary.Add(key, error.Split(','));
            }

            return errorDictionary;
        }
    }

    public class UnauthorizedError : ApiProblemDetailsError
    {
        public UnauthorizedError(HttpContext httpContext, string username, string resource)
            : base(httpContext, HttpStatusCode.Unauthorized, "Unauthorized", CreateErrorDictionary(username, resource))
        {
        }

        private static IDictionary<string, string[]> CreateErrorDictionary(string username, string resource)
        {
            var errorDictionary = new Dictionary<string, string[]>(StringComparer.Ordinal)
            {
                { username, new[] { $"is not authorized to access {resource}." } }
            };

            return errorDictionary;
        }
    }

    public class NotFoundError : ApiProblemDetailsError
    {
        public NotFoundError(HttpContext httpContext, string entityName)
            : base(httpContext, HttpStatusCode.NotFound, "Not Found", CreateErrorDictionary(entityName))
        {
        }

        private static IDictionary<string, string[]> CreateErrorDictionary(string entityName)
        {
            var errorDictionary = new Dictionary<string, string[]>(StringComparer.Ordinal)
            {
                { entityName, new[] { $"'{entityName}' not found." } }
            };

            return errorDictionary;
        }
    }

You can check here

I hope this work for you.

gorums avatar May 17 '23 20:05 gorums

And this is the way I'm calling my CustomError

var result = await userManager.CreateAsync(user, password);
 if (!result.Succeeded)
 {
      return Result.Fail(new InvalidUserError(httpContextAccessor.HttpContext, result.Errors));
 }

image

gorums avatar May 17 '23 20:05 gorums

Hey guys, first version pushed.

Goal here is domain(result)<->api-presentation(problemdetails).

The glue in the middle: https://github.com/ElysiumLabs/FluentProblemDetails

angusbreno avatar Jan 08 '24 20:01 angusbreno

Hey guys, first version pushed.

Goal here is domain(result)<->api-presentation(problemdetails).

The glue in the middle: https://github.com/ElysiumLabs/FluentProblemDetails

This looks interesting. Does this work with minimal APIs?

frasermclean avatar Apr 10 '24 00:04 frasermclean

@frasermclean now it work 😁 Changed the name of repo: https://github.com/ElysiumLabs/FluentResults.Extensions.AspNetCore

angusbreno avatar Apr 17 '24 18:04 angusbreno

@altmann if you want I can PR my repo into yours (extensions). What you think?

angusbreno avatar Apr 17 '24 18:04 angusbreno