Reputation: 797
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
Reputation: 32740
Using a list of property names or Attributes
solves very different problems:
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:
Attribute
.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