Introduction
After spending quite a bit of time typing out the code to assign values returned from a DataReader
to the properties of my business object, I began to think that there must be a better way. Then I started to play with DotNetNuke. I began reading some of the documentation for DNN and ran into a rather interesting helper class that is explained in the DotNetNuke Data Access document. The Custom Business Object Helper class that they used to populate their business objects was a code saver. You write the code to retrieve a DataReader
with the information you need to set the properties on your object. Then you call FillObject
or FillCollection
on the helper class, pass in your DataReader
and the type of your business object, and it gives you an instance of that object fully populated with the data. It uses reflection and the names of your business object's properties to match the DataReader
field to your object's property. For instance, if you have a table in your database named Customers
and you execute a query to return one of Customer
's rows in a DataReader
you can populate your Customer
object's properties based on the names of the Fields in the DataReader
. If your business object has a property called FirstName
and your table has a field called FirstName
, then the value of the FirstName
field in the DataReader
is assigned to the FirstName
property on your business object. This can save quite a bit of development time when you have an object with many properties. It also helps when you add fields to your table because all you need to do is add a property with the same name to your business object and the helper class takes care of populating it. Along with the ability to populate one instance of your object with values, it also gives you the ability to populate a collection of objects if the DataReader
returns more than one row.
Despite the benefits of the DNN helper class, there were several issues that I felt needed to be solved. The first one was that the properties on the business object and the fields in the database had to have the same name. Sometimes the database and queries have already been constructed and the field names wouldn't make very good property names for your business object (imagine having to prefix all you business object properties with 'fld', which was a fairly common way to prefix field names in a database). The second issue was that the FillCollection
method returned an ArrayList
. Many developers prefer to create their own collection classes for their objects. This way they can stick any methods that operate on the group of business objects inside that collection class. Using the DNN helper class, you could only get back an ArrayList
. The last issue was that if the database field value was null
, the value would be set by the Null
helper class, this class took a string
representation of the objects type and assigned the default value for the type (e.g. 0
for System.Int32
, false
for System.Boolean
, etc.). If you wanted to change the default value, you had to edit this class and then recompile it.
Thanks to Generics in .NET 2.0, Custom Attributes, and Generic Constraints, these issues can be resolved.
Custom Attributes
The problem with the property names having to be the same as the database field names, and the assignment of a default value when the field contained a DBNull
value, can be solved using custom attributes. Creating a custom attribute is easy, just create a class that derives from System.Attribute
. There are a few rules to follow when creating a custom attribute. Its name must end with 'Attribute
' and it should be marked with the AttributeUsage
attribute (Yep, you need an attribute for your attribute). The AttributeUsage
attribute tells the compiler to which program entities the attribute can be applied: classes, modules, methods, properties, etc. A custom attribute class can only have fields, properties and methods that accept and return values of the following types: bool, byte, short, int, long, char, float, double, string, object, System.Type, and public Enum
. It can also receive and return one dimensional arrays of the preceeding types. A custom type must expose one or more public
constructors, and typically the constructors will accept arguments that are mandatory for the attribute. In the code below, the mandatory attribute field is the NullValue
field, which is the value the property will take if the datareader
contains a null
value for the field. There is also an overloaded version of this constructor that takes the field name as well. The custom attribute class that BusinessObjectHelper
exposes is DataMappingAttribute
(in the Attributes.cs file). Its code is shown below:
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
}
}
This class is a very simple implementation of a custom attribute. As you can see, it inherits from System.Attribute
and has the AttributeUsage
attribute applied to it. The AttributeTargets.Property
value passed in to the AttributeUsage
attribute tells the compiler that this attribute can only be used on properties. If you attempt to apply this attribute to a method, you will get the following error:
Attribute 'DataMapping' is not valid on this declaration type.
It is valid on 'property, indexer' declarations only.
This is good because this property is used to set the field name of the value to be read from the datareader
and the default value of the value read from the datareader
is equal to DBNull
. This attribute would not do us any good if it was applied to a method. You apply the DataMapping
attribute to a property on your business object as shown below:
public class MyData
{
private string _firstName;
[DataMapping("FirstName", "Unknown")]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
}
The first argument passed to the DataMapping
attribute is the field name in the database. The second argument is the default value for the property if the datareader contains a DBNull
value for the field. If the FirstName
field in the database is null
, then the FirstName
property of the business object will be set to 'Unknown
'. So, now that we know how to create a custom attribute, let's move on and see how to use them in our code.
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;
}
The PropertyMappingInfo
type is another simple class that exposes properties for saving the field name of the matching database field, the default value of the property, and a PropertyInfo
object that contains properties and methods that allow us to work with the business object's type. Inside the foreach
loop, we iterate through each one of the properties in the business object type (objType
). GetProperties
returns a collection of PropertyInfo
objects that we can use to find out all we need to know about the business object's properties. The Attribute.GetCustomAttribute
method returns a reference to the instance of DataMappingAttribute
that was applied to the property we are currently working with. If the attribute was not applied to the property, the method returns null
and we simply ignore it.
There are several ways to get a reference to an attribute, this is called reflecting on an attribute. If you just need to check whether an attribute is associated with an element, use the Attribute.IsDefined static
method or the IsDefined
instance method associated with the Assembly, Module, Type, ParamerterInfo, or MemberInfo
classes. This technique doesn't instantiate the attribute object in memory and is the fastest. If you need to check whether a single-instance attribute is associated with an element and you also need to read the attributes fields and properties (which is the case in the method above), we use the Attribute.GetCustomAttribute static
method (don't use this technique with attributes that can appear multiple times on an element, because you might get an AmbiguousMatchException
). If you want to check whether a multiple instance attribute is associated with an element and you need to read the field and properties of the attribute, use the Attribute.GetCustomAttributes static
method or the GetCustomAttributes
instance method exposed by the Assembly, Module, Type, ParameterInfo, MemberInfo
classes. You must use this method when reading all the attributes associated with an element, regardless of the attribute type.
Now that we have a collection of PropertyMappingInfo
objects for the type's properties, we will store this collection in a cache, because reflection is expensive, and there is no reason the PropertyMappingInfo
should change while the application is running. The cache can be cleared in code, however, if you want to refresh the PropertyMappingInfo
collections. The class that implements the cache, wraps a dictionary object that actually holds the cached data. Its code is shown below:
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();
}
}
}
Below is the code that retrieves the PropertyMappingInfo
collection for the type passed in to the method. First, we check to see if we have a cached version of the PropertyMappingInfo
object collection using the type's name as the key. If one cannot be found, we call the method described above to create the PropertyMappingInfo
collection and then add it to the cache. Finally we return a PropertyMappingInfo
collection.
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;
}
There is one more method that we need to look at quickly. The GetOrdinals
method. This method is used to get the ordinal position of the field in the datareader
using the fields name. Having an array of indexes for the fields that corresponds with the PropertyMappingInfo
collection avoids having to search through the datareader
's fields, which is what we need to do if we used the GetValue("fieldName")
method from the datareader
instead of the GetValue(index)
method.
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)
{
}
}
}
return ordinals;
}
Now that we know how to create a custom attribute and reflect on that attribute, we are ready to move on to the good stuff.
Generics and Generic Constraints
Generics enable developers to define a class that takes a type as an argument, and depending on the type of argument, the generic definition will return a different concrete class. Generics are similar to templates in C++. However, generics have some benefits that templates do not, mainly, constraints.
In the CBO static
class located in CBO.cs, we have the FillObject
generic method.
<PropertyMappingInfo> public static T FillObject<T>
(Type objType, IDataReader dr) where T : class, new()
This method takes a Type
object (the type of your business object), a DataReader
(the datareader
that contains the values for your business object), and returns T
, huh? T
's value depends on how you call the method. The T
is simply a placeholder for the real type that will be specified when the method is called. So to call this method to populate a custom object of type MyData
, you would write the following:
MyData data = CBO.FillObject<MyData>(typeof(MyData), dr);
The value between the <
and >
is the type that T
will become. So, everywhere we specify an object of type T
in the method, it will now be an object of type MyData
. The return value would be an instance of the MyData
type populated with the data from the datareader
. By making this a generic method, we do away with the need to cast the object to type MyData
from type System.Object
. In the DNN implementation, the method would return an object. This was the only way to return any type of object from the method prior to generics, since all classes in .NET inherit from System.Object
. Now, by using a generic method instead of getting back an object that needs to be cast to MyData
, we get back an object of type MyData
, and no longer need to cast it. You may be wondering what the where T : class, new()
stuff is all about at the end of the FillObject
method. These are generic constraints, and I'll get to those shortly. Let's look at the workhorse of the CBO
class, the CreateObject
method. This method is called by FillObject
and FillCollection
, and it is responsible for actually assigning the values to the corresponding properties in the business object. It follows the DNN implementation almost exactly, except that it has been translated to C#, uses a generic return type instead of Object
, and works with the PropertyMappingInfo
class instead of directly with the object's type.
private static T CreateObject<T>(IDataReader dr,
List<PropertyMappingInfo> propInfoList, int[] ordinals) where T : class, new()
{
T obj = new T();
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
{
propInfoList[i].PropertyInfo.SetValue(obj, value, null);
}
catch
{
try
{
if (type.BaseType.Equals(typeof(System.Enum)))
{
propInfoList[i].PropertyInfo.SetValue(
obj, System.Enum.ToObject(type, value), null);
}
else
{
propInfoList[i].PropertyInfo.SetValue(
obj, Convert.ChangeType(value, type), null);
}
}
catch
{
}
}
}
}
return obj;
}
The method really isn't that complicated. All we are doing is looping through all the PropertyMappingInfo
objects we created using the attributes we added to the business object's properties. For each one of these objects, we check that the property can be written to. If it can be written to, we check to see if there is a matching field in the ordinals array and if there is a value in the datareader
. If there is, we set value
to the value of the datareader
, otherwise we leave the value set to the default. Then we first try to set the property using an implicit conversion. If the value cannot be implicitly converted to the property's type, then we try explicitly converting it. If the property is an enumeration, then we need to use the Enum.ToObject
method to convert the value. Otherwise, we use the ChangeType static
method on the Convert
object. If that fails, then we give up and move on to the next property.
You may notice that the first line in this method creates an instance of an object of type T
using the normal instantiation method instead of reflection. How can we know that the type passed in can be instantiated and has a public
constructor with no parameters. Well, this is where constraints come in. You'll notice after the method's parameter list we have where T : class, new()
. This is a generic constraint that says the type passed in that T
represents has to be a reference type and must declare a public
, parameterless constructor.
C# supports five different constraints:
- Interface constraint - The type argument must implement the specified interface.
- Inheritance constraint - The type argument must derive from the specified base class.
- Class constraint - The type argument must be a reference type.
- Struct constraint - The type argument must be a value type.
- New constraint - The type argument must expose a
public
, parameterless (default) constructor.
To add generic constraints, you use the syntax where T : [constraint]
. You can enforce more than one constraint on the same or on different generic parameters using the same syntax: where T : [contraint 1], [constraint 2] where V : [contraint 1], [constraint 2]
. The following method signature shows multiple constraints being applied. By specifying the class
and new()
constraints, we know that the type argument is a reference type and it exposes a default constructor. So, we can safely use new
on this object to create an instance of the specified type. This is what allows us to return a specific type of object instead of just object, and it assures us that we won't try to create an instance of a value type or a type that does not expose a default (parameterless) constructor. Using constraints gives us the ability to write generic methods and classes that can use specific behavior depending on the constraint. For instance, if we wrote a generic method to return the maximum of several values, we would need to be sure that the type specified for T
implements the IComparable
interface, assuring us that we can safely compare the values. Let's look at the FillCollection
method for an example of multiple generic types and constraints.
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;
}
Here we are declaring a generic method that takes two generic types. Both of these generic types have constraints applied to them. If you look to the right of the method name, you see <T, C>
. This means that we expect this method to be called with two types specified, in this case, the type of your business object (T
) and the type of the collection that will hold the business objects (C
). Also notice that this method returns an object of type C
(our business object collection type). We use the same constraints for T
here that we used in the FillObject
and CreateObject
methods. The constraints on the collection type (C
) are that it must implement the ICollection<T>
interface, which is the base interface for classes in the System.Collections.Generic
namespace. This means that any business object collection class that inherits from one of the generic collection classes or implements this interface from scratch, can be used as the collection object. We also need to be sure that it exposes a public
default constructor so that we can create an instance of the type in our method. A reference to this object will be returned from the method.
Let's walk through what's happening here. First, we create an instance of the collection type specified. We know we can call new on this because of our new()
constraint. After we have an instance of the collection class we get the PropertyMappingInfo
collection and our ordinal array. Then all we need to do is loop through the rows in the datareader
and call CreateObject
(which will instantiate and populate the object). Once we have a reference to our populated business object, we add it to the collection. We know that we can call Add
on this collection because the interface constraint (ICollection<T>
) was specified, so the object must implement this interface, and that includes the Add
method. Finally, we close the datareader
and return the collection.
Here is some code that shows how to call the FillCollection
method:
IDataReader dr = cmd.ExecuteReader();
MyCustomList dataList = CBO.FillCollection<MyData, MyCustomList>(typeof(MyData), dr);
Background
I've been a member of the CodeProject for several years now, many of the articles on this site have been a great help to me at work and at play. So, now I hope I can add to that and make a contribution of my own. I really wanted to write this article not only to contribute to the many useful pieces of software on CodeProject, but also to try and explain a bit about Generics, Generic Constraints, Reflection and Custom Attributes, as well as give an example of how these technologies can be used together to create (what I hope) is a useful helper class that can be reused in many different projects. I hope that I have managed to help those who have helped me so much.
Using the Code
Using the code is fairly straight forward. Add a reference to the BusinessObjectHelper.dll assembly to your project and add the DataMapping
attribute to each property of your business object that you want to assign from the datareader
. Below is an example of a business object class marked with DataMapping
attributes:
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; }
}
}
The first argument passed in to the DataMapping
attribute is the name of the field in the database that corresponds to the property. The second argument is the default value for the property if the value is null
in the database. If you do not specify the field name, then the property name will be used instead. So, if you have fields in the database that do have the same name as the property, then you do not need to include the field name in the attribute. You must specify a default value, however.
After you have setup your business object, to populate it all you need to do is call the FillObject
or FillCollection static
methods on the CBO class, and pass in the type of your business object and the datareader
. You also need to specify the type for the generic method. In this case, I would call FillObject
as follows:
IDataReader dr = cmd.ExecuteReader();
MyData data = CBO.FillObject<MyData>(typeof(MyData), dr);
If you need to populate a collection of objects from a datareader
, call the FillCollection static
method on the CBO
class. I have a custom collection type called MyCustomList
and a business object of type MyData
. To fill MyCustomList
with MyData
objects and get a reference to the populated collection, I would call FillCollection
as follows:
MyCustomList dataList = CBO.FillCollection<MyData, MyCustomList>(typeof(MyData), dr);
If you are interested in learning more about C#, including generics, custom attributes, and reflection, I would recommend the following books:
- Programming Microsoft Visual C# 2005: The Base Class Library by Francesco Balena. ISBN - 0735623082
- CLR Via C# - Second Edition by Jeffery Richtor. ISBN - 0735621632
Updates to the Code
- Updated the
CreateObject
method because it was not working well with default values for DateTime
fields or Enums