There is a very nice function in the new ASP.NET MVC code that allows a Controller to load an object from values passed in from a HttpRequest (UPDATE: called UpdateFrom, see ScottGu's post). It did a best guess on matching values to object properties, but most (99%?) of the time that is all you need. Well, when I started using Domain-Driven Design (DDD) and Test-Driven Development (TDD) on the latest additions to our Community Manager.NET product I wanted to find something similar - but I couldn't find anything as terse and useful.
So I did what any good dev would, I read the sourcecode and derived my own functions to do something similar, but with the SqlDataReaders that my repositories would be getting out of SQL Server (it almost feels like the ActiveRecord pattern ...). Below are the result, two LoadToObject() functions (and a PopulateTypeException class) in my BaseRepository class that give me the functionality I need. I'm putting these out there in case someone else needs this, and in the hope that any glaringly obvious errors might get picked up by others!
One issue I had with the Microsoft version of this function was they only load public properties of an object, not public fields. Now I know that in C# you can setup properties as easily as fields, but in VB it's still a hassle. I personally like using fields for simple value storing because it is clearer to see what they do in the code and leave more space for real method calls.
The final problem I ran into was that my custom Html data type needed to implement it's own TypeConverter so that the HTML in the SQL Server ntext field could be converted to a strongly typed Html type. But that can go into another blog post if people are interested.
''' <summary>
''' Populates an object's public fields and properties with values from a SqlDataReader.
''' </summary>
''' <param name="readerToLoad">The SqlDataReader that needs to be loaded into the object.</param>
''' <param name="obj">The object we want to populate.</param>
''' <param name="objectPrefix">A prefix to the object field/property names. Tries to match using "." and "_".</param>
''' <returns>The object passed in with fields/properties loaded.</returns>
''' <remarks>
''' Does a best guess as to how to match names from the SqlDataReader to the object.
''' </remarks>
Protected Function LoadToObject(ByVal readerToLoad As SqlDataReader, ByVal obj As Object, ByVal objectPrefix As String) As Object
Dim newColl As New System.Collections.Specialized.NameValueCollection()
Dim x As Integer
If obj Is Nothing Then
Throw New ArgumentNullException("obj", "Object must have a value in order to be loaded to.")
End If
For x = 0 To (readerToLoad.FieldCount - 1)
newColl.Add(readerToLoad.GetName(x), readerToLoad.GetValue(x))
Next
Return LoadToObject(newColl, obj, objectPrefix)
End Function
''' <summary>
''' Populates an object's public fields and properties with values from a NameValueCollection.
''' </summary>
''' <param name="valueCollectionToLoad">The NameValueCollection that needs to be loaded into the object.</param>
''' <param name="obj">The object we want to populate.</param>
''' <param name="objectPrefix">A prefix to the object field/property names. Tries to match using "." and "_".</param>
''' <returns>The object passed in with fields/properties loaded.</returns>
''' <remarks>
''' Does a best guess as to how to match names from the NameValueCollection to the object.
''' </remarks>
Protected Function LoadToObject(ByVal valueCollectionToLoad As System.Collections.Specialized.NameValueCollection, ByVal obj As Object, ByVal objectPrefix As String) As Object
Dim objType As Type = obj.GetType()
Dim objName As String = objType.Name
Dim exceptionList As New StringBuilder()
Dim props As PropertyInfo() = objType.GetProperties()
Dim fields As FieldInfo() = objType.GetFields()
Dim ex As PopulateTypeException = Nothing
Dim prop As PropertyInfo
Dim field As FieldInfo
' try writing to the object's properties
For Each prop In props
'check the key, going to be forgiving here, allowing for full declaration or just propname
Dim key As String = prop.Name
If objectPrefix <> String.Empty Then
key = objectPrefix + key
End If
If valueCollectionToLoad(key) = Nothing Then
key = objName + "." + prop.Name
End If
If valueCollectionToLoad(key) = Nothing Then
key = objName + "_" + prop.Name
End If
If valueCollectionToLoad(key) <> Nothing Then
Dim conv As TypeConverter = TypeDescriptor.GetConverter(prop.PropertyType)
Dim value As Object = valueCollectionToLoad(key)
If conv.CanConvertFrom(System.Type.GetType("System.String", True, True)) Then
Try
value = conv.ConvertFrom(valueCollectionToLoad(key))
prop.SetValue(obj, value, Nothing)
Catch e As Exception
Dim message As String = prop.Name + " is not a valid " + prop.PropertyType.Name + "; " + e.Message
If ex Is Nothing Then
ex = New PopulateTypeException("Errors occurred during object binding - review the LoadExceptions property of this exception for more details")
End If
Dim info As New PopulateTypeException.ExceptionInfo()
info.AttemptedValue = value
info.PropertyName = prop.Name
info.ErrorMessage = message
ex.LoadExceptions.Add(info)
End Try
Else
Throw New Exception(String.Format("No type converter available for type: {0}", prop.PropertyType))
End If
End If
Next
' now try writing to the object's public fields
For Each field In fields
'check the key, going to be forgiving here, allowing for full declaration or just fieldname
Dim key As String = field.Name
If objectPrefix <> String.Empty Then
key = objectPrefix + key
End If
If valueCollectionToLoad(key) = Nothing Then
key = objName + "." + field.Name
End If
If valueCollectionToLoad(key) = Nothing Then
key = objName + "_" + field.Name
End If
If valueCollectionToLoad(key) <> Nothing Then
Dim conv As TypeConverter = TypeDescriptor.GetConverter(field.FieldType)
Dim value As Object = valueCollectionToLoad(key)
If conv.CanConvertFrom(System.Type.GetType("System.String", True, True)) Then
Try
value = conv.ConvertFrom(valueCollectionToLoad(key))
field.SetValue(obj, value)
Catch e As Exception
Dim message As String = field.Name + " is not a valid " + field.FieldType.Name + "; " + e.Message
If ex Is Nothing Then
ex = New PopulateTypeException("Errors occurred during object binding - review the LoadExceptions property of this exception for more details")
End If
Dim info As New PopulateTypeException.ExceptionInfo()
info.AttemptedValue = value
info.PropertyName = field.Name
info.ErrorMessage = message
ex.LoadExceptions.Add(info)
End Try
Else
Throw New Exception(String.Format("No type converter available for type: {0}", field.FieldType))
End If
End If
Next
If Not (ex Is Nothing) Then
Throw ex
Else
Return obj
End If
End Function
''' <summary>
''' Contains exceptions that arise during the populating of an object by the LoadToObject() method.
''' </summary>
<Global.System.Serializable()> Public Class PopulateTypeException
Inherits Exception
Public Class ExceptionInfo
Public PropertyName As String
Public AttemptedValue As Object
Public ErrorMessage As String
End Class
Public LoadExceptions As New List(Of ExceptionInfo)
Private PopulateTypeException()
Public Sub New(ByVal message As String)
MyBase.new(message)
End Sub
Public Sub New(ByVal message As String, ByVal inner As Exception)
MyBase.new(message, inner)
End Sub
Protected Sub New(ByVal info As System.Runtime.Serialization.SerializationInfo, ByVal context As System.Runtime.Serialization.StreamingContext)
MyBase.New(info, context)
End Sub
End Class
No comments:
Post a Comment