From 2a9640533a677e8f9548db8aba09a51430effe48 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sat, 23 Sep 2023 11:57:33 +0200 Subject: [PATCH 1/7] Add shell.nix --- shell.nix | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 shell.nix diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..3ee0422 --- /dev/null +++ b/shell.nix @@ -0,0 +1,12 @@ +{pkgs ? import {}}: let + dotnet = with pkgs.dotnetCorePackages; + combinePackages [ + sdk_7_0 + aspnetcore_7_0 + ]; +in + pkgs.mkShell { + packages = [dotnet]; + + DOTNET_ROOT = "${dotnet}"; + } From e5eae5bf5a3cb527cf78bc6b455c85693b001b4b Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Sun, 24 Sep 2023 17:05:49 +0200 Subject: [PATCH 2/7] Add query root rewrite support --- .../Services/ProjectableExpressionReplacer.cs | 74 ++++++++++++++++--- .../Services/ProjectionExpressionResolver.cs | 5 -- ...erPropertyQueryRootExpression.verified.txt | 2 + .../QueryRootTests.cs | 39 ++++++++++ 4 files changed, 106 insertions(+), 14 deletions(-) create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.cs 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()); + } + } +} From 669b02a9f975f8fe4d12c09599719ba2eb5ac019 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 10 Oct 2023 17:45:27 +0200 Subject: [PATCH 3/7] Use a second path for rewritting query path --- .gitignore | 4 +- .../Services/ProjectableExpressionReplacer.cs | 57 ++++++++++++++++--- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index f2f723c..4e57e37 100644 --- a/.gitignore +++ b/.gitignore @@ -363,4 +363,6 @@ MigrationBackup/ FodyWeavers.xsd # Received verify test results -*.received.* \ No newline at end of file +*.received.* + +.idea diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index 010fb4c..db66e3d 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -1,10 +1,10 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; namespace EntityFrameworkCore.Projectables.Services @@ -13,11 +13,15 @@ namespace EntityFrameworkCore.Projectables.Services { readonly IProjectionExpressionResolver _resolver; readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new(); + readonly QueryRootReplacer _queryRootReplacer; readonly Dictionary _projectableMemberCache = new(); + private bool _disableRootRewrite = false; + private IEntityType? _entityType; public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver) { _resolver = projectionExpressionResolver; + _queryRootReplacer = new(_resolver); } bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) @@ -36,11 +40,42 @@ namespace EntityFrameworkCore.Projectables.Services return reflectedExpression is not null; } + [return: NotNullIfNotNull(nameof(node))] + public override Expression? Visit(Expression? node) + { + var ret = base.Visit(node); + + if (_disableRootRewrite) + { + return ret; + } + + switch (node) + { + // Probably a First() or ToList() + case MethodCallExpression { Arguments.Count: > 0 } call when _entityType != null: + { + var self = _AddProjectableSelect(call.Arguments.First(), _entityType); + return call.Update(null, call.Arguments.Skip(1).Prepend(self)); + } + // Probably a foreach call + case QueryRootExpression root: + return _AddProjectableSelect(root, root.EntityType); + default: + return ret; + } + } + protected override Expression VisitMethodCall(MethodCallExpression node) { // Get the overriding methodInfo based on te type of the received of this expression var methodInfo = node.Object?.Type.GetConcreteMethod(node.Method) ?? node.Method; + if (methodInfo.Name == nameof(Queryable.Select)) + { + _disableRootRewrite = true; + } + if (TryGetReflectedExpression(methodInfo, out var reflectedExpression)) { for (var parameterIndex = 0; parameterIndex < reflectedExpression.Parameters.Count; parameterIndex++) @@ -110,12 +145,16 @@ namespace EntityFrameworkCore.Projectables.Services protected override Expression VisitExtension(Expression node) { - if (node is not QueryRootExpression root) + if (node is QueryRootExpression root) { - return node; + _entityType = root.EntityType; } + return base.VisitExtension(node); + } - var projectableProperties = root.EntityType.ClrType.GetProperties() + private Expression _AddProjectableSelect(Expression node, IEntityType entityType) + { + var projectableProperties = entityType.ClrType.GetProperties() .Where(x => x.IsDefined(typeof(ProjectableAttribute), false)) .Where(x => x.CanWrite) .ToList(); @@ -125,7 +164,7 @@ namespace EntityFrameworkCore.Projectables.Services return node; } - var properties = root.EntityType.GetProperties() + var properties = 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 @@ -140,15 +179,15 @@ namespace EntityFrameworkCore.Projectables.Services .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); + .MakeGenericMethod(entityType.ClrType, entityType.ClrType); + var xParam = Expression.Parameter(entityType.ClrType); return Expression.Call( null, select, node, Expression.Lambda( Expression.MemberInit( - Expression.New(root.EntityType.ClrType), + Expression.New(entityType.ClrType), properties.Select(x => Expression.Bind(x, Expression.MakeMemberAccess(xParam, x))) .Concat(projectableProperties .Select(x => Expression.Bind(x, _ReplaceParam(_resolver.FindGeneratedExpression(x), xParam))) From 19829ccd0ae79fbf1579bcb24275bab1dccfe49c Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 10 Oct 2023 17:51:34 +0200 Subject: [PATCH 4/7] Add some failing tests --- samples/BasicSample/BasicSample.csproj | 2 +- samples/BasicSample/Program.cs | 50 ++++++++++++++++---------- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/samples/BasicSample/BasicSample.csproj b/samples/BasicSample/BasicSample.csproj index 6e97dbc..dfef09f 100644 --- a/samples/BasicSample/BasicSample.csproj +++ b/samples/BasicSample/BasicSample.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 disable true $(BaseIntermediateOutputPath)Generated diff --git a/samples/BasicSample/Program.cs b/samples/BasicSample/Program.cs index 0ca49e5..4cd2957 100644 --- a/samples/BasicSample/Program.cs +++ b/samples/BasicSample/Program.cs @@ -1,16 +1,11 @@ -using EntityFrameworkCore.Projectables; -using EntityFrameworkCore.Projectables.Extensions; -using Microsoft.Data.Sqlite; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using System; -using System.Collections; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; -using System.Diagnostics; using System.Linq; +using EntityFrameworkCore.Projectables; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; namespace BasicSample { @@ -22,12 +17,14 @@ namespace BasicSample public ICollection Orders { get; set; } - [Projectable] - public string FullName - => FirstName + " " + LastName; + [Projectable(UseMemberBody = nameof(_FullName))] + public string FullName { get; set; } + private string _FullName => FirstName + " " + LastName; - [Projectable] - public double TotalSpent => Orders.Sum(x => x.PriceSum); + [Projectable(UseMemberBody = nameof(_TotalSpent))] + [NotMapped] + public double TotalSpent { get; set; } + private double _TotalSpent => Orders.Sum(x => x.PriceSum); [Projectable] public Order MostValuableOrder @@ -86,7 +83,7 @@ namespace BasicSample class Program { - static void Main(string[] args) + public static void Main(string[] args) { using var dbConnection = new SqliteConnection("Filename=:memory:"); dbConnection.Open(); @@ -95,6 +92,8 @@ namespace BasicSample .AddDbContext((provider, options) => { options .UseSqlite(dbConnection) + .LogTo(Console.WriteLine) + .EnableSensitiveDataLogging() .UseProjectables(); }) .BuildServiceProvider(); @@ -105,9 +104,9 @@ namespace BasicSample var product1 = new Product { Name = "Red pen", Price = 1.5 }; var product2 = new Product { Name = "Blue pen", Price = 2.1 }; - var user = new User { - FirstName = "Jon", - LastName = "Doe", + var user = new User { + FirstName = "Jon", + LastName = "Doe", Orders = new List { new Order { Items = new List { @@ -130,6 +129,19 @@ namespace BasicSample dbContext.SaveChanges(); // What did our user spent in total + + { + foreach (var u in dbContext.Users) + { + Console.WriteLine($"User name: {u.FullName}"); + } + } + + { + var result = dbContext.Users.FirstOrDefault(); + Console.WriteLine($"Our first user {result.FullName} has spent {result.TotalSpent}"); + } + { var query = dbContext.Users .Select(x => new { From aa7cf4f9c6005e1138129ecda88ab1dcc27e199a Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Tue, 10 Oct 2023 22:17:32 +0200 Subject: [PATCH 5/7] Fix failing tests --- Directory.Build.props | 2 +- samples/BasicSample/Program.cs | 3 +-- .../Extensions/ExpressionExtensions.cs | 2 +- .../Internal/CustomQueryCompiler.cs | 2 +- .../Services/ProjectableExpressionReplacer.cs | 25 +++++++++---------- ...erPropertyQueryRootExpression.verified.txt | 2 +- .../ProjectableExpressionReplacerTests.cs | 14 +++++------ 7 files changed, 24 insertions(+), 26 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index d79998e..cd0f7dd 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -30,4 +30,4 @@ - \ No newline at end of file + diff --git a/samples/BasicSample/Program.cs b/samples/BasicSample/Program.cs index 4cd2957..c2be8b4 100644 --- a/samples/BasicSample/Program.cs +++ b/samples/BasicSample/Program.cs @@ -22,7 +22,6 @@ namespace BasicSample private string _FullName => FirstName + " " + LastName; [Projectable(UseMemberBody = nameof(_TotalSpent))] - [NotMapped] public double TotalSpent { get; set; } private double _TotalSpent => Orders.Sum(x => x.PriceSum); @@ -92,7 +91,7 @@ namespace BasicSample .AddDbContext((provider, options) => { options .UseSqlite(dbConnection) - .LogTo(Console.WriteLine) + // .LogTo(Console.WriteLine) .EnableSensitiveDataLogging() .UseProjectables(); }) diff --git a/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs index 565e784..49da220 100644 --- a/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs +++ b/src/EntityFrameworkCore.Projectables/Extensions/ExpressionExtensions.cs @@ -18,6 +18,6 @@ namespace EntityFrameworkCore.Projectables.Extensions /// Replaces all calls to properties and methods that are marked with the Projectable attribute with their respective expression tree /// public static Expression ExpandProjectables(this Expression expression) - => new ProjectableExpressionReplacer(new ProjectionExpressionResolver()).Visit(expression); + => new ProjectableExpressionReplacer(new ProjectionExpressionResolver()).Replace(expression); } } diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs index 5a300a2..2a85c9c 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryCompiler.cs @@ -34,6 +34,6 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal => _decoratedQueryCompiler.ExecuteAsync(Expand(query), cancellationToken); Expression Expand(Expression expression) - => _projectableExpressionReplacer.Visit(expression); + => _projectableExpressionReplacer.Replace(expression); } } diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index db66e3d..f9e8212 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -13,15 +13,13 @@ namespace EntityFrameworkCore.Projectables.Services { readonly IProjectionExpressionResolver _resolver; readonly ExpressionArgumentReplacer _expressionArgumentReplacer = new(); - readonly QueryRootReplacer _queryRootReplacer; readonly Dictionary _projectableMemberCache = new(); - private bool _disableRootRewrite = false; + private bool _disableRootRewrite; private IEntityType? _entityType; public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver) { _resolver = projectionExpressionResolver; - _queryRootReplacer = new(_resolver); } bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) @@ -41,19 +39,19 @@ namespace EntityFrameworkCore.Projectables.Services } [return: NotNullIfNotNull(nameof(node))] - public override Expression? Visit(Expression? node) + public Expression? Replace(Expression? node) { - var ret = base.Visit(node); + var ret = Visit(node); if (_disableRootRewrite) { return ret; } - switch (node) + switch (ret) { // Probably a First() or ToList() - case MethodCallExpression { Arguments.Count: > 0 } call when _entityType != null: + case MethodCallExpression { Arguments.Count: > 0, Object: null } call when _entityType != null: { var self = _AddProjectableSelect(call.Arguments.First(), _entityType); return call.Update(null, call.Arguments.Skip(1).Prepend(self)); @@ -96,7 +94,7 @@ namespace EntityFrameworkCore.Projectables.Services var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body); _expressionArgumentReplacer.ParameterArgumentMapping.Clear(); - return Visit( + return base.Visit( updatedBody ); } @@ -128,13 +126,13 @@ namespace EntityFrameworkCore.Projectables.Services var updatedBody = _expressionArgumentReplacer.Visit(reflectedExpression.Body); _expressionArgumentReplacer.ParameterArgumentMapping.Clear(); - return Visit( + return base.Visit( updatedBody ); } else { - return Visit( + return base.Visit( reflectedExpression.Body ); } @@ -190,7 +188,7 @@ namespace EntityFrameworkCore.Projectables.Services Expression.New(entityType.ClrType), properties.Select(x => Expression.Bind(x, Expression.MakeMemberAccess(xParam, x))) .Concat(projectableProperties - .Select(x => Expression.Bind(x, _ReplaceParam(_resolver.FindGeneratedExpression(x), xParam))) + .Select(x => Expression.Bind(x, _GetAccessor(x, xParam))) ) ), xParam @@ -198,12 +196,13 @@ namespace EntityFrameworkCore.Projectables.Services ); } - private Expression _ReplaceParam(LambdaExpression lambda, ParameterExpression para) + private Expression _GetAccessor(PropertyInfo property, ParameterExpression para) { + var lambda = _resolver.FindGeneratedExpression(property); _expressionArgumentReplacer.ParameterArgumentMapping.Add(lambda.Parameters[0], para); var updatedBody = _expressionArgumentReplacer.Visit(lambda.Body); _expressionArgumentReplacer.ParameterArgumentMapping.Clear(); - return updatedBody; + return base.Visit(updatedBody); } } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt index 7d6c73b..e8c699d 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/QueryRootTests.UseMemberPropertyQueryRootExpression.verified.txt @@ -1,2 +1,2 @@ SELECT [e].[Id], [e].[Id] * 5 -FROM [Entity] AS [e] +FROM [Entity] AS [e] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs index 133baeb..a9ed5dd 100644 --- a/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs +++ b/tests/EntityFrameworkCore.Projectables.Tests/Services/ProjectableExpressionReplacerTests.cs @@ -61,7 +61,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -77,7 +77,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -93,7 +93,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -109,7 +109,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -125,7 +125,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -141,7 +141,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } @@ -157,7 +157,7 @@ namespace EntityFrameworkCore.Projectables.Tests.Services ); var subject = new ProjectableExpressionReplacer(resolver); - var actual = subject.Visit(input); + var actual = subject.Replace(input); Assert.Equal(expected.ToString(), actual.ToString()); } From 4fb6013aed8967d69c4f29569060b914e26775d5 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Wed, 11 Oct 2023 01:49:38 +0200 Subject: [PATCH 6/7] Handle cases like .First(where) and .Sum --- samples/BasicSample/Program.cs | 20 +++++ .../Services/ProjectableExpressionReplacer.cs | 84 ++++++++++++++++--- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/samples/BasicSample/Program.cs b/samples/BasicSample/Program.cs index c2be8b4..b16b119 100644 --- a/samples/BasicSample/Program.cs +++ b/samples/BasicSample/Program.cs @@ -134,11 +134,31 @@ namespace BasicSample { Console.WriteLine($"User name: {u.FullName}"); } + + foreach (var u in dbContext.Users.ToList()) + { + Console.WriteLine($"User name: {u.FullName}"); + } + + foreach (var u in dbContext.Users.OrderBy(x => x.FullName)) + { + Console.WriteLine($"User name: {u.FullName}"); + } + } + + { + foreach (var u in dbContext.Users.Where(x => x.TotalSpent >= 1)) + { + Console.WriteLine($"User name: {u.FullName}"); + } } { var result = dbContext.Users.FirstOrDefault(); Console.WriteLine($"Our first user {result.FullName} has spent {result.TotalSpent}"); + + result = dbContext.Users.FirstOrDefault(x => x.TotalSpent > 1); + Console.WriteLine($"Our first user {result.FullName} has spent {result.TotalSpent}"); } { diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index f9e8212..f1c4f65 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; @@ -17,9 +18,26 @@ namespace EntityFrameworkCore.Projectables.Services private bool _disableRootRewrite; private IEntityType? _entityType; + private readonly MethodInfo _select; + private readonly MethodInfo _where; + public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver) { _resolver = projectionExpressionResolver; + _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 + ); + _where = typeof(Queryable).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.Name == nameof(Queryable.Where)) + .First(x => + x.GetParameters().Last().ParameterType // Expression> + .GetGenericArguments().First() // Func + .GetGenericArguments().Length == 2 // Separate between Func and Func + ); } bool TryGetReflectedExpression(MemberInfo memberInfo, [NotNullWhen(true)] out LambdaExpression? reflectedExpression) @@ -45,6 +63,7 @@ namespace EntityFrameworkCore.Projectables.Services if (_disableRootRewrite) { + // This boolean is enabled when a "Select" is encountered return ret; } @@ -53,10 +72,62 @@ namespace EntityFrameworkCore.Projectables.Services // Probably a First() or ToList() case MethodCallExpression { Arguments.Count: > 0, Object: null } call when _entityType != null: { + // if return type != IQueryable { + // if return type is IEnuberable { + // // case of a ToList() + // return (ret.arg[0]).Select(...).ToList() or the other method + // } else { + // // case of a Max() + // return ret; + // } + // } else if retrun type == entitytype { + // // case of a first() + // return obj.MyMap(x => new Obj {}); + // } + + + if (call.Method.ReturnType.IsAssignableTo(typeof(IQueryable))) + { + // Generic case where the return type is still a IQueryable + return _AddProjectableSelect(call, _entityType); + } + + if (call.Method.ReturnType == _entityType.ClrType) + { + // case of a .First(), .SingleAsync() + if (call.Arguments.Count != 1 && true /* Add && arg.count == 1 exist */) + { + // .First(x => whereCondition), since we need to add a select after the last condition but + // before the query become executed by EF (before the .First()), we rewrite the .First(where) + // as .Where(where).Select(x => ...).First() + + var where = Expression.Call(null, _where.MakeGenericMethod(_entityType.ClrType), call.Arguments); + // The call instance is based on the wrong polymorphied method. + var first = call.Method.DeclaringType?.GetMethods() + .FirstOrDefault(x => x.Name == call.Method.Name && x.GetParameters().Length == 1); + if (first == null) + { + // Unknown case that should not happen. + return call; + } + + return Expression.Call(null, first.MakeGenericMethod(_entityType.ClrType), _AddProjectableSelect(where, _entityType)); + } + + // .First() without arguments is the same case as bellow so we let it fallthrough + } + else if (!call.Method.ReturnType.IsAssignableTo(typeof(IEnumerable))) + { + // case of something like a .Max(), .Sum() + return call; + } + + // return type is IEnumerable or EntityType (in case of fallthrough from a .First()) + + // case of something like .ToList(), .ToArrayAsync() var self = _AddProjectableSelect(call.Arguments.First(), _entityType); return call.Update(null, call.Arguments.Skip(1).Prepend(self)); } - // Probably a foreach call case QueryRootExpression root: return _AddProjectableSelect(root, root.EntityType); default: @@ -170,14 +241,7 @@ namespace EntityFrameworkCore.Projectables.Services .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(entityType.ClrType, entityType.ClrType); + var select = _select.MakeGenericMethod(entityType.ClrType, entityType.ClrType); var xParam = Expression.Parameter(entityType.ClrType); return Expression.Call( null, From c15b9704198c42181d19c81a5dbc4ad8f878ce83 Mon Sep 17 00:00:00 2001 From: Zoe Roux Date: Thu, 12 Oct 2023 00:41:00 +0200 Subject: [PATCH 7/7] Support both net6 and net7 --- Directory.Build.props | 8 ++++++-- samples/BasicSample/BasicSample.csproj | 6 +++--- samples/BasicSample/Program.cs | 3 +++ samples/ReadmeSample/ReadmeSample.csproj | 6 +++--- .../EntityFrameworkCore.Projectables.csproj | 2 +- .../Services/ProjectableExpressionReplacer.cs | 8 ++++++-- ...ntityFrameworkCore.Projectables.FunctionalTests.csproj | 2 +- 7 files changed, 23 insertions(+), 12 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index cd0f7dd..932ca59 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,11 +23,15 @@ - net6.0 + net7.0;net6.0 6.0.0 6.0.0 $(EFCoreVersion) - + + + 7.0.0 + $(EFCoreVersion) + diff --git a/samples/BasicSample/BasicSample.csproj b/samples/BasicSample/BasicSample.csproj index dfef09f..937f9ed 100644 --- a/samples/BasicSample/BasicSample.csproj +++ b/samples/BasicSample/BasicSample.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/samples/BasicSample/Program.cs b/samples/BasicSample/Program.cs index b16b119..08c25fc 100644 --- a/samples/BasicSample/Program.cs +++ b/samples/BasicSample/Program.cs @@ -159,6 +159,9 @@ namespace BasicSample result = dbContext.Users.FirstOrDefault(x => x.TotalSpent > 1); Console.WriteLine($"Our first user {result.FullName} has spent {result.TotalSpent}"); + + var spent = dbContext.Users.Sum(x => x.TotalSpent); + Console.WriteLine($"Our users combined spent: {spent}"); } { diff --git a/samples/ReadmeSample/ReadmeSample.csproj b/samples/ReadmeSample/ReadmeSample.csproj index a1f59ea..2da4034 100644 --- a/samples/ReadmeSample/ReadmeSample.csproj +++ b/samples/ReadmeSample/ReadmeSample.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/src/EntityFrameworkCore.Projectables/EntityFrameworkCore.Projectables.csproj b/src/EntityFrameworkCore.Projectables/EntityFrameworkCore.Projectables.csproj index 079b595..9aa456d 100644 --- a/src/EntityFrameworkCore.Projectables/EntityFrameworkCore.Projectables.csproj +++ b/src/EntityFrameworkCore.Projectables/EntityFrameworkCore.Projectables.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0;net6.0 README.md diff --git a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs index f1c4f65..57fe3aa 100644 --- a/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projectables/Services/ProjectableExpressionReplacer.cs @@ -128,8 +128,8 @@ namespace EntityFrameworkCore.Projectables.Services var self = _AddProjectableSelect(call.Arguments.First(), _entityType); return call.Update(null, call.Arguments.Skip(1).Prepend(self)); } - case QueryRootExpression root: - return _AddProjectableSelect(root, root.EntityType); + case QueryRootExpression root when _entityType != null: + return _AddProjectableSelect(root, _entityType); default: return ret; } @@ -214,7 +214,11 @@ namespace EntityFrameworkCore.Projectables.Services protected override Expression VisitExtension(Expression node) { +#if NET7_0_OR_GREATER + if (node is EntityQueryRootExpression root) +#else if (node is QueryRootExpression root) +#endif { _entityType = root.EntityType; } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj index 2e4c691..0c895e5 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/EntityFrameworkCore.Projectables.FunctionalTests.csproj @@ -8,7 +8,7 @@ - +