This article is based on material originally published on DNN Developer Zone (www.dnndevzone.com). This version has been updated to cover the changes made in DotNetNuke 5.0.
Introduction
In a Blog a few months agoon DotNetNuke.com I described the results of some profiling tests I had carried out. In that blog I discussed the CBO class, when to use it and more importantly, when not to use it. Since writing that Blog post there have been some changes to how the CBO class works. This article will focus on how the CBO class works and how it can be used - in a future article I will describe the IHydratable Interface, and how it can be used to improve performance.
The CBO Class
The CBO class is a utility class in the DotNetNuke Library that performs Business Object hydration. Hydration is a term that refers to the filling of an instance of a class, in this case from a DataReader. CBO exposes both standard and generic versions of two methods; FillObject (which is used to hydrate a single object) and FillCollection (which is used to hydrate a collection of objects), as well as multiple versions of a generic FillDictionary method.
These methods take the type of the object to hydrate as a parameter and use reflection to determine how to hydrate the object. As discussed in the blog entry, reflection is an expensive process and could be replaced by writing custom hydrators. Let's first examine the process to hydrate a single object instance in the Links module (LinkController.GetLink()) using the CBO class. Later in this article we will describe how to use a custom hydrator that avoids the use of reflection thus improving performance.
Listing 1: The GetLink method in the LinkController Class |
1: Public Function GetLink(ByVal ItemID As Integer, ByVal ModuleId As Integer) As LinkInfo 2: Return CType(CBO.FillObject(DataProvider.Instance().GetLink(ItemID, ModuleId),_ 3: End Function |
Listing 1 shows the GetLink method in the LinkController class. This method is fairly straightforward, as it calls a similarly named method in the DataProvider, which returns a DataReader containing a single link identified by the ItemId and Module Id. This DataReader is then passed to the FillObject method together with the type of object to fill (LinkInfo in this case).
Using the FillObject method to hydrate a single object
The FillObject method of the CBO class called above is a static (or shared) method that takes a DataReader and a type as parameters and returns an object. This method has an override that also takes a Boolean property - closeReader- that determines whether to close the DataReader after the object has been created (see Listing 2).
Listing 2: The FillObject methods |
1: Public Shared Function FillObject(ByVal dr As IDataReader, ByVal objType As Type) As Object 2: Return CreateObjectFromReader(objType, dr, True) 3: End Function 4:
5: Public Shared Function FillObject(ByVal dr As IDataReader, ByVal objType As Type, 6: Return CreateObjectFromReader(objType, dr, closeReader) 7: End Function |
This override is used for situations when the the caller needs to continue to use the IDataReader. An example of this would be when Data Layer returns two or more sets of data, one is used to hydrate one set of objects and the other is used for some other reason - perhaps to hydrate a second set of objects. Both overrides call the private method CreateObjectFromReader (Listing 3)
Listing 3: CreateObjectFromReader |
1: Private Shared Function CreateObjectFromReader(ByVal objType As Type, ByVal dr As IDataReader, 2: Dim objObject As Object = Nothing 3:
4: Dim canRead As Boolean = True 5: If closeReader Then 6: canRead = False 7: ' read datareader 8: If dr.Read() Then 9: canRead = True 10: End If 11: End If 12:
13: If canRead Then 14: 'Create the Object 15: objObject = CreateObject(objType, False) 16:
17: 'hydrate the custom business object 18: FillObjectFromReader(objObject, dr)
19: End If 20:
21: ' Ensure DataReader is closed 22: CloseDataReader(dr, closeReader)
23:
24: Return objObject 25: End Function |
The first part of this method (lines 4-11) determine if there is a record to read. Next in line 15 we create an object of the correct type and in line 18 we call FillObjectFromReader to hydrate the object from the DataReader.
Before we look in detail at the FillObjectFromReader method lets examine how the FillCollection methods work.
Using the FillCollection method to hydrate a collection of objects
The public FillCollection methods behave similarly. In the Links module, for example, the LinksController class has a method to return all the Links
Listing 4: The GetLinks method in the LinkController Class |
1: Public Function GetLinks(ByVal ModuleId As Integer) As ArrayList 2: Return CBO.FillCollection(DataProvider.Instance().GetLinks(ModuleId), GetType(LinkInfo)) 3: End Function |
This method calls the FillCollection method, passing, as before, the DataReader and the type of the object to hydrate.
Listing 5: The FillCollection method |
1: Public Shared Function FillCollection(ByVal dr As IDataReader, ByVal objType As Type) As ArrayList 2: Return DirectCast(FillListFromReader(objType, dr, New ArrayList(), True), ArrayList) 3: End Function |
As with the case of the FillObject method the FillCollection method calls a private FillListFromReader method, which fills the collection passed to it with objects created from the DataReader (see Listing 6).
Listing 6 The FillListFromReader method |
1: Private Shared Function FillListFromReader(ByVal objType As Type, ByVal dr As IDataReader, 2: Dim objObject As Object 3:
4: ' iterate datareader 5: While dr.Read 6: 'Create the Object 7: objObject = CreateObjectFromReader(objType, dr, False) 8:
9: ' add to collection 10: objList.Add(objObject)
11: End While 12:
13: ' Ensure DataReader is closed 14: CloseDataReader(dr, closeReader)
15:
16: Return objList 17: End Function |
The important thing to note being that the FillListFromReader method calls the same CreateObjectFromReader method that is called by FillObject, with the closeReader parameter set to false.
The FillObjectFromReader method
As we have discussed above all calls to FillObject, FillCollection (and FillDictionary) are routed through the private method FillObjectFromDataReader (Listing 7)
Listing 7: The FillObjectFromReader method |
1: Private Shared Sub FillObjectFromReader(ByVal objObject As Object, ByVal dr As IDataReader) 2: 'Determine if object is IHydratable 3: If TypeOf objObject Is IHydratable Then 4: 'Use IHydratable's Fill 5: Dim objHydratable As IHydratable = TryCast(objObject, IHydratable) 6: If objHydratable IsNot Nothing Then 7: objHydratable.Fill(dr)
8: End If 9: Else 10: 'Use Reflection 11: HydrateObject(objObject, dr)
12: End If 13: End Sub |
This method determines if the object implements the IHydratable interface (I will discus this interface in more detail in a future blog). If it does it calls the objects Fill method, if not it calls the HydrateObject method which uses Reflection to hydrate the object (see Listing 8)
Listing 8: The HydrateObject method |
1: Private Shared Sub HydrateObject(ByVal objObject As Object, ByVal dr As IDataReader) 2: Dim objPropertyInfo As PropertyInfo = Nothing 3: Dim objPropertyType As Type = Nothing 4: Dim objDataValue As Object 5: Dim objDataType As Type 6: Dim intIndex As Integer 7:
8: ' get cached object mapping for type 9: Dim objMappingInfo As ObjectMappingInfo = GetObjectMapping(objObject.GetType) 10:
11: ' fill object with values from datareader 12: For intIndex = 0 To dr.FieldCount - 1 13: 'If the Column matches a Property in the Object Map's PropertyInfo Dictionary 14: If objMappingInfo.Properties.TryGetValue(dr.GetName(intIndex).ToUpperInvariant, 15: 'Get its type 16: objPropertyType = objPropertyInfo.PropertyType
17:
18: 'If property can be set 19: If objPropertyInfo.CanWrite Then 20: 'Get the Data Value from the data reader 21: objDataValue = dr.GetValue(intIndex)
22:
23: 'Get the Data Value's type 24: objDataType = objDataValue.GetType 25:
26: If IsDBNull(objDataValue) Then 27: ' set property value to Null 28: objPropertyInfo.SetValue(objObject, Null.SetNull(objPropertyInfo), Nothing) 29: ElseIf objPropertyType.Equals(objDataType) Then 30: 'Property and data objects are the same type 31: objPropertyInfo.SetValue(objObject, objDataValue, Nothing) 32: Else 33: ' business object info class member data type does not match 34: ' datareader member data type 35: Try 36: 'need to handle enumeration conversions differently than other 37: 'base(types) 38: If objPropertyType.BaseType.Equals(GetType(System.Enum)) Then 39: ' check if value is numeric and if not convert to integer 40: ' ( supports databases like Oracle ) 41: If IsNumeric(objDataValue) Then 42: objPropertyInfo.SetValue(objObject, System.Enum.ToObject( 43: Else 44: objPropertyInfo.SetValue(objObject, System.Enum.ToObject( 45: End If 46: ElseIf objPropertyType.FullName.Equals("System.Guid") Then 47: ' guid is not a datatype common across all databases 48: ' ( ie. Oracle ) 49: objPropertyInfo.SetValue(objObject, Convert.ChangeType( 50: ElseIf objPropertyType.FullName.Equals("System.Version") Then 51: objPropertyInfo.SetValue(objObject, 52: Else 53: ' try explicit conversion 54: objPropertyInfo.SetValue(objObject, objDataValue, Nothing) 55: End If 56: Catch 57: objPropertyInfo.SetValue(objObject, Convert.ChangeType(objDataValue, 58: End Try 59: End If 60: End If 61: End If 62: Next 63: End Sub |
This blog describes the overall approach to hydration in the DotNetNuke LIbrary. In Part 2 I will describe the IHydratable Interface, and how you can improve the performance of your own modules.