Skip to content

Avoid lambda compilation as much as possible #2957

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Dec 14, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
using System;
using System.Linq.Expressions;
using System.Reflection;
using NHibernate.Impl;
using NUnit.Framework;
using Expression = System.Linq.Expressions.Expression;

namespace NHibernate.Test.Criteria.Lambda
{
public static class DateTimeExtensions
{
public static long ToLongDateTime(this DateTime date)
{
return Convert.ToInt64(date.ToString("yyyyMMddHHmmss"));
}
}

[TestFixture]
public class ExpressionProcessorFindValueFixture
{
private static object GetValue<T>(Expression<Func<T>> expression)
{
try
{
return ExpressionProcessor.FindValue(expression.Body);
}
catch (TargetInvocationException e)
{
throw e.InnerException;
}
}

private static int GetIntegerDate()
{
return Convert.ToInt32(DateTime.Now.ToString("yyyyMMdd"));
}

[Test]
public void GivenStaticPropertyInstanceMethodCall()
{
var actual = GetValue(() => DateTime.Now.ToString("yyyyMMdd"));
var expected = DateTime.Now.ToString("yyyyMMdd");

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenStaticPropertyInstanceMultipleMethodCall()
{
var actual = GetValue(() => DateTime.Now.AddDays(365).ToString("yyyyMMdd"));
var expected = DateTime.Now.AddDays(365).ToString("yyyyMMdd");

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenStaticPropertyInstanceMethodCallThenCast()
{
var actual = GetValue(() => Convert.ToInt32(DateTime.Now.AddDays(365).ToString("yyyyMMdd")));
var expected = Convert.ToInt32(DateTime.Now.AddDays(365).ToString("yyyyMMdd"));

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenStaticMethodCall()
{
var actual = GetValue(() => GetIntegerDate());
var expected = GetIntegerDate();

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenExtensionMethodCall()
{
var date = DateTime.Now;
var actual = GetValue(() => date.ToLongDateTime());
var expected = date.ToLongDateTime();

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenNestedPropertyAccess()
{
var animal = new { Snake = new { Animal = new { Name = "Scorpion" } } };
var actual = GetValue(() => animal.Snake.Animal.Name);
var expected = animal.Snake.Animal.Name;

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenGuidToStringCast()
{
var guid = Guid.NewGuid();
Expression<Func<string>> expression = () => $"{guid}";
var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();
var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenNestedPropertyToIntegerCast()
{
var animal = new { Snake = new { Animal = new { Weight = 9.89 } } };
Expression<Func<int>> expression = () => (int) animal.Snake.Animal.Weight;
var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();
var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenNestedPropertyToIntegerConvert()
{
var animal = new { Snake = new { Animal = new { Weight = 9.89 } } };
Expression<Func<int>> expression = () => Convert.ToInt32(animal.Snake.Animal.Weight);
var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();
var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);
}

[Test]
public void GivenNullToIntegerCastFails()
{
object value = null;
Expression<Func<int>> expression = () => (int) value;

Assert.Throws<NullReferenceException>(() => GetValue(expression));

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

Assert.Throws<NullReferenceException>(() => ((dynamic) lambdaExpression.DynamicInvoke()).Invoke());
}

[Test]
public void GivenNullableIntegerToIntegerCastFails()
{
int? value = null;
Expression<Func<int>> expression = () => (int) value;

Assert.Throws<InvalidOperationException>(() => GetValue(expression));

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

Assert.Throws<InvalidOperationException>(() => ((dynamic) lambdaExpression.DynamicInvoke()).Invoke());

}

[Test]
public void GivenIntegerToNullableIntegerCast()
{
int value = 12345;
Expression<Func<int?>> expression = () => value;

var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);

}

[Test]
public void GivenInt16ToIntegerCast()
{
short value = 12345;
Expression<Func<int>> expression = () => value;

var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);

}

[Test]
public void GivenNullableDecimalToDecimalCast()
{
decimal? value = 9.89m;
Expression<Func<decimal>> expression = () => (decimal) value;

var actual = GetValue(expression);

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();
var expected = ((dynamic) lambdaExpression.DynamicInvoke()).Invoke();

Assert.AreEqual(expected, actual);

}

[Test]
public void GivenStringToIntegerCastFails()
{
object value = "Abatay";
Expression<Func<int>> expression = () => (int) value;

Assert.Throws<InvalidCastException>(() => GetValue(expression));

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

Assert.Throws<InvalidCastException>(() => ((dynamic) lambdaExpression.DynamicInvoke()).Invoke());
}

[Test]
public void GivenBooleanToCharCastFails()
{
object isTrue = true;
Expression<Func<char>> expression = () => (char) isTrue;

Assert.Throws<InvalidCastException>(() => GetValue(expression));

//Check with expression compile and invoke
var lambdaExpression = Expression.Lambda(expression).Compile();

Assert.Throws<InvalidCastException>(() => ((dynamic) lambdaExpression.DynamicInvoke()).Invoke());
}
}
}
68 changes: 52 additions & 16 deletions src/NHibernate/Impl/ExpressionProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,32 +248,68 @@ private static ICriterion Le(ProjectionInfo property, object value)
}

/// <summary>
/// Invoke the expression to extract its runtime value
/// Walk or Invoke expression to extract its runtime value
/// </summary>
public static object FindValue(Expression expression)
{
if (expression.NodeType == ExpressionType.Constant)
return ((ConstantExpression) expression).Value;

if (expression.NodeType == ExpressionType.MemberAccess)
object value;
switch (expression.NodeType)
{
var memberAccess = (MemberExpression) expression;
if (memberAccess.Expression == null || memberAccess.Expression.NodeType == ExpressionType.Constant)
{
var constantValue = ((ConstantExpression) memberAccess.Expression)?.Value;
var member = memberAccess.Member;
switch (member.MemberType)
case ExpressionType.Constant:
var constantExpression = (ConstantExpression) expression;
return constantExpression.Value;
case ExpressionType.MemberAccess:
var memberExpression = (MemberExpression) expression;
value = memberExpression.Expression != null ? FindValue(memberExpression.Expression) : null;

switch (memberExpression.Member.MemberType)
{
case MemberTypes.Field:
return ((FieldInfo) member).GetValue(constantValue);
return ((FieldInfo) memberExpression.Member).GetValue(value);
case MemberTypes.Property:
return ((PropertyInfo) member).GetValue(constantValue);
return ((PropertyInfo) memberExpression.Member).GetValue(value);
}
}
break;
case ExpressionType.Call:
var methodCallExpression = (MethodCallExpression) expression;
var args = new object[methodCallExpression.Arguments.Count];
for (int i = 0; i < args.Length; i++)
args[i] = FindValue(methodCallExpression.Arguments[i]);

if (methodCallExpression.Object == null) //extension or static method
{
return methodCallExpression.Method.Invoke(null, args);
}
else
{
var callingObject = FindValue(methodCallExpression.Object);

return methodCallExpression.Method.Invoke(callingObject, args);
}
case ExpressionType.Convert:
var unaryExpression = (UnaryExpression) expression;
if ((Nullable.GetUnderlyingType(unaryExpression.Type) ?? unaryExpression.Type) == unaryExpression.Operand.Type
|| unaryExpression.Type == typeof(object))
{
return FindValue(unaryExpression.Operand);
}
else if (unaryExpression.Method != null && unaryExpression.Type != unaryExpression.Operand.Type)
{
return unaryExpression.Method.Invoke(null, new[] { FindValue(unaryExpression.Operand) });
}
else if (unaryExpression.Type == (Nullable.GetUnderlyingType(unaryExpression.Operand.Type) ?? unaryExpression.Operand.Type))
{
value = FindValue(unaryExpression.Operand);
if (value != null || Nullable.GetUnderlyingType(unaryExpression.Type) != null)
{
return value;
}
}
break;
}

var valueExpression = Expression.Lambda(expression).Compile();
object value = valueExpression.DynamicInvoke();
var lambdaExpression = Expression.Lambda(expression).Compile(true);
value = lambdaExpression.DynamicInvoke();
return value;
}

Expand Down
13 changes: 13 additions & 0 deletions src/NHibernate/Impl/LambdaExpressionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Linq.Expressions;

namespace NHibernate.Impl
{
#if NET461
internal static class LambdaExpressionExtensions
{
public static Delegate Compile(this LambdaExpression expression, bool preferInterpretation) =>
expression.Compile(); //Concurrent Compile() call causes "Garbage Collector" suspend all threads too often
}
#endif
}
9 changes: 4 additions & 5 deletions src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Linq.Expressions;
using System.Reflection;
using NHibernate.Engine;
using NHibernate.Impl;
using NHibernate.Linq.Functions;
using NHibernate.Param;
using NHibernate.Type;
Expand Down Expand Up @@ -150,12 +151,10 @@ protected override Expression VisitUnary(UnaryExpression node)
node.Method != null && // The implicit/explicit operator method
node.Operand is ConstantExpression constantExpression)
{
// Instead of getting constantExpression.Value, we override the value by compiling and executing this subtree,
// performing the cast.
var lambda = Expression.Lambda<Func<object>>(Expression.Convert(node, typeof(object)));
var compiledLambda = lambda.Compile();
// Instead of getting constantExpression.Value, invoke method
var value = node.Method.Invoke(null, new[] { constantExpression.Value });

AddConstantExpressionParameter(constantExpression, compiledLambda());
AddConstantExpressionParameter(constantExpression, value);
}

return base.VisitUnary(node);
Expand Down