Validating lists in C# - c#

We're using DataAnnotations to validate our model.
A very simplified version of our model is:
public class Model
{
public List<Thing> Things;
}
public class Thing
{
[Required]
public string Name {get;set;}
}
Now, the funny thing is that if I create a Thing with no name and add it to the model, I would expect validation to fail, but it passes (shock horror!).
var model = new Model ();
var invalidThing = new Thing (); // No name would fail validation
model.Things.Add(invalidThing );
var validationContext = new ValidationContext(model);
var validationResults = new List<ValidationResult>();
var isValid = Validator.TryValidateObject(model, validationContext, validationResults, true);
Assert.False (isValid); // This fails!
I think the reason for this is that when you validate the model, it validates each property but not items in the property if it's a collection. Things is a property that has no validation, so it passes (despite the fact that it contains invalid item).
How can we ensure that validation also validates items in collection properties? Is there some out-of-the-box validator I could use?

I have fixed this by creating a custom validator for collections that checks validation on each item. A simplified code would look like this:
public class ValidateEachItemAttribute : ValidationAttribute
{
protected readonly List<ValidationResult> validationResults = new List<ValidationResult>();
public override bool IsValid(object value)
{
var list = value as IEnumerable;
if (list == null) return true;
var isValid = true;
foreach (var item in list)
{
var validationContext = new ValidationContext(item);
var isItemValid = Validator.TryValidateObject(item, validationContext, validationResults, true);
isValid &= isItemValid;
}
return isValid;
}
// I have ommitted error message formatting
}
Now decorating the model this way would work as expected:
public class Model
{
[ValidateEachItem]
public List<Thing> Things;
}

Another alternative, if this is ASP.NET MVC, would be to implement IValidatableObject in your model. Like:
public class Model: IValidatableObject
{
public List<Thing> Things;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
//your validation logic here
}
}
Then the result of ModelState.IsValid in your controller will depend on that implementation of Validate method. This is useful when multiple properties of your model are dependent on each other.

The default behavior in your question is not surprising, let's describe it.
Let's say you have a property of type Dictionary<string, Thing> or a property of type Something<Thing> or an untyped collection containing Thing objects in the Model, then how could we expect the Validator perform a validation against Thing objects which are stored in those properties?
We cannot expect Validator to perform a validation against Thing objects which are stored in those properties, because it doesn't have any information about how it should validate those properties. To validate a property, Validator looks for ValidationAttribute for that property and since it doesn't find any validation attribute, it doesn't validate that property.
As a result, you need to create some ValidationAttribute to do that for you and decorate properties with validation attributes. You can implement something like what you implemented in your answer.
Note: In context of ASP.NET MVC you don't need to be worried about it. Default model binder takes care of all validation attributes when model binding, even when model binding to a list.
It's what the default model binder does. When creating each element, when assigning values to properties, it validates property and add the validation errors to model state. At last, all properties and objects are validated and model state contains all validation errors.

Related

Sanitising parameters in ASP.Net Core 2.2

I have a seemingly rather specific problem. My top-level controller and models for complex parameters are auto-generated (Nswag). Some of the model consists of enums.
I have parameters (in query or body) which have to contain backslashes. The values of these in the auto-generated enums automatically have backslashes replaced with underscores. To make model validation work, I have to somehow catch parameters binding with these enums and change them before binding occurs therefore.
For example, given a query
?param=A\B
(or a body with param="a\b") and the Enum:
public enum SomeEnum
{
[System.Runtime.Serialization.EnumMember(Value = #"A\B")]
A_B = 0
}
Model validation fails because A\B isn't found in the enum, naturally.
I have tried filters, custom model binders etc. and custom model binding seems to be the best place as it can be made to apply at precisely the point of binding that specific model. Now, the problem is that I need to modify the incoming parameter and bind to a modified version with underscores. I can't for the life of me find out how to do this. I implemented a custom IModelBinder class, which is called properly but ModelBindingResult.Success(model) doesn't alter what is bound to.
Just to be clear, this has nothing to do with URL encoding or binding to collections etc. This is all working fine.
I essentially need to modify parameters being bound with a specific Enum so that they match the auto-generated enum properties. Any ideas much appreciated.
It seems that a custom binder is the correct way to do it, when you code it properly ...
Below is the binder class that works nicely. SSASPropertyNameBinder is the enum whose values can contain backslashes. This class is mostly boiler plate from the MS ASP.Net Core docs on custom model binders - the interesting bit is at the end.
public class SSASPropertyNameBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var modelName = bindingContext.ModelName;
// Try to fetch the value of the argument by name
var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
if (valueProviderResult == ValueProviderResult.None)
{
return Task.CompletedTask;
}
var value = valueProviderResult.FirstValue;
// Check if the argument value is null or empty
if (string.IsNullOrEmpty(value))
{
return Task.CompletedTask;
}
ValueProviderResult newValueProviderResult = new ValueProviderResult(valueProviderResult.FirstValue.Replace(#"\", "_"));
bindingContext.ModelState.SetModelValue(modelName, newValueProviderResult);
SSASServerPropertyName spn;
// Check if a valid SSAS property
if (Enum.TryParse<SSASServerPropertyName>(newValueProviderResult.FirstValue, out spn))
{
bindingContext.Result = ModelBindingResult.Success(spn);
}
else
{
bindingContext.ModelState.TryAddModelError(modelName, $"Invalid SSAS Property: {valueProviderResult.FirstValue}");
}
return Task.CompletedTask;
}
}

Customized DataAnnotationsModelMetadataProvider not working

I have many properties that require 1 or more validation attributes, like the following:
public class TestModel
{
[Some]
[StringLength(6)]
[CustomRequired] // more attributes...
public string Truck { get; set; }
}
Please note all the above annotations work.
I do not want to write that all the time because whenever Some is applied, all other attributes are also applied to the property. I want to be able to do this:
public class TestModel
{
[Some]
public string Truck { get; set; }
}
Now this is doable by inheriting; therefore, I wrote a custom DataAnnotationsModelMetadataProvider and overrode the CreateMetadata. This looks for anything that is decorated with Some and then adds more attributes to it:
public class TruckNumberMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var attributeList = attributes.ToList();
if (attributeList.OfType<SomeAttribute>().Any())
{
attributeList.Add(new StringLengthAttribute(6));
attributeList.Add(new CustomRequiredAttribute());
}
return base.CreateMetadata(attributeList, containerType, modelAccessor, modelType, propertyName);
}
}
These are the attributes in case it helps:
public class CustomRequiredAttribute : RequiredAttribute
{
public CustomRequiredAttribute()
{
this.ErrorMessage = "Required";
}
}
public class SomeAttribute : RegularExpressionAttribute
{
public SomeAttribute()
: base(#"^[1-9]\d{0,5}$")
{
}
}
Usage
#Html.TextBoxFor(x => x.Truck)
HTML Rendered:
<input name="Truck" id="Truck" type="text" value=""
data-val-required="The Truck field is required."
data-val-regex-pattern="^[1-9]\d{0,5}$"
data-val-regex="The field Truck must match the regular expression '^[1-9]\d{0,5}$'."
data-val="true">
</input>
Problems/Questions
The CustomRequired was applied. But why does it pick up the message from the base class RequiredAttribute if I am using CustomRequired. The data-val-required should just say Required.
The StringLenth of 6 chars is not applied. There is no sign of anything rendered for StringLength, why?
What your custom DataAnnotationsModelMetadataProvider is doing is creating/modifying the ModelMetada associated with your property.
If you inspect the ModelMetadata class you will note that it contains properties such as string DisplayName and string DisplayFormatString which are set based on the application of the [Display] and [DisplayFormat] attributes. It also contains a bool IsRequired attribute that determines if the value of a property is required (a bit more on that later).
It does not contain anything relating to a regular expression or the maximum length, or indeed anything related to validation (except the IsRequired property and the ModelType which is used to validate that that the value can be converted to the type).
It is the HtmlHelper methods that are responsible for generating the html that is passed to the view. To generate the data-val-* attributes, your TextBoxFor() method internally calls the GetUnobtrusiveValidationAttributes() method of the HtmlHelper class which in turn calls methods in the DataAnnotationsModelValidatorProvider class which ultimately generate a Dictionary of the data-val attribute names and values used to generate the html.
I'll leave it to you to explore the source code if you want more detail (refer links below) but to summarize, it gets a collection of all attributes applied to your Truck property that inherit from ValidationAttribute to build the dictionary. In your case the only ValidationAttribute is [Some] which derives from RegularExpressionAttribute so the data-val-regex and data-val-regex-pattern attributes are added.
But because you have added your CustomRequiredAttribute in the TruckNumberMetadataProvider, the IsRequired property of the ModelMetadata has been set to true. If you insect the GetValidators() of DataAnnotationsModelValidatorProvider you will see that a RequiredAttribute is automatically added to the collection of attributes because you have not applied one to the property. The relevant snippet of code is
if (AddImplicitRequiredAttributeForValueTypes && metadata.IsRequired && !attributes.Any(a => a is RequiredAttribute))
{
attributes = attributes.Concat(new[] { new RequiredAttribute() });
}
which results in the data-val-required attribute being added to the html (and it uses the default message because it knows nothing about your CustomRequiredAttribute)
Source code files to get you started if you want to understand the internal workings
HtmlHelper.cs - refer GetUnobtrusiveValidationAttributes() methods at line 413
ModelValidatorProviders.cs - gets the various ValidatorProviders used for validation
DataAnnotationsModelValidatorProvider.cs - the ValidatorProvider for ValidationAttributes
One possible solution if you really want to use just one single ValidationAttribute would be to have it implement IClientValidatable and in the GetClientValidationRules() method, add rules, for example
var rule = new ModelClientValidationRule
{
ValidationType = "required",
ErrorMessage = "Required"
}
which will be read by the ClientDataTypeModelValidatorProvider (and delete your TruckNumberMetadataProvider class). However that will create a maintenance nightmare so I recommend you just add the 3 validation attributes to your property

What does ModelState.IsValid do?

When I do a create method i bind my object in the parameter and then I check if ModelState is valid so I add to the database:
But when I need to change something before I add to the database (before I change it the ModelState couldn't be valid so I have to do it)
why the model state still non valid.
What does this function check exactly?
This is my example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "EncaissementID,libelle,DateEncaissement,Montant,ProjetID,Description")] Encaissement encaissement) {
encaissement.Montant = Convert.ToDecimal(encaissement.Montant);
ViewBag.montant = encaissement.Montant;
if (ModelState.IsValid) {
db.Encaissements.Add(encaissement);
db.SaveChanges();
return RedirectToAction("Index", "Encaissement");
};
ViewBag.ProjetID = new SelectList(db.Projets, "ProjetId", "nomP");
return View(encaissement);
}
ModelState.IsValid indicates if it was possible to bind the incoming values from the request to the model correctly and whether any explicitly specified validation rules were broken during the model binding process.
In your example, the model that is being bound is of class type Encaissement. Validation rules are those specified on the model by the use of attributes, logic and errors added within the IValidatableObject's Validate() method - or simply within the code of the action method.
The IsValid property will be true if the values were able to bind correctly to the model AND no validation rules were broken in the process.
Here's an example of how a validation attribute and IValidatableObject might be implemented on your model class:
public class Encaissement : IValidatableObject
{
// A required attribute, validates that this value was submitted
[Required(ErrorMessage = "The Encaissment ID must be submitted")]
public int EncaissementID { get; set; }
public DateTime? DateEncaissement { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
// Validate the DateEncaissment
if (!this.DateEncaissement.HasValue)
{
results.Add(new ValidationResult("The DateEncaissement must be set", new string[] { "DateEncaissement" });
}
return results;
}
}
Here's an example of how the same validation rule may be applied within the action method of your example:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "EncaissementID,libelle,DateEncaissement,Montant,ProjetID,Description")] Encaissement encaissement) {
// Perform validation
if (!encaissement.DateEncaissement.HasValue)
{
this.ModelState.AddModelError("DateEncaissement", "The DateEncaissement must be set");
}
encaissement.Montant = Convert.ToDecimal(encaissement.Montant);
ViewBag.montant = encaissement.Montant;
if (ModelState.IsValid) {
db.Encaissements.Add(encaissement);
db.SaveChanges();
return RedirectToAction("Index", "Encaissement");
};
ViewBag.ProjetID = new SelectList(db.Projets, "ProjetId", "nomP");
return View(encaissement);
}
It's worth bearing in mind that the value types of the properties of your model will also be validated. For example, you can't assign a string value to an int property. If you do, it won't be bound and the error will be added to your ModelState too.
In your example, the EncaissementID value could not have a value of "Hello" posted to it, this would cause a model validation error to be added and IsValid will be false.
It is for any of the above reasons (and possibly more) that the IsValid bool value of the model state will be false.
ModelState.IsValid will basically tell you if there is any issues with your data posted to the server, based on the data annotations added to the properties of your model.
If, for instance, you have a [Required(ErrorMessage = "Please fill")], and that property is empty when you post your form to the server, ModelState will be invalid.
The ModelBinder also checks some basic stuff for you. If, for instance, you have a BirthDate datepicker, and the property that this picker is binding to, is not a nullable DateTime type, your ModelState will also be invalid if you have left the date empty.
Here, and here are some useful posts to read.
You can find a great write-up on ModelState and its uses here.
Specifically, the IsValid property is a quick way to check if there are any field validation errors in ModelState.Errors. If you're not sure what's causing your Model to be invalid by the time it POST's to your controller method, you can inspect the ModelState["Property"].Errors property, which should yield at least one form validation error.
Edit: Updated with proper dictionary syntax from #ChrisPratt
This is not meant to be the best answer, but I find my errors by stepping through the ModelState Values to find the one with the error in Visual Studio's debugger:
My guess is that everyone with a question about why their ModelState is not valid could benefit from placing a breakpoint in the code, inspecting the values, and finding the one (or more) that is invalid.
This is not the best way to run a production website, but this is how a developer finds out what is wrong with the code.

Shared validation error message for more than one field

In an ASP.NET MVC project I'm trying to validate several fields against a regular expression. However I would like to have only one validation message displayed if any of them fails (and highlight the ones failing).
I can make a custom validation for that, and annotating one of them with the id's of the rest kind of work although only highlights the one decorated with the attribute. But it looks to me as an overkill as I just want to reduce the message to one.
In the same form I will try to do the same for two checkboxes, both must be checked.
So as far as I understand if I use the summary to put a generic message won't be able to tell if it's failing for the checkboxes or for the fields.
Is there a simple way of achieving this?
You could write a custom validation that targets your entire view model class. I wrote the following when I wanted to check that at least one property was set. You can see that this targets the class itself and should therefore give you one message.
/// <summary>
/// A configurable class wide attribute that is used to determine if at least one property of a class has received a value.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class SingleValueConfigurableAttribute : ValidationAttribute, IClientValidatable
{
public SingleValueConfigurableAttribute(string errorKey)
{ ErrorMessage = Properties.Settings.Default[errorKey].ToString(); }
public override bool IsValid(object value)
{
var typeInfo = value.GetType();
var propertyInfo = typeInfo.GetProperties();
return propertyInfo.Any(property => null != property.GetValue(value, null));
}
public override string FormatErrorMessage(string name)
{
return ErrorMessage;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = String.IsNullOrEmpty(ErrorMessage) ? FormatErrorMessage(metadata.DisplayName) : ErrorMessage,
ValidationType = "enforcetrue"
};
}
}

How does DataAnnotations really work in MVC?

This is more of a theoretical question.
I'm currently examining the MVC 3 validation by using ComponentModel.DataAnnotations, and everything works automagically, especially on client side.
Somehow something checks for those attributes, and generates javascript for the validation (or html5 attributes, if using unobtrusive mode), and it works.
My question is that what generates the client side javascript and how can I access and modify it? For example I want to handle the given dataannotation attributes a little differently, or handle custom attributes (I have found that I can derive them from ValidationAttribute, but maybe for some reason I don't want).
Can someone explain it to me what really happens?
(Or links to good explanations would also be good, as I have only found tutorials for actually using dataannotations)
EDIT: Also with deriving from ValidationAttribute, the client-side validation is not working automatically. Why?
MVC3 has a new jQuery Validation mechanism that link jQuery Validation and Validation Attributes Metadata, this is the jquery.validate.unobtrusive file that takes all data- attributes and work with them, just like before when you set the
<add key="UnobtrusiveJavaScriptEnabled" value="false" />
All you need to do is come up with your own Custom Validation Attributes, for that you have 2 options:
Create a Custom Validation Attribute that inherits the ValidationAttribute interface and
override the IsValid
or
Create a Self Validate Model use the model IValidatebleObject that all you need is to return the Validate method
in MVC3 you now have a method that you can override that has a ValidationContext object, where you can simply get all references, properties and values of any other object in the form
Create your own, and that unobtrusive file will handle the mapping of what your custom validator needs and will work out together with the jQuery Validation plugin.
YOU DO NOT Change the javascript... that's sooo 90's and not MVC way!
for example if you want to validate, let's say 2 dates that the last can not be less than the first (period of time for example)
public class TimeCard
{
public DateTime StartDate { get; set; }
[GreaterThanDateAttribute("StartDate")]
public DateTime EndDate { get; set; }
}
creating a Custom Validation
public class GreaterThanDateAttribute : ValidationAttribute
{
public string GreaterThanDateAttribute(string otherPropertyName)
:base("{0} must be greater than {1}")
{
OtherPropertyName = otherPropertyName;
}
public override string FormatErrorMessage(string name)
{
return String.Format(ErrorMessageString, name, OtherPropertyName);
}
public override ValidateionResult IsValid(object value, ValidationContext validationContext)
{
var otherPropertyInfo = validationContext.ObjectTYpe.GetProperty(OtherPropertyName);
var otherDate = (DateTime)otherPropertyInfo.GetValue(validationContext.ObjectInstance, null);
var thisDate = (DateTime)value;
if( thisDate <= otherDate )
{
var message = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(message);
}
return null;
}
}
if using the Self Validating model then the code would be just
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if( EndDate <= StartDate )
yield return new ValidationResult("EndDate must be grater than StartDate");
}
Keep in mind that the Custom Validation is Generic, that's why much code, and Self Validating Model only works on the model applied.
Hope it helps
added
I didn't explain the Custom Client Validation part, fell free to ask if you need examples, but basically:
It's easier in MVC3 (if of course, you understand jQuery.Validate) all you need to do is:
Implement IClientValidateble
Implement a jQuery validation method
Implement an unobtrusive adapter
To create this 3 things, let's take this GreaterThanDateAttribute into account and create the Custom Client Side Validation. For that we need to code this:
append to the GreaterThanDateAttribute
public IEnumerable<ModelCLientValidation> GetCLientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelCLientValidationRule();
rule.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName());
rule.ValidationType = "greater"; // This is what the jQuery.Validation expects
rule.ValidationParameters.Add("other", OtherPropertyName); // This is the 2nd parameter
yield return rule;
}
Then you need to write the new jQuery Validator and the metadata adapter that will link the jQuery.Validation with your code providing the correct data- attributes for that field (if of course, UnobtrusiveJavaScriptEnabled is true)
create a new js file and attach to your <head> for example as
<script src="#Url.Content("~/Scripts/customValidation.js")" type="text/javascript"></script>
and append the new validation
jQuery.validator.addMethod("greater", function(value, element, param) {
// we need to take value and compare with the value in 2nd parameter that is hold in param
return Date.parse(value) > Date.parse($(param).val());
});
and then we write the adapter
jQuery.validator.unobtrusive.adapters.add("greater", ["other"], function(options) {
// pass the 'other' property value to the jQuery Validator
options.rules["greater"] = "#" + options.param.other;
// when this rule fails, show message that comes from ErrorMessage
options.messages["greater"] = options.message;
});

Categories

Resources