Click here to Skip to main content
Click here to Skip to main content

A Custom Business Objects Helper Class

, 24 Jul 2006 CPOL
Rate this:
Please Sign up or sign in to vote.
This article shows how to construct a custom business object helper that will populate business objects from a datareader using generics, reflection, and custom attributes

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)
            {
                // 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 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();

            // 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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

adargel

United States United States
No Biography provided

Comments and Discussions

 
QuestionNot Work on Nested Object Pinmembersky391330-Oct-11 19:21 
GeneralMy vote of 1 PinmemberWestieBoy22-May-11 20:14 
QuestionMyCustomList Class example PinmemberMember 166383410-Jan-10 7:02 
QuestionHow to hide Business object's Base class properties Pinmemberpaheli26-Mar-09 0:15 
AnswerRe: How to hide Business object's Base class properties Pinmemberdarcon7726-Mar-11 14:05 
GeneralFix- unable to handle Nullable Types PinmemberChrisOnNet15-Dec-08 9:04 
Questionhow do we handle business entities with custom objects as properties Pinmembermsaivara20-Jun-08 11:36 
GeneralLimited functionality Pinmembermbonano13-Nov-07 7:05 
GeneralSource Code Credit PinmemberPaul Trippett26-Feb-07 16:42 
Questionhave question PinmemberRuslanKulubaev12-Oct-06 4:29 
GeneralReflection and performance Pinmemberseesharper8-Sep-06 1:11 
GeneralRe: Reflection and performance PinmemberStephen Long17-Dec-06 9:52 
GeneralRe: Reflection and performance Pinmemberseesharper18-Dec-06 7:48 
GeneralDataMapping & Objects PinmemberChristine Lipfert30-Aug-06 7:54 
GeneralPerfomance PinmemberRobertMadrid30-Aug-06 0:22 
NewsRe: Perfomance Pinmembersholliday22-Mar-07 7:51 
GeneralRe: Perfomance Pinmembersholliday22-Mar-07 8:03 
GeneralRe: Perfomance Pinmembersholliday22-Mar-07 8:53 
GeneralRe: Perfomance Pinmembervon_dabke10-Jul-07 7:22 
GeneralClose Datareader PinmemberRobertMadrid24-Aug-06 3:06 
GeneralRe: Close Datareader PinmemberRobertMadrid24-Aug-06 3:15 
GeneralRe: Close Datareader PinmemberRobertMadrid24-Aug-06 3:29 
GeneralGreat idea! PinmemberJohn Rayner2-Aug-06 16:03 
AnswerRe: Great idea! Pinmemberadargel3-Aug-06 4:55 
GeneralRe: Great idea! PinmemberStephen Long7-Aug-06 9:33 
GeneralRe: Great idea! Pinmemberadargel7-Aug-06 10:36 
GeneralRe: Great idea! [modified] PinmemberStephen Long7-Aug-06 10:46 
GeneralRe: Great idea! PinmemberStephen Long10-Aug-06 5:18 
GeneralRe: Great idea! PinmemberStephen Long10-Aug-06 6:20 
GeneralGreat stuff Pinmemberjspora1-Aug-06 20:07 
GeneralGreat artcile and extending it PinmemberJorge Varas31-Jul-06 10:21 
GeneralGreat Article! Pinmemberwortho55531-Jul-06 1:31 
Questionhow to assign a default value to a DateTime property. Thanks. PinmemberRickieChina23-Jul-06 20:39 
AnswerRe: how to assign a default value to a DateTime property. Thanks. Pinmemberadargel24-Jul-06 5:40 
GeneralRe: how to assign a default value to a DateTime property. Thanks. PinmemberRickieChina25-Jul-06 18:11 
GeneralSimilar to Gentle.NET opensource ORM tool Pinmembersbohlen17-Jul-06 15:25 
GeneralRe: Similar to Gentle.NET opensource ORM tool Pinmemberadargel18-Jul-06 5:45 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.1411022.1 | Last Updated 24 Jul 2006
Article Copyright 2006 by adargel
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid