The specific functionality of MVC model validation that I want to leverage is validating data BEFORE it has been assigned to properties of an object instance.
For example if I have the class:
public class Test
{
[Required(ErrorMessage="Id is required")]
public int Id { get; set; }
[Required(ErrorMessage="Name is required")]
[RegularExpression(Constants.SomeRegex, ErrorMessage = "Please enter a valid value for Name")]
public int Name { get; set; }
}
I want to be able to validate that the value assigned to 'Id' can at least be assigned before trying to create an instance. In this case that would mean being assignable to an integer - so the value "ABC" would fail validation.
Of course I can't create an instance of Test with the value "ABC" for Id, it's not assignable to Int32.
MVC controllers implement this functionality - errors will be reported back before an instance of the model class can be created.
To this end, I have so far attempted using System.CompondentModel.DataAnnotations.Validator
public bool IsValid(IDictionary<object, object> data, out OfferConfig offerConfig)
{
offerConfig = new OfferConfig();
var context = new ValidationContext(offerConfig, data);
var results = new List<ValidationResult>();
return Validator.TryValidateObject(offerConfig, context, results, true);
}
And passing in an instance implementing IDictionary
var dict = new Dictionary<object, object>
{
{"Id", dataTable.Rows[i][0].ToString()},
{"Name", dataTable.Rows[i][1].ToString()}
}
Like so:
Test testInstance;
bool isValid = IsValid(dict, out testInstance);
But maybe the Validator doesn't work as I'm expecting. Is the data argument supposed to be string object representations of the model properties? The validation results out appear as if values simply have not been assigned rather than being incorrect.
Hopefully someone can see what I'm trying to achieve here...
Simply create new validation attribute in which you will place our validation logic. Like this:
public class StringIdLengthRangeAttribute : ValidationAttribute
{
public int Minimum { get; set; }
public int Maximum { get; set; }
public StringLengthRangeAttribute()
{
this.Minimum = 0;
this.Maximum = int.MaxValue;
}
public override bool IsValid(object value)
{
string strValue = value as string;
if (!string.IsNullOrEmpty(strValue))
{
int len = strValue.Length;
return len >= this.Minimum && len <= this.Maximum;
}
return true;
}
}
This is example validation of length - replace it with your validation logic.
And your class:
public class Test
{
[Required]
[StringIdLengthRange(Minimum = 10, Maximum = 20)]
public string Id { get; set; }
}
With such attribute you are able to use that logic on any other fields.
Related
I am using Microsoft.Extension.Options in ASP.NET Core 3.1 and I want to validate entries in an configuration file.
For this I want that, e.g. a RangeAttribute is applied to each element of an IEnumerable.
class MyConfiguration
{
[ApplyToItems]
[Range(1, 10)]
publlic IList<int> MyConfigValues { get; set; }
}
Or something like that. How do I write the ApplyToItems method?
As far as I know there is no way to retrieve the other ValidationAttributes while a possible ApplyToItems is validated.
Alternatively I could imagine something like:
[Apply(Range(1, 10)]
public List<int> MyConfigValues { get; set; }
but is that even valid syntax? How would I write an Attribute like Apply that takes other Attributes as parameter without falling back on something like
[Apply(new RangeAttribute(1, 10)]
which does not look nice.
To create a custom data annotation validator follow these gudelines:
Your class has to inherit from System.ComponentModel.DataAnnotations.ValidationAttribute class.
Override bool IsValid(object value) method and implement validation logic inside it.
That's it.
(from How to create Custom Data Annotation Validators)
So in your case it could be something like this:
public class ApplyRangeAttribute : ValidationAttribute
{
public int Minimum { get; set; }
public int Maximum { get; set; }
public ApplyRangeAttribute()
{
this.Minimum = 0;
this.Maximum = int.MaxValue;
}
public override bool IsValid(object value)
{
if (value is IList<int> list)
{
if (list.Any(i => i < Minimum || i > Maximum))
{
return false;
}
return true;
}
return false;
}
}
Edit
Here's how you would use it:
class MyConfiguration
{
[ApplyRange(Minimum = 1, Maximum = 10)]
public IList<int> MyConfigValues { get; set; }
}
This question is related to this question. I managed to get one step further, but I am now unable to initialize my whole object with default values in order to prevent it from being null at list level. The goal of this is to hand down the "null" values to my SQL query. Ultimately what I want is one record in my DB that will express: This row has been recorded, but the related values were "null".
I have tried Brian's fiddle and it does not seem to work for me to initialize the whole model with standard values.
Expectation: Upon object initialisation the "null" values should be used and then overwritten in case there is a value coming through JSON deserialisation.
Here is what I have tried. None of this will have the desired effect. I receive this error:
Application_Error: System.ArgumentNullException: Value cannot be null.
Every time I try to access one of the lists in the data model.
namespace Project.MyJob
{
public class JsonModel
{
public JsonModel()
{
Type_X type_x = new Type_X(); // This works fine.
List<Actions> action = new List<Actions>(); // This is never visible
/*in my object either before I initialise JObject or after. So whatever
gets initialised here never makes it to my object. Only Type_X appears
to be working as expected. */
action.Add(new Actions {number = "null", time = "null", station =
"null", unitState = "null"}) // This also does not prevent my
//JsonModel object from being null.
}
public string objectNumber { get; set; }
public string objectFamily { get; set; }
public string objectOrder { get; set; }
public string location { get; set; }
public string place { get; set; }
public string inventionTime { get; set; }
public string lastUpdate { get; set; }
public string condition { get; set; }
public Type_X Type_X { get; set; }
public List<Actions> actions { get; set; }
}
public class Actions
{
public Actions()
{
// None of this seems to play a role at inititialisation.
count = "null";
datetime = "null";
place = "null";
status = "null";
}
// public string count { get; set; } = "null"; should be the same as above
// but also does not do anything.
public string count { get; set; }
public string datetime { get; set; }
public string place { get; set; }
public string status { get; set; }
}
public class Type_X
{
public Type_X
{
partA = "null"; // This works.
}
public string partA { get; set; }
public string PartB { get; set; }
public string partC { get; set; }
public string partD { get; set; }
public string partE { get; set; }
}
}
This is how I now initialize the object based on Brian's answer.
JObject = JsonConvert.DeserializeObject< JsonModel >(json.ToString(), new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore});
When I try to iterate over Actions' content, it (logically) gives me above mentioned null error.
for (int i = 0, len = JObject.actions.Count(); i < len; i++)
My current understanding of constructor initialisations:
If I define values such as count = "null"; they should appear in any new object that is created.
If default values are present I would then also expect that a list that has items with default values (such as count for ex.) would be of Count() 1 and not null. How is that even possible?
This will get you out of your bind:
private List<Actions> _actions = new List<Actions>();
public List<Actions> actions { get => _actions; set => _actions = value ?? _actions; }
This causes trying to set actions to null to set it to the previous value, and it is initially not null so it can never be null.
I'm not absolutely sure I'm reading your question right, so here's the same fragment for partA:
private string _partA = "null";
public string partA { get => _partA; set => _partA = value ?? _partA; }
I have found that in some cases, initializing generic lists with their default constructor on your model increases ease of use. Otherwise you will always want to validate they are not null before applying any logic(even something as simple as checking list length). Especially if the entity is being hydrated outside of user code, i.e. database, webapi, etc...
One option is to break up your initialization into two parts. Part 1 being the basic initialization via default constructor, and part 2 being the rest of your hydration logic:
JObject = new List < YourModel >();
... < deserialization code here >
Alternatively you could do this in your deserialization code, but it would add a bit of complexity. Following this approach will allow you to clean up your code in other areas since each access will not need to be immediately proceeded by a null check.
I have following object
public class TestClass
{
[Required]
public int TestId { get; set; }
}
I validate using:
List<ValidationResult> results = new List<ValidationResult>();
var vc = new ValidationContext(data);
if (Validator.TryValidateObject(data, vc, results, true))
return;
This validates perfectly fine if data is of type TestClass but not when I pass list of TestClass items (List<TestClass>)
How can I validate the items withing a list without iterating?
TryValidateObject expects an object and not a list of. You have to write a helper class. Furthermore it even doesn't recursively check the Validation. See this SO question for more...
Usually I add the Validate method in the class that I need to validate, and I foreach the property that has a list of T.
For example:
public class ClassToValidate : IValidatableObject
{
// Property with a non-custom type
[Required(AllowEmptyStrings = false, ErrorMessage = "\"Language\" is required. It can not be empty or whitespace")]
public string Language { get; set; }
// Property with a custom type
public Source Source { get; set; }
// Property with a custom type list
public List<Streaming> StreamingList { get; set; } = new List<Streaming>();
// Validate method that you need to implement because of "IValidatableObject"
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// The validation result that will be returned
List<ValidationResult> result = new List<ValidationResult>();
// Language doesn't need to be added in the Validate method because it isn't a custom object
// Custom single T object to be validated
Validator.TryValidateObject(Source, new ValidationContext(Source), result, true);
// Custom list<T> object to be validated
foreach (var streaming in StreamingList)
{
Validator.TryValidateObject(streaming, new ValidationContext(streaming), result, true);
}
return result;
}
}
public class Source
{
[Range(16, 192, ErrorMessage = "\"Bitrate\" must be between 16 and 192")]
public int? Bitrate { get; set; }
public string Codec { get; set; }
}
public class Streaming
{
[Required(AllowEmptyStrings = false, ErrorMessage = "\"Url\" can not be empty")]
public string Url { get; set; }
[Range(0, 1000, ErrorMessage = "\"Offset\" must be between 0 and 1000")]
public int Offset { get; set; }
}
I'm trying to leverage the DataAnnotations.Validator outside of MVC. I have two services that validate their respective models. Both models inherit from a Base class I wrote that had a ValidateModel() method.
public class BaseValidatableDomainModel : IValidatableDomainModel
{
public BaseValidatableDomainModel()
{
ModelState = new ModelStateDictionary();
}
public ModelStateDictionary ModelState { get; set; }
public virtual void ValidateModel()
{
var validationContext = new ValidationContext(this, serviceProvider: null, items: null);
var results = new List<ValidationResult>();
Validator.TryValidateObject(this, validationContext, results);
foreach (var thisInvalidResult in results)
{
ModelState.AddModelError(thisInvalidResult.MemberNames.FirstOrDefault(),thisInvalidResult.ErrorMessage);
}
}
}
I have a test for each service that verifies the service behaves correctly when the data is invalid. The one model correctly errors for PhoneNumber == null:
[Required]
public string PhoneNumber { get; set; }
However, the other model does not error when CompanyId is 0. CompanyId is defined as this:
public class CompanyAddressDomainModel : BaseValidatableDomainModel
{
// Other fields
[Required]
[Range(1, int.MaxValue, ErrorMessage = "Company is required")]
public int CompanyId { get; set; }
[Required]
public AddressInputDomainModel Address { get; set; }
}
The code calls validation like this:
CompanyAddressDomainModel companyAddress = // set values
companyAddress.ValidateModel();
if (!companyAddress.ModelState.IsValid)
{
return companyAddress;
}
Why would it catch some validation, and not others? As far as I can tell, the two services and models are defined the same. If more information is needed, please let me know.
If I test with CompanyId == 0 and Address == null I do see the address error, but not the CompanyId error.
Looks like I have to tell TryValidateObject to validate all.
public static bool TryValidateObject(
object instance,
ValidationContext validationContext,
ICollection<ValidationResult> validationResults,
bool validateAllProperties
)
Like this:
Validator.TryValidateObject(this, validationContext, results, true);
I have a Model with 4 properties which are of type string. I know you can validate the length of a single property by using the StringLength annotation. However I want to validate the length of the 4 properties combined.
What is the MVC way to do this with data annotation?
I'm asking this because I'm new to MVC and want to do it the correct way before making my own solution.
You could write a custom validation attribute:
public class CombinedMinLengthAttribute: ValidationAttribute
{
public CombinedMinLengthAttribute(int minLength, params string[] propertyNames)
{
this.PropertyNames = propertyNames;
this.MinLength = minLength;
}
public string[] PropertyNames { get; private set; }
public int MinLength { get; private set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var properties = this.PropertyNames.Select(validationContext.ObjectType.GetProperty);
var values = properties.Select(p => p.GetValue(validationContext.ObjectInstance, null)).OfType<string>();
var totalLength = values.Sum(x => x.Length) + Convert.ToString(value).Length;
if (totalLength < this.MinLength)
{
return new ValidationResult(this.FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
}
and then you might have a view model and decorate one of its properties with it:
public class MyViewModel
{
[CombinedMinLength(20, "Bar", "Baz", ErrorMessage = "The combined minimum length of the Foo, Bar and Baz properties should be longer than 20")]
public string Foo { get; set; }
public string Bar { get; set; }
public string Baz { get; set; }
}
Self validated model
Your model should implement an interface IValidatableObject. Put your validation code in Validate method:
public class MyModel : IValidatableObject
{
public string Title { get; set; }
public string Description { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Title == null)
yield return new ValidationResult("*", new [] { nameof(Title) });
if (Description == null)
yield return new ValidationResult("*", new [] { nameof(Description) });
}
}
Please notice: this is a server-side validation. It doesn't work on client-side. You validation will be performed only after form submission.
ExpressiveAnnotations gives you such a possibility:
[Required]
[AssertThat("Length(FieldA) + Length(FieldB) + Length(FieldC) + Length(FieldD) > 50")]
public string FieldA { get; set; }
To improve Darin's answer, it can be bit shorter:
public class UniqueFileName : ValidationAttribute
{
private readonly NewsService _newsService = new NewsService();
public override bool IsValid(object value)
{
if (value == null) { return false; }
var file = (HttpPostedFile) value;
return _newsService.IsFileNameUnique(file.FileName);
}
}
Model:
[UniqueFileName(ErrorMessage = "This file name is not unique.")]
Do note that an error message is required, otherwise the error will be empty.
Background:
Model validations are required for ensuring that the received data we receive is valid and correct so that we can do the further processing with this data. We can validate a model in an action method. The built-in validation attributes are Compare, Range, RegularExpression, Required, StringLength. However we may have scenarios wherein we required validation attributes other than the built-in ones.
Custom Validation Attributes
public class EmployeeModel
{
[Required]
[UniqueEmailAddress]
public string EmailAddress {get;set;}
public string FirstName {get;set;}
public string LastName {get;set;}
public int OrganizationId {get;set;}
}
To create a custom validation attribute, you will have to derive this class from ValidationAttribute.
public class UniqueEmailAddress : ValidationAttribute
{
private IEmployeeRepository _employeeRepository;
[Inject]
public IEmployeeRepository EmployeeRepository
{
get { return _employeeRepository; }
set
{
_employeeRepository = value;
}
}
protected override ValidationResult IsValid(object value,
ValidationContext validationContext)
{
var model = (EmployeeModel)validationContext.ObjectInstance;
if(model.Field1 == null){
return new ValidationResult("Field1 is null");
}
if(model.Field2 == null){
return new ValidationResult("Field2 is null");
}
if(model.Field3 == null){
return new ValidationResult("Field3 is null");
}
return ValidationResult.Success;
}
}
Hope this helps. Cheers !
References
Code Project - Custom Validation Attribute in ASP.NET MVC3
Haacked - ASP.NET MVC 2 Custom Validation
A bit late to answer, but for who is searching.
You can easily do this by using an extra property with the data annotation:
public string foo { get; set; }
public string bar { get; set; }
[MinLength(20, ErrorMessage = "too short")]
public string foobar
{
get
{
return foo + bar;
}
}
That's all that is too it really. If you really want to display in a specific place the validation error as well, you can add this in your view:
#Html.ValidationMessage("foobar", "your combined text is too short")
doing this in the view can come in handy if you want to do localization.
Hope this helps!