Question Create Property Setter Delegate via DynamicMethod

Joined
May 28, 2010
Messages
5
Programming Experience
5-10
I'm building an abstract class that handles some common operations for all of my data access objects. The class will utilize generics, so I can't make direct calls to constructors, business object properties, etc. Reflection is way too slow for what I want to accomplish, so I am using a DynamicMethod, ILGenerator, and Delegate caching instead (sample will follow).

The method I'm currently working on is a Private Shared method that populates a list of business objects using records from a data table. I'm having a problem building the property setter via IL. I'm getting an object reference error, but I'm not sure how to fix it. I believe the error comes from the "target" being Nothing, but the parameter I'm passing as the target is not Nothing at the time of invocation. Here are some snippets of what I've got so far:

VB.NET:
'class declaration -- TList is strongly-typed list of T
Public MustInherit Class DataAccessBase(Of T, TList)
...
'delegate declaration 
Delegate Sub GenericSetter(ByVal target As Object, ByVal value As Object)
...
'method to obtain delegate instance to be stored in cache
Private Shared Function CreateSetMethod(ByVal pInfo As PropertyInfo) As GenericSetter
    Dim setDelegate As GenericSetter = Nothing

    Try
        'get the set method from the property info
        Dim setmethod As MethodInfo = pInfo.GetSetMethod

        'if no setter, return nothing
        If setmethod Is Nothing Then
            Return Nothing
        End If

        'create the dynamic method
        Dim args As Type() = New Type() {GetType(Object), GetType(Object)}
        Dim setter As New DynamicMethod("_set" + pInfo.Name, Nothing, args, pInfo.DeclaringType)
        Dim ILGen As ILGenerator = setter.GetILGenerator

        ILGen.Emit(OpCodes.Ldarg_0)
        ILGen.Emit(OpCodes.Castclass, pInfo.DeclaringType)
        ILGen.Emit(OpCodes.Ldarg_1)

        If pInfo.PropertyType.IsClass Then
            ILGen.Emit(OpCodes.Castclass, pInfo.PropertyType)
        Else
            ILGen.Emit(OpCodes.Unbox_Any, pInfo.PropertyType)
        End If

        ILGen.EmitCall(OpCodes.Callvirt, setter, Nothing)
        ILGen.Emit(OpCodes.Ret)

        'create the delegate and return it                
        setDelegate = CType(setter.CreateDelegate(GetType(GenericSetter)), GenericSetter)

    Catch ex As Exception
        Throw New Exception(ex.ToString)
    End Try

    Return setDelegate
End Function
...
'method to populate the business object using the cache -- this is where
'the exception is thrown - within the contents of the IF statement and I
'have made sure that obj and SetterCache.Item(pInfo) is not nothing
Protected Shared Sub PopulateObject(ByRef obj As T, ByVal r As DataRow)
    Try
        For Each pInfo As PropertyInfo In obj.GetType.GetProperties
            If r.Table.Columns.Contains(pInfo.Name) AndAlso Not IsDBNull(r(pInfo.Name)) Then
                'EXCEPTION THROWN HERE...
                CType(SetterCache.Item(pInfo), GenericSetter)(obj, r(pInfo.Name))
            End If
        Next
    Catch ex As Exception
        Throw New Exception(ex.ToString)
    End Try
End Sub

I'm assuming the obj is not being passed correctly as the target, thus causing the target to be Nothing. Please advise.
 
Fixed it, but new problem now...

I found the offending line:
VB.NET:
ILGen.EmitCall(OpCodes.Callvirt, [COLOR="Red"]setter[/COLOR], Nothing)
It should have been:
VB.NET:
ILGen.EmitCall(OpCodes.Callvirt, [COLOR="YellowGreen"]setMethod[/COLOR], Nothing)

My new problem is that as it is setting the value of a property that return an enum, I am getting a "Specified cast is not valid" exception. The type of the value being passed in is Short and the Property return type is PeriodTypes. I tried the following in the Immediate Window:
VB.NET:
CType(r(pInfo.Name), PeriodTypes)

It returns Day{1} without exception. Any ideas?
 
You're moving into conversions, not casting. CType attempts conversion when cast is not possible, try DirectCast from a number to enum and you'll see the cast is not possible (with different underlying types). Your enum type is here the default Integer while the datarow column is Short, and this is the conversion that need to take place before the cast to the enum type is possible.

You could ignore conversions and, either change the underlying enum type to Short (Enum PeriodTypes As Short), or provide correctly typed data from the datarow (define Integer for column type instead of Short) - in both cases the datarow value will be cast to the enum type without problem with the existing code.

If you decide to include conversions also there is Convert.ChangeType available, but it will not work directly from the box with enums for some reason. It would however convert values such as Integer to String etc. For enums I found Enum.GetUnderlyingType to do the trick.

Why do you need to CType SetterCache values to type GenericSetter? You should probably use a Dictionary(Of PropertyInfo, GenericSetter) here.

Here is a sample including conversions for PopulateObject:
VB.NET:
Dim o As Object = r(pInfo.Name)
Dim pType As Type = pInfo.PropertyType
If Not pType.IsAssignableFrom(o.GetType) Then
    If pType.IsEnum Then
        pType = System.Enum.GetUnderlyingType(pType)
    End If
    o = Convert.ChangeType(o, pType)
End If
SetterCache(pInfo)(obj, o)
A note about the conversions, if this is something you would include (given the extreme need for speed excluding regular reflection SetValue calls). ChangeType utilizes IConvertible interface, which for the most part is used for converting between base types. If you need to expand this you should add support for type converters. TypeDescriptor.GetConverter can get you a TypeConverter for both source and target type, either which may support conversion to or from the other, this can be checked with CanConvertTo/-From methods before doing ConvertTo/-From.
 
Thanks, John. Your answer sheds light on the problem. Before reading your post, I found System.Enum.ToObject does the conversion I need. However, it caused me to introduce an IF check within the loop, which is not desirable. The smallint datatype from the database is more accurate than the Integer datatype used by default in my enumeration. I really should start specifying the correct datatype on my enumerations, as most of them have values less than 20.

Also, you are correct about not needing the CType for the GenericSetter. I either copied a similar piece of code from somewhere or I had a case of cerebrum flatulence.

Thanks again for your help.
 
Given the performance consideration you should stick with the default Enum type as Integer; Numeric Data Types
 
Back
Top