EngelbertCoder
EngelbertCoder

Reputation: 797

How to validate the properties of a class hierarchy with a list of property names

I have a class structure like the following

public Class A 
{
    public B b;
    public C c;
    public string strA;
}

public Class B 
{
    public D d;
    public string strB;
}

public Class C 
{
    public string strC1;
    public string strC2;
}

public Class D 
{
    public string strD1;
    public string strD2;
}

For an object of class A,

A objA

I need to validate for example :

that they are nonempty strings. (Of course, it should be validated that the objects objA.b, objA.b.d and objA.c are not null)

I want to realize it with a method like

public bool ValidatePropertiesWithList(object obj, List<string> listOfFieldNamesToValidate, out string nameOfThePropertyThatViolatesTheValidation)

So I want to validate that the properties with the names in listOfFieldNamesToValidate are not empty, and return with an out parameter the name of the property (if any) that violates this.

Should I use Reflection to realize this validation or are Validation Attributes a better choice for me?

Using obj.GetType().GetProperties() seems to be a good place to start but I dont know how I can handle the hierarchical structure of the classes.

Is it possible to mark the properties of the classes with property attributes so that I can get rid of the listOfFieldNamesToValidate parameter in an elegant way?

Upvotes: 1

Views: 1282

Answers (1)

InBetween
InBetween

Reputation: 32740

Using a list of property names or Attributes solves very different problems:

  • A list of names should be used if its not known at compile time what properties should be validated; the caller is who knows and therefore its he who must supply the needed info.
  • Using Attributes necessarily means that someone knows at compile time the properties that need validation (this someone is, in the general case, not you; think in a plug-in scenario). Attributes are really handy to manage code scalability reducing dependencies and coupling; changing validation implementations when more classes, properties, validation rules, etc. appear can be very bug prone. Adding a simple Attribute to the new properties is relatively easy and hard to mess up.

Supposing the Attribute path is the one you really want, I've implemented a general case validator that does a couple of nifty things:

  1. It automatically validates all properties marked with the specified Attribute.
  2. It allows you to define validation rules for different property types.
  3. It will not only match exact types, it will also use any valid reference conversions when searching for an applicable rule; for instance an object rule would be applied to a string property if no other rule with a more specific match is found. Note that this will not work with user defined implict conversions; an int rule will not be covered by a long rule and so on. This feature can be disabled.

I haven't tested this extensively but it should work reasonably well:

[AttributeUsage(AttributeTargets.Property)]
public class ValidateAttribute: Attribute
{
}

public class Validator<TAttribute> where TAttribute : Attribute
{
    private readonly Dictionary<Type, Predicate<object>> rules;

    public Validator()
    {
        rules = new Dictionary<Type, Predicate<object>>();
    }

    public bool UnregisterRule(Type t) => rules.Remove(t);
    public void RegisterRule<TRule>(Predicate<TRule> rule) => rules.Add(typeof(TRule), o => rule((TRule)o));

    public bool Validate<TTarget>(TTarget target, IList<string> failedValidationsBag, bool onlyExactTypeMatchRules = false)
    {
        var valid = true;
        var properties = typeof(TTarget).GetProperties().Where(p => p.GetCustomAttribute<TAttribute>() != null);

        foreach (var p in properties)
        {
            var value = p.GetValue(target);
            Predicate<object> predicate = null;

            //Rule aplicability works as follows:
            //
            //1. If the type of the property matches exactly the type of a rule, that rule is chosen and we are finished.
            //   If no exact match is found and onlyExactMatchRules is true no rule is chosen and we are finished.
            //
            //2. Build a candidate set as follows: If the type of a rule is assignable from the type of the property,
            //   add the type of the rule to the candidate set.
            //   
            //   2.1.If the set is empty, no rule is chosen and we are finished.
            //   2.2 If the set has only one candidate, the rule with that type is chosen and we're finished.
            //   2.3 If the set has two or more candidates, keep the most specific types and remove the rest.
            //       The most specific types are those that are not assignable from any other type in the candidate set.
            //       Types are removed from the candidate set until the set either contains one single candidate or no more
            //       progress is made.
            //
            //       2.3.1 If the set has only one candidate, the rule with that type is chosen and we're finished.
            //       2.3.2 If no more progress is made, we have an ambiguous rules scenario; there is no way to know which rule
            //             is better so an ArgumentException is thrown (this can happen for example when we have rules for two
            //             interfaces and an object subject to validation implements them both.) 
            Type ruleType = null;

            if (!rules.TryGetValue(p.PropertyType, out predicate) && !onlyExactTypeMatchRules)
            {
                var candidateTypes = rules.Keys.Where(k => k.IsAssignableFrom(p.PropertyType)).ToList();
                var count = candidateTypes.Count;

                if (count > 0)
                {
                    while (count > 1)
                    {
                        candidateTypes = candidateTypes.Where(type => candidateTypes.Where(otherType => otherType != type)
                                                       .All(otherType => !type.IsAssignableFrom(otherType)))
                                                       .ToList();

                        if (candidateTypes.Count == count) 
                            throw new ArgumentException($"Ambiguous rules while processing {target}: {string.Join(", ", candidateTypes.Select(t => t.Name))}");

                        count = candidateTypes.Count;
                    }

                    ruleType = candidateTypes.Single();
                    predicate = rules[ruleType];
                }
            }

            valid = checkRule(target, ruleType ?? p.PropertyType, value, predicate, p.Name, failedValidationsBag) && valid;
        }

        return valid;
    }

    private bool checkRule<T>(T target, Type ruleType, object value, Predicate<object> predicate, string propertyName, IList<string> failedValidationsBag)
    {
        if (predicate != null && !predicate(value))
        {
            failedValidationsBag.Add($"{target}: {propertyName} failed validation. [Rule: {ruleType.Name}]");
            return false;
        }

        return true;
    }
}

And you'd use it as follows:

public class Bar
{
    public Bar(int value)
    {
        Value = value;
    }

    [Validate]
    public int Value { get; }
}

public class Foo
{
    public Foo(string someString, ArgumentException someArgumentExcpetion, Exception someException, object someObject, Bar someBar)
    {
        SomeString = someString;
        SomeArgumentException = someArgumentExcpetion;
        SomeException = someException;
        SomeObject = someObject;
        SomeBar = someBar;
    }

    [Validate]
    public string SomeString { get; }
    [Validate]
    public ArgumentException SomeArgumentException { get; }
    [Validate]
    public Exception SomeException { get; }
    [Validate]
    public object SomeObject { get; }
    [Validate]
    public Bar SomeBar { get; }
}

static class Program
{
    static void Main(string[] args)
    {
        var someObject = new object();
        var someArgumentException = new ArgumentException();
        var someException = new Exception();
        var foo = new Foo("", someArgumentException, someException, someObject, new Bar(-1));
        var validator = new Validator<ValidateAttribute>();
        var bag = new List<string>();
        validator.RegisterRule<string>(s => !string.IsNullOrWhiteSpace(s));
        validator.RegisterRule<Exception>(exc => exc == someException);
        validator.RegisterRule<object>(obj => obj == someObject);
        validator.RegisterRule<int>(i => i > 0);
        validator.RegisterRule<Bar>(b => validator.Validate(b, bag));
        var valid = validator.Validate(foo, bag);
    }
}

And of course, bag's content is the expected:

Foo: SomeString failed validation. [Rule: String]
Foo: SomeArgumentException failed validation. [Rule: Exception]
Bar: Value failed validation. [Rule: Int32]
Foo: SomeBar failed validation. [Rule: Bar]

Upvotes: 2

Related Questions