2

I'm new to developing in .Net, so I thought I'd start off with a .Net Core course. So far so good; I'm trying to create an API that requires specific keys to exist in a JSON object. If at least one of the keys is missing, I would expect it to be invalid.

[HttpPost("new")]
public IActionResult CreateGPSPoint([FromBody] ModelExample dataObject)
{
   if (!ModelState.IsValid)
   {
       return BadRequest(ModelState);
   }
}

IsValid returns true, however, even if I omit some, or all, of the keys in the JSON payload that I send. On inspection, those keys that were missing are set to 0 on the subsequent model's properties; so here's what my model looks like.

public class ModelExample
{
    [Required(AllowEmptyStrings = false)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public float Height{ get; set; }

    [Required(AllowEmptyStrings = false)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public decimal Width{ get; set; }

    [Required(AllowEmptyStrings = false)]
    [DisplayFormat(ConvertEmptyStringToNull = false)]
    public int Depth{ get; set; }


    //Populated by the entity later, but feel free to critique nevertheless
    public int Id { get; set; }
}

Note that, since this kind of question has been brought up a few times elsewhere here, I have tried various combinations of Required(AllowEmptyStrings = false) and DisplayFormat(ConvertEmptyStringToNull = false) - my assumption was that these would be checked when the JSON object was "converted(?)" to the Model; however, the result has always been the same.

Initially I had thought this could be an issue with Automapper (which I am using), but validations pass before any entity/model mappings occur.

Those specific fields that I missed out can never be null, either, since no value will be set to 0 (and null would still be a valid value anyway).

My thought was to just interpret the data as a JSON object (instead of a ModelExample) and check that those keys exist early on in my controller logic (something like with Rails' "dataObject&.dig[:key]" - but I don't know if this is either possible or appropriate, or if there is a .Net tactic I've missed out.

My question really is; is something being done incorrectly, or missing from the above?

Many thanks in advance if anyone can provide enlightenment on how the above works!

Trevelyan
  • 87
  • 10
  • 2
    Refer [What does it mean for a property to be Required and nullable?](https://stackoverflow.com/questions/43688968/what-does-it-mean-for-a-property-to-be-required-and-nullable/43689575#43689575) –  Oct 09 '18 at 10:11

1 Answers1

6

When class properties are initialized they get a default value.

For reference types, this is NULL, and for structs the value can vary.

float, decimal, and int are all structs and get initialized to their equivalent of 0.

e.g. public int Depth { get; set; } will be initialized to 0.

You send up a JSON object without those properties, or with those properties undefined, and they don't get set, meaning the default value 0 is always used.

0 exists, and so satisfies the the validation of "required".

To fix this, make the property types nullable.

e.g.

[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public float? Height{ get; set; }

[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public decimal? Width{ get; set; }

[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public int? Depth{ get; set; }

That way, when the properties are undefined in the JSON then they'll get a value of NULL and NULL doesn't satisfy the "required" validation.

e.g. public int? Depth { get; set; } will be initialized to NULL.


Another option is using the BindRequiredAttribute.

Indicates that a property is required for model binding. When applied to a property, the model binding system requires a value for that property. When applied to a type, the model binding system requires values for all properties that type defines.

e.g.

[BindRequired]
[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public float Height{ get; set; }

[BindRequired]
[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public decimal Width{ get; set; }

[BindRequired]
[Required(AllowEmptyStrings = false)]
[DisplayFormat(ConvertEmptyStringToNull = false)]
public int Depth{ get; set; }

https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-2.1#notes-on-the-use-of-the-required-attribute

The BindRequired attribute [...] is useful to ensure form data is complete. When applied to a property, the model binding system requires a value for that property.

  • Nice explanation, thanks! Didn't know about the nullables, either. This fixed it, too, of course! – Trevelyan Oct 09 '18 at 09:56
  • Couldn't it be resolved by RangeAttribute? Assuming that we are only interested in Depth > 0. Then you would not have a nullable property in the place where the value is required. – Michal B. Oct 09 '18 at 11:14
  • @MichalB. IMO it's more obvious to use the validation in the way it's intended. If a field is required it should be marked required, and not range. if it should fall within a certain range then range should be used. –  Oct 09 '18 at 11:22
  • @AndyJ: in that case we could create our own RequiredNonDefaultAttribute and disallow 0 for integers. This would resolve the problem and we would not need to define Depth as nullable in case it is required and its value should not be zero. Wouldn't that be nicer than making Depth nullable? – Michal B. Oct 09 '18 at 12:44
  • @MichalB. I don't have a problem with nullables, so I don't think so, no. Also what if 0 is an allowed value? You'd still have to use Required and nullables in that situation. I think it's nicer to use the same valid approach consistently. But another alternative is using [`BindRequired`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.modelbinding.bindrequiredattribute?view=aspnetcore-2.1) which would force all properties to exist in the JSON being sent. But that's not an option if some properties are optional. –  Oct 09 '18 at 13:12
  • 1
    @MichalB. Actually I was wrong about it not working for optional types. I'm updating my answer with the extra info. –  Oct 09 '18 at 13:23