diff --git a/src/EntityFrameworkCore.Projections/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs index fa25c3b..fae9e5c 100644 --- a/src/EntityFrameworkCore.Projections/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs +++ b/src/EntityFrameworkCore.Projections/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs @@ -13,7 +13,7 @@ namespace EntityFrameworkCore.Projections.Infrastructure.Internal { readonly IQueryTranslationPreprocessorFactory _decoratedFactory; readonly QueryTranslationPreprocessorDependencies _queryTranslationPreprocessorDependencies; - readonly ProjectableExpressionReplacer _projectableExpressionReplacer = new(); + readonly ProjectableExpressionReplacer _projectableExpressionReplacer = new(new ProjectionExpressionResolver()); public CustomQueryTranslationPreprocessorFactory(IQueryTranslationPreprocessorFactory decoratedFactory, QueryTranslationPreprocessorDependencies queryTranslationPreprocessorDependencies) { diff --git a/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs b/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs index 79b3a7d..a075cc3 100644 --- a/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs +++ b/src/EntityFrameworkCore.Projections/Services/ExpressionArgumentReplacer.cs @@ -7,7 +7,7 @@ using System.Threading.Tasks; namespace EntityFrameworkCore.Projections.Services { - public class ExpressionArgumentReplacer : ExpressionVisitor + public sealed class ExpressionArgumentReplacer : ExpressionVisitor { readonly IEnumerable<(ParameterExpression parameter, Expression argument)>? _parameterArgumentMapping; diff --git a/src/EntityFrameworkCore.Projections/Services/IProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projections/Services/IProjectionExpressionResolver.cs new file mode 100644 index 0000000..220f938 --- /dev/null +++ b/src/EntityFrameworkCore.Projections/Services/IProjectionExpressionResolver.cs @@ -0,0 +1,10 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace EntityFrameworkCore.Projections.Services +{ + public interface IProjectionExpressionResolver + { + LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo); + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projections/Services/ParameterExtractor.cs b/src/EntityFrameworkCore.Projections/Services/ParameterExtractor.cs deleted file mode 100644 index edbb929..0000000 --- a/src/EntityFrameworkCore.Projections/Services/ParameterExtractor.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; - -namespace EntityFrameworkCore.Projections.Services -{ - public sealed class ParameterExtractor : ExpressionVisitor - { - List? _extractedParameters = null; - - protected override Expression VisitParameter(ParameterExpression node) - { - if (_extractedParameters is null) - { - _extractedParameters = new List(); - } - - _extractedParameters.Add(node); - return base.VisitParameter(node); - } - - public IEnumerable ExtractedParameters - => _extractedParameters ?? Enumerable.Empty(); - } -} diff --git a/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs b/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs index 8efefab..a0b6b53 100644 --- a/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs +++ b/src/EntityFrameworkCore.Projections/Services/ProjectableExpressionReplacer.cs @@ -7,13 +7,18 @@ using System.Threading.Tasks; namespace EntityFrameworkCore.Projections.Services { - public class ProjectableExpressionReplacer : ExpressionVisitor + public sealed class ProjectableExpressionReplacer : ExpressionVisitor { - readonly ProjectionExpressionResolver _resolver = new(); + readonly IProjectionExpressionResolver _resolver; + + public ProjectableExpressionReplacer(IProjectionExpressionResolver projectionExpressionResolver) + { + _resolver = projectionExpressionResolver; + } protected override Expression VisitMethodCall(MethodCallExpression node) { - if (node.Method.GetCustomAttributes(true).OfType().Any()) + if (node.Method.GetCustomAttributes(false).OfType().Any()) { var reflectedExpression = _resolver.FindGeneratedExpression(node.Method); diff --git a/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs index 41011a9..c3845a0 100644 --- a/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs +++ b/src/EntityFrameworkCore.Projections/Services/ProjectionExpressionResolver.cs @@ -10,7 +10,7 @@ using EntityFrameworkCore.Projections.Extensions; namespace EntityFrameworkCore.Projections.Services { - public sealed class ProjectionExpressionResolver + public sealed class ProjectionExpressionResolver : IProjectionExpressionResolver { readonly ConcurrentDictionary _lookupCache = new(); @@ -36,7 +36,7 @@ namespace EntityFrameworkCore.Projections.Services } return expressionFactoryMethod.Invoke(null, null) as LambdaExpression ?? throw new InvalidOperationException("Expected lambda"); - }); + }); } } } diff --git a/tests/EntityFrameworkCore.Projections.Tests/Services/ExpressionArgumentReplacerTests.cs b/tests/EntityFrameworkCore.Projections.Tests/Services/ExpressionArgumentReplacerTests.cs new file mode 100644 index 0000000..e3ac39f --- /dev/null +++ b/tests/EntityFrameworkCore.Projections.Tests/Services/ExpressionArgumentReplacerTests.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Services; +using Xunit; + +namespace EntityFrameworkCore.Projections.Tests.Services +{ + public class ExpressionArgumentReplacerTests + { + [Fact] + public void VisitParameter_MapsParametersWithArguments() + { + var parameter = Expression.Parameter(typeof(int)); + var argument = Expression.Constant(1); + var subject = new ExpressionArgumentReplacer(new[] { (parameter, (Expression)argument) }); + + var result = subject.Visit(parameter); + + Assert.Equal(argument, result); + } + + } +} diff --git a/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectableExpressionReplacerTests.cs b/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectableExpressionReplacerTests.cs index 7cac009..d9d6a1e 100644 --- a/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectableExpressionReplacerTests.cs +++ b/tests/EntityFrameworkCore.Projections.Tests/Services/ProjectableExpressionReplacerTests.cs @@ -2,12 +2,164 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; using System.Text; using System.Threading.Tasks; +using EntityFrameworkCore.Projections.Services; +using Xunit; namespace EntityFrameworkCore.Projections.Tests.Services { public class ProjectableExpressionReplacerTests { + public class ProjectableExpressionResolverStub : IProjectionExpressionResolver + { + readonly Func _implementation; + + public ProjectableExpressionResolverStub(Func implementation) + { + _implementation = implementation; + } + + public LambdaExpression FindGeneratedExpression(MemberInfo projectableMemberInfo) => _implementation(projectableMemberInfo); + } + + public class Entity + { + public int Id { get; set; } + + [Projectable] + public int SimpleProperty => 0; + + [Projectable] + public int SimpleMethod() => 0; + + [Projectable] + public int SimpleMethodWithArguments(int a, object b) => 0; + + [Projectable] + public int SimpleStatefullProperty => Id; + + [Projectable] + public int SimpleStatefullMethod() => Id; + + [Projectable] + public static int SimpleStaticMethod() => 0; + + [Projectable] + public static int SimpleStaticMethodWithArguments(int a, Entity b) => 0; + } + + [Fact] + public void VisitMember_SimpleProperty() + { + Expression> input = x => x.SimpleProperty; + Expression> expected = x => 0; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleMethod() + { + Expression> input = x => x.SimpleMethod(); + Expression> expected = x => 0; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleMethodWithArguments() + { + Expression> input = x => x.SimpleMethodWithArguments(1, true); + Expression> expected = x => 0; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleStatefullProperty() + { + Expression> input = x => x.SimpleStatefullProperty; + Expression> expected = x => x.Id; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleStatefullMethod() + { + Expression> input = x => x.SimpleStatefullMethod(); + Expression> expected = x => x.Id; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleStaticMethod() + { + Expression> input = x => Entity.SimpleStaticMethod(); + Expression> expected = x => 0; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } + + [Fact] + public void VisitMember_SimpleStaticMethodWithArguments() + { + Expression> input = x => Entity.SimpleStaticMethodWithArguments(0, x); + Expression> expected = x => 0; + + var resolver = new ProjectableExpressionResolverStub( + x => expected + ); + var subject = new ProjectableExpressionReplacer(resolver); + + var actual = subject.Visit(input); + + Assert.Equal(expected.ToString(), actual.ToString()); + } } }