Code Monkey home page Code Monkey logo

Comments (10)

savornicesei avatar savornicesei commented on August 23, 2024 1

Hi all,

I don't think this library should handle all existing ProblemDetails objects in AspNet world. It takes arround 40 lines to map Ardalis.IResult to a ProblemDetails object.
I believe that adding just some guidance/examples in the (missing 😛 ) documentation would be enough.

As a first example how I did it:

  1. Added the ProblemDetails class from AspNetCore to my .NET framework MVC 5 project
  2. Added some extension methods for Ardalis.Result
public static class ArdalisResultExtensions
{
    public static int GetHttpStatusCode(this IResult result)
    {
        return result.Status switch
        {
            ResultStatus.Error => StatusCodes.Status422UnprocessableEntity,
            ResultStatus.Forbidden => StatusCodes.Status403Forbidden,
            ResultStatus.Unauthorized => StatusCodes.Status401Unauthorized,
            ResultStatus.Invalid => StatusCodes.Status400BadRequest,
            ResultStatus.NotFound => StatusCodes.Status404NotFound,
            ResultStatus.Conflict => StatusCodes.Status409Conflict,
            ResultStatus.CriticalError => StatusCodes.Status500InternalServerError,
            ResultStatus.Unavailable => StatusCodes.Status503ServiceUnavailable,
            ResultStatus.NoContent => StatusCodes.Status204NoContent,
            _ => StatusCodes.Status200OK
        };
    }
    public static ProblemDetails AsProblemDetails(this IResult result)
    {
        Guard.Against.Null(result, nameof(result));

        return new ProblemDetails()
        {
            Title = result.Status.ToString(),
            Detail = result.IsInvalid()
                ? JsonConvert.SerializeObject(result.ValidationErrors)
                : JsonConvert.SerializeObject(result.Errors),
            Status = result.GetHttpStatusCode(),
            Type = result.ValueType.FullName
        };
    }
}   
  1. And a new Json* method in my base MVC controller
protected internal ActionResult JsonProblemDetails(IResult result)
{
    return new JsonProblemDetailsResult(result);
}

Now all my ajax calls to MVC endpoints return a standard response, with error codes that enable localized messages to the user from frontend.

from result.

nhwilly avatar nhwilly commented on August 23, 2024

I believe that creating a bespoke, AR ProblemDetails class is the best approach. And it could be modeled closely to the FE ProblemDetails class. I say this as that might help solve creating the same JSON payload returning to the client whether a FE-FV pipeline ValidationFailure occurred or an AR ValidationError was returned.

Same deserialization could occur.

Or does this create an implied dependency?

from result.

nhwilly avatar nhwilly commented on August 23, 2024

If we want to standardize the response, then we need to override the FE validation response. There's a hook to do that. FYI

from result.

nhwilly avatar nhwilly commented on August 23, 2024

Legit question:

ValidationError contains a lot of detail, while standard Error contains a collection of strings.

I can use JSON attributes to configure the response to take the shape most people are used to seeing while the deserialization still works.

Is my custom problem details better off supporting two different types of collections, or just one where many values are nullable depending on the type of content?

from result.

nhwilly avatar nhwilly commented on August 23, 2024

Update:

Creating a custom ProblemDetails class

As a first pass, I've written an extension method to use in place of ToMinimalApiResult.

This approach requires a replacement for MVC's ProblemDetails which carries two additional properties. One is a collection of strings and the other a collection of a new ValidationErrorDto class.

This class allows for Ardalis.Result.ValidationError and FluentValidation.ValidationFailure to be mapped to a consistent validation detail item.

This approach creates two problems:

  1. The Asp Net Core Results.Problem method won't take a complex type. We can resort to manually constructing the response, but that means that we must supply a Json serialized string and where do we get those? A method argument works but seems nasty. That code looks like this:
  private static Microsoft.AspNetCore.Http.IResult UnavailableEntity(IResult result)
  {
    var problem = new ProblemDetails
    {
      Type = "https://tools.ietf.org/html/rfc7231#section-6.6.4",
      Title = "Critical errors occurred",
      Status = (int)HttpStatusCode.ServiceUnavailable,
      Errors = result.Errors.Any() ? result.Errors : null,
      ValidationErrors = result.ValidationErrors.Any() ? result.ValidationErrors.ToDto() : null
    };

    return Results.Json(problem, _jsonOptions, ProblemContentType, 500);
  }

Using MVC's ProblemDetail Extensions

I like this idea best as it keeps things close to the framework. However, the validation errors and errors will end up in Extensions which looks like this:

Extensions = new Dictionary<string, object?> { { "errors", validationErrors } }

This ProblemDetails property carries this attribute:
[JsonExtensionData]

Everything serializes just fine. Clients, however can't deserialize Extensions because they've been serialized as object.

It is possible to resolve this with a JsonConverter and concrete type to be used on the client.

Examples in the wild show people checking the Type property of the response (which can be obtained by deserializing to ProblemDetails). Based on the Type value, you can specify the target class, such as ProblemDetailsInvalid or ProblemDetailsWithErrors to get at the Extensions values.

Current Package Enhancement

In the current version of the ToMinimalApiResult method, IResult is cast before processing the response. In that case we are losing the CorrelationId before we can put it into a ProblemDetails.

In Any Case

  1. A new extension method would have to be added to Ardalis.Result to alter how Errors are put into whatever we develop, making them accessible to clients as an enumerable.
  2. Client classes will have to be supplied because JsonConverters are weird and no one wants to do that. Probably one for ValidationErrors and one Errors.

Considerations for using MVC ProblemDetails

  1. If we are using a json converter on the client, we can put both validation error entries and string error entries in the Extensions dictionary with a key of error. In this way resulting payloads would look more normal to non C# consumers.

Working on this today...

Looking for input

from result.

ardalis avatar ardalis commented on August 23, 2024

Success - 2xx
ValidationErrors - 4xx
Errors - 500
ProblemDetails

{
  "type": "MyValidationErrorType",
  "title": null,
  "status": 400,
  "detail": null,
  "instance": null,
  "errors": {
    "severity": "Info"
  }
}

{
  "type": "MyValidationErrorType",
  "title": null,
  "status": 400,
  "detail": null,
  "instance": null,
  "errors": [
    {
      "identifier": "PropertyName",
      "errorMessage": "you idiot!",
      "errorCode": "myCode",
      "severity": 2
    }
  ]
}


{
  "type": "MyValidationErrorType",
  "title": null,
  "status": 400,
  "detail": null,
  "instance": null,
  "validationErrors": [
    {
      "identifier": "PropertyName",
      "errorMessage": "you idiot!",
      "errorCode": "myCode",
      "severity": 2
    }
  ],
  "errors": {
    "severity": "Info"
  }
}

from result.

nhwilly avatar nhwilly commented on August 23, 2024

OK, I've written something that works for me. But during our dB meeting you spoke about swapping some statements around to avoid casting as IResult and losing CorrelationId and LocationId as they are not part of the interface.

Here's the awful workaround I ended up with, but I don't know how you intended it to work.

When I try to cast Result<T> to Result it returns null. I guess I'd expect that.

Am I using this Result library wrong? All of my usages are handling Result<T> Nothing calls Result.

public static Microsoft.AspNetCore.Http.IResult ToApiResult<T>(this Result<T> result)
{
  return result.Status switch
  {
    ResultStatus.Ok => result is Result ? Results.Ok() : Results.Ok(result.GetValue()),
    ResultStatus.Created => Results.Created(result.Location, result.GetValue()),
    ResultStatus.NotFound => NotFoundEntity(result),
    ResultStatus.Unauthorized => Results.Unauthorized(),
    ResultStatus.Forbidden => Results.Forbid(),
    ResultStatus.Invalid => BadEntity(result),
    ResultStatus.Error => UnprocessableEntity(result),
    ResultStatus.Conflict => ConflictEntity(result),
    ResultStatus.Unavailable => UnavailableEntity(result),
    ResultStatus.CriticalError => CriticalEntity(result),
    _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."),
  };
}

internal static Microsoft.AspNetCore.Http.IResult ToApiResult(this Result result)
{
  return result.Status switch
  {
    ResultStatus.Ok => result is Result ? Results.Ok() : Results.Ok(result.GetValue()),
    ResultStatus.Created => Results.Created(result.Location, result.GetValue()),
    ResultStatus.NotFound => NotFoundEntity(result),
    ResultStatus.Unauthorized => Results.Unauthorized(),
    ResultStatus.Forbidden => Results.Forbid(),
    ResultStatus.Invalid => BadEntity(result),
    ResultStatus.Error => UnprocessableEntity(result),
    ResultStatus.Conflict => ConflictEntity(result),
    ResultStatus.Unavailable => UnavailableEntity(result),
    ResultStatus.CriticalError => CriticalEntity(result),
    _ => throw new NotSupportedException($"Result {result.Status} conversion is not supported."),
  };
}

from result.

ardalis avatar ardalis commented on August 23, 2024

I don't see where you are trying to cast Result from Result<T> in that code. Or is that the point? You couldn't, so you had to duplicate the switch statement?

from result.

nhwilly avatar nhwilly commented on August 23, 2024

The result of casting Result<T> to Result is null. So, yes. Had to duplicate the statemen.t

from result.

ardalis avatar ardalis commented on August 23, 2024

Definitely could (always) use more docs...

We do have them at: https://result.ardalis.com/

from result.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.