0

I'm trying to perform some validation on my objects that is are not tied to a UI. For example I have these three classes:

public class XDeftable {
    [ObjectCollectionValidator(typeof(XSchedGroup))]
    public List<XSchedGroup> SCHED_GROUP { get; set; }
}

[IdentifyingProperty("TABLE_NAME")]
public class XSchedGroup {
    [ObjectCollectionValidator(typeof(XJob))]
    public List<XJob> JOB { get; set; }
    [Required]
    public string TABLE_NAME { get; set; }
}

[IdentifyingProperty("JOBNAME")]
public class XJob : ICalendar {
    [Required]
    public string JOBNAME { get; set; }
    [Range(-62, 62)]
    public string SHIFTNUM { get; set; }
    [ObjectCollectionValidator(typeof(XTagNames))]
    public List<XTagNames> TAG_NAMES { get; set; }
}

XDeftable -> XSchedGroup -> XJob -> XTagNames

When an object fails validation things work exactly as one would expect but if I simply inspect the ValidationResult for it's Key and Message I end up with something like: "JOBNAME | Field is required."

The problem with this is that considering I might have hundreds of jobs in a single scheduling group the validation is useless since I don't know which particular job failed. I've searched through every bit of documentation I could find regarding validation and C# and haven't found any way of getting more data. I created the attribute IdentifyingProperty to allow me to tag which property of the class identifies the particular instance of the class. I had a previous custom validation solution that I mocked up based off of this Git Repo: https://github.com/reustmd/DataAnnotationsValidatorRecursive/tree/master/DataAnnotationsValidator/DataAnnotationsValidator. It worked alright but I wanted to swap over to something more robust.

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
public sealed class IdentifyingProperty : Attribute {
    public string Name { get; set; }

    public IdentifyingProperty(string name) {
        this.Name = name;
    }
}

So far I've been able to come up with the following:

public ValidationResults Validate(XDeftable deftable) {
    var results = new ObjectValidator(typeof(XDeftable)).Validate(deftable);
    var detailedResults = new ValidationResults();

    foreach (var item in results) {
        var targetType = item.Target.GetType();

        var identProp = targetType.GetCustomAttribute<IdentifyingProperty>();
        if (identProp != null) {
            var pi = targetType.GetProperty(identProp.Name);
            var newKey = String.Format("{0}[{1}].{2}", targetType.Name, pi.GetValue(item.Target).ToString(), item.Key);
            detailedResults.AddResult(new ValidationResult(item.Message, item.Target, newKey, item.Tag, item.Validator));
        }
        else {
            detailedResults.AddResult(item);
        }
    }

    return detailedResults;
}

This will at least return me "XJob[JOBNAME].SHIFTNUM | The field SHIFTNUM must be between -62 and 62." I'd still like it if there was a way for me to get results that follow the chain of containers such as: XSchedGroup[TABLE_NAME].XJob[JOBNAME].SHIFTNUM.

Kittoes0124
  • 4,930
  • 3
  • 26
  • 47

2 Answers2

1

Instead of manipulating the Key, I would piggy-back on the Tag property since this is just what it is for ("The meaning for a tag is determined by the client code consuming the ValidationResults").

So sticking with your approach something like:

public ValidationResults Validate(XDeftable deftable)
{
    var results = new ObjectValidator(typeof(XDeftable)).Validate(deftable);
    var detailedResults = new ValidationResults();

    Microsoft.Practices.EnterpriseLibrary.Validation.ValidationResult result = null;

    foreach (var item in results)
    {
        result = item;

        var targetType = item.Target.GetType();

        var attribute = (IdentifyingPropertyAttribute)
                            targetType.GetCustomAttributes(
                                typeof(IdentifyingPropertyAttribute), 
                                false)
                            .SingleOrDefault();

        if (attribute != null)
        {
            var propertyInfo = targetType.GetProperty(attribute.Name);

            if (propertyInfo != null)
            {
                object propertyValue = propertyInfo.GetValue(item.Target) ?? "";

                result = new Microsoft.Practices.EnterpriseLibrary.Validation.ValidationResult(
                            item.Message, 
                            item.Target, 
                            item.Key, 
                            propertyValue.ToString(), 
                            item.Validator);
            }
        }

        detailedResults.AddResult(result);
    }

    return detailedResults;
}
Randy Levy
  • 22,566
  • 4
  • 68
  • 94
  • Is there a way that I could get each "parent"? It would really be helpful for debugging/logging if I could get the whole hierarchy of the object. – Kittoes0124 Aug 02 '13 at 20:37
  • The ValidationResult contains a reference to the Target object that was validated. However, there is no concept of where that object exists in an graph. The only approach I can think of is to attach some additional data to each class with the information you would like. E.g. a `ParentName` property (via interface or base class) that all objects have which can then be found via the Target object. – Randy Levy Aug 03 '13 at 15:40
0

I used something similar once

        public class RequiredPropertyAttribute : Attribute
        {
            public bool Required { get { return true; } }
        }
        public class RequiredListAttribute : Attribute
        {
            public bool Required { get { return true; } }
        }

and the following for validation. It checks against the attributes I specified, and returns which ones are not populated that are required to be.

            public List<string> IterateProperties(object _o)
            {
                List<string> problems = new List<string>();

                foreach (PropertyInfo info in _o.GetType().GetProperties())
                {
                    bool isGenericType = info.PropertyType.IsGenericType;
                    Type infoType = info.PropertyType;

                    if (infoType.IsGenericType && infoType.GetGenericTypeDefinition() == typeof(List<>))
                    {
                        infoType = infoType.GetGenericArguments()[0];

                        if (infoType.IsNested)
                        {
                            System.Collections.IList subObjects = (System.Collections.IList)info.GetValue(_o, null);

                            object[] requiredListAttributes = info.GetCustomAttributes(typeof(RequiredListAttribute), true);
                            if (requiredListAttributes.Length > 0 && subObjects.Count == 0)
                            {
                                problems.Add(String.Format("List {0} in class {1} must have at least 1 row", info.Name, info.PropertyType.ToString()));
                            }
                            else
                            {
                                foreach (object sub in subObjects)
                                {
                                    problems.AddRange(this.IterateProperties(sub));
                                }
                            }
                        }
                    }
                    else
                    {
                        if (infoType.IsNested)
                        {
                            object sub = info.GetValue(_o, null);
                            if (sub != null)
                            {
                                problems.AddRange(this.IterateProperties(sub));
                            }
                        }
                    }


                    object[] attributes = info.GetCustomAttributes(typeof(RequiredPropertyAttribute), true);
                    foreach (object o in attributes)
                    {
                        if (info.GetValue(_o, null) == null)
                        {
                            problems.Add(String.Format("Attribute {0} in class {1} cannot be null", info.Name, info.PropertyType.ToString()));
                        }
                    }
                }

                return problems;
            }
        }
Sam
  • 113
  • 6
  • Sorry, but not really what I'm looking for at all. The Enterprise Library's Validation Block is handling all of my validation just fine. What it's not doing is logging enough information for me to do anything about the validation failures. It simply shows the name of the property that failed validation and the error message as to why it failed. – Kittoes0124 Aug 01 '13 at 21:41