GithubHelp home page GithubHelp logo

Comments (5)

zdenek-jelinek avatar zdenek-jelinek commented on May 18, 2024 2

Thank you for your response and the proposed workaround.

Unfortunately, I think what you proposed with checking model state and returning custom response goes against the web API story with features like [ApiController]/ModelStateInvalidFilter. If I had to go to every controller to adjust this, I'd rather just make the request optional and add a custom attribute + schema filter for swashbuckle to show them as required in the swagger doc than to write model state checks everywhere.

The custom ProblemDetailsFactory, I think will need a lot of work to detect these cases reliably. I'm not even sure if the routing validation metadata are accessible from there through HttpContext. Not to mention that the original type is internal so I'd have to reimplement it and keep it up to date with each release. This does not seem like a good trade-off to me.

I think the idea of @jonasof fits really well with the existing conventions model and also reflects the exact requirements/conventions for the case. I'll consider it if the official answer is negative.

As already stated, I'm in no hurry to have this but I'd be really glad if the validation story got more polished so that we don't have to debug and hack together a bunch of workarounds to get a world class web API.

from aspnetcore.

jonasof avatar jonasof commented on May 18, 2024 1

Hi @zdenek-jelinek, In the last week I've spent some time with the same issue. One thing that seems to solve this is to make the request parameter optional with the nullable operator:

public IActionResult CreatePersonAsync(PersonRequest? personRequest)
{
  if (personRequest == null)
  {
    return BadRequest();
  }

  return Ok();
}

But it seems "ugly" because the request variable is not meant to be optional.

After some debugging, I created a workaround to remove the body parameter from the errors response array by disabling the "isRequired" metadata of the request body parameter:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.Routing;

/// <summary>
///   When ImplicitRequiredAttributeForNonNullableReferenceTypes is enabled, the aspnet framework
///   automatically marks the FromBody parameters as required. This is not a problem except that, 
///   when some DTO field is incorrect, the response body comes as:
///   
///   {
///     "errors": {
///   ->  "request": [
///   ->    "The request field is required."
///   ->  ],
///       "someActualMissingJsonParam": [
///         "The input was not valid."
///       ]
///     }
///   }
///   
///   The "The request field is required" error is misleading to the consumers,
///   because it's not an actual JSON property, but the internal request variable name.
///   
///   To remove that error from the response body, this class sets the IsRequired validation metadata to false 
///   when it's coming from the [FromBody] attribute, or when it's coming from a complex field with no 
///   BindingSource attribute (FromQuery, FromPath, ...).
///   
///   That metadata is being set by the aspnet framework at:
/// https://github.com/dotnet/aspnetcore/blob/v6.0.25/src/Mvc/Mvc.DataAnnotations/src/DataAnnotationsMetadataProvider.cs#L412-L415
/// </summary>
public class BodyModelRequiredMetadataRemoverProvider : IValidationMetadataProvider
{
    private readonly ModelMetadataProvider _modelMetadataProvider;

    public BodyModelRequiredMetadataRemoverProvider()
    {
        _modelMetadataProvider = new EmptyModelMetadataProvider();
    }

    public void CreateValidationMetadata(ValidationMetadataProviderContext context)
    {
        if (!IsAParameterFromAControllerAction(context))
        {
            return;
        }

        if (HasFromBodyAttribute(context) || IsModelBeingImplicitlyParsedFromBody(context))
        {
            context.ValidationMetadata.IsRequired = false;
        }
    }

    private bool IsAParameterFromAControllerAction(ValidationMetadataProviderContext context)
    {
        if (context.Key.ParameterInfo == null)
        {
            return false;
        }

        return context.Key.ParameterInfo.Member.CustomAttributes.Any(
            at => at.AttributeType.BaseType?.Equals(typeof(HttpMethodAttribute)) ?? false
        );
    }

    private bool HasFromBodyAttribute(ValidationMetadataProviderContext context)
    {
        return context.Attributes.Any(
            at => at.GetType().Equals(typeof(FromBodyAttribute))
        );
    }

    private bool IsModelBeingImplicitlyParsedFromBody(ValidationMetadataProviderContext context)
    {
        return !HasAnyBindingSourceAttribute(context) && IsComplexType(context);
    }

    /// <summary>
    ///   Checks for other BindingSource attributes like "FromQuery", "FromPath"...
    ///   If they are present, returns false.
    /// 
    ///   This check is needed because for these it's not intended to override IsRequired to false.
    /// </summary>
    private bool HasAnyBindingSourceAttribute(ValidationMetadataProviderContext context)
    {
        return context.Attributes.Any(
            at => at.GetType().GetInterfaces().Any(
                i => i.Equals(typeof(IBindingMetadataProvider))
            )
        );
    }

    /// <summary>
    ///   Checks if the model being validated is from a complex type.
    ///
    ///   It's needed because the DTO classes with multiple attributes are considered complex types, 
    ///   for they we want to omit the errors in the response.
    /// 
    ///   But for the simple types, like int, string... Is not intended to hide them from the response errors.
    /// </summary>
    private bool IsComplexType(ValidationMetadataProviderContext context)
    {
        return _modelMetadataProvider.GetMetadataForType(context.Key.ModelType).IsComplexType;
    }
}

Then add it in the controllers option "ModelMetadataDetailsProviders":

builder.Services.AddControllers((options) => options.ModelMetadataDetailsProviders.Add(new BodyModelRequiredMetadataRemoverProvider()))

Now the output is:

{
   "errors": {
      "someActualMissingJsonParam": [
         "The input was not valid."
       ]
    }
 }

Instead of:

{
  "errors": {
    "request": [
      "The request field is required."
    ],
    "someActualMissingJsonParam": [
      "The input was not valid."
    ]
  }
}

As I said it's a workaround, I haven't tested all the scenarios but it worked as expected so far.

I think a proper option to omit those error messages in the asp.net core framework is needed. As you mentioned, returning the internal variable name is misleading to the API consumers.

from aspnetcore.

zdenek-jelinek avatar zdenek-jelinek commented on May 18, 2024

Regarding priority: This has been happening for years, I don't need this tomorrow. On the other hand, I don't want to be explaining this cryptic error to some grumpy dev 10 years from now.

If there's anything I could do to move this forward, I am happy to. But I'm lacking in knowledge of use-cases such as MVC/Pages (especially form contents) that also come into play here.

from aspnetcore.

captainsafia avatar captainsafia commented on May 18, 2024

@jonasof Thanks for sharing your workarounds here.

Another possible option is to to implement a custom ValidationProblemDetails response object that is returned directly from the action when the model state is invalid.

You can also create a custom implementation of the ProblemDetailsFactory to modify the return objects as needed. See here for more info.

Given the complex nature of MVC's model binding/validation layer, I dunno that we would change the built-in behavior here or add a specific flag for this. But I think the customization options that exist can help with this.

from aspnetcore.

dotnet-policy-service avatar dotnet-policy-service commented on May 18, 2024

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.

from aspnetcore.

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.