2

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:

  • Build the expression tree, destined for use with Queryable.Where, the root expression being a LambdaExpression, constructed using Expression.Lambda(expression, Expression.Parameter(GetType(type), "name"))
  • Visit the expression tree (using LinqKit), build hash table of parameters encountered
  • Subsequent parameters of the same name are substituted with the first parameter encountered of identical name

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:

  • Build the parameters as an array of ParameterExpression
  • Construct the root Lambda, using Expression.Lambda(expression, parameterArray)
  • Visit the expression tree (using LinqKit), substitute parameters encountered with parameters from parameterArray

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

Rabid
  • 2,984
  • 2
  • 25
  • 25

1 Answers1

3

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.

Ben M
  • 22,262
  • 3
  • 67
  • 71
  • Indeed, in the successful scenario that is indeed the case- the parameters used in the creation of the lambda expression are the ones used in substitution of all matching parameters elsehwere in the expression, and it compiles. On a second visit to the expression tree, I verified that the parameter expressions in the first scenario all point to the intended references (inc. the lambda's parameters): conceptually, the expression trees are the same. It's almost if the lambda expression retains an internal reference to it's original parameters. – Rabid Jul 16 '09 at 19:49
  • So, in your usage above, LinqKit's ExpressionVisitor correctly visits & replaces the lambda's parameters, and not just those referenced in its body? – Ben M Jul 16 '09 at 19:57
  • Actually- on reflection- I haven't verified that. But I shall ... and correct the behaviour by extension if that's the case, and retry. I shall report back tomorrow :) – Rabid Jul 16 '09 at 20:55
  • Infact I just checked the source ala http://www.albahari.com/nutshell/LinqKitSource.zip, and yes! You are correct! Excellent find, thanks for your input! – Rabid Jul 16 '09 at 21:00