I was working on a small project the other day when I starting to think about using a mapping layer to map a datarow object to a strongly typed object and how hard it would be. For instance if you have a Customer object that has CustomerId, FirstName, LastName and PhoneNumber. You can convert a datarow into a Customer object by using this code:
Public Class Customer
Private mCustomerId As Integer
Private mFirstName As String
Private mLastName As String
Private mPhoneNumber As String
Public Property CustomerId() As Integer
Get
Return mcustomerId
End Get
Set(ByVal value As Integer)
mCustomerId = value
End Set
End Property
Public Property FirstName() As String
Get
Return mFirstName
End Get
Set(ByVal value As String)
mFirstName = value
End Set
End Property
Public Property LastName() As String
Get
Return mLastName
End Get
Set(ByVal value As String)
mLastName = value
End Set
End Property
Public Property PhoneNumber() As String
Get
Return mPhoneNumber
End Get
Set(ByVal value As String)
mPhoneNumber = value
End Set
End Property
Public Sub New(ByVal customerId As Integer, ByVal firstName As String, ByVal lastName As String, ByVal phoneNumber As String)
mCustomerId = customerId
mFirstName = firstName
mLastName = lastName
mPhoneNumber = phoneNumber
End Sub
End Class
Public Class CustomerMapper
Public Function MapToObject(ByVal dr As DataRow) As Customer
Return New Customer(Convert.ToInt32(dr("customerid")), dr("firstName"), dr("lastName"), dr("phonenumber"))
End Function
End Class
I was pondering the thought of having to write a mapper class for each class that I could potentially have in a project and started to wince a little bit. Especially where I have a class with 10 or more properties. Yikes!
Now I realize there are MANY MANY MANY object relation mappers but I wanted something lightweight and could not use LINQ TO SQL (VS 2005 project) easily.
So I came up with another way to do this using reflection, attributes and generics.
Here is the actual mapping code (The format was messed up when it was copied to my blog editor):
Public Shared Function MapDatarowToObject(Of T)(ByVal dr As DataRow) As T
If dr Is Nothing Then
Return Nothing
End If
Dim instance As T = Activator.CreateInstance(Of T)()
Dim properties() As PropertyInfo = instance.GetType().GetProperties()
If (properties.Length > 0) Then
For Each propertyObject As PropertyInfo In properties
Dim valueSet As Boolean = False
For Each attributeObject As Object In PropertyObject.GetCustomAttributes(False)
If attributeObject.GetType() Is GetType(MapperColumn) Then
Dim columnAttributeObject As MapperColumn = CType(attributeObject, MapperColumn)
If (columnAttributeObject.ColumnName <> String.Empty) Then
If dr.Table.Columns.Contains(columnAttributeObject.ColumnName) AndAlso
Not dr(columnAttributeObject.ColumnName) Is DBNull.Value Then
propertyObject.SetValue(instance, dr(columnAttributeObject.ColumnName), Nothing)
valueSet = True
End If
End If
End If
Next
If Not valueSet Then
If dr.Table.Columns.Contains(propertyObject.Name) AndAlso Not dr(propertyObject.Name) Is DBNull.Value Then
propertyObject.SetValue(instance, dr(propertyObject.Name), Nothing)
End If
End If
Next
End If
Return instance
End Function
Now I need to explain it. Let’s start with the method definition:
Public Shared Function MapDatarowToObject(Of T)(ByVal dr As DataRow) As T
I’m creating a method that needs to be called by defining a type for “T”. The only parameter is a datarow. The use is something like this:
Example #1
Dim dr As DataRow = GetDataRowFromTable(…)
Dim customer As Customer = MapDatarowToObject(Of Customer)(dr)
Next I need to create an instance of the generic type used to call the method (T). This is done by using Activator.CreateInstance():
Dim instance As T = Activator.CreateInstance(Of T)()
Using Example #1 this call will create an instance of the Customer object. This is the because the Customer class was used as the type for “T”. I need this instance to hydrate the properties on the object.
The next step is to get a list of the properties defined on T. This is done by using the instance we just created on the previous line of code and using reflection to get an array of PropertyInfo objects:
Dim properties() As PropertyInfo = instance.GetType().GetProperties()
In our case we will get back four properties: CustomerId, FirstName, LastName & PhoneNumber.
Now that we have our properties we need to iterate through them and do some mapping.
Basically how this works is if we have a column in the datarow that matches a property in the instance object (Customer) then the property value gets set. I quickly realized that sometimes the datarow column name does not match the name of a property in the instance object. The solution was to create an attribute that can be read while iterating the properties. The attribute provides a column name to map to the property. Here is the attribute class:
<AttributeUsage(AttributeTargets.Property)> _
Public Class MapperColumn
Inherits Attribute
Private mColumnName As String
Public Sub New(ByVal columnName As String)
mColumnName = columnName
End Sub
Public Property ColumnName() As String
Get
Return mColumnName
End Get
Set(ByVal value As String)
mColumnName = value
End Set
End Property
End Class
You’ll notice that I restrict the usage to only properties. This is because ONLY properties can be set by using reflection. Public fields cannot be set using reflection.
The usage of the MapperColumn attribute if easy.
<MapperColumn(“id”)> _
Public Property CustomerId
End Property
This will allow the column name “ID” from a datarow object to be mapped to the CustomerId property.
Let’s break down the properties iteration. We start by iterating the properties:
For Each propertyObject As PropertyInfo In properties
Next we check for any custom attributes on the current property:
For Each attributeObject As Object In propertyObject.GetCustomAttributes(False)
If any are found then we check to see if they are of type MapperColumn:
If attributeObject.GetType() Is GetType(MapperColumn) Then
If it is a MapperColumn then we get the instance of the MapperColumn:
Dim columnAttributeObject As MapperColumn = CType(attributeObject, MapperColumn)
Now we can use the MapperColumn attribute class to get the name of the datarow column we should be mapping to the current property. I do a little checking to make sure the ColumnName property from the MapperColumn object isn’t empty and make sure that the datarow has a column that matches the ColumnName property from the MapperColumn object. If it does, we set the value using PropertyInfo.SetValue.
If (columnAttributeObject.ColumnName <> String.Empty) Then
If dr.Table.Columns.Contains(columnAttributeObject.ColumnName) AndAlso Not dr(columnAttributeObject.ColumnName) Is DBNull.Value Then
propertyObject.SetValue(instance, dr(columnAttributeObject.ColumnName), Nothing)
valueSet = True
End If
End If
We also set the local variable “valueSet” Boolean to true. This is to prevent the code below from trying to set the value based on the name of current property. As a side note, if you do add the MapperColumn attribute to a property and the above code does not set the value, the code below will attempt to set the value based on the current property name.
If the value of the property isn’t set using the MapperColumn block of code then we will check if a column name in the datarow matches the current property name. If it does, we set the value using PropertyInfo.SetValue.
If Not valueSet Then
If dr.Table.Columns.Contains(propertyObject.Name) AndAlso Not dr(propertyObject.Name) Is DBNull.Value Then
propertyObject.SetValue(instance, dr(propertyObject.Name), Nothing)
End If
End If
The only thing left to do is return the instance of the object we created and hydrated.
And that is basically it. We now can map custom model objects to datarow objects using a lightweight method. I realize that this could be refactored to a few method but that's not the point of the post.
My next post will show you how to convert a datatable to a list of custom objects.
Until next time.
posted @ Thursday, April 24, 2008 11:04 AM