How to add additional validation to Model field? - c#

I have a second thing:
<td>
#Html.LabelFor(m => m.Number, titleHtmlAttrs)
</td>
<td>
<span class="element-value2">
#Html.EditorFor(m => m.Number)
#Html.ValidationTooltipFor(m => m.Number)
</span>
</td>
And this is how this field looks like in model:
[Display(Name = "Special Number")]
[StringLength(20)]
public string Number { get; set; }
Which means that if I wanted to change this field, i can have any value from empty to 20.
It's ok, but now I need an additional validation.
In model I have some fields:
public DateTime? TimeOf { get; set; }
public bool HasType { get; set; }
New validation should work ONLY if TimeOf is not null and HasType is true. New validation should prevent empty values in Number. Basically, change (from empty to 20) to (from 1 to 20).
How could I correctly accomplish this?
P.S Sorry about my bad English.

For complex validation logic, look at implementing IValidatableObject in your ViewModel and then you can place your conditional validation logic inside the Validate method. (Caveat, this is obviously server side)
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.HasType)
{
// Do other conditional validation
if (validationFails)
{
yield return new ValidationResult("descriptive error goes here");
}
}
// Other validation here.

UPDATE It seems I have misunderstood the question. As the other answer has already pointed out, you could implement the IValidatableObject for achieving this. Something like:
public class YourModelName : IValidatableObject
{
[StringLength(20)]
public string Number{ get; set; }
[Required]
public DateTime? TimeOf { get; set; }
public bool HasType { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (TimeOf != null && HasType)
Validator.TryValidateProperty(this.Number,
new ValidationContext(this, null, null) { MemberName = "Number" },
results);
if (TimeOf == null)
results.Add(new ValidationResult("Date Time must have a value"));
if (!HasType)
results.Add(new ValidationResult("Must be true"));
return results;
}
}
OLD ANSWER:
You could write your custom validator for more complex validation conditions. Something like:
public class SomeCustomValidator : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
string number = value as string;
if (value == null) throw new InvalidOperationException("Can only be used on string properties");
if (!value.IsEmpty && value.Length <= 20)
{
return ValidationResult.Success;
}
return new ValidationResult("Name must be a non-empty string smaller than 20 chars"));
}
}
And for HasType, another custom one:
public class IsTrueAttribute : ValidationAttribute
{
public override bool IsValid(object value)
{
if (value == null) return false;
if (value.GetType() != typeof(bool)) throw new InvalidOperationException("can only be used on boolean properties.");
return (bool) value == true;
}
}
And on TimeOf you could use the required attribute to make sure it has a value:
[Required(ErrorMessage="Must have value")]
public DateTime? TimeOf {get;set;}
And use the custom attributes on the other two:
[SomeCustomValidator(ErrorMessage="Error msg...")]
public string Number {get;set;}
[IsTrueAttribute(ErrorMessage="Must be true")]
public bool HasType {get;set;}

Related

How to pass a field or dynamically changing value to a custom validator in C#?

I have a class
public class File {
[Required, MinLength(1, ErrorMessage = "The Header should contain at least one element")]
public List<string> Header { get; set; }
[ColumnLenghts]
public List<int> ColumnLenghts { get; set; }
//other fields
}
and in my custom validator I want to check
public class ColumnLenghtsAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
List<int> columnLengths = (List<int>) value;
// other checks
if(columnLengths.Count != Header.Count) {
return new ValidationResult(" number of column length does not match to headers");
}
return ValidationResult.Success;
}
}
Problem:
I could not find a way to pass Header field to the custom validator.
I have tried
public class File {
[Required, MinLength(1, ErrorMessage = "The Header should contain at least one element")]
public List<string> Header { get; set; }
[ColumnLenghts (headerCount = Header.Count)]
public List<int> ColumnLenghts { get; set; }
//other fields
}
public class ColumnLenghtsAttribute : ValidationAttribute
{
public int headerCount {get; set;}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
List<int> columnLengths = (List<int>) value;
// other checks
if(columnLengths.Count != headerCount) {
return new ValidationResult(" number of column length does not match to headers");
}
return ValidationResult.Success;
}
}
and I am getting the error: an object reference is required for the non-static field
Question: Is it possible to pass a field or maybe dynamically changing value to a custom validator?

Custom Validation Attribute: Comparing one property to another property's inner property

I have a class StarActivityModel, and I want to validate that the inputted value for StarChange, is less than the Client's property of StarCount. To do this I have attempted to create a Custom Validation Attribute but am having trouble getting the StarCount value.
public class StarActivityModel : BaseModel
{
[Display(Name = "App User")]
public Client? Client { get; set; }
[Display(Name = "Star Change")]
public int? StarChange { get; set; }
}
public class Client
{
public virtual int StarCount { get; set; }
}
My attempt at a custom Validation Attribute
[AttributeUsage(AttributeTargets.Property)]
public class ValidStarChangeAttribute : ValidationAttribute
{
private readonly string _comparisonProperty;
public ValidStarChangeAttribute(string testedPropertyName)
{
_comparisonProperty = testedPropertyName;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var propertyInfo = validationContext.ObjectType.GetProperty(_comparisonProperty);
//Compare passed StarCount and Client starcount
if((int) value > //Somehow get client StarCount)
return //Code an error message
return ValidationResult.Success;
}
}
You could search the ValidationContex class for the method and property you need in custom model validation.
I modified your codes and it seems work well
Codes:
public class ValidStarChangeAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
StarActivityModel staractivemodel = (StarActivityModel)validationContext.ObjectInstance;
if (value != null)
{
if((int)value< staractivemodel.Client.StarCount)
{
return ValidationResult.Success;
}
}
return new ValidationResult("error");
}
}
Result:

TryValidateObject not returning results on Range

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);

How to put conditional Required Attribute into class property to work with WEB API?

I just want to put conditional Required Attribute which is work with WEB API
Example
public sealed class EmployeeModel
{
[Required]
public int CategoryId{ get; set; }
public string Email{ get; set; } // If CategoryId == 1 then it is required
}
I am using Model State validation via (ActionFilterAttribute)
You can implement your own ValidationAttribute. Perhaps something like this:
public class RequireWhenCategoryAttribute : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var employee = (EmployeeModel) validationContext.ObjectInstance;
if (employee.CategoryId == 1)
return ValidationResult.Success;
var emailStr = value as string;
return string.IsNullOrWhiteSpace(emailStr)
? new ValidationResult("Value is required.")
: ValidationResult.Success;
}
}
public sealed class EmployeeModel
{
[Required]
public int CategoryId { get; set; }
[RequireWhenCategory]
public string Email { get; set; } // If CategoryId == 1 then it is required
}
This is just a sample. It may have casting issues, and I'm not sure this is the best approach to solve this problem.
Here's my 2 cents. It will give you a nice message like "AssigneeId is required for the current AssigneeType value Salesman" It works for enums too.
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = false)]
public class RequiredForAnyAttribute : ValidationAttribute
{
/// <summary>
/// Values of the <see cref="PropertyName"/> that will trigger the validation
/// </summary>
public string[] Values { get; set; }
/// <summary>
/// Independent property name
/// </summary>
public string PropertyName { get; set; }
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var model = validationContext.ObjectInstance;
if (model == null || Values == null)
{
return ValidationResult.Success;
}
var currentValue = model.GetType().GetProperty(PropertyName)?.GetValue(model, null)?.ToString();
if (Values.Contains(currentValue) && value == null)
{
var propertyInfo = validationContext.ObjectType.GetProperty(validationContext.MemberName);
return new ValidationResult($"{propertyInfo.Name} is required for the current {PropertyName} value {currentValue}");
}
return ValidationResult.Success;
}
}
Use it like this
public class SaveModel {
[Required]
public AssigneeType? AssigneeType { get; set; }
[RequiredForAny(Values = new[] { nameof(AssigneeType.Salesman) }, PropertyName = nameof(AssigneeType))]
public Guid? AssigneeId { get; set; }
}

ASP.NET MVC: Custom Validation by DataAnnotation

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!

Categories

Resources