将参数传递给提供给属性的Type

本文关键字:Type 属性 参数传递 | 更新日期: 2023-09-27 18:15:01

我有一个用来装饰对象属性的属性。该属性标识需要对其执行验证的属性。我实际上是在实现策略模式,并将所有验证(实际上只有大约6种类型)构建到可以跨多个类使用的单个对象中。我想做的是为验证类提供参数,而不必为每个验证对象变体创建属性。

我的属性是这样的:

[AttributeUsage(AttributeTargets.Property)]
public class ValidationRuleAttribute : Attribute
{
    public ValidationRuleAttribute(Type validationRule, string customFailureMessage = "")
    {
        if (typeof(IValidationRule).IsAssignableFrom(validationRule))
        {
            this.ValidationRule = string.IsNullOrEmpty(customFailureMessage) 
                ? Activator.CreateInstance(validationRule, customFailureMessage) as IValidationRule
                : Activator.CreateInstance(validationRule) as IValidationRule;
        }
        else
        {
            throw new ArgumentException(
                string.Format(
                    "ValidationRule attributes can only be used with IValidationRule implementations. The '{0}' Tyoe is not supported.",
                    validationRule.Name));
        }
    }
    public IValidationRule ValidationRule { get; private set; }
}

作为一个例子,我有一个简单的StringIsNotNull验证对象。我想对它进行扩展,允许我指定最小字符串长度要求。所以StringIsNotEmptyValidation会变成StringHasMinimumLengthValidation

public class StringIsNotEmptyValidation : IValidationRule
{
    private readonly string customErrorMessage;
    public StringIsNotEmptyValidation()
    {
    }
    public StringIsNotEmptyValidation(string customErrorMessage)
    {
        this.customErrorMessage = customErrorMessage;
    }
    public string ResultMessage { get; private set; }
    public IValidationMessage Validate(System.Reflection.PropertyInfo property, IValidatable sender)
    {
        string value = property.GetValue(sender).ToString();            
        // Validate
        bool isFailed = string.IsNullOrWhiteSpace(value);
        if (isFailed)
        {
            if (string.IsNullOrEmpty(this.customErrorMessage))
            {
                DisplayNameAttribute displayName = property.GetCustomAttribute<DisplayNameAttribute>(true);
                string errorMessage = displayName == null
                    ? string.Format("You can not leave {0} empty.", property.Name)
                    : string.Format("You can not leave {0} empty.", displayName.DisplayName);
                this.ResultMessage = errorMessage;
                return new ValidationErrorMessage(errorMessage);
            }
            else
            {
                this.ResultMessage = this.customErrorMessage;
                return new ValidationErrorMessage(customErrorMessage);
            }
        }
        this.ResultMessage = string.Empty;
        return null;
    }
}

在我的模型中,我用属性和验证对象来修饰我的属性。

[RepositoryParameter(DbType.String)]
[ValidationRule(typeof(StringIsNotEmptyValidation))]
public string WorkDescription
{
    get
    {
        return this.workDescription ?? string.Empty;
    }
    set
    {
        this.SetPropertyByReference(ref this.workDescription, value);
        if (this.HasValidationMessageType<ValidationErrorMessage>(this.GetPropertyName(p => p.WorkDescription)))
        {
            this.Validate();
        }
    }
}

我想要做的是这样写我的属性用法:

[ValidationRule(new StringIsNotEmptyValidation(minimumLength: 4))]

由于不能在属性构造函数中实例化对象,因此我被迫在属性构造函数中提供如下属性:

[ValidationRule(typeof(StringIsNotEmptyValidation), minLength: 4)]

我不喜欢这样,因为如果我有一个ObjectIsNotNullStringIsInRange,我将需要做两件事:

  1. 为每个参数变化(或过载)创建一个新属性
  2. 在构造函数中设置验证规则实例,这些实例将具有不同的属性名称。

验证对象实现以下接口

public interface IValidationRule
{
    string ResultMessage { get; }
    IValidationMessage Validate(PropertyInfo property, IValidatable sender);
}

我不想用大量的属性来膨胀我的接口,这些属性可能会被使用,也可能不会被使用,这取决于实现它的规则。这也使得为规则对象分配属性参数变得困难。

所以我的问题是我如何为IValidationRule具体类提供参数,而不创建多个属性类型来促进这一点?这样我就可以进行跨对象验证。传递给验证规则的PropertyInfo来自PropertyInfo的缓存。我需要保持反射的数量使用下来,否则我只是使用属性为每个规则参数和使用sender反射找出使用的范围。

更新

在与Corey讨论这个问题后,它确实出现在通用应用程序中支持属性,它只是缺少DataAnnotations命名空间。为了访问属性,我必须向System添加一个using语句。反射,以便访问一系列暴露GetCustomAttribute方法的扩展方法。它们现在是扩展方法,而不是内置于Type类中。

所以我想最后,我可以在属性中创建我的验证逻辑,而不是单独的对象。我想不出走这条路有什么缺点。

为了访问通用应用程序中的属性,您必须将System.Reflection作为using语句包含,然后通过GetRuntimeProperties()扩展方法进行访问。

var validationRule = this
    .GetType()
    .GetRuntimeProperties() // Can be GetRuntimeFields or GetRuntimeMethods as well.
    .FirstOrDefault(p => p.GetCustomAttribute<IntegerInRangeAttribute>() != null);

将参数传递给提供给属性的Type

所以这里有几个选项。

首先,也是经常使用的,是为您想要处理的每种类型的规则具有不同的属性。您已经为每个规则构建了类,所以不要使用一些封装属性来实例化它们,只需将每个规则设置为一个属性:

[StringMinLengthRule(5)]
public string SomeString { get; set; }

将验证逻辑构建到属性中——比如使用一个完成大部分工作的基本属性,调用一个虚拟方法来执行实际的验证。然后,您可以枚举规则属性并从您的验证方法调用它们。

接下来,您可以在您的属性上拥有许多不同的属性,这些属性可以在声明期间设置,以为您的各种规则提供属性:

[Validation(RuleType.StringMinLength, MinLength = 5)]
public string SomeString { get; set; }

您仍然可以在ValidationAttribute本身中处理规则,或者在运行时创建IValidationRule实例来处理实际的验证。不幸的是,没有什么可以阻止您添加Validation属性,该属性为规则类型设置了错误的属性,从而导致在运行时尝试验证实例时出现错误。

终于,一些工作,但可能不应该…它有点丑:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public class ValidationRuleAttribute : Attribute
{
    public IValidationRule ValidationRule { get; private set; }
    public ValidationRuleAttribute(RuleType type, params object[] parms)
    {
        if (type == RuleType.NotNull)
        {
            if (parms.Length != 0)
                throw new ArgumentException("RuleType.NotNull requires 0 parameters", "parms");
            ValidationRule = new NotNullValidation();
        }
        if (type == RuleType.StringMinLength)
        {
            if (parms.Length != 1)
                throw new ArgumentException("RuleType.StringMinLength requires 1 parameter", "parms");
            if (!(parms[0] is int))
                throw new ArgumentException("RuleType.StringMinLength requires an integer", "parms");
            ValidationRule = new StringLengthValidation((int)parms[0]);
        }
    }
}

最大的问题是,它不会抱怨,直到你试图在运行时实例化一个类,有一个坏的Validation属性。您的代码可以非常愉快地运行,直到它尝试创建那个坏类的实例,此时所有属性都将被实际构造,并且那些ArgumentException开始飞行。

事实上,只有第一个选项不会出现运行时问题,因为您可以通过使用正确的构造函数格式来控制所提供的参数类型。你仍然可以让它做一些愚蠢的事情——比如要求字符串的长度必须小于0——但这取决于你:P