Keywords: IValidatableObject | Conditional Validation | C# | ASP.NET
Abstract: This article explores the core concepts of the IValidatableObject interface, focusing on how to implement conditional object validation. By referencing high-scoring answers from Stack Overflow, we detail the validation process order and provide rewritten code examples demonstrating the use of Validator.TryValidateProperty to ignore specific property validations. The article also covers performance optimization techniques (such as yield return) and integration methods with ASP.NET MVC ModelState, aiming to offer developers comprehensive and practical technical guidance.
Introduction to IValidatableObject
The IValidatableObject interface in C# is used to define object-level validation logic, allowing developers to implement complex, cross-property validation rules within a single method. This serves as a crucial complement to property-level validation, such as using data annotation attributes, and is ideal for scenarios where validation requirements need to be dynamically adjusted based on object state. For example, when an enable flag is false, one might want to ignore range validation for certain properties, which is a typical use case for conditional validation.
Validation Process Order and Key Insights
According to Jeff Handley's blog post (as cited in Answer 2), the validator (Validator) follows a specific order when processing object validation: first, it validates property-level attributes (e.g., [Required]), and if any validation fails, it aborts the process and returns errors; second, it validates object-level attributes; finally, if the object implements IValidatableObject (in the desktop framework), it calls its Validate method. This order explains why relying solely on property annotations cannot directly achieve conditional validation, as validation halts early if property validation fails. Therefore, conditional logic must be handled at the object level through the IValidatableObject.Validate method.
Code Example for Implementing Conditional Validation
Based on the best answer (Answer 1), we rewrite a more generic example to demonstrate how to use IValidatableObject and Validator.TryValidateProperty for conditional validation. The following code snippet defines the ValidateMe class, where the Enable property controls whether other properties are validated.
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)
{
// Use Validator.TryValidateProperty to validate individual properties only when Enable is true
Validator.TryValidateProperty(this.Prop1,
new ValidationContext(this, null, null) { MemberName = "Prop1" },
results);
Validator.TryValidateProperty(this.Prop2,
new ValidationContext(this, null, null) { MemberName = "Prop2" },
results);
// Add custom validation logic, such as comparing properties
if (this.Prop1 > this.Prop2)
{
results.Add(new ValidationResult("Prop1 must be greater than Prop2"));
}
}
return results;
}
}When invoking validation, it is essential to set the validateAllProperties parameter to false to ensure the validator only checks properties with the [Required] attribute, thereby allowing the IValidatableObject.Validate method to handle other conditional logic. An example is provided below:
public void ExecuteValidation()
{
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);
}Performance Optimization: Using Yield Return for Lazy Validation
Answer 3 highlights that since the Validate method returns IEnumerable<ValidationResult>, the yield return keyword can be used to implement lazy validation generation. This is particularly beneficial when validation checks involve IO or CPU-intensive operations, as results can be generated on-demand rather than computing all validations at once. For example:
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (this.Enable)
{
// Add complex validation logic here
if (this.Prop1 > this.Prop2)
{
yield return new ValidationResult("Prop1 must be greater than Prop2");
}
}
}This approach enhances code efficiency and readability, especially when dealing with large-scale objects or network latency.
Practical Integration with ASP.NET MVC
In ASP.NET MVC applications, validation results often need to be integrated with ModelState to display error messages. Answer 3 provides a practical example showing how to convert IValidatableObject validation results into ModelState entries. Below is an extended code snippet demonstrating how to handle validation in a custom model binder or controller:
var validationResults = someObject.Validate(validationContext);
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)));
}This ensures that validation errors are correctly passed to front-end views, improving user experience.
Conclusion and Best Practice Recommendations
In summary, IValidatableObject is a powerful tool for implementing complex, conditional object validation in C#. By combining property-level validation with object-level logic, developers can flexibly handle various business scenarios. Key takeaways include: understanding the validation order to avoid early abort, leveraging Validator.TryValidateProperty for fine-grained control, and using yield return for performance optimization. In practice, it is recommended to modularize validation logic and consider seamless integration with frameworks like ASP.NET MVC. As the .NET ecosystem evolves, this approach plays a vital role in data integrity and user experience.