繁体   English   中英

如何使用属性名称列表来验证类层次结构的属性

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

我有一个像下面这样的类结构

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;
}

对于A类的对象,

A objA

我需要验证例如:

  • objA.b.strB
  • objA.bDstrD2
  • objA.c.strC1

它们是非空字符串。 (当然,应该验证对象objA.b,objA.bd和objA.c不为空)

我想用类似的方法来实现

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

因此,我想验证具有listOfFieldNamesToValidate中名称的属性不为空,并使用out参数返回违反此属性的属性名称(如果有)。

我应该使用反射来实现此验证,还是对我来说验证属性是更好的选择?

使用obj.GetType()。GetProperties()似乎是一个不错的起点,但是我不知道如何处理类的层次结构。

是否可以用属性属性标记类的属性,以便我可以以一种简洁的方式摆脱listOfFieldNamesToValidate参数?

使用属性名称或Attributes列表可以解决非常不同的问题:

  • 如果在编译时不知道应验证哪些属性,则应使用名称列表。 来电者是谁知道,因此,其谁必须提供所需要的信息。
  • 使用Attributes必然意味着有人在编译时就知道需要验证的属性(通常情况下,这个人不是您,而是在插件场景中思考)。 Attributes非常方便管理代码可伸缩性,从而减少依赖关系和耦合。 当出现更多的类,属性,验证规则等时,更改验证实现可能很容易出错。 向新属性添加简单的Attribute相对容易,而且很难搞乱。

假设Attribute路径是您真正想要的路径,我已经实现了一个通用的案例验证器,它可以完成几件很漂亮的事情:

  1. 它会自动验证标记有指定Attribute所有Attribute
  2. 它允许您为不同的属性类型定义验证规则。
  3. 它不仅会匹配精确的类型,还将在搜索适用规则时使用任何有效的引用转换; 例如,如果找不到其他具有更特定匹配项的规则,则将object规则应用于string属性。 请注意,这不适用于用户定义的隐式转换。 一个int规则将不会被一个long规则所覆盖,依此类推。 可以禁用此功能。

我没有对此进行广泛的测试,但是它应该可以正常工作:

[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;
    }
}

您将按以下方式使用它:

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);
    }
}

当然, bag的内容是预期的:

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM