|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
IntroductionAfter 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 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 Thanks to Generics in .Net 2.0, Custom Attributes, and Generic Constraints, these issues can be resolved. Custom AttributesThe 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 attibute 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: 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
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 if 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 appy the public class MyData
{
private string _firstName;
[DataMapping("FirstName", "Unknown")]
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
}
The first argument passed to the 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 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 Now that we have a collection of 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 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 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;
}
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 ConstaintsGenerics 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 <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 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;
}
The method really isn't that complicated. All we are doing is looping through all the 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 C# supports five different constraints:
To add generic constraints you use the syntax 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 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 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);
BackgroundI'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 peices of software on the 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 codeUsing the code is fairly straight forward. Add a reference to the 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 After you have setup your business object, to populate it all you need to do is call the 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 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:
Updates to the Code
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||