How do I use IValidatableObject?
I understand that IValidatableObject
is used to validate an object in a way that lets one compare properties against each other.
I'd still like to have attributes to validate individual properties, but I want to ignore failures on some properties in certain cases.
Am I trying to use it incorrectly in the case below? If not how do I implement this?
public class ValidateMe : IValidatableObject
{
[Required]
public bool Enable { get; set; }
[Range(1, 5)]
public int Prop1 { get; set; }
[Range(1, 5)]
public int Prop2 { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (!this.Enable)
{
/* Return valid result here.
* I don't care if Prop1 and Prop2 are out of range
* if the whole object is no开发者_开发百科t "enabled"
*/
}
else
{
/* Check if Prop1 and Prop2 meet their range requirements here
* and return accordingly.
*/
}
}
}
First off, thanks to @paper1337 for pointing me to the right resources...I'm not registered so I can't vote him up, please do so if anybody else reads this.
Here's how to accomplish what I was trying to do.
Validatable class:
public class ValidateMe : IValidatableObject
{
[Required]
public bool Enable { get; set; }
[Range(1, 5)]
public int Prop1 { get; set; }
[Range(1, 5)]
public int Prop2 { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (this.Enable)
{
Validator.TryValidateProperty(this.Prop1,
new ValidationContext(this, null, null) { MemberName = "Prop1" },
results);
Validator.TryValidateProperty(this.Prop2,
new ValidationContext(this, null, null) { MemberName = "Prop2" },
results);
// some other random test
if (this.Prop1 > this.Prop2)
{
results.Add(new ValidationResult("Prop1 must be larger than Prop2"));
}
}
return results;
}
}
Using Validator.TryValidateProperty()
will add to the results collection if there are failed validations. If there is not a failed validation then nothing will be add to the result collection which is an indication of success.
Doing the validation:
public void DoValidation()
{
var toValidate = new ValidateMe()
{
Enable = true,
Prop1 = 1,
Prop2 = 2
};
bool validateAllProperties = false;
var results = new List<ValidationResult>();
bool isValid = Validator.TryValidateObject(
toValidate,
new ValidationContext(toValidate, null, null),
results,
validateAllProperties);
}
It is important to set validateAllProperties
to false for this method to work. When validateAllProperties
is false only properties with a [Required]
attribute are checked. This allows the IValidatableObject.Validate()
method handle the conditional validations.
Quote from Jeff Handley's Blog Post on Validation Objects and Properties with Validator:
When validating an object, the following process is applied in Validator.ValidateObject:
- Validate property-level attributes
- If any validators are invalid, abort validation returning the failure(s)
- Validate the object-level attributes
- If any validators are invalid, abort validation returning the failure(s)
- If on the desktop framework and the object implements IValidatableObject, then call its Validate method and return any failure(s)
This indicates that what you are attempting to do won't work out-of-the-box because the validation will abort at step #2. You could try to create attributes that inherit from the built-in ones and specifically check for the presence of an enabled property (via an interface) before performing their normal validation. Alternatively, you could put all of the logic for validating the entity in the Validate
method.
You also could take a look a the exact implemenation of Validator
class here
Just to add a couple of points:
Because the Validate()
method signature returns IEnumerable<>
, that yield return
can be used to lazily generate the results - this is beneficial if some of the validation checks are IO or CPU intensive.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.Enable)
{
// ...
if (this.Prop1 > this.Prop2)
{
yield return new ValidationResult("Prop1 must be larger than Prop2");
}
Also, if you are using MVC ModelState
, you can convert the validation result failures to ModelState
entries as follows (this might be useful if you are doing the validation in a custom model binder):
var resultsGroupedByMembers = validationResults
.SelectMany(vr => vr.MemberNames
.Select(mn => new { MemberName = mn ?? "",
Error = vr.ErrorMessage }))
.GroupBy(x => x.MemberName);
foreach (var member in resultsGroupedByMembers)
{
ModelState.AddModelError(
member.Key,
string.Join(". ", member.Select(m => m.Error)));
}
I implemented a general usage abstract class for validation
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace App.Abstractions
{
[Serializable]
abstract public class AEntity
{
public int Id { get; set; }
public IEnumerable<ValidationResult> Validate()
{
var vResults = new List<ValidationResult>();
var vc = new ValidationContext(
instance: this,
serviceProvider: null,
items: null);
var isValid = Validator.TryValidateObject(
instance: vc.ObjectInstance,
validationContext: vc,
validationResults: vResults,
validateAllProperties: true);
/*
if (true)
{
yield return new ValidationResult("Custom Validation","A Property Name string (optional)");
}
*/
if (!isValid)
{
foreach (var validationResult in vResults)
{
yield return validationResult;
}
}
yield break;
}
}
}
Implement validation logic using IValidatableObject or property level validation (attributes) than use System.ComponentModel.DataAnnotations.Validator class like this
var validationContext = new ValidationContext(model,, null, null);
var validations = new Collection<ValidationResult>();
Validator.TryValidaObject(model, validationContext, validations, true)
any errors should be present in validations collection (ErrorMessage property should be not empty).
https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator?view=net-6.0
The problem with the accepted answer is that it now depends on the caller for the object to be properly validated. I would either remove the RangeAttribute and do the range validation inside the Validate method or I would create a custom attribute subclassing RangeAttribute that takes the name of the required property as an argument on the constructor.
For example:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
class RangeIfTrueAttribute : RangeAttribute
{
private readonly string _NameOfBoolProp;
public RangeIfTrueAttribute(string nameOfBoolProp, int min, int max) : base(min, max)
{
_NameOfBoolProp = nameOfBoolProp;
}
public RangeIfTrueAttribute(string nameOfBoolProp, double min, double max) : base(min, max)
{
_NameOfBoolProp = nameOfBoolProp;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(_NameOfBoolProp);
if (property == null)
return new ValidationResult($"{_NameOfBoolProp} not found");
var boolVal = property.GetValue(validationContext.ObjectInstance, null);
if (boolVal == null || boolVal.GetType() != typeof(bool))
return new ValidationResult($"{_NameOfBoolProp} not boolean");
if ((bool)boolVal)
{
return base.IsValid(value, validationContext);
}
return null;
}
}
I liked cocogza's answer except that calling base.IsValid resulted in a stack overflow exception as it would re-enter the IsValid method again and again. So I modified it to be for a specific type of validation, in my case it was for an e-mail address.
[AttributeUsage(AttributeTargets.Property)]
class ValidEmailAddressIfTrueAttribute : ValidationAttribute
{
private readonly string _nameOfBoolProp;
public ValidEmailAddressIfTrueAttribute(string nameOfBoolProp)
{
_nameOfBoolProp = nameOfBoolProp;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (validationContext == null)
{
return null;
}
var property = validationContext.ObjectType.GetProperty(_nameOfBoolProp);
if (property == null)
{
return new ValidationResult($"{_nameOfBoolProp} not found");
}
var boolVal = property.GetValue(validationContext.ObjectInstance, null);
if (boolVal == null || boolVal.GetType() != typeof(bool))
{
return new ValidationResult($"{_nameOfBoolProp} not boolean");
}
if ((bool)boolVal)
{
var attribute = new EmailAddressAttribute {ErrorMessage = $"{value} is not a valid e-mail address."};
return attribute.GetValidationResult(value, validationContext);
}
return null;
}
}
This works much better! It doesn't crash and produces a nice error message. Hope this helps someone!
The thing i don't like about iValidate is it seems to only run AFTER all other validation.
Additionally, at least in our site, it would run again during a save attempt. I would suggest you simply create a function and place all your validation code in that. Alternately for websites, you could have your "special" validation in the controller after the model is created. Example:
public ActionResult Update([DataSourceRequest] DataSourceRequest request, [Bind(Exclude = "Terminal")] Driver driver)
{
if (db.Drivers.Where(m => m.IDNumber == driver.IDNumber && m.ID != driver.ID).Any())
{
ModelState.AddModelError("Update", string.Format("ID # '{0}' is already in use", driver.IDNumber));
}
if (db.Drivers.Where(d => d.CarrierID == driver.CarrierID
&& d.FirstName.Equals(driver.FirstName, StringComparison.CurrentCultureIgnoreCase)
&& d.LastName.Equals(driver.LastName, StringComparison.CurrentCultureIgnoreCase)
&& (driver.ID == 0 || d.ID != driver.ID)).Any())
{
ModelState.AddModelError("Update", "Driver already exists for this carrier");
}
if (ModelState.IsValid)
{
try
{
精彩评论