自定义业务对象帮助类
本文介绍了如何使用泛型、反射和自定义属性来构造一个自定义业务对象助手,该助手可以从DataReader填充业务对象。
引言
在花了大量时间编写代码将DataReader
返回的值赋给我的业务对象的属性之后,我开始觉得一定有更好的方法。然后我开始玩DotNetNuke。我开始阅读DNN的一些文档,并遇到一个非常有趣的助手类,该类在DotNetNuke数据访问文档中有解释。他们用于填充业务对象的自定义业务对象助手类可以节省大量代码。您编写代码以检索一个DataReader
,其中包含设置对象属性所需的信息。然后,您可以调用助手类上的FillObject
或FillCollection
,传入您的DataReader
和您的业务对象的类型,它将为您提供一个完全填充了数据的对象实例。它使用反射和您的业务对象的属性名称来匹配DataReader
字段与您的对象属性。例如,如果您的数据库中有一个名为Customers
的表,并且您执行了一个查询以在DataReader
中返回Customer
的一行,那么您可以根据DataReader
的字段名称来填充您的Customer
对象的属性。如果您的业务对象有一个名为FirstName
的属性,而您的表有一个名为FirstName
的字段,那么DataReader
中的FirstName
字段的值将被赋给您的业务对象上的FirstName
属性。当您的对象有很多属性时,这可以节省大量的开发时间。当您向表中添加字段时,这也很有帮助,因为您只需要为您的业务对象添加一个同名的属性,助手类就会负责填充它。除了能够用值填充一个对象实例外,如果DataReader
返回多行,它还可以让您填充对象集合。
尽管DNN助手类有其优点,但我认为有几个问题需要解决。第一个问题是业务对象上的属性和数据库中的字段必须同名。有时数据库和查询已经构建好,并且字段名称对您的业务对象来说并不是很好的属性名称(想象一下需要将所有业务对象属性都加上“fld”前缀,这在数据库中是一种相当常见的字段命名方式)。第二个问题是FillCollection
方法返回一个ArrayList
。许多开发人员更喜欢为他们的对象创建自己的集合类。这样,他们就可以将操作业务对象组的任何方法放在该集合类内部。使用DNN助手类,您只能得到一个ArrayList
。最后一个问题是,如果数据库字段的值是null
,该值将由Null
助手类设置,该类将对象的类型的字符串表示形式赋给该类型(例如,对于System.Int32
为0
,对于System.Boolean
为false
,等等)的默认值。如果您想更改默认值,则必须编辑该类然后重新编译。
得益于.NET 2.0中的泛型、自定义属性和泛型约束,这些问题都可以得到解决。
自定义属性
属性名称必须与数据库字段名称相同的问题,以及在字段包含DBNull
值时赋默认值的问题,都可以使用自定义属性来解决。创建自定义属性很简单,只需创建一个从System.Attribute
派生的类即可。创建自定义属性有一些规则需要遵守。其名称必须以“Attribute
”结尾,并且应使用AttributeUsage
属性进行标记(是的,您需要一个属性来标记您的属性)。AttributeUsage
属性告诉编译器该属性可以应用于哪些程序实体:类、模块、方法、属性等。自定义属性类只能包含接受和返回以下类型的值的字段、属性和方法:bool, byte, short, int, long, char, float, double, string, object, System.Type, and public Enum
。它还可以接收和返回上述类型的单维数组。自定义类型必须公开一个或多个public
构造函数,并且通常构造函数会接受属性所必需的参数。在下面的代码中,必需的属性字段是NullValue
字段,它是在datareader
包含null
值时属性应取的值。还有一个该构造函数的重载版本,它也接受字段名称。BusinessObjectHelper
公开的自定义属性类是DataMappingAttribute
(在Attributes.cs文件中)。其代码如下所示:
namespace BusinessObjectHelper
{
[AttributeUsage(AttributeTargets.Property)]
public sealed class DataMappingAttribute : System.Attribute
{
#region Private Variables
private string _dataFieldName;
private object _nullValue;
#endregion
#region Constructors
public DataMappingAttribute(string dataFieldName, object nullValue) : base()
{
_dataFieldName = dataFieldName;
_nullValue = nullValue;
}
public DataMappingAttribute(object nullValue) : this(string.Empty, nullValue){}
#endregion
#region Public Properties
public string DataFieldName
{
get { return _dataFieldName; }
}
public object NullValue
{
get { return _nullValue; }
}
#endregion
}
}
这个类是自定义属性的一个非常简单的实现。如您所见,它继承自System.Attribute
并应用了AttributeUsage
属性。传递给AttributeUsage
属性的AttributeTargets.Property
值告诉编译器此属性只能用于属性。如果您尝试将此属性应用于方法,您将收到以下错误:
Attribute 'DataMapping' is not valid on this declaration type.
It is valid on 'property, indexer' declarations only.
这很好,因为此属性用于设置要从datareader
读取的值的字段名称,并且从datareader
读取的值的默认值等于DBNull
。如果此属性应用于方法,则对我们没有任何好处。您将DataMapping
属性应用于业务对象上的属性,如下所示:
public class MyData
{
private string _firstName;
[DataMapping("FirstName", "Unknown")]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
}
传递给DataMapping
属性的第一个参数是数据库中的字段名称。第二个参数是当datareader中字段值为DBNull
时的属性的默认值。如果数据库中的FirstName
字段为null
,则业务对象的FirstName
属性将设置为“Unknown
”。现在我们知道了如何创建自定义属性,让我们继续看看如何在代码中使用它们。
private static List<PropertyMappingInfo> LoadPropertyMappingInfo(Type objType)
{
List<PropertyMappingInfo> mapInfoList = new List<PropertyMappingInfo>();
foreach (PropertyInfo info in objType.GetProperties())
{
DataMappingAttribute mapAttr = (DataMappingAttribute)
Attribute.GetCustomAttribute(info, typeof(DataMappingAttribute));
if (mapAttr != null)
{
PropertyMappingInfo mapInfo =
new PropertyMappingInfo(mapAttr.DataFieldName, mapAttr.NullValue, info);
mapInfoList.Add(mapInfo);
}
}
return mapInfoList;
}
PropertyMappingInfo
类型是另一个简单的类,它公开用于保存匹配数据库字段的字段名称、属性的默认值以及一个包含属性和方法以允许我们使用业务对象类型的PropertyInfo
对象的属性。在foreach
循环中,我们遍历业务对象类型(objType
)的每个属性。GetProperties
返回一个PropertyInfo
对象集合,我们可以使用它来查找关于业务对象属性所需的所有信息。Attribute.GetCustomAttribute
方法返回一个指向应用于我们当前正在处理的属性的DataMappingAttribute
实例的引用。如果属性未应用于该属性,该方法将返回null
,我们会将其忽略。
获取属性引用的方法有很多,这称为对属性进行反射。如果您只需要检查属性是否与某个元素相关联,请使用Attribute.IsDefined static
方法或与Assembly, Module, Type, ParamerterInfo, or MemberInfo
类关联的IsDefined
实例方法。此技术不会在内存中实例化属性对象,因此速度最快。如果您需要检查单个实例属性是否与某个元素相关联,并且还需要读取属性的字段和属性(如上例所示),则使用Attribute.GetCustomAttribute static
方法(不要将此技术用于可以多次出现在元素上的属性,因为您可能会遇到AmbiguousMatchException
)。如果您想检查多个实例属性是否与某个元素相关联,并且需要读取属性的字段和属性,则使用Assembly, Module, Type, ParameterInfo, MemberInfo
类公开的Attribute.GetCustomAttributes static
方法或GetCustomAttributes
实例方法。您必须在使用此方法来读取与元素关联的所有属性,而不考虑属性类型。
现在我们有了一个类型的属性的PropertyMappingInfo
对象集合,我们将把这个集合存储在缓存中,因为反射非常耗费资源,而且PropertyMappingInfo
在应用程序运行时不会改变,所以没有理由不缓存。尽管如此,缓存也可以在代码中清除,如果您想刷新PropertyMappingInfo
集合。实现缓存的类包装了一个实际上持有缓存数据的字典对象。其代码如下所示:
namespace BusinessObjectHelper
{
internal static class MappingInfoCache
{
private static Dictionary<string, List<PropertyMappingInfo>> cache =
new Dictionary<string,List<PropertyMappingInfo>>();
internal static List<PropertyMappingInfo> GetCache(string typeName)
{
List<PropertyMappingInfo> info = null;
try
{
info = (List<PropertyMappingInfo>) cache[typeName];
}
catch(KeyNotFoundException){}
return info;
}
internal static void SetCache(string typeName,
List<PropertyMappingInfo> mappingInfoList)
{
cache[typeName] = mappingInfoList;
}
public static void ClearCache()
{
cache.Clear();
}
}
}
下面是检索传入方法中的类型的PropertyMappingInfo
集合的代码。首先,我们检查是否已经有一个PropertyMappingInfo
对象集合的缓存版本,使用该类型的名称作为键。如果找不到,我们就调用上面描述的方法来创建PropertyMappingInfo
集合,然后将其添加到缓存中。最后,我们返回一个PropertyMappingInfo
集合。
private static List<PropertyMappingInfo> GetProperties(Type objType)
{
List<PropertyMappingInfo> info = MappingInfoCache.GetCache(objType.Name);
if (info == null)
{
info = LoadPropertyMappingInfo(objType);
MappingInfoCache.SetCache(objType.Name, info);
}
return info;
}
还有一个方法我们需要快速看一下。GetOrdinals
方法。此方法用于使用字段名称获取datareader
中字段的序数位置。拥有一个与PropertyMappingInfo
集合对应的字段索引数组,可以避免搜索datareader
的字段,而这正是我们所需要的,如果我们使用datareader
的GetValue("fieldName")
方法而不是GetValue(index)
方法的话。
private static int[] GetOrdinals(List<PropertyMappingInfo> propMapList, IDataReader dr)
{
int[] ordinals = new int[propMapList.Count];
if (dr != null)
{
for (int i = 0; i <= propMapList.Count - 1; i++)
{
ordinals[i] = -1;
try
{
ordinals[i] = dr.GetOrdinal(propMapList[i].DataFieldName);
}
catch(IndexOutOfRangeException)
{
// FieldName does not exist in the datareader.
}
}
}
return ordinals;
}
现在我们知道了如何创建自定义属性并对该属性进行反射,我们就可以继续学习更有趣的内容了。
泛型和泛型约束
泛型使开发人员能够定义一个以类型作为参数的类,并且根据参数的类型,泛型定义将返回一个不同的具体类。泛型与C++中的模板类似。然而,泛型有一些模板没有的优点,主要是约束。
在CBO.cs中的CBOstatic
类中,我们有FillObject
泛型方法。
<PropertyMappingInfo> public static T FillObject<T>
(Type objType, IDataReader dr) where T : class, new()
该方法接受一个Type
对象(您的业务对象的类型)、一个DataReader
(包含您的业务对象值的datareader
),并返回T
,嗯?T
的值取决于您如何调用该方法。T
只是一个占位符,代表将在调用方法时指定的实际类型。因此,要调用此方法来填充类型为MyData
的自定义对象,您将编写以下代码:
MyData data = CBO.FillObject<MyData>(typeof(MyData), dr);
<
和>
之间的值是T
将变成的类型。所以,在方法中我们指定T
类型的对象的地方,现在都将是MyData
类型的对象。返回值将是MyData
类型的实例,并填充了来自datareader
的数据。通过将其设为一个泛型方法,我们无需将对象从System.Object
类型转换为MyData
类型。在DNN实现中,该方法将返回一个对象。在泛型出现之前,这是从方法返回任何类型的对象的唯一方法,因为.NET中的所有类都继承自System.Object
。现在,通过使用泛型方法而不是返回需要转换为MyData
的对象,我们得到了MyData
类型的对象,并且不再需要转换它。您可能想知道FillObject
方法末尾的where T : class, new()
是什么意思。这些是泛型约束,我将很快解释。让我们看看CBO
类的核心工作方法CreateObject
。此方法由FillObject
和FillCollection
调用,并负责实际将值赋给业务对象中的相应属性。它几乎完全遵循DNN实现,只是它已被翻译成C#,使用了泛型返回类型而不是Object
,并且与PropertyMappingInfo
类一起工作,而不是直接与对象的类型。
private static T CreateObject<T>(IDataReader dr,
List<PropertyMappingInfo> propInfoList, int[] ordinals) where T : class, new()
{
T obj = new T();
// iterate through the PropertyMappingInfo objects for this type.
for (int i = 0; i <= propInfoList.Count - 1; i++)
{
if (propInfoList[i].PropertyInfo.CanWrite)
{
Type type = propInfoList[i].PropertyInfo.PropertyType;
object value = propInfoList[i].DefaultValue;
if (ordinals[i] != -1 && dr.IsDBNull(ordinals[i])== false)
value = dr.GetValue(ordinals[i]);
try
{
// try implicit conversion first
propInfoList[i].PropertyInfo.SetValue(obj, value, null);
}
catch
{
// data types do not match
try
{
// need to handle enumeration types differently
// than other base types.
if (type.BaseType.Equals(typeof(System.Enum)))
{
propInfoList[i].PropertyInfo.SetValue(
obj, System.Enum.ToObject(type, value), null);
}
else
{
// try explicit conversion
propInfoList[i].PropertyInfo.SetValue(
obj, Convert.ChangeType(value, type), null);
}
}
catch
{
// error assigning the datareader value to a property
}
}
}
}
return obj;
}
该方法实际上并不复杂。我们所做的就是遍历我们使用添加到业务对象属性的属性创建的所有PropertyMappingInfo
对象。对于这些对象中的每一个,我们检查该属性是否可以写入。如果可以写入,我们检查序数数组中是否有匹配的字段,并且datareader
中是否有值。如果有,我们将value
设置为datareader
的值,否则我们将其保留为默认值。然后,我们首先尝试通过隐式转换来设置属性。如果无法将值隐式转换为属性的类型,那么我们尝试显式转换。如果属性是枚举,那么我们需要使用Enum.ToObject
方法来转换值。否则,我们使用Convert
对象上的ChangeType static
方法。如果失败,我们就放弃,然后继续处理下一个属性。
您可能会注意到此方法的第一行使用常规实例化方法而不是反射来创建一个T
类型的对象实例。我们怎么知道传入的类型可以实例化并且具有无参数的public
构造函数呢?嗯,这就是约束的用武之地。您会注意到在方法的参数列表之后,我们有where T : class, new()
。这是一个泛型约束,它表示T
所代表的传入类型必须是引用类型,并且必须声明一个public
的、无参数的构造函数。
C#支持五种不同的约束:
- 接口约束 - 类型参数必须实现指定的接口。
- 继承约束 - 类型参数必须派生自指定的基类。
- 类约束 - 类型参数必须是引用类型。
- 结构约束 - 类型参数必须是值类型。
- New约束 - 类型参数必须公开一个
public
的、无参数(默认)的构造函数。
要添加泛型约束,请使用语法where T : [constraint]
。您可以使用相同的语法对同一或不同的泛型参数强制执行多个约束:where T : [contraint 1], [constraint 2] where V : [contraint 1], [constraint 2]
。以下方法签名显示了应用了多个约束。通过指定class
和new()
约束,我们知道类型参数是引用类型并且它公开了一个默认构造函数。因此,我们可以安全地使用new
对该对象进行实例化,以创建指定类型的实例。这使我们能够返回特定类型的对象而不是仅返回对象,并且它确保我们不会尝试创建值类型或不公开默认(无参数)构造函数的类型的实例。使用约束使我们能够编写泛型方法和类,这些方法和类可以根据约束使用特定的行为。例如,如果我们编写了一个泛型方法来返回多个值的最大值,我们就需要确保为T
指定的类型实现了IComparable
接口,从而确保我们可以安全地比较这些值。让我们以FillCollection
方法为例,说明多个泛型类型和约束。
public static C FillCollection<T, C>(Type objType,
IDataReader dr) where T : class, new() where C : ICollection<T>, new()
{
C coll = new C();
try
{
List<PropertyMappingInfo> mapInfo = GetProperties(objType);
int[] ordinals = GetOrdinals(mapInfo, dr);
while (dr.Read())
{
T obj = CreateObject<T>(dr, mapInfo, ordinals);
coll.Add(obj);
}
}
finally
{
if (dr.IsClosed == false)
dr.Close();
}
return coll;
}
在这里,我们声明了一个接受两个泛型类型的泛型方法。这两个泛型类型都应用了约束。如果您查看方法名称的右侧,您会看到<T, C>
。这意味着我们期望此方法被调用时指定了两个类型,在这种情况下,是您的业务对象的类型(T
)以及将保存业务对象的集合的类型(C
)。还要注意,此方法返回类型为C
(我们的业务对象集合类型)的对象。在这里,我们对T
使用了与在FillObject
和CreateObject
方法中相同的约束。对集合类型(C
)的约束是它必须实现ICollection<T>
接口,该接口是System.Collections.Generic
命名空间中类的基本接口。这意味着任何继承自通用集合类之一或从头开始实现此接口的业务对象集合类都可以用作集合对象。我们还需要确保它公开一个public
的默认构造函数,以便我们可以在方法中创建该类型的实例。该方法的引用将从此方法返回。
让我们逐步了解这里发生了什么。首先,我们创建了指定集合类型的实例。由于我们的new()
约束,我们知道可以调用new。在获得集合类的实例后,我们获取PropertyMappingInfo
集合和我们的序数数组。然后,我们只需要循环遍历datareader
中的行并调用CreateObject
(这将实例化并填充对象)。一旦我们获得了已填充的业务对象的引用,我们就将其添加到集合中。由于指定了接口约束(ICollection<T>
),我们知道可以对此集合调用Add
,因此该对象必须实现此接口,其中包括Add
方法。最后,我们关闭datareader
并返回集合。
以下是一些代码,展示了如何调用FillCollection
方法。
IDataReader dr = cmd.ExecuteReader();
MyCustomList dataList = CBO.FillCollection<MyData, MyCustomList>(typeof(MyData), dr);
背景
我已经是CodeProject的会员多年了,这个网站上的许多文章在我的工作和娱乐中都给了我很大的帮助。所以,现在我希望我能为之添砖加瓦,做出自己的贡献。我写这篇文章不仅仅是为了贡献CodeProject上许多有用的软件,也是为了解释一下泛型、泛型约束、反射和自定义属性,并举例说明这些技术如何协同工作,创建一个(我希望)可在多个项目中重用的有用助手类。我希望我能够帮助到那些曾给我巨大帮助的人。
Using the Code
使用代码非常直接。将BusinessObjectHelper.dll程序集的引用添加到您的项目中,并将DataMapping
属性添加到您希望从datareader
中分配值的业务对象的每个属性上。下面是一个带有DataMapping
属性标记的业务对象类的示例:
public class MyData
{
private string _firstName;
[DataMapping("Unknown")]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
private MyEnum _enumType;
[DataMapping("TheEnum", MyEnum.NotSet)]
public MyEnum EnumType
{
get { return _enumType; }
set { _enumType = value; }
}
private Guid _myGuid;
[DataMapping("MyGuid", null)]
public Guid MyGuid
{
get
{
return _myGuid;
}
set { _myGuid = value; }
}
private double _cost;
[DataMapping("MyDecimal", 0.0)]
public double Cost
{
get { return _cost; }
set { _cost = value; }
}
private bool _isOK;
[DataMapping("MyBool", false)]
public bool IsOK
{
get { return _isOK; }
set { _isOK = value; }
}
}
传递给DataMapping
属性的第一个参数是数据库中与属性对应的字段的名称。第二个参数是当数据库中的值为null
时属性的默认值。如果您不指定字段名称,则将使用属性名称。所以,如果您数据库中的字段名称与属性名称相同,则无需在属性中包含字段名称。但是,您必须指定一个默认值。
设置好业务对象后,要填充它,您只需调用CBO类上的FillObject
或FillCollection static
方法,并传入您的业务对象的类型和datareader
。您还需要为泛型方法指定类型。在这种情况下,我会像这样调用FillObject
:
IDataReader dr = cmd.ExecuteReader();
MyData data = CBO.FillObject<MyData>(typeof(MyData), dr);
如果您需要从datareader
填充对象集合,请调用CBO
类上的FillCollection static
方法。我有一个自定义集合类型MyCustomList
和一个类型为MyData
的业务对象。要用MyData
对象填充MyCustomList
并获取填充集合的引用,我会像这样调用FillCollection
:
MyCustomList dataList = CBO.FillCollection<MyData, MyCustomList>(typeof(MyData), dr);
如果您有兴趣了解更多关于C#的知识,包括泛型、自定义属性和反射,我推荐以下书籍:
- Programming Microsoft Visual C# 2005: The Base Class Library (作者: Francesco Balena)。ISBN - 0735623082
- CLR Via C# - 第二版 (作者: Jeffery Richtor)。ISBN - 0735621632
代码更新
- 更新了
CreateObject
方法,因为它在处理DateTime
字段或Enums
的默认值时效果不佳。