Add query root rewrite support

This commit is contained in:
2023-09-24 17:05:49 +02:00
parent 2a9640533a
commit e5eae5bf5a
4 changed files with 106 additions and 14 deletions
@@ -1,14 +1,11 @@
using System;
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Xml.Linq;
using EntityFrameworkCore.Projectables.Extensions;
using Microsoft.EntityFrameworkCore.Query;
namespace EntityFrameworkCore.Projectables.Services
{
@@ -28,10 +25,10 @@ namespace EntityFrameworkCore.Projectables.Services
if (!_projectableMemberCache.TryGetValue(memberInfo, out reflectedExpression))
{
var projectableAttribute = memberInfo.GetCustomAttribute<ProjectableAttribute>(false);
reflectedExpression = projectableAttribute is not null
reflectedExpression = projectableAttribute is not null
? _resolver.FindGeneratedExpression(memberInfo)
: (LambdaExpression?)null;
: null;
_projectableMemberCache.Add(memberInfo, reflectedExpression);
}
@@ -50,7 +47,7 @@ namespace EntityFrameworkCore.Projectables.Services
{
var parameterExpession = reflectedExpression.Parameters[parameterIndex];
var mappedArgumentExpression = (parameterIndex, node.Object) switch {
(0, not null) => node.Object,
(0, not null) => node.Object,
(_, not null) => node.Arguments[parameterIndex - 1],
(_, null) => node.Arguments.Count > parameterIndex ? node.Arguments[parameterIndex] : null
};
@@ -60,7 +57,7 @@ namespace EntityFrameworkCore.Projectables.Services
_expressionArgumentReplacer.ParameterArgumentMapping.Add(parameterExpession, mappedArgumentExpression);
}
}
var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body);
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
@@ -110,5 +107,64 @@ namespace EntityFrameworkCore.Projectables.Services
return base.VisitMember(node);
}
protected override Expression VisitExtension(Expression node)
{
if (node is not QueryRootExpression root)
{
return node;
}
var projectableProperties = root.EntityType.ClrType.GetProperties()
.Where(x => x.IsDefined(typeof(ProjectableAttribute), false))
.Where(x => x.CanWrite)
.ToList();
if (!projectableProperties.Any())
{
return node;
}
var properties = root.EntityType.GetProperties()
.Where(x => !x.IsShadowProperty())
.Select(x => x.GetMemberInfo(false, false))
// Remove projectable properties from the ef properties. Since properties returned here for auto
// properties (like `public string Test {get;set;}`) are generated fields, we also need to take them into account.
.Where(x => projectableProperties.All(y => x.Name != y.Name && x.Name != $"<{y.Name}>k__BackingField"));
// Replace db.Entities to db.Entities.Select(x => new Entity { Property1 = x.Property1, Rewritted = rewrittedProperty })
var select = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public)
.Where(x => x.Name == nameof(Queryable.Select))
.First(x =>
x.GetParameters().Last().ParameterType // Expression<Func<T, Ret>>
.GetGenericArguments().First() // Func<T, Ret>
.GetGenericArguments().Length == 2 // Separate between Func<T, Ret> and Func<T, int, Ret>
)
.MakeGenericMethod(root.EntityType.ClrType, root.EntityType.ClrType);
var xParam = Expression.Parameter(root.EntityType.ClrType);
return Expression.Call(
null,
select,
node,
Expression.Lambda(
Expression.MemberInit(
Expression.New(root.EntityType.ClrType),
properties.Select(x => Expression.Bind(x, Expression.MakeMemberAccess(xParam, x)))
.Concat(projectableProperties
.Select(x => Expression.Bind(x, _ReplaceParam(_resolver.FindGeneratedExpression(x), xParam)))
)
),
xParam
)
);
}
private Expression _ReplaceParam(LambdaExpression lambda, ParameterExpression para)
{
_expressionArgumentReplacer.ParameterArgumentMapping.Add(lambda.Parameters[0], para);
var updatedBody = _expressionArgumentReplacer.Visit(lambda.Body);
_expressionArgumentReplacer.ParameterArgumentMapping.Clear();
return updatedBody;
}
}
}
@@ -1,13 +1,8 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.Extensions;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
namespace EntityFrameworkCore.Projectables.Services
{
@@ -0,0 +1,2 @@
SELECT [e].[Id], [e].[Id] * 5
FROM [Entity] AS [e]
@@ -0,0 +1,39 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Threading.Tasks;
using EntityFrameworkCore.Projectables.FunctionalTests.Helpers;
using Microsoft.EntityFrameworkCore;
using VerifyXunit;
using Xunit;
namespace EntityFrameworkCore.Projectables.FunctionalTests
{
[UsesVerify]
public class QueryRootTests
{
public record Entity
{
public int Id { get; set; }
[Projectable(UseMemberBody = nameof(Computed2))]
public int Computed1 => Id;
private int Computed2 => Id * 2;
[Projectable(UseMemberBody = nameof(_ComputedWithBaking))]
[NotMapped]
public int ComputedWithBacking { get; set; }
private int _ComputedWithBaking => Id * 5;
}
[Fact]
public Task UseMemberPropertyQueryRootExpression()
{
using var dbContext = new SampleDbContext<Entity>();
var query = dbContext.Set<Entity>();
return Verifier.Verify(query.ToQueryString());
}
}
}