¿Cómo puedo decirle al validador de anotaciones de datos que también valide las propiedades secundarias complejas?

¿Puedo validar automáticamente objetos secundarios complejos al validar un objeto padre e incluir los resultados en el ICollection ?

Si ejecuto el siguiente código:

 using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace ConsoleApplication1 { public class Person { [Required] public string Name { get; set; } public Address Address { get; set; } } public class Address { [Required] public string Street { get; set; } [Required] public string City { get; set; } [Required] public string State { get; set; } } class Program { static void Main(string[] args) { Person person = new Person { Name = null, Address = new Address { Street = "123 Any St", City = "New York", State = null } }; var validationContext = new ValidationContext(person, null, null); var validationResults = new List(); var isValid = Validator.TryValidateObject(person, validationContext, validationResults); Console.WriteLine(isValid); validationResults.ForEach(r => Console.WriteLine(r.ErrorMessage)); Console.ReadKey(true); } } } 

Obtengo el siguiente resultado:

False
The Name field is required.

Pero esperaba algo similar a:

False
The Name field is required.
The State field is required.


Ofrecí una recompensa por una mejor solución de validación de objeto hijo, pero no obtuve ningún candidato, idealmente

  • validación de objetos secundarios a una profundidad arbitraria
  • manejando múltiples errores por objeto
  • identificando correctamente los errores de validación en los campos de objetos secundarios.

Todavía estoy sorprendido de que el marco no sea compatible con esto.

Tendrá que hacer su propio atributo de validador (por ejemplo, [CompositeField] ) que valida las propiedades secundarias.

Problema: orden de carpeta de modelo

Desafortunadamente, este es el comportamiento estándar de Validator.TryValidateObject que

no valida recursivamente los valores de las propiedades del objeto

Como se señala en el artículo de Jeff Handley sobre la validación de objetos y propiedades con el Validator , de forma predeterminada, el validador se validará en orden:

  1. Atributos de nivel de propiedad
  2. Atributos de nivel de objeto
  3. Implementación a nivel de modelo IValidatableObject

El problema es, en cada paso del camino …

Si alguno de los validadores no es válido, Validator.ValidateObject abortará la validación y devolverá el error (s)

Problema – Campos de carpetas de modelo

Otro posible problema es que el archivador modelo solo ejecutará la validación en los objetos que ha decidido vincular. Por ejemplo, si no proporciona entradas para los campos dentro de los tipos complejos en su modelo, el encuadernador del modelo no necesitará verificar esas propiedades en absoluto porque no ha llamado al constructor sobre esos objetos. De acuerdo con el gran artículo de Brad Wilson sobre validación de entrada vs. Validación de modelo en ASP.NET MVC :

La razón por la que no “buceamos” en el objeto Address recursivamente es que no había nada en el formulario que vincule ningún valor dentro de Address.

Solución – Validar objeto al mismo tiempo que Propiedades

Una forma de resolver este problema es convertir las validaciones a nivel de objeto en validación de nivel de propiedad agregando un atributo de validación personalizado a la propiedad que devolverá con el resultado de validación del objeto en sí.

El artículo de Josh Carroll sobre validación recursiva usando DataAnnotations proporciona una implementación de una de esas estrategias (originalmente en esta pregunta SO ). Si queremos validar un tipo complejo (como Dirección), podemos agregar un atributo ValidateObject personalizado a la propiedad, por lo que se evalúa en el primer paso

 public class Person { [Required] public String Name { get; set; } [Required, ValidateObject] public Address Address { get; set; } } 

Deberá agregar la siguiente implementación de ValidateObjectAttribute :

 public class ValidateObjectAttribute: ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var results = new List(); var context = new ValidationContext(value, null, null); Validator.TryValidateObject(value, context, results, true); if (results.Count != 0) { var compositeResults = new CompositeValidationResult(String.Format("Validation for {0} failed!", validationContext.DisplayName)); results.ForEach(compositeResults.AddResult); return compositeResults; } return ValidationResult.Success; } } public class CompositeValidationResult: ValidationResult { private readonly List _results = new List(); public IEnumerable Results { get { return _results; } } public CompositeValidationResult(string errorMessage) : base(errorMessage) {} public CompositeValidationResult(string errorMessage, IEnumerable memberNames) : base(errorMessage, memberNames) {} protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult) {} public void AddResult(ValidationResult validationResult) { _results.Add(validationResult); } } 

Solución: Valide el modelo al mismo tiempo que las propiedades

Para los objetos que implementan IValidatableObject , cuando verificamos ModelState, también podemos verificar si el modelo en sí es válido antes de devolver la lista de errores. Podemos agregar cualquier error que deseemos llamando a ModelState.AddModelError( field , error ) . Como se especifica en Cómo forzar a MVC a validar IValidatableObject , podemos hacerlo así:

 [HttpPost] public ActionResult Create(Model model) { if (!ModelState.IsValid) { var errors = model.Validate(new ValidationContext(model, null, null)); foreach (var error in errors) foreach (var memberName in error.MemberNames) ModelState.AddModelError(memberName, error.ErrorMessage); return View(post); } } 

Además , si desea una solución más elegante, puede escribir el código una vez proporcionando su propia implementación de encuadernador de modelo personalizado en Application_Start () con ModelBinderProviders.BinderProviders.Add(new CustomModelBinderProvider()); . Hay buenas implementaciones aquí y aquí

También encontré esto y encontré este hilo. Aquí hay un primer pase:

 namespace Foo { using System.ComponentModel.DataAnnotations; using System.Linq; ///  /// Attribute class used to validate child properties. ///  ///  /// See: http://stackoverflow.com/questions/2493800/how-can-i-tell-the-data-annotations-validator-to-also-validate-complex-child-pro /// Apparently the Data Annotations validator does not validate complex child properties. /// To do so, slap this attribute on a your property (probably a nested view model) /// whose type has validation attributes on its properties. /// This will validate until a nested  /// fails. The failed validation result will be returned. In other words, it will fail one at a time. ///  public class HasNestedValidationAttribute : ValidationAttribute { ///  /// Validates the specified value with respect to the current validation attribute. ///  /// The value to validate. /// The context information about the validation operation. ///  /// An instance of the  class. ///  protected override ValidationResult IsValid(object value, ValidationContext validationContext) { var isValid = true; var result = ValidationResult.Success; var nestedValidationProperties = value.GetType().GetProperties() .Where(p => IsDefined(p, typeof(ValidationAttribute))) .OrderBy(p => p.Name);//Not the best order, but at least known and repeatable. foreach (var property in nestedValidationProperties) { var validators = GetCustomAttributes(property, typeof(ValidationAttribute)) as ValidationAttribute[]; if (validators == null || validators.Length == 0) continue; foreach (var validator in validators) { var propertyValue = property.GetValue(value, null); result = validator.GetValidationResult(propertyValue, new ValidationContext(value, null, null)); if (result == ValidationResult.Success) continue; isValid = false; break; } if (!isValid) { break; } } return result; } } }