65.9K
CodeProject 正在变化。 阅读更多。
Home

基于属性的业务对象验证方法

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (25投票s)

2008 年 12 月 22 日

CPOL

6分钟阅读

viewsIcon

63294

downloadIcon

1212

一篇展示如何使用特性来验证业务对象文章。

Attribute_Validation_src

1. 引言

本文将展示如何使用特性来验证您的业务对象。您可能不得不编写大量的代码来验证您的业务对象,而没有利用特性的优势。特性的价值在于您需要将相同的规则应用于多个属性时,您所要做的就是相应地修饰您的属性。有关基于特性的验证的其他文章,请参阅 Visual Studio Magazine简单的基于特性的验证

2. 背景

.NET 编译器使我们能够丰富地将元数据嵌入到程序集中,这些元数据可以在稍后使用反射进行访问。特性是可放置在类、接口、方法、程序集、模块或其他项上的装饰子句。它们成为程序集元数据的一部分,并可用于为编译器分配规则,或使开发人员能够重用代码来执行各种操作,包括验证、跟踪和类型转换。特性继承自 System.Attribute 类。

特性的示例

[Serializable, XmlRoot(Namespace = "www.idi.ac.ug")]
public class Person : EntityObject {
    [XmlAttribute]
    public string Firstname {get ; set;}

    [XmlAttribute]
    public string Lastname {get ; set;}
}

一些常用的特性包括

  • [Obsolete] 告知编译器发出警告,因为被修饰的方法已过时。
  • [Serializable] 告知编译器该对象可以被序列化到某些存储中,如 XML、文本或二进制。
  • [Assembly: ] 这些是应用在整个程序集上的程序集级特性。
  • [DefaultValue] 用于赋予一个默认值。

在我们的例子中,我们希望了解如何构建自己的自定义特性,以简化业务对象的验证。特性的可用性无疑会为您节省时间、代码,以及不必单独验证业务对象的每个属性的压力。

3. 业务对象

考虑一个名为 Person 的业务对象,我们决定让它继承自基类 EntityObject。我们让基类实现了 IEntityObject 接口,尽管这对于本文来说并不重要。这里使用的特性是 [Required][InRange][DefaultValue]

/// <summary>
/// Person class.
/// This class represents the person 
/// Author: Malisa Ncube
/// </summary>
public class Person : EntityObject
{
    /// <summary>
    /// Property that describes the Title of this <see cref="Person">
    /// </see></summary>
    [DefaultValue("Mr")]
    public string Title { get; set; }

    /// <summary>
    /// Property that describes the FirstName of this <see cref="Person">
    /// </see></summary>
    [Required]
    public string FirstName { get; set; }

    /// <summary>
    /// Property that describes the LastName of this <see cref="Person">
    /// </see></summary>
    [Required(ErrorMessage = "LastName must have a value")]
    public string LastName { get; set; }

    /// <summary>
    /// Property that describes the Age of this <see cref="Person">
    /// </see></summary>
    [InRange(18, 95)]
    [DefaultValue(30)]
    public int? Age { get; set; }

    public override void Validate(object sender, ValidateEventArgs e)
    {
        base.Validate(sender, e);

        //Custom business rules
        if (this.Age == 25)
        { Errors.Add(new Error(this, "Person", 
                     "A person cannot be 25 years of age!")); }
    }
}

在调用方法中,我想实例化 Person 对象,并确保在基类提供的 object.Save() 方法中验证该对象。您可能还有其他方法可以使用,例如,在属性更改后立即验证。我选择将验证推迟到最后,因为该过程涉及反射,这可能会很昂贵。

// Create new instance of the Person class
Person person = new Person();

//Assign values to properties

person.Firstname = "John";
person.Lastname = "Doe";

person.Age = 15;
//Should cause the object to be invalid and fail to save

string errMsg = "Could not save Person!\n\n"; 

if (!person.Save())
{
    //Collect all error messages into one error string
    foreach(Error err in person.Errors) 
    {
        errMsg += err.Message + "\n";
    }
    MessageBox.Show(errMsg, "Error", MessageBoxButtons.OK, 
                    MessageBoxIcon.Exclamation);
}
else
{
    //We show the following message if the person object is valid
    MessageBox.Show("Person = Valid");
}

4. 特性

让我们看一下下面介绍的特性类。我们将从 [Required] 特性开始,我想快速提醒您注意,实际的类名是 RequiredAttribute,.NET 允许您写成 [Required] 而不是 [RequiredAttribute],尽管后者仍然有效。

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RequiredAttribute : System.Attribute
{
    private bool required;
    private string errorMessage;
    
    public bool Required
    {
        get
        {
            return required;
        }
        set
        {
            required = value;
        }
    }

    public string ErrorMessage
    {
        get
        {
            return errorMessage;
        }
        set
        {
            errorMessage = value;
        }
    }

    public RequiredAttribute()
    {
        required = true;
    }

    public RequiredAttribute(bool required)
    {
        this.required = required;
    }

    public RequiredAttribute(string errorMessage)
    {
        this.errorMessage = errorMessage;
    }
}

在上面的 Required 特性中,[AttributeUsage] 是一个特性的特性——很有趣,不是吗?它使您能够确定或限制该特性可以在何处使用,在我的上述示例中,我们只能在属性上使用 Required 特性。AttributeTarget 是一个枚举,您可以在其中选择该特性可用的范围。默认值是 All;但是,它可以是 AssemblyClassConstructorDelegateEnumEventFieldInterfaceMethodParameterPropertyReturnValuestruct

您可以使用 AttributeUsageAllowMultiple 参数来确定同一特性是否可以在同一目标上使用多次。在此示例中,[Required] 特性在一个属性上只能出现一次。

特性只能包含常量。

5. 业务对象基类

我决定放置一个公共 Errors 集合,用于存储验证过程中遇到的错误。我还添加了事件处理程序,可以在验证方法执行时触发。这使得在必要时可以劫持验证过程,并在对象创建后注入验证规则。Validate() 方法是虚拟的,因此可以被重写以允许自定义业务规则。这就是您会用来确保男性人物的 Pregnant 布尔属性不会被设置为 true 的方法。

/// <summary>
/// EntityObject class.
/// This Entity base class 
/// Author: Malisa Ncube
/// </summary>
public class EntityObject : IEntityObject
{
    
    #region Internal Fields
    /// <summary>
    /// The Errors collection to keep the errors. Tthe validation method populates this.
    /// </summary>
    public List<error> Errors = new List<error>();

    #endregion

    #region Delegate and Events
    /// <summary>
    /// OnValidateEventHandler delegate to enable injection of custom validation routines
    /// </summary>
    public delegate void OnValidateEventHandler(object sender, ValidateEventArgs e);
    public delegate void OnValidatedEventHandler(object sender, ValidateEventArgs e);

    public OnValidateEventHandler OnValidate;
    public OnValidatedEventHandler OnValidated;

   .....

}

6. 反射

我们使用反射,并且 Validate 方法遍历所有属性以查找关联的自定义特性。然后,我们将属性值与特性规则进行比较,如果违反了规则,我们就会将错误添加到 Errors 集合中。

了解所有属性的魔力在于 System.Reflection 命名空间。然后,我们将使用 PropertyInfo 来存储对象的所有属性,并使用 GetType.GetProperties() 方法,如下所示。

PropertyInfo info = this.GetType().GetProperties();

我们进一步检查每个属性上的特性,看它是否匹配,例如 RequiredAttribute;如果匹配,我们然后检查属性值是否违反了特性规则。如果消息未在特性声明中包含,我们也会提供适当的错误消息。

基对象 EntityObjectValidate 方法如下所示

public class EntityObject : IEntityObject
{
    
    #region Internal Fields
    /// <summary>
    /// The Errors collection to keep the errors. Tthe validation method populates this.
    /// </summary>
    public List<error> Errors = new List<error>();

    #endregion

   .....

    /// <summary>
    /// Validate method performs the validation process and allows overriding 
    /// </summary>
    public virtual void Validate(object sender, ValidateEventArgs e)
    {
        //Initialise the error collection
        Errors.Clear();

        //Enable calling the OnValidate event before validation takes place
        if (this.OnValidate != null) this.OnValidate(this, new ValidateEventArgs());
        try
        {

            foreach (PropertyInfo info in this.GetType().GetProperties())
            {
                /* Get property value assigned to property */
                object data = info.GetValue(this, null);

                /* Set Default value if value is empty */
                foreach (object customAttribute in 
                  info.GetCustomAttributes(typeof(DefaultValueAttribute), true))
                {
                    if (data == null)
                    {
                        info.SetValue(this, (customAttribute 
                                             as DefaultValueAttribute).Default, null);
                        data = info.GetValue(this, null);
                    }
                }

                /* Check if property value is required */
                foreach (object customAttribute in 
                         info.GetCustomAttributes(typeof(RequiredAttribute), true))
                {
                    if (string.IsNullOrEmpty((string)data))
                    {
                        Errors.Add(new Error(this, info.Name, 
                          string.IsNullOrEmpty((customAttribute 
                                 as RequiredAttribute).ErrorMessage) ? 
                          string.Format("{0} is required", info.Name) : 
                             (customAttribute as RequiredAttribute).ErrorMessage));
                    }
                }

                /* Evaluate whether the property value lies within range */
                foreach (object customAttribute in 
                         info.GetCustomAttributes(typeof(InRangeAttribute), true))
                {

                    if (!(((IComparable)data).CompareTo((customAttribute as 
                            InRangeAttribute).Min) > 0) ||
                        !(((IComparable)data).CompareTo((customAttribute as 
                            InRangeAttribute).Max) < 0))
                    {
                        Errors.Add(new Error(this, info.Name, 
                        string.IsNullOrEmpty((customAttribute 
                               as InRangeAttribute).ErrorMessage) ? 
                        string.Format("{0} is out of range", info.Name) : 
                           (customAttribute as InRangeAttribute).ErrorMessage));
                    }
                }
            }
        }
        catch (Exception ex)
        {
            //
            throw new Exception("Could not validate Object!", ex);
        }
        finally
        {
            //Enable calling the OnValidated event after validation has taken place
            if (this.OnValidated != null) this.OnValidated(this, new ValidateEventArgs());
        }
    }
}

7. 重写 Validate 方法

下面显示的代码展示了如何重写业务对象的 Validate 方法并添加自定义验证规则

///

public class Person : EntityObject

   ....


    public override void Validate(object sender, ValidateEventArgs e)
    {
        base.Validate(sender, e);

        //Custom business rules
        if (this.Age == 25) 
        { Errors.Add(new Error(this, "Person", 
                     "A person cannot be 25 years of age!")); }
    }
}

8. 其他考虑事项

  1. 您可以决定在每个特性中都有一个 Validate 方法,该方法接收反射数据的参数并以适当的方式对其进行转换。这将使您不必担心在业务对象中测试属性值的有效性,而是让业务对象调用特性的实例并以相同的方式验证值。
  2. 当您需要将业务对象绑定到 WinForms / WPF / WebForms 时,您可能需要利用 .NET 框架提供的漂亮接口来实现 ErrorProviders,以便告知用户无效的输入。
    • IErrorInfo
    • INotifyPropertyChanged
  3. 您也可以选择在业务对象创建时缓存属性及其特性,以提高性能。如果您希望执行即时验证,而不是等到用户保存对象,这将是最佳方法。
  4. 您可以决定拥有更复杂的特性,这些特性会调用委托进行验证。这将是另一种方法,可以使您创建更强大、更灵活的业务规则。
  5. 使用 PostSharp 等 AOP(面向切面编程)框架,您可以将特性添加到您的对象中,执行验证、跟踪和其他有趣的操作。

请查看 Validation AspectsValidation Framework

9. 结论

我希望这对那些想了解基于特性的编程是什么的人有所帮助。在撰写本文时,我意识到类型安全是程序员需要仔细考虑的事情。确保,例如,您使用 [DefaultValue(30)] 而不是字符串 [DefaultValue("30")]

10. 历史

  • 2008/12/22:首次发布。
© . All rights reserved.