IValidatableObject Interface provides a way for an object to be invalidated. It has a method named Validate(ValidationContext) that determines whether the specified object is valid.
ValidationContext(Object) Initializes a new instance of the ValidationContext class using the specified object instance. ValidationContext(Object, IDictionary<Object,Object>) Initializes a new instance of the ValidationContext class using the specified object and an optional property bag.
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;
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With