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

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

Open
jeffward01 opened this issue Dec 19, 2022 · 7 comments

Comments

@jeffward01
Copy link

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

@altmann
Copy link
Owner

altmann commented Feb 4, 2023

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. ;)

@gorums
Copy link

gorums commented May 17, 2023

@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
Copy link

gorums commented May 17, 2023

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

@angusbreno
Copy link

Hey guys, first version pushed.

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

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

@frasermclean
Copy link

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?

@angusbreno
Copy link

angusbreno commented Apr 17, 2024

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

@angusbreno
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants