0

I know how to fix a nullrefence but in this case it's finding/fixing it in an expression tree.

I'm not familiar enough with expression tree to do it myself so can someone educate me with this one?

This code will work with the first property(Prop1) but not the second(Prop4)

Option Strict On
Option Explicit On

Imports System.Linq.Expressions
Imports System.Reflection
Imports System.Runtime.CompilerServices

Module Module1
    Const loopRun As Integer = 25000
    Const benchRun As Integer = 5

    Private myObj As New Obj With {.Prop1 = "hello",
                                   .Prop4 = 123,
                                   .Prop7 = Now,
                                   .Prop10 = Obj.test.value2}

    Sub Main()
        DisplayValue()

        Console.Read()
    End Sub

    Private Sub DisplayValue()
        Dim value As Object

        For Each i In Cache.expressionGetDict
            value = i.Value(myObj)
            Console.WriteLine("Original expressionGetDict.{0}={1}", i.Key, i.Value(myObj))

            Cache.expressionSetDict(i.Key)(myObj, Nothing) ''on Prop4, null reference

            Console.WriteLine("Cleared expressionGetDict.{0}={1}", i.Key, i.Value(myObj))
            Cache.expressionSetDict(i.Key)(myObj, value)
            Console.WriteLine("Old expressionGetDict.{0}={1}", i.Key, i.Value(myObj))
            Console.WriteLine()
        Next
    End Sub

End Module

Public Class Obj

    Public Enum test As Byte
        value1 = 10
        value2 = 50
        value3 = 250
    End Enum

    Public Property Prop1 As String
    Public Property Prop4 As Integer
    Public Property Prop7 As DateTime
    Public Property Prop10 As test
End Class


Public Module Cache
    Public ReadOnly expressionGetDict As New Dictionary(Of String, Func(Of Object, Object))
    Public ReadOnly expressionSetDict As New Dictionary(Of String, Action(Of Object, Object))

    Sub New()
        For Each p In GetType(Obj).GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
            expressionGetDict.Add(p.Name, p.GetValueGetter)
            expressionSetDict.Add(p.Name, p.GetValueSetter)
        Next
    End Sub

End Module

Public Module PropertyInfoExtensions
    <Extension> _
    Public Function GetValueGetter(propertyInfo As PropertyInfo) As Func(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")

        Dim instanceCast As UnaryExpression = If(Not propertyInfo.DeclaringType.IsValueType, Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim getterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetGetMethod())

        Dim convert As UnaryExpression = Expression.TypeAs(getterCall, GetType(Object))

        Dim lambda As Expression(Of Func(Of Object, Object)) = Expression.Lambda(Of Func(Of Object, Object))(convert, instance)

        Return lambda.Compile
    End Function

    <Extension> _
    Public Function GetValueSetter(propertyInfo As PropertyInfo) As Action(Of Object, Object)
        Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")
        Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")

        Dim instanceCast As UnaryExpression = If((Not propertyInfo.DeclaringType.IsValueType), Expression.TypeAs(instance, propertyInfo.DeclaringType), Expression.Convert(instance, propertyInfo.DeclaringType))

        Dim valueCast As UnaryExpression = If((Not propertyInfo.PropertyType.IsValueType), Expression.TypeAs(value, propertyInfo.PropertyType), Expression.Convert(value, propertyInfo.PropertyType))

        Dim setterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetSetMethod(), valueCast)

        Dim lambda As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(setterCall, instance, value)

        Return lambda.Compile()
    End Function

End Module

using the answer from Shlomo, I created a working solution for .net 3.5

Public Function GetValueSetter(propertyInfo As PropertyInfo) As Action(Of Object, Object)
    Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")
    Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")
    Dim nullCheckedValue = Expression.Condition(
            Expression.Equal(value, Expression.Constant(Nothing, GetType(Object))),
            Expression.Convert(GetDefaultExpression(propertyInfo.PropertyType), GetType(Object)),
            value
        )

    Dim instanceCast As UnaryExpression = Expression.Convert(instance, propertyInfo.DeclaringType)

    Dim valueCast As UnaryExpression = Expression.Convert(nullCheckedValue, propertyInfo.PropertyType)

    Dim setterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetSetMethod(), valueCast)

    Dim lambda As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(setterCall, instance, value)

    Return lambda.Compile
End Function

Private Function GetDefaultExpression(type As Type) As Expression
    If type.IsValueType Then
        Return Expression.Constant(Activator.CreateInstance(type), GetType(Object))
    End If
    Return Expression.Constant(Nothing, GetType(Object))
End Function
Community
  • 1
  • 1
Fredou
  • 18,946
  • 9
  • 53
  • 107

1 Answers1

1

As best I can tell, you have a couple things going weird here.

  • This line in the GetValueGetter Dim convert As UnaryExpression = Expression.TypeAs(getterCall, GetType(Object)) Should be an Expression.Convert, not a TypeAs. TypeAs works only with reference types, and three of your four properties are value types. However, that doesn't appear to be your current error.

  • Similarly, VB.NET's Nothing is tripping you up. VB.NET compiles Nothing at compile time. Since your dynamically generated functions are of type Object, so the Nothing assignment tries to assign the Object's Nothing (which is the null reference) to Prop4. Since Prop4 is a value type, you get the null reference exception. You want Integer's Nothing assigned to Prop4.

With the following modifications, I got the code to work:

Set module cache like this:

Public Module Cache
    Public ReadOnly expressionGetDict As New Dictionary(Of String, Func(Of Object, Object))
    Public ReadOnly expressionSetDict As New Dictionary(Of String, Action(Of Object, Object))
    Public ReadOnly propertyTypeDict As New Dictionary(Of String, Type)

    Sub New()
        For Each p In GetType(Obj).GetProperties(BindingFlags.Instance Or BindingFlags.[Public])
            expressionGetDict.Add(p.Name, p.GetValueGetter.Compile())
            expressionSetDict.Add(p.Name, p.GetValueSetter.Compile())
            propertyTypeDict(p.Name) = p.PropertyType
        Next
    End Sub

End Module

replaced Cache.expressionSetDict(i.Key)(myObj, Nothing) (in DisplayValue as so:

Dim propertyType = Cache.propertyTypeDict(i.Key)
Dim typedNothing = CTypeDynamic(Nothing, propertyType)
Cache.expressionSetDict(i.Key)(myObj, typedNothing) 'on Prop4,  no longer a null reference exception

Edit:

The problem is also solvable in the expression-build. Instead of doing the above, you can modify GetValueSetter accordingly:

Public Function GetValueSetter(propertyInfo As PropertyInfo) As Expression(Of Action(Of Object, Object))
    Dim instance As ParameterExpression = Expression.Parameter(GetType(Object), "instance")
    Dim value As ParameterExpression = Expression.Parameter(GetType(Object), "value")
    Dim nullCheckedValue = Expression.Condition(
            Expression.ReferenceEqual(value, Expression.Default(GetType(Object))),
            Expression.Convert(Expression.Constant(CTypeDynamic(Nothing, propertyInfo.PropertyType)), GetType(Object)),
            value
        )

    Dim instanceCast As UnaryExpression = Expression.Convert(instance, propertyInfo.DeclaringType)

    Dim valueCast As UnaryExpression = Expression.Convert(nullCheckedValue, propertyInfo.PropertyType)

    Dim setterCall As MethodCallExpression = Expression.[Call](instanceCast, propertyInfo.GetSetMethod(), valueCast)

    Dim lambda As Expression(Of Action(Of Object, Object)) = Expression.Lambda(Of Action(Of Object, Object))(setterCall, instance, value)

    Return lambda
End Function

This second solution embeds a null-check in the expression-generated function, and replaces a null value with default(T) as C-Sharpists would say. In VB-parlance I guess you would say you're replacing the wrong Nothing with the right Nothing.

Shlomo
  • 13,064
  • 3
  • 26
  • 40
  • is there a way to change the expression tree to handle the null instead of modifying the code that call it? – Fredou Nov 09 '15 at 19:09
  • i moved it into a working solution for .net 3.5, seem to work – Fredou Nov 10 '15 at 00:58
  • I created a code review question if you are interested to jump in it http://codereview.stackexchange.com/questions/110364/calling-getter-or-setter-with-expression-tree – Fredou Nov 10 '15 at 15:45