Skip to content

Commit 9949008

Browse files
authored
Support MemberExpression and specific MethodCallExpression (#4268)
This commit adds support for MemberExpression and specific MethodCallExpression as Expressions that can be passed to Field and PropertyName, in order to resolve a string value from the expression. The specific MethodCallExpression support is to allow F# quotations enclosing Lambda expressions to be supported, when converted using Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpression Fixes #4240
1 parent 9cf9ce4 commit 9949008

File tree

4 files changed

+150
-2
lines changed

4 files changed

+150
-2
lines changed

src/Nest/CommonAbstractions/Extensions/ExpressionExtensions.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,41 @@ internal static object ComparisonValueFromExpression(this Expression expression,
3030

3131
if (expression == null) return null;
3232

33-
if (!(expression is LambdaExpression lambda)) throw new Exception($"Not a lambda expression: {expression}");
33+
switch (expression)
34+
{
35+
case LambdaExpression lambdaExpression:
36+
type = lambdaExpression.Parameters.FirstOrDefault()?.Type;
37+
break;
38+
case MemberExpression memberExpression:
39+
type = memberExpression.Member.DeclaringType;
40+
break;
41+
case MethodCallExpression methodCallExpression:
42+
// special case F# method call expressions on FuncConvert
43+
// that are used to convert F# quotations representing lambda expressions, to expressions.
44+
// https://github.com/dotnet/fsharp/blob/7adaacf150dd79f072efe42d43168c9cd6edbced/src/fsharp/FSharp.Core/Linq.fs#L796
45+
//
46+
// For example:
47+
//
48+
// type Doc = { Message: string; State: string }
49+
// let field (f:Expr<'a -> 'b>) =
50+
// Microsoft.FSharp.Linq.RuntimeHelpers.LeafExpressionConverter.QuotationToExpression f
51+
// |> Nest.Field.op_Implicit
52+
//
53+
// let fieldExpression = field <@ fun (d: Doc) -> d.Message @>
54+
//
55+
if (methodCallExpression.Method.DeclaringType.FullName == "Microsoft.FSharp.Core.FuncConvert" &&
56+
methodCallExpression.Arguments.FirstOrDefault() is LambdaExpression lambda)
57+
type = lambda.Parameters.FirstOrDefault()?.Type;
58+
else
59+
throw new Exception($"Unsupported {nameof(MethodCallExpression)}: {expression}");
60+
break;
61+
default:
62+
throw new Exception(
63+
$"Expected {nameof(LambdaExpression)}, {nameof(MemberExpression)} or "
64+
+ $"{nameof(MethodCallExpression)}, received: {expression.GetType().Name}");
65+
66+
}
3467

35-
type = lambda.Parameters.FirstOrDefault()?.Type;
3668
var visitor = new ToStringExpressionVisitor();
3769
var toString = visitor.Resolve(expression);
3870
cachable = visitor.Cachable;

src/Nest/CommonAbstractions/Infer/Field/Field.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public Field(PropertyInfo property, double? boost = null, string format = null)
5656
/// </remarks>
5757
public string Format { get; set; }
5858

59+
// TODO: Rename to CacheableExpression in 8.0.0
5960
public bool CachableExpression { get; }
6061

6162
/// <summary>

src/Tests/Tests/ClientConcepts/HighLevel/Caching/FieldResolverCacheTests.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
using System.Collections.Generic;
33
using System.Diagnostics;
44
using System.Linq;
5+
using System.Linq.Expressions;
6+
using System.Reflection;
57
using System.Text;
68
using Elastic.Xunit.XunitPlumbing;
79
using FluentAssertions;
10+
using Microsoft.FSharp.Core;
811
using Nest;
912
using Tests.Domain;
1013
using Xunit.Abstractions;
@@ -534,5 +537,116 @@ public override string ToString() =>
534537
$"First hit for {Name} took {FirstHit}ms, Cached hit took {CachedHit}ms ({FirstHit / CachedHit}x faster).";
535538
}
536539
}
540+
541+
public class ExpressionExtensions
542+
{
543+
private class Doc
544+
{
545+
public string Prop { get; set; }
546+
}
547+
548+
private readonly FieldInfo _propertyNameComparisonField;
549+
private readonly FieldInfo _propertyNameTypeField;
550+
private FieldInfo _fieldComparisonField;
551+
private FieldInfo _fieldTypeField;
552+
553+
public ExpressionExtensions()
554+
{
555+
_propertyNameComparisonField = typeof(PropertyName).GetField("_comparisonValue", BindingFlags.Instance | BindingFlags.NonPublic);
556+
_propertyNameTypeField = typeof(PropertyName).GetField("_type", BindingFlags.Instance | BindingFlags.NonPublic);
557+
_fieldComparisonField = typeof(Field).GetField("_comparisonValue", BindingFlags.Instance | BindingFlags.NonPublic);
558+
_fieldTypeField = typeof(Field).GetField("_type", BindingFlags.Instance | BindingFlags.NonPublic);
559+
}
560+
561+
[U]
562+
public void CanCacheLambdaExpressionPropertyName()
563+
{
564+
Expression<Func<Doc, string>> expression = d => d.Prop;
565+
566+
var property = new PropertyName(expression);
567+
property.CacheableExpression.Should().BeTrue();
568+
569+
var value = _propertyNameComparisonField.GetValue(property);
570+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
571+
_propertyNameTypeField.GetValue(property).Should().Be(typeof(Doc));
572+
}
573+
574+
[U]
575+
public void CanCacheMemberExpressionPropertyName()
576+
{
577+
var parameterExpression = Expression.Parameter(typeof(Doc), "x");
578+
var expression = Expression.Property(parameterExpression, typeof(Doc).GetProperty(nameof(Doc.Prop)));
579+
580+
var property = new PropertyName(expression);
581+
property.CacheableExpression.Should().BeTrue();
582+
583+
var value = _propertyNameComparisonField.GetValue(property);
584+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
585+
_propertyNameTypeField.GetValue(property).Should().Be(typeof(Doc));
586+
}
587+
588+
[U]
589+
public void CanCacheFSharpFuncMethodCallExpressionPropertyName()
590+
{
591+
Expression<Func<Doc, string>> lambdaExpression = d => d.Prop;
592+
var expression = Expression.Call(
593+
typeof(FuncConvert),
594+
"ToFSharpFunc",
595+
new[] { typeof(Doc), typeof(string) },
596+
lambdaExpression);
597+
598+
var property = new PropertyName(expression);
599+
property.CacheableExpression.Should().BeTrue();
600+
601+
var value = _propertyNameComparisonField.GetValue(property);
602+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
603+
_propertyNameTypeField.GetValue(property).Should().Be(typeof(Doc));
604+
}
605+
606+
[U]
607+
public void CanCacheLambdaExpressionField()
608+
{
609+
Expression<Func<Doc, string>> expression = d => d.Prop;
610+
611+
var field = new Field(expression);
612+
field.CachableExpression.Should().BeTrue();
613+
614+
var value = _fieldComparisonField.GetValue(field);
615+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
616+
_fieldTypeField.GetValue(field).Should().Be(typeof(Doc));
617+
}
618+
619+
[U]
620+
public void CanCacheMemberExpressionField()
621+
{
622+
var parameterExpression = Expression.Parameter(typeof(Doc), "x");
623+
var expression = Expression.Property(parameterExpression, typeof(Doc).GetProperty(nameof(Doc.Prop)));
624+
625+
var field = new Field(expression);
626+
field.CachableExpression.Should().BeTrue();
627+
628+
var value = _fieldComparisonField.GetValue(field);
629+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
630+
_fieldTypeField.GetValue(field).Should().Be(typeof(Doc));
631+
}
632+
633+
[U]
634+
public void CanCacheFSharpFuncMethodCallExpressionField()
635+
{
636+
Expression<Func<Doc, string>> lambdaExpression = d => d.Prop;
637+
var expression = Expression.Call(
638+
typeof(FuncConvert),
639+
"ToFSharpFunc",
640+
new[] { typeof(Doc), typeof(string) },
641+
lambdaExpression);
642+
643+
var field = new Field(expression);
644+
field.CachableExpression.Should().BeTrue();
645+
646+
var value = _fieldComparisonField.GetValue(field);
647+
value.Should().BeOfType<string>().And.Be(nameof(Doc.Prop));
648+
_fieldTypeField.GetValue(field).Should().Be(typeof(Doc));
649+
}
650+
}
537651
}
538652
}

src/Tests/Tests/Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717
<ItemGroup>
1818
<ProjectReference Include="..\Tests.Core\Tests.Core.csproj" />
19+
<PackageReference Include="FSharp.Core" Version="4.7.0" />
1920

2021
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
2122
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.2" />

0 commit comments

Comments
 (0)