问题
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.
回答1:
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; }
}
回答2:
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.
回答3:
non-nullable required types.
You do not. It is either required - then there is no sense in it being nullable - or it is not required, then you nullable makes sense, but it makes no sense to require it.
Attributes are always for the whole request. You are in a logical problem because you try to use them not as intended.
If it is optional, the user should actually submit a patch, not a put/post.
回答4:
There was the way to do that, at least it works for me, try [BindRequired]
for non-nullable types.
来源:https://stackoverflow.com/questions/50910093/asp-net-core-require-non-nullable-types