Rabid
Rabid

Reputation: 3034

Compiling a Delegate with Expression.Lambda() - Parameter Out Of Scope, but is it really?

I came across an interesting problem today whilst implementing a feature into a dynamic expression building library. More specifically, but irrelevantly, a feature to define operator precedence in an expression.

When the LINQ engine was compiling the final expression, I was encountering a InvalidOperationException declaring Lambda parameter out of scope.

The problem manifests itself after assigning the relevant ParameterExpression objects.

Working with a complete and well formed lambda expression tree, I discovered that reassigning the Lambda's ParameterExpression objects to valid references was invalid when compiling the Lambda.

This is a short description of the behaviour I initially employed before I applied a fix:

The result was an expression tree whereby all the ParameterExpression references of the same name were all pointing to the same object- but the InvalidOperationException was encountered when compiling.

The fix I applied employed the following behaviour:

The end result compiles fine, even though the Lambda expression structure is conceptually the same as the output from the former behaviour.

The question is: Why does the first fail, and the second succeed?

Below is a test fixture class to reproduce (excuse the vb), with the test cases and a couple of supporting classes (depends on nUnit, LinqKit):

note: TestFixture & Test attribute declarations are missing- how to do in markdown???



Imports LinqKit
Imports NUnit.Framework
Imports System.Linq.Expressions

 _
Public Class ParameterOutOfScopeTests

    Public Class TestObject
        Public Name As String
        Public DateOfBirth As DateTime = DateTime.Now
        Public DateOfDeath As DateTime?
    End Class

    Public Class ParameterNormalisation
        Inherits ExpressionVisitor

        Public Sub New(ByVal expression As Expression)
            _expression = expression
        End Sub

        Private _expression As expression
        Private _parameter As ParameterExpression
        Private _name As String

        Public Function Normalise(ByVal parameter As ParameterExpression) As Expression
            _parameter = parameter
            _name = parameter.Name
            _expression = Me.Visit(_expression)
            Return _expression
        End Function

        Public Function Normalise(ByVal name As String) As Expression
            _name = name
            _expression = Me.Visit(_expression)
            Return _expression
        End Function

        Protected Overrides Function VisitParameter(ByVal p As System.Linq.Expressions.ParameterExpression) As System.Linq.Expressions.Expression

            Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Parameter visited: " & p.Name & " " & p.GetHashCode)
            If p.Name.Equals(_name) Then

                If _parameter Is Nothing Then
                    _parameter = p
                    Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Primary parameter identified: " & p.GetHashCode)
                ElseIf Not p Is _parameter Then
                    Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Secondary parameter substituted: " & p.GetHashCode & " with " & _parameter.GetHashCode)
                    Return MyBase.VisitParameter(_parameter)
                Else
                    Debug.WriteLine("ClientExpressionParameterNormalisation.VisitParameter:: Parameter already common: " & p.GetHashCode & " with " & _parameter.GetHashCode)
                End If

            End If

            Return MyBase.VisitParameter(p)

        End Function


    End Class

     _
    Public Sub Lambda_Parameter_Out_Of_Scope_As_Expected()

        Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
        Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue

        Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)

        Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")

        Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)
        Dim delegateOne As [Delegate] = lambdaOne.Compile

    End Sub

     _
    Public Sub Lambda_Compiles()

        Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
        Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue

        Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)

        Dim normaliser As New ParameterNormalisation(treeThree)
        Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")
        treeThree = normaliser.Normalise(realParameter)

        Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)
        Dim delegateOne As [Delegate] = lambdaOne.Compile

    End Sub

     _
    Public Sub Lambda_Fails_But_Is__Conceptually__Sound()

        Dim treeOne As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) test.DateOfBirth > Now And test.Name.Contains("name")
        Dim treeTwo As Expression(Of Func(Of TestObject, Boolean)) = Function(test As TestObject) Not test.DateOfDeath.HasValue

        Dim treeThree As Expression = Expression.And(treeOne.Body, treeTwo.Body)

        Dim realParameter As ParameterExpression = Expression.Parameter(GetType(TestObject), "test")
        Dim lambdaOne As LambdaExpression = Expression.Lambda(treeThree, realParameter)

        Dim normaliser As New ParameterNormalisation(lambdaOne)
        lambdaOne = DirectCast(normaliser.Normalise("test"), LambdaExpression)

        Dim delegateOne As [Delegate] = lambdaOne.Compile

    End Sub

End Class

Upvotes: 2

Views: 2539

Answers (1)

Ben M
Ben M

Reputation: 22492

AFAIK expression trees don't consider two ParameterExpression objects created with identical arguments as "the same parameter".

Without having tested your code, then, that's what sticks out: as I read the first (failing) scenario, you replace all same-named parameters with the first such encountered, but that first encountered parameter is not the same ParameterExpression object as the one you create in your call to Expression.Lambda(). In the second (succeeding) scenario, it is.

EDITED I should add that I haven't used LinqKit's ExpressionVisitor, but as far as I'm aware it's based on code that I have used, in which VisitLambda is not very robust:

    protected virtual Expression VisitLambda(LambdaExpression lambda)
    {
        Expression body = this.Visit(lambda.Body);
        if (body != lambda.Body)
        {
            return Expression.Lambda(lambda.Type, body, lambda.Parameters);
        }
        return lambda;
    }

Note that the body of the expression is visited, but not its parameters. If LinqKit hasn't improved this, that would be the point of failure.

Upvotes: 3

Related Questions