Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Core [Require] non-nullable types

Here, the question was posed how to validate non-nullable required types.

The provided solution to make the field nullable like the following is not desirable in my case.

[Required]
public int? Data { get; set; }

How can you change the behavior to instead make the following fail validation in the cases where the field is omitted from the request.

[Required]
public int Data { get; set; }

I have tried a custom validator, but these do not have information about the raw value and only see the default 0 value. I have also tried a custom model binder but it seems to work at the level of the entire request model instead of the integer fields which a want. My binder experiment looks like this:

public class RequiredIntBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType != typeof(int))
            throw new InvalidOperationException($"{nameof(RequiredIntBinder)} can only be applied to integer properties");

        var value = bindingContext.ValueProvider.GetValue(bindingContext.BinderModelName);
        if (value == ValueProviderResult.None)
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return Task.CompletedTask;
        }

        return new SimpleTypeModelBinder(bindingContext.ModelType).BindModelAsync(bindingContext);
    }
}

public class RequiredIntBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (context.Metadata.ModelType == typeof(int))
        {
            return new BinderTypeModelBinder(typeof(RequiredIntBinder));
        }

        return null;
    }
}

and is registered with mvc like this

options.ModelBinderProviders.Insert(0, new RequiredIntBinderProvider());

but the model binder is never used. I feel like I might be close but cannot connect the last dots.

like image 385
Morten Christiansen Avatar asked Jun 18 '18 12:06

Morten Christiansen


People also ask

Why do we need nullable types in C#?

You typically use a nullable value type when you need to represent the undefined value of an underlying value type. For example, a Boolean, or bool , variable can only be either true or false . However, in some applications a variable value can be undefined or missing.

What is non-nullable type in C#?

A struct that contains non-nullable reference types allows assigning default for it without any warnings. Consider the following example: C# Copy.

How do you fix converting null literal or possible null value to non-nullable type?

Cannot convert null literal to non-nullable reference type. Fortunately, there is a solution to this warning – all you do is add the Elvis operator after the variable 'text' and the warning goes away.

What is a non-nullable type?

Nullable variables may either contain a valid value or they may not — in the latter case they are considered to be nil . Non-nullable variables must always contain a value and cannot be nil . In Oxygene (as in C# and Java), the default nullability of a variable is determined by its type.


2 Answers

Solution working with json requests

You cannot validate an already created model instance, because a non-nullable property has always a value (no matter whether it was assigned from json or is a default value). The solution is to report the missing value already during deserialization.

Create a contract resolver

public class RequiredPropertiesContractResolver : DefaultContractResolver
{
    protected override JsonObjectContract CreateObjectContract(Type objectType)
    {
        var contract = base.CreateObjectContract(objectType);

        foreach (var contractProperty in contract.Properties)
        {
            if (contractProperty.PropertyType.IsValueType
                && contractProperty.AttributeProvider.GetAttributes(typeof(RequiredAttribute), inherit: true).Any())
            {
                contractProperty.Required = Required.Always;
            }
        }

        return contract;
    }
}

and then assign it to SerializerSettings:

services.AddMvc()
        .AddJsonOptions(jsonOptions =>
        {
            jsonOptions.SerializerSettings.ContractResolver = new RequiredPropertiesContractResolver();
        });

The ModelState is then invalid for non-nullable properties with the [Required] attribute if the value is missing from json.


Example

Json body

var jsonBody = @"{ Data2=123 }"

is invalid for model

class Model
{
    [Required]
    public int Data { get; set; }

    public int Data2 { get; set; }
}
like image 68
frakon Avatar answered Oct 16 '22 12:10

frakon


Everything from the request is just a string. The modelbinder matches up keys in the request body with property names, and then attempts to coerce them to the appropriate type. If the property is not posted or is posted with an empty string, that will obviously fail when trying to convert to an int. As a result, you end up with the default value for the type. In the case of an int that's 0, while the default value of int? is null.

Only after this binding process is complete is the model then validated. Remember you're validating the model not the post body. There's no reasonable way to validate the post body, since again, it's just a a bunch of key-value pair strings. Therefore, in the case of an int property that's required, but not posted, the value is 0, which is a perfectly valid value for an int, and the validation is satisfied. In the case of int?, the value is null, which is not a valid int, and thus fails validation. That is why the nullable is required, if you want to require a non-nullable type have a value. It's the only way that an empty value can be differentiated from simply a "default" value.

If you are using view models, as you should be, this should not be an issue. You can bind to a nullable int with a required attribute, and you will be assured that it will have a value, despite being nullable, if your model state is valid. Then, you can map that over to a straight int on your entity. That is the correct way to handle things.

like image 36
Chris Pratt Avatar answered Oct 16 '22 12:10

Chris Pratt