Reputation: 3034
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:
Queryable.Where
, the root expression being a LambdaExpression
, constructed using Expression.Lambda(expression, Expression.Parameter(GetType(type), "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:
ParameterExpression
Expression.Lambda(expression, parameterArray)
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
Upvotes: 2
Views: 2539
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