Skip to content

Commit c780a01

Browse files
authored
Merge pull request #51 from EFNext/fix/issue-50
feat: implement Expression.Quote for lambda expressions in IQueryable chains to resolve issue #50
2 parents b704352 + 87ab916 commit c780a01

5 files changed

Lines changed: 157 additions & 2 deletions

File tree

src/ExpressiveSharp.Generator/Emitter/ExpressionTreeEmitter.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,6 +996,15 @@ private string EmitConversion(IConversionOperation conversion)
996996
if (conversion.Operand is IThrowOperation throwOp && throwOp.Exception is not null)
997997
return EmitThrowWithType(throwOp, conversion.Type);
998998

999+
// Quoted-lambda conversion (Queryable.Where etc.) has no runtime coercion operator.
1000+
if (IsExpressionOfTDelegate(conversion.Type))
1001+
{
1002+
var quoteResult = NextVar();
1003+
var quoteOperand = EmitOperation(conversion.Operand);
1004+
AppendLine($"var {quoteResult} = {Expr}.Quote({quoteOperand});");
1005+
return quoteResult;
1006+
}
1007+
9991008
var resultVar = NextVar();
10001009
var operandVar = EmitOperation(conversion.Operand);
10011010
var targetTypeFqn = conversion.Type?.ToDisplayString(_fqnFormat) ?? "object";
@@ -3208,6 +3217,12 @@ private string EmitUnsupported(IOperation operation)
32083217

32093218
private string NextVar() => $"{_varPrefix}expr_{_varCounter++}";
32103219

3220+
private static bool IsExpressionOfTDelegate(ITypeSymbol? type)
3221+
=> type is INamedTypeSymbol { IsGenericType: true } named
3222+
&& named.OriginalDefinition.ContainingNamespace?.ToDisplayString() == "System.Linq.Expressions"
3223+
&& named.OriginalDefinition.Name == "Expression"
3224+
&& named.TypeArguments.Length == 1;
3225+
32113226
private void AppendLine(string line)
32123227
{
32133228
_lines.Add($" {line}");
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// <auto-generated/>
2+
#nullable disable
3+
4+
#nullable enable
5+
using ExpressiveSharp.Mapping;
6+
using Foo;
7+
8+
namespace ExpressiveSharp.Generated
9+
{
10+
static partial class Foo_Row
11+
{
12+
// [ExpressiveProperty("Previous")]
13+
// private Row? PreviousExpr => QueryContext.Query<Row>().Where(f => f.GroupId == GroupId && f.CreatedAt < CreatedAt).OrderByDescending(f => f.CreatedAt).FirstOrDefault();
14+
static global::System.Linq.Expressions.Expression<global::System.Func<global::Foo.Row, global::Foo.Row>> Previous_Expression()
15+
{
16+
var p__this = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Row), "@this");
17+
var expr_4 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::Foo.QueryContext).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Query" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 0)).MakeGenericMethod(typeof(global::Foo.Row)), global::System.Array.Empty<global::System.Linq.Expressions.Expression>()); // QueryContext.Query<Row>()
18+
var p_f_6 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Row), "f"); // f => f.GroupId == GroupId && f.CreatedAt < CreatedAt
19+
var expr_9 = global::System.Linq.Expressions.Expression.Property(p_f_6, typeof(global::Foo.Row).GetProperty("GroupId")); // f.GroupId
20+
var expr_10 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Row).GetProperty("GroupId")); // GroupId
21+
var expr_8 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.Equal, expr_9, expr_10);
22+
var expr_12 = global::System.Linq.Expressions.Expression.Property(p_f_6, typeof(global::Foo.Row).GetProperty("CreatedAt")); // f.CreatedAt
23+
var expr_13 = global::System.Linq.Expressions.Expression.Property(p__this, typeof(global::Foo.Row).GetProperty("CreatedAt")); // CreatedAt
24+
var expr_11 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.LessThan, expr_12, expr_13, false, typeof(global::System.DateTime).GetMethod("op_LessThan", global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static, null, new global::System.Type[] { typeof(global::System.DateTime), typeof(global::System.DateTime) }, null));
25+
var expr_7 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.AndAlso, expr_8, expr_11);
26+
var expr_14 = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Row, bool>>(expr_7, p_f_6);
27+
var expr_5 = global::System.Linq.Expressions.Expression.Quote(expr_14);
28+
var expr_3 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Queryable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Where" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].IsGenericType && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(global::System.Func<,>))).MakeGenericMethod(typeof(global::Foo.Row)), new global::System.Linq.Expressions.Expression[] { expr_4, expr_5 });
29+
var p_f_16 = global::System.Linq.Expressions.Expression.Parameter(typeof(global::Foo.Row), "f"); // f => f.CreatedAt
30+
var expr_17 = global::System.Linq.Expressions.Expression.Property(p_f_16, typeof(global::Foo.Row).GetProperty("CreatedAt")); // f.CreatedAt
31+
var expr_18 = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Row, global::System.DateTime>>(expr_17, p_f_16);
32+
var expr_15 = global::System.Linq.Expressions.Expression.Quote(expr_18);
33+
var expr_2 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Queryable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "OrderByDescending" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].IsGenericType && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(global::System.Func<,>))).MakeGenericMethod(typeof(global::Foo.Row), typeof(global::System.DateTime)), new global::System.Linq.Expressions.Expression[] { expr_3, expr_15 });
34+
var expr_1 = global::System.Linq.Expressions.Expression.Convert(expr_2, typeof(global::System.Linq.IQueryable<global::Foo.Row>));
35+
var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Queryable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "FirstOrDefault" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter)).MakeGenericMethod(typeof(global::Foo.Row)), new global::System.Linq.Expressions.Expression[] { expr_1 });
36+
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.Row, global::Foo.Row>>(expr_0, p__this);
37+
}
38+
}
39+
}
40+
41+
42+
// ===
43+
44+
// <auto-generated/>
45+
#nullable enable
46+
47+
namespace Foo
48+
{
49+
partial class Row
50+
{
51+
private global::Foo.Row? _previous;
52+
private bool _previousHasValue;
53+
public global::Foo.Row? Previous
54+
{
55+
get => _previousHasValue ? _previous : PreviousExpr;
56+
init
57+
{
58+
_previousHasValue = true;
59+
_previous = value;
60+
}
61+
}
62+
}
63+
}

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.BodyUsingQueryableWhereOrderBy_EmitsValidExpressionTree.verified.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ namespace ExpressiveSharp.Generated
1919
var expr_6 = global::System.Linq.Expressions.Expression.Constant(0, typeof(int)); // 0
2020
var expr_5 = global::System.Linq.Expressions.Expression.MakeBinary(global::System.Linq.Expressions.ExpressionType.GreaterThan, p_x_4, expr_6);
2121
var expr_7 = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<int, bool>>(expr_5, p_x_4);
22-
var expr_3 = global::System.Linq.Expressions.Expression.Convert(expr_7, typeof(global::System.Linq.Expressions.Expression<global::System.Func<int, bool>>));
22+
var expr_3 = global::System.Linq.Expressions.Expression.Quote(expr_7);
2323
var expr_1 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Queryable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "Where" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 1 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].IsGenericType && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(global::System.Func<,>))).MakeGenericMethod(typeof(int)), new global::System.Linq.Expressions.Expression[] { expr_2, expr_3 });
2424
var p_x_9 = global::System.Linq.Expressions.Expression.Parameter(typeof(int), "x"); // x => x
2525
var expr_10 = global::System.Linq.Expressions.Expression.Lambda<global::System.Func<int, int>>(p_x_9, p_x_9);
26-
var expr_8 = global::System.Linq.Expressions.Expression.Convert(expr_10, typeof(global::System.Linq.Expressions.Expression<global::System.Func<int, int>>));
26+
var expr_8 = global::System.Linq.Expressions.Expression.Quote(expr_10);
2727
var expr_0 = global::System.Linq.Expressions.Expression.Call(global::System.Linq.Enumerable.First(global::System.Linq.Enumerable.Where(typeof(global::System.Linq.Queryable).GetMethods(global::System.Reflection.BindingFlags.Public | global::System.Reflection.BindingFlags.NonPublic | global::System.Reflection.BindingFlags.Static), m => m.Name == "OrderBy" && m.IsGenericMethodDefinition && m.GetGenericArguments().Length == 2 && m.GetParameters().Length == 2 && m.GetParameters()[0].ParameterType.IsGenericType && !m.GetParameters()[0].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.IsGenericType && !m.GetParameters()[1].ParameterType.IsGenericParameter && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].IsGenericType && m.GetParameters()[1].ParameterType.GetGenericArguments()[0].GetGenericTypeDefinition() == typeof(global::System.Func<,>))).MakeGenericMethod(typeof(int), typeof(int)), new global::System.Linq.Expressions.Expression[] { expr_1, expr_8 });
2828
var expr_11 = global::System.Linq.Expressions.Expression.Convert(expr_0, typeof(global::System.Collections.Generic.IEnumerable<int>));
2929
return global::System.Linq.Expressions.Expression.Lambda<global::System.Func<global::Foo.BugB, global::System.Collections.Generic.IEnumerable<int>>>(expr_11, p__this);

tests/ExpressiveSharp.Generator.Tests/ExpressiveGenerator/ExpressivePropertyTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,4 +546,46 @@ public partial class BugB {
546546
return Verifier.Verify(string.Join("\n\n// ===\n\n",
547547
result.GeneratedTrees.Select(t => t.ToString())));
548548
}
549+
550+
[TestMethod]
551+
public Task BodyUsingQueryableLambdaChain_EmitsExpressionQuoteNotConvert()
552+
{
553+
// Issue #50: lambda → Expression<TDelegate> must emit Expression.Quote.
554+
var compilation = CreateCompilation(
555+
"""
556+
#nullable enable
557+
using ExpressiveSharp.Mapping;
558+
559+
namespace Foo {
560+
public partial class Row {
561+
public int Id { get; init; }
562+
public int GroupId { get; init; }
563+
public DateTime CreatedAt { get; init; }
564+
565+
[ExpressiveProperty("Previous")]
566+
private Row? PreviousExpr =>
567+
QueryContext.Query<Row>()
568+
.Where(f => f.GroupId == GroupId && f.CreatedAt < CreatedAt)
569+
.OrderByDescending(f => f.CreatedAt)
570+
.FirstOrDefault();
571+
}
572+
573+
internal static class QueryContext {
574+
public static IQueryable<T> Query<T>() => Array.Empty<T>().AsQueryable();
575+
}
576+
}
577+
""");
578+
var result = RunExpressiveGenerator(compilation);
579+
580+
Assert.AreEqual(0, result.Diagnostics.Length,
581+
"Unexpected diagnostics: " + string.Join(", ", result.Diagnostics.Select(d => d.Id + ": " + d.GetMessage())));
582+
583+
var generated = string.Join("\n", result.GeneratedTrees.Select(t => t.ToString()));
584+
Assert.IsFalse(
585+
generated.Contains("Expression.Convert(") && generated.Contains("typeof(global::System.Linq.Expressions.Expression<"),
586+
"Lambda → Expression<TDelegate> must emit Expression.Quote, not Expression.Convert.");
587+
588+
return Verifier.Verify(string.Join("\n\n// ===\n\n",
589+
result.GeneratedTrees.Select(t => t.ToString())));
590+
}
549591
}

tests/ExpressiveSharp.IntegrationTests/Tests/SynthesizedExpressiveTests.cs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,27 @@ public void CrossReferencedSynthesized_ExpandExpressives_RewritesNestedReference
139139
Assert.AreEqual("Jane Doe", result[0]);
140140
}
141141

142+
[TestMethod]
143+
public void IQueryableLambdaChain_FactoryEmitsQuote_RegistryInitSucceeds()
144+
{
145+
// Issue #50: registry static-ctor previously threw on lambda → Expression<TDelegate>.
146+
var rows = new List<GroupedRow>
147+
{
148+
new() { Id = 1, GroupId = 1 },
149+
new() { Id = 2, GroupId = 1 },
150+
new() { Id = 3, GroupId = 2 },
151+
};
152+
153+
var entity = new GroupedRow { Id = 4, GroupId = 1, Source = rows.AsQueryable() };
154+
var source = new List<GroupedRow> { entity }.AsQueryable();
155+
156+
Expression<Func<GroupedRow, int>> previousIdExpr = r => r.PreviousId;
157+
var expanded = (Expression<Func<GroupedRow, int>>)previousIdExpr.ExpandExpressives();
158+
var result = source.Select(expanded.Compile()).Single();
159+
160+
Assert.AreEqual(2, result, "Should pick the highest-Id row in the same group below this one");
161+
}
162+
142163
[TestMethod]
143164
public void ExpandExpressives_MemberInit_RewritesRhsOfProjection()
144165
{
@@ -190,6 +211,20 @@ public partial class CrossReferencedPerson
190211
private string FullNameExpr => FirstName + " Doe";
191212
}
192213

214+
// Issue #50: IQueryable<T> chain with lambda arguments inside an [ExpressiveProperty] body.
215+
public partial class GroupedRow
216+
{
217+
public int Id { get; set; }
218+
public int GroupId { get; set; }
219+
public IQueryable<GroupedRow> Source { get; set; } = Array.Empty<GroupedRow>().AsQueryable();
220+
221+
[ExpressiveProperty("PreviousId")]
222+
private int PreviousIdExpr => Source.Where(e => e.GroupId == GroupId && e.Id < Id)
223+
.OrderByDescending(e => e.Id)
224+
.Select(e => e.Id)
225+
.FirstOrDefault();
226+
}
227+
193228
/// <summary>
194229
/// Nullable value-type target — exercises the ternary+flag shape of the synthesized property.
195230
/// The flag distinguishes "not materialized" from "materialized to null", which the coalesce

0 commit comments

Comments
 (0)