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






4.93/5 (25投票s)
一篇展示如何使用特性来验证业务对象文章。
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
;但是,它可以是 Assembly
、Class
、Constructor
、Delegate
、Enum
、Event
、Field
、Interface
、Method
、Parameter
、Property
、ReturnValue
或 struct
。
您可以使用 AttributeUsage
的 AllowMultiple
参数来确定同一特性是否可以在同一目标上使用多次。在此示例中,[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
;如果匹配,我们然后检查属性值是否违反了特性规则。如果消息未在特性声明中包含,我们也会提供适当的错误消息。
基对象 EntityObject
的 Validate
方法如下所示
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. 其他考虑事项
- 您可以决定在每个特性中都有一个
Validate
方法,该方法接收反射数据的参数并以适当的方式对其进行转换。这将使您不必担心在业务对象中测试属性值的有效性,而是让业务对象调用特性的实例并以相同的方式验证值。 - 当您需要将业务对象绑定到 WinForms / WPF / WebForms 时,您可能需要利用 .NET 框架提供的漂亮接口来实现 ErrorProviders,以便告知用户无效的输入。
IErrorInfo
INotifyPropertyChanged
- 您也可以选择在业务对象创建时缓存属性及其特性,以提高性能。如果您希望执行即时验证,而不是等到用户保存对象,这将是最佳方法。
- 您可以决定拥有更复杂的特性,这些特性会调用委托进行验证。这将是另一种方法,可以使您创建更强大、更灵活的业务规则。
- 使用 PostSharp 等 AOP(面向切面编程)框架,您可以将特性添加到您的对象中,执行验证、跟踪和其他有趣的操作。
请查看 Validation Aspects 和 Validation Framework。
9. 结论
我希望这对那些想了解基于特性的编程是什么的人有所帮助。在撰写本文时,我意识到类型安全是程序员需要仔细考虑的事情。确保,例如,您使用 [DefaultValue(30)]
而不是字符串 [DefaultValue("30")]
。
10. 历史
- 2008/12/22:首次发布。