Search code examples
c#asp.net-corevalidationasp.net-core-webapi

Validator.TryValidateObject() is not validating child objects


I am using the below code to validate my models explicitly, it validates the parent objects fine but fails to validate the child objects. Any suggestions around this?

if (!Validator.TryValidateObject(entity, context, errorResults, true))
{
    
}

Solution

  • Try to validate the object recursively:

            public static (bool isValid, ValidationResult[] errors) Validate(object input, ICollection<ValidationResult> results = null)
            {
                if (results == null)
                {
                    results = new List<ValidationResult>();
                }
                var context = new ValidationContext(input);
    
                // Validate the current object
                bool isValid = Validator.TryValidateObject(input, context, results, true);
    
                // Use reflection to get properties
                var properties = input.GetType().GetProperties()
                    .Where(prop => prop.CanRead && prop.PropertyType.IsClass);
    
                // Recursively validate nested objects
                foreach (var prop in properties)
                {
                    var value = prop.GetValue(input);
                    if (value != null)
                    {
                        if (value is IEnumerable<object> list)
                        {
                            foreach (var item in list)
                            {
                                var (nestedIsValid, nestedErrors) = Validate(item, results);
                                isValid &= nestedIsValid;
                            }
                        }
                        else
                        {
                            var (nestedIsValid, nestedErrors) = Validate(value, results);
                            isValid &= nestedIsValid;
                        }
                    }
                }
    
                return (isValid, results.ToArray());
            }
    

    Here is a full example you can run it directly:

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Linq;
    
    namespace LearnModelValidate
    {
        public class Program
        {
            public static void Main(string[] args)
            {
                var widget = new Widget
                {
                    Price = 1557,
                    Name = "test",
                    Password = "Invalidpassword",
                    SubWidget = new Widget
                    {
                        Id = 1,
                        Name = "Sub widget",
                        Price = 100,
                        Password = "Invalid password"
                    }
                };
    
                var (isValid, errors) = Validate(widget);
    
                Console.WriteLine($"Is valid: {isValid}");
    
                foreach (var error in errors)
                {
                    Console.WriteLine(error.ErrorMessage);
                }
            }
    
            public static (bool isValid, ValidationResult[] errors) Validate(object input, ICollection<ValidationResult> results = null)
            {
                if (results == null)
                {
                    results = new List<ValidationResult>();
                }
                var context = new ValidationContext(input);
    
                // Validate the current object
                bool isValid = Validator.TryValidateObject(input, context, results, true);
    
                // Use reflection to get properties
                var properties = input.GetType().GetProperties()
                    .Where(prop => prop.CanRead && prop.PropertyType.IsClass);
    
                // Recursively validate nested objects
                foreach (var prop in properties)
                {
                    var value = prop.GetValue(input);
                    if (value != null)
                    {
                        if (value is IEnumerable<object> list)
                        {
                            foreach (var item in list)
                            {
                                var (nestedIsValid, nestedErrors) = Validate(item, results);
                                isValid &= nestedIsValid;
                            }
                        }
                        else
                        {
                            var (nestedIsValid, nestedErrors) = Validate(value, results);
                            isValid &= nestedIsValid;
                        }
                    }
                }
    
                return (isValid, results.ToArray());
            }
        }
    
        public class Widget
        {
            [Required(ErrorMessage = "The {0} is required!")]
            public int? Id { get; set; }
    
            [Required]
            [MinLength(10, ErrorMessage = "The {0} requires min length: {1}")]
            public string Name { get; set; }
    
            [Range(1, 100)]
            public decimal Price { get; set; }
    
            [NoSpace]
            public string Password { get; set; }
    
            public Widget SubWidget { get; set; }
        }
    
        public class NoSpace : ValidationAttribute
        {
            public override bool IsValid(object value)
            {
                if (value is string val)
                {
                    return !val.Contains(" ") && !val.Contains("\r") && !val.Contains("\n");
                }
                return false;
            }
    
            protected override ValidationResult IsValid(object value, ValidationContext validationContext)
            {
                if (IsValid(value))
                {
                    return ValidationResult.Success;
                }
                else
                {
                    return new ValidationResult($"The {validationContext.DisplayName} can not contain space!");
                }
            }
        }
    }
    

    Hopefully you will get result:

    Is valid: False
    The Id is required!
    The Name requires min length: 10
    The field Price must be between 1 and 100.
    The Password can not contain space!