0

I have an object (header) that has a list of sub-ojects (details) that I want to do custom validation on prior to accepting data. I've tried ModelState.IsValid and TryValidateModel, but it doesn't seem to fire the Validate method on the sub-objects (only the header object).

So on submission I see the validation fire for the header, but not the sub-ojects. Then if I do a TryValidateModel I again see (breakpoint) the validation method get called on the header, but not on the sub-objects.

The annotated validation (must be number, etc...) seems to be working on the sub-objects, just not the custom logic added via the IValidatableObject interface. Any help would be greatly appreciated.

ChrisHDog
  • 4,473
  • 8
  • 51
  • 77

3 Answers3

1

I make an attribute ( [ValidateObject] ) and it will validate the attribute you put on your class, like you would think it suppose to do.

public class Personne
{
    [ValidateObject]
    public Address Address { get; set; }
    //[...]
}

(Address is a custom class.)

It can be used for validating:

  • Object properties on a model. (like above)
  • Collection of sub object.

    [ValidateObject]
    public List<Address> Address { get; set; }
    
  • It support multi level of dept, if "Address" had a propertie of type "ZipCode" with the attribute [ValidateObject] it would be validate tho.

Code:

public class ValidateObjectAttribute : ValidationAttribute
{
    public ValidateObjectAttribute()
    {

    }
    private ValidationContext ValidationContext { get; set; }
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        this.ValidationContext = validationContext;
        var results = new List<ValidationResult>();

        try
        {
            var isIterable = this.IsIterable(value);
            if (isIterable)
            {
                int currentItemPosition = -1;
                foreach (var objectToValidate in value as IEnumerable<object>)
                {
                    currentItemPosition++;
                    var resultTemp = ValidationsForObject(objectToValidate, true, currentItemPosition);
                    if (resultTemp != null)
                        results.AddRange(resultTemp);

                }
                if (results.Count <= 0)
                    results = null;
            }
            else
                results = ValidationsForObject(value);

            if (results != null)
            {
                //Build a validation result 
                List<string> memberNames = new List<string>();
                results.ForEach(r => memberNames.AddRange(r.MemberNames));

                var compositeResultsReturn = new CompositeValidationResult($"Validation for {validationContext.DisplayName} failed!", memberNames.AsEnumerable());
                results.ForEach(r => compositeResultsReturn.AddResult(r));

                return compositeResultsReturn;
            }

        }
        catch (Exception) { }

        return ValidationResult.Success;
    }

    private List<ValidationResult> ValidationsForObject (object objectToValidate, bool IsIterable = false, int position = -1)
    {
        var results = new List<ValidationResult>();
        var contextTemp = new ValidationContext(objectToValidate, null, null);
        var resultsForThisItem = new List<ValidationResult>();

        var isValid = Validator.TryValidateObject(objectToValidate, contextTemp, resultsForThisItem, true);
        if (isValid)
            return null;

        foreach (var validationResult in resultsForThisItem)
        {
            List<string> propNames = new List<string>();// add prefix to properties
            foreach (var nameOfProp in validationResult.MemberNames)
            {
                if (IsIterable)
                    propNames.Add($"{this.ValidationContext.MemberName}[{position}].{nameOfProp}");
                else
                    propNames.Add($"{this.ValidationContext.MemberName}.{nameOfProp}");
            }
            var customFormatValidation = new ValidationResult(validationResult.ErrorMessage, propNames);
            results.Add(customFormatValidation);
        }       

        return results;
    }


    private bool IsIterable(object value)
    {
        ////COULD WRITE THIS, but its complicated to debug...
        //if (value.GetType().GetInterfaces().Any(
        //i => i.IsGenericType &&
        //i.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
        //{
        //    // foreach...
        //}
        Type valueType = value.GetType();
        var interfaces = valueType.GetInterfaces();
        bool isIterable = false;
        foreach (var i in interfaces)
        {
            var isGeneric = i.IsGenericType;
            bool isEnumerable = i.GetGenericTypeDefinition() == typeof(IEnumerable<>);
            isIterable = isGeneric && isEnumerable;
            if (isIterable)
                break;
        }
        return isIterable;
    }
}

public class CompositeValidationResult : ValidationResult
{
    private readonly List<ValidationResult> _results = new List<ValidationResult>();

    public IEnumerable<ValidationResult> Results
    {
        get
        {
            return _results;
        }
    }

    public CompositeValidationResult(string errorMessage) : base(errorMessage)
    {
    }

    public CompositeValidationResult(string errorMessage, IEnumerable<string> memberNames) : base(errorMessage, memberNames)
    {
    }

    protected CompositeValidationResult(ValidationResult validationResult) : base(validationResult)
    {
    }

    public void AddResult(ValidationResult validationResult)
    {
        _results.Add(validationResult);
    }
}

This will work if you model is correctly binded :)

You might want to add the required attribute, making sure the the object is not null itself.

[Required]

Hope it help!

Iannick
  • 146
  • 1
  • 11
0

I wonder if you root object has errors preventing child validation. See Recursive validation using annotations and IValidatableObject This URL mentions that scenario and also code to force validation on the child from the root

As per the posting triggering off the validation from the root object


  public IEnumerable Validate(ValidationContext validationContext)
    {
        var context = new ValidationContext(this.Details, validationContext.ServiceContainer, validationContext.Items);
        var results = new List();
        Validator.TryValidateObject(this.Details, context, results);
        return results;
    }

Community
  • 1
  • 1
Adam Tuliper
  • 29,982
  • 4
  • 53
  • 71
  • It doesn't look like the root object has errors (non-expected and ModelState.IsValid = true) – ChrisHDog Sep 28 '12 at 06:08
  • Try the other suggestion of ensuring the root validates the child in your Validate method as shown in that post or use the attribute based approach. – Adam Tuliper Sep 28 '12 at 14:36
0

The TryValidateObject didn't appear to fire custom validation, only data annotations? I went down the path of adding validation of the details to the header validation by doing the following:

foreach (var detail in this.Details)
{
   var validationResults = detail.Validate(validationContext);
   foreach (var validationResult in validationResults)
   {
      yield return validationResult;
   }
}

This worked in terms of validation, but the UI didn't display the error messages. Even though I have ValidationMessagesFor on the UI.

Validation not working solved here: MVC3 Master-Details Validation not Displaying

Community
  • 1
  • 1
ChrisHDog
  • 4,473
  • 8
  • 51
  • 77