diff --git a/src/NHibernate.Test/Criteria/Lambda/ExpressionProcessorFindValueFixture.cs b/src/NHibernate.Test/Criteria/Lambda/ExpressionProcessorFindValueFixture.cs new file mode 100644 index 00000000000..d3ee7736433 --- /dev/null +++ b/src/NHibernate.Test/Criteria/Lambda/ExpressionProcessorFindValueFixture.cs @@ -0,0 +1,404 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; +using NHibernate.Impl; +using NUnit.Framework; + +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(Expression> 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 StaticPropertyInstanceMethodCall() + { + var actual = GetValue(() => DateTime.Now.ToString("yyyyMMdd")); + var expected = DateTime.Now.ToString("yyyyMMdd"); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void StaticPropertyInstanceMultipleMethodCall() + { + var actual = GetValue(() => DateTime.Now.AddDays(365).ToString("yyyyMMdd")); + var expected = DateTime.Now.AddDays(365).ToString("yyyyMMdd"); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void StaticPropertyInstanceMethodCallThenCast() + { + 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 StaticMethodCall() + { + var actual = GetValue(() => GetIntegerDate()); + var expected = GetIntegerDate(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void ExtensionMethodCall() + { + var date = DateTime.Now; + var actual = GetValue(() => date.ToLongDateTime()); + var expected = date.ToLongDateTime(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NestedPropertyAccess() + { + 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 GuidToStringCast() + { + var guid = Guid.NewGuid(); + Expression> expression = () => $"{guid}"; + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NestedPropertyToIntegerCast() + { + var animal = new { Snake = new { Animal = new { Weight = 9.89 } } }; + Expression> expression = () => (int) animal.Snake.Animal.Weight; + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NestedPropertyToIntegerConvert() + { + var animal = new { Snake = new { Animal = new { Weight = 9.89 } } }; + Expression> expression = () => Convert.ToInt32(animal.Snake.Animal.Weight); + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullToIntegerCastFails() + { + object value = null; + Expression> expression = () => (int) value; + + Assert.Throws(() => GetValue(expression)); + + //Check with expression compile and invoke + Assert.Throws(() => expression.Compile().Invoke()); + } + + [Test] + public void NullableIntegerToIntegerCastFails() + { + int? value = null; + Expression> expression = () => (int) value; + + Assert.Throws(() => GetValue(expression)); + + //Check with expression compile and invoke + Assert.Throws(() => expression.Compile().Invoke()); + } + + [Test] + public void NullableIntegerToIntegerCast() + { + int? value = 1; + Expression> expression = () => (int) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullableIntegerToNullableLongImplicitCast() + { + int? value = 1; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void IntegerToIntegerCast() + { + int value = 1; + Expression> expression = () => (int) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void IntegerToNullableIntegerImplicitCast() + { + int value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void IntegerToNullableLongImplicitCast() + { + int value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void IntegerToNullableDecimalImplicitCast() + { + int value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void IntegerToObjectImplicitCast() + { + int value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullableIntegerToObjectImplicitCast() + { + int? value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullableNullIntegerToObjectImplicitCast() + { + int? value = null; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void ObjectToIntegerCast() + { + object value = 12345; + Expression> expression = () => (int) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void ObjectToNullableIntegerCast() + { + object value = 12345; + Expression> expression = () => (int?) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullObjectToNullableIntegerCast() + { + object value = null; + Expression> expression = () => (int?) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void Int16ToIntegerImplicitCast() + { + short value = 12345; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void NullableDecimalToDecimalCast() + { + decimal? value = 9.89m; + Expression> expression = () => (decimal) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void StringToObjectImplicitCast() + { + string value = "Hello World"; + Expression> expression = () => value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void StringObjectToStringCast() + { + object value = "Hello World"; + Expression> expression = () => (string) value; + + var actual = GetValue(expression); + + //Check with expression compile and invoke + var expected = expression.Compile().Invoke(); + + Assert.AreEqual(expected, actual); + } + + [Test] + public void StringToIntegerCastFails() + { + object value = "Abatay"; + Expression> expression = () => (int) value; + + Assert.Throws(() => GetValue(expression)); + + //Check with expression compile and invoke + Assert.Throws(() => expression.Compile().Invoke()); + } + + [Test] + public void BooleanToCharCastFails() + { + object isTrue = true; + Expression> expression = () => (char) isTrue; + + Assert.Throws(() => GetValue(expression)); + + //Check with expression compile and invoke + Assert.Throws(() => expression.Compile().Invoke()); + } + } +} diff --git a/src/NHibernate/Impl/ExpressionProcessor.cs b/src/NHibernate/Impl/ExpressionProcessor.cs index 6c401fd8851..74bea3fdf90 100644 --- a/src/NHibernate/Impl/ExpressionProcessor.cs +++ b/src/NHibernate/Impl/ExpressionProcessor.cs @@ -248,32 +248,55 @@ private static ICriterion Le(ProjectionInfo property, object value) } /// - /// Invoke the expression to extract its runtime value + /// Walk or Invoke expression to extract its runtime value /// public static object FindValue(Expression expression) { - if (expression.NodeType == ExpressionType.Constant) - return ((ConstantExpression) expression).Value; + object findValue(Expression e) => e != null ? FindValue(e) : null; - if (expression.NodeType == ExpressionType.MemberAccess) + 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; + switch (memberExpression.Member.MemberType) { case MemberTypes.Field: - return ((FieldInfo) member).GetValue(constantValue); + return ((FieldInfo) memberExpression.Member).GetValue(findValue(memberExpression.Expression)); case MemberTypes.Property: - return ((PropertyInfo) member).GetValue(constantValue); + return ((PropertyInfo) memberExpression.Member).GetValue(findValue(memberExpression.Expression)); } - } + break; + case ExpressionType.Call: + var methodCallExpression = (MethodCallExpression) expression; + var args = methodCallExpression.Arguments.ToArray(arg => FindValue(arg)); + var callingObject = findValue(methodCallExpression.Object); + return methodCallExpression.Method.Invoke(callingObject, args); + case ExpressionType.Convert: + var unaryExpression = (UnaryExpression) expression; + + if (unaryExpression.Method != null) + return unaryExpression.Method.Invoke(null, new[] { FindValue(unaryExpression.Operand) }); + + var toType = unaryExpression.Type; + var fromType = unaryExpression.Operand.Type; + if (toType == typeof(object) || toType == fromType || Nullable.GetUnderlyingType(toType) == fromType) + return FindValue(unaryExpression.Operand); + + if (toType == Nullable.GetUnderlyingType(fromType)) + { + var val = FindValue(unaryExpression.Operand); + if (val != null) + return val; + } + + break; } - var valueExpression = Expression.Lambda(expression).Compile(); - object value = valueExpression.DynamicInvoke(); + var lambdaExpression = Expression.Lambda(expression).Compile(true); + var value = lambdaExpression.DynamicInvoke(); return value; } diff --git a/src/NHibernate/Impl/LambdaExpressionExtensions.cs b/src/NHibernate/Impl/LambdaExpressionExtensions.cs new file mode 100644 index 00000000000..780bc735ef3 --- /dev/null +++ b/src/NHibernate/Impl/LambdaExpressionExtensions.cs @@ -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 +} diff --git a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs index 76467a8035e..437570d8d58 100644 --- a/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs +++ b/src/NHibernate/Linq/Visitors/ExpressionParameterVisitor.cs @@ -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; @@ -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>(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);