diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryProvider.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryProvider.cs new file mode 100644 index 0000000..af392f2 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryProvider.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Transactions; +using EntityFrameworkCore.Projectables.Services; +using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Query.Internal; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "Needed")] + public sealed class CustomQueryProvider : IQueryCompiler + { + readonly IQueryCompiler _decoratedQueryCompiler; + readonly ProjectableExpressionReplacer _projectableExpressionReplacer; + + public CustomQueryProvider(IQueryCompiler decoratedQueryCompiler) + { + _decoratedQueryCompiler = decoratedQueryCompiler; + _projectableExpressionReplacer = new ProjectableExpressionReplacer(new ProjectionExpressionResolver()); + } + + public Func CreateCompiledAsyncQuery(Expression query) + => _decoratedQueryCompiler.CreateCompiledAsyncQuery(Expand(query)); + public Func CreateCompiledQuery(Expression query) + => _decoratedQueryCompiler.CreateCompiledQuery(Expand(query)); + public TResult Execute(Expression query) + => _decoratedQueryCompiler.Execute(Expand(query)); + public TResult ExecuteAsync(Expression query, CancellationToken cancellationToken) + => _decoratedQueryCompiler.ExecuteAsync(Expand(query), cancellationToken); + + Expression Expand(Expression expression) + => _projectableExpressionReplacer.Visit(expression); + } +} diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs deleted file mode 100644 index 2b54bcc..0000000 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomQueryTranslationPreprocessorFactory.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using System.Threading.Tasks; -using EntityFrameworkCore.Projectables.Services; -using Microsoft.EntityFrameworkCore.Query; - -namespace EntityFrameworkCore.Projectables.Infrastructure.Internal -{ - public class CustomQueryTranslationPreprocessorFactory : IQueryTranslationPreprocessorFactory - { - readonly IQueryTranslationPreprocessorFactory _decoratedFactory; - readonly QueryTranslationPreprocessorDependencies _queryTranslationPreprocessorDependencies; - readonly ProjectableExpressionReplacer _projectableExpressionReplacer = new(new ProjectionExpressionResolver()); - - public CustomQueryTranslationPreprocessorFactory(IQueryTranslationPreprocessorFactory decoratedFactory, QueryTranslationPreprocessorDependencies queryTranslationPreprocessorDependencies) - { - _decoratedFactory = decoratedFactory; - _queryTranslationPreprocessorDependencies = queryTranslationPreprocessorDependencies; - } - - public QueryTranslationPreprocessor Create(QueryCompilationContext queryCompilationContext) - => new CustomQueryTranslationPreprocessor(_queryTranslationPreprocessorDependencies, queryCompilationContext, _projectableExpressionReplacer); - } - - public class CustomQueryTranslationPreprocessor : QueryTranslationPreprocessor - { - readonly ProjectableExpressionReplacer _projectableExpressionReplacer; - - public CustomQueryTranslationPreprocessor(QueryTranslationPreprocessorDependencies dependencies, QueryCompilationContext queryCompilationContext, ProjectableExpressionReplacer projectableExpressionReplacer) : base(dependencies, queryCompilationContext) - { - _projectableExpressionReplacer = projectableExpressionReplacer; - } - - public override Expression Process(Expression query) - => base.Process(_projectableExpressionReplacer.Visit(query)); - } -} diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs index be8870a..15d055b 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -37,13 +37,13 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType!); } - var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryTranslationPreprocessorFactory)); + var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler)); if (targetDescriptor is null) { - throw new InvalidOperationException("No QueryTranslationPreprocessorFactory is configured yet. Please make sure to configure a database provider first"); ; + throw new InvalidOperationException("No QueryProvider is configured yet. Please make sure to configure a database provider first"); ; } - - var decoratorObjectFactory = ActivatorUtilities.CreateFactory(typeof(CustomQueryTranslationPreprocessorFactory), new [] { targetDescriptor.ServiceType }); + + var decoratorObjectFactory = ActivatorUtilities.CreateFactory(typeof(CustomQueryProvider), new [] { targetDescriptor.ServiceType }); services.Replace(ServiceDescriptor.Describe( targetDescriptor.ServiceType, @@ -52,7 +52,6 @@ namespace EntityFrameworkCore.Projectables.Infrastructure.Internal )); } - public void Validate(IDbContextOptions options) { } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectOverMethodTakingDbContext.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectOverMethodTakingDbContext.verified.txt new file mode 100644 index 0000000..18e88e9 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectOverMethodTakingDbContext.verified.txt @@ -0,0 +1,10 @@ +SELECT [t0].[Id], [t0].[RecordDate], [t0].[UserId] +FROM [User] AS [u] +LEFT JOIN ( + SELECT [t].[Id], [t].[RecordDate], [t].[UserId] + FROM ( + SELECT [o].[Id], [o].[RecordDate], [o].[UserId], ROW_NUMBER() OVER(PARTITION BY [o].[UserId] ORDER BY [o].[RecordDate] DESC) AS [row] + FROM [Order] AS [o] + ) AS [t] + WHERE [t].[row] <= 1 +) AS [t0] ON [u].[Id] = [t0].[UserId] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs index f777f36..84af05c 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs @@ -38,12 +38,18 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests public IEnumerable Last2Orders => Orders.OrderByDescending(x => x.RecordDate).Take(2); + [Projectable] + public EntityFrameworkCore.Projectables.FunctionalTests.ComplexModelTests.Order GetLastOrderFromExternalDbContext(DbContext dbContext) + => dbContext.Set().Where(x => x.UserId == Id).OrderByDescending(x => x.RecordDate).FirstOrDefault(); + } public class Order { public int Id { get; set; } + public int UserId { get; set; } + public DateTime RecordDate { get; set; } } @@ -69,5 +75,16 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task ProjectOverMethodTakingDbContext() + { + using var dbContext = new SampleDbContext(); + + var query = dbContext.Set() + .Select(x => x.GetLastOrderFromExternalDbContext(dbContext)); + + return Verifier.Verify(query.ToQueryString()); + } } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs index 92b6058..4b76d6c 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/EntityExtensions.cs @@ -1,4 +1,7 @@ -namespace EntityFrameworkCore.Projectables.FunctionalTests.ExtensionMethods +using System.Linq; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.ExtensionMethods { public static class EntityExtensions { @@ -7,5 +10,9 @@ [Projectable] public static int Foo(this Entity entity) => entity.Id + 1; + + [Projectable] + public static Entity? LeadingEntity(this Entity entity, DbContext dbContext) + => dbContext.Set().Where(y => y.Id > entity.Id).FirstOrDefault(); } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.ExtensionMethodAcceptingDbContext.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.ExtensionMethodAcceptingDbContext.verified.txt new file mode 100644 index 0000000..8e1bd72 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.ExtensionMethodAcceptingDbContext.verified.txt @@ -0,0 +1,7 @@ +SELECT [t].[Id] +FROM [Entity] AS [e] +OUTER APPLY ( + SELECT TOP(1) [e0].[Id] + FROM [Entity] AS [e0] + WHERE [e0].[Id] > [e].[Id] +) AS [t] \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs index acb060b..cf84943 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ExtensionsMethods/ExtensionMethodTests.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using EntityFrameworkCore.Projectables.FunctionalTests.Helpers; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Scaffolding.Metadata; using ScenarioTests; using VerifyXunit; using Xunit; @@ -36,5 +37,19 @@ namespace EntityFrameworkCore.Projectables.FunctionalTests.ExtensionMethods return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task ExtensionMethodAcceptingDbContext() + { + using var dbContext = new SampleDbContext(); + + var sampleQuery = dbContext.Set() + .Select(x => dbContext.Set().Where(y => y.Id > x.Id).FirstOrDefault()); + + var query = dbContext.Set() + .Select(x => x.LeadingEntity(dbContext)); + + return Verifier.Verify(query.ToQueryString()); + } } }