diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index 8cfb703..010fb4c 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -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(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> + .GetGenericArguments().First() // Func + .GetGenericArguments().Length == 2 // Separate between Func and Func + ) + .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; + } } } diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs index f913049..b9062dd 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectionExpressionResolver.cs @@ -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 { diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt new file mode 100644 index 0000000..7d6c73b --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt @@ -0,0 +1,2 @@ +SELECT [e].[Id], [e].[Id] * 5 +FROM [Entity] AS [e] diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.cs new file mode 100644 index 0000000..bef2283 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.cs @@ -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(); + + var query = dbContext.Set(); + + return Verifier.Verify(query.ToQueryString()); + } + } +}