Skip to content

Commit e5eae5b

Browse files
committed
Add query root rewrite support
1 parent 2a96405 commit e5eae5b

4 files changed

Lines changed: 106 additions & 14 deletions

File tree

src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
using System;
2-
using System.Buffers;
32
using System.Collections.Generic;
43
using System.Diagnostics.CodeAnalysis;
54
using System.Linq;
65
using System.Linq.Expressions;
76
using System.Reflection;
8-
using System.Text;
9-
using System.Threading.Tasks;
10-
using System.Xml.Linq;
117
using EntityFrameworkCore.Projectables.Extensions;
8+
using Microsoft.EntityFrameworkCore.Query;
129

1310
namespace EntityFrameworkCore.Projectables.Services
1411
{
@@ -28,10 +25,10 @@ bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out La
2825
if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression))
2926
{
3027
var projectableAttribute = memberInfo.GetCustomAttribute<ProjectableAttribute>(false);
31-
32-
reflectedExpression = projectableAttribute is not null
28+
29+
reflectedExpression = projectableAttribute is not null
3330
? _resolver.FindGeneratedExpression(memberInfo)
34-
: (LambdaExpression?)null;
31+
: null;
3532

3633
_projectableMemberCache.Add(memberInfo, reflectedExpression);
3734
}
@@ -50,7 +47,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
5047
{
5148
var parameterExpession = reflectedExpression.Parameters[parameterIndex];
5249
var mappedArgumentExpression = (parameterIndex, node.Object) switch {
53-
(0, not null) => node.Object,
50+
(0, not null) => node.Object,
5451
(_, not null) => node.Arguments[parameterIndex - 1],
5552
(_, null) => node.Arguments.Count > parameterIndex ? node.Arguments[parameterIndex] : null
5653
};
@@ -60,7 +57,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
6057
_expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpession, mappedArgumentExpression);
6158
}
6259
}
63-
60+
6461
var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
6562
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
6663

@@ -110,5 +107,64 @@ PropertyInfo property when nodeExpression is not null
110107

111108
return base.VisitMember(node);
112109
}
110+
111+
protected override Expression VisitExtension(Expression node)
112+
{
113+
if (node is not QueryRootExpression root)
114+
{
115+
return node;
116+
}
117+
118+
var projectableProperties = root.EntityType.ClrType.GetProperties()
119+
.Where(x => x.IsDefined(typeof(ProjectableAttribute), false))
120+
.Where(x => x.CanWrite)
121+
.ToList();
122+
123+
if (!projectableProperties.Any())
124+
{
125+
return node;
126+
}
127+
128+
var properties = root.EntityType.GetProperties()
129+
.Where(x => !x.IsShadowProperty())
130+
.Select(x => x.GetMemberInfo(false, false))
131+
// Remove projectable properties from the ef properties. Since properties returned here for auto
132+
// properties (like `public string Test {get;set;}`) are generated fields, we also need to take them into account.
133+
.Where(x => projectableProperties.All(y => x.Name != y.Name && x.Name != $"<{y.Name}>k__BackingField"));
134+
135+
// Replace db.Entities to db.Entities.Select(x => new Entity { Property1 = x.Property1, Rewritted = rewrittedProperty })
136+
var select = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public)
137+
.Where(x => x.Name == nameof(Queryable.Select))
138+
.First(x =>
139+
x.GetParameters().Last().ParameterType // Expression<Func<T, Ret>>
140+
.GetGenericArguments().First() // Func<T, Ret>
141+
.GetGenericArguments().Length == 2 // Separate between Func<T, Ret> and Func<T, int, Ret>
142+
)
143+
.MakeGenericMethod(root.EntityType.ClrType, root.EntityType.ClrType);
144+
var xParam = Expression.Parameter(root.EntityType.ClrType);
145+
return Expression.Call(
146+
null,
147+
select,
148+
node,
149+
Expression.Lambda(
150+
Expression.MemberInit(
151+
Expression.New(root.EntityType.ClrType),
152+
properties.Select(x => Expression.Bind(x, Expression.MakeMemberAccess(xParam, x)))
153+
.Concat(projectableProperties
154+
.Select(x => Expression.Bind(x, _ReplaceParam(_resolver.FindGeneratedExpression(x), xParam)))
155+
)
156+
),
157+
xParam
158+
)
159+
);
160+
}
161+
162+
private Expression _ReplaceParam(LambdaExpression lambda, ParameterExpression para)
163+
{
164+
_expressionArgumentReplacer.ParameterArgumentMapping.Add(lambda.Parameters[0], para);
165+
var updatedBody = _expressionArgumentReplacer.Visit(lambda.Body);
166+
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
167+
return updatedBody;
168+
}
113169
}
114170
}

src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
using System;
2-
using System.Collections.Concurrent;
3-
using System.Collections.Generic;
42
using System.Linq;
53
using System.Linq.Expressions;
64
using System.Reflection;
7-
using System.Text;
8-
using System.Threading.Tasks;
95
using EntityFrameworkCore.Projectables.Extensions;
10-
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
116

127
namespace EntityFrameworkCore.Projectables.Services
138
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SELECT [e].[Id], [e].[Id] * 5
2+
FROM [Entity] AS [e]
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.ComponentModel.DataAnnotations.Schema;
2+
using System.Threading.Tasks;
3+
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
4+
using Microsoft.EntityFrameworkCore;
5+
using VerifyXunit;
6+
using Xunit;
7+
8+
namespace EntityFrameworkCore.Projectables.FunctionalTests
9+
{
10+
[UsesVerify]
11+
public class QueryRootTests
12+
{
13+
public record Entity
14+
{
15+
public int Id { get; set; }
16+
17+
[Projectable(UseMemberBody = nameof(Computed2))]
18+
public int Computed1 => Id;
19+
20+
private int Computed2 => Id * 2;
21+
22+
[Projectable(UseMemberBody = nameof(_ComputedWithBaking))]
23+
[NotMapped]
24+
public int ComputedWithBacking { get; set; }
25+
26+
private int _ComputedWithBaking => Id * 5;
27+
}
28+
29+
[Fact]
30+
public Task UseMemberPropertyQueryRootExpression()
31+
{
32+
using var dbContext = new SampleDbContext<Entity>();
33+
34+
var query = dbContext.Set<Entity>();
35+
36+
return Verifier.Verify(query.ToQueryString());
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)