From 7e6fd8e9d7cd533335e2b6052b2a399a469a825d Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 9 Apr 2025 21:14:39 +0200 Subject: [PATCH 1/4] Handle global query filters (Fixes #81) --- .../Internal/CustomConventionSetPlugin.cs | 14 ++++++++++ ...rojectablesExpandQueryFiltersConvention.cs | 26 +++++++++++++++++++ .../Internal/ProjectionOptionsExtension.cs | 4 +++ ...odelTests.ProjectQueryFilters.verified.txt | 15 +++++++++++ .../ComplexModelTests.cs | 12 +++++++++ ...ampleUserWithGlobalQueryFilterDbContext.cs | 21 +++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs create mode 100644 src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs new file mode 100644 index 0000000..4bb5def --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class CustomConventionSetPlugin : IConventionSetPlugin +{ + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.ModelFinalizingConventions.Add(new ProjectablesExpandQueryFiltersConvention()); + + return conventionSet; + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs new file mode 100644 index 0000000..0b788e3 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs @@ -0,0 +1,26 @@ +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class ProjectablesExpandQueryFiltersConvention : IModelFinalizingConvention +{ + + /// + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var queryFilter = entityType.GetQueryFilter(); + if (queryFilter != null) + { + // Expands query filters + entityType.SetQueryFilter(queryFilter.ExpandProjectables() as LambdaExpression); + } + } + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs index 3cb2af7..7271a14 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; namespace EntityFrameworkCore.Projectables.Infrastructure.Internal { @@ -49,6 +50,9 @@ static object CreateTargetInstance(IServiceProvider services, ServiceDescriptor return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType!); } + // Custom convention to handle global query filters, etc + services.AddScoped(); + if (_compatibilityMode is CompatibilityMode.Full) { var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler)); diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt new file mode 100644 index 0000000..1739868 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt @@ -0,0 +1,15 @@ +SELECT [t0].[RecordDate] +FROM [User] AS [u] +INNER JOIN ( + SELECT [t].[RecordDate], [t].[UserId] + FROM ( + SELECT [o0].[RecordDate], [o0].[UserId], ROW_NUMBER() OVER(PARTITION BY [o0].[UserId] ORDER BY [o0].[RecordDate] DESC) AS [row] + FROM [Order] AS [o0] + ) AS [t] + WHERE [t].[row] <= 2 +) AS [t0] ON [u].[Id] = [t0].[UserId] +WHERE ( + SELECT TOP(1) [o].[Id] + FROM [Order] AS [o] + WHERE [u].[Id] = [o].[UserId] + ORDER BY [o].[RecordDate] DESC) > 100 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs index 6ce61b3..a87da15 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs @@ -86,5 +86,17 @@ public Task ProjectOverMethodTakingDbContext() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task ProjectQueryFilters() + { + using var dbContext = new SampleUserWithGlobalQueryFilterDbContext(); + + var query = dbContext.Set() + .SelectMany(x => x.Last2Orders) + .Select(x => x.RecordDate); + + return Verifier.Verify(query.ToQueryString()); + } } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs new file mode 100644 index 0000000..b8d284a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs @@ -0,0 +1,21 @@ +using EntityFrameworkCore.Projectables.Infrastructure; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.Helpers +{ + public class SampleUserWithGlobalQueryFilterDbContext : SampleDbContext + { + public SampleUserWithGlobalQueryFilterDbContext(CompatibilityMode compatibilityMode = CompatibilityMode.Full) : base(compatibilityMode) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => { + b.HasQueryFilter(u => u.LastOrder.Id > 100); + }); + } + } +} From 0e5c280548d56dfe7e7565ad49c893461f91656d Mon Sep 17 00:00:00 2001 From: Koen Date: Sun, 4 May 2025 20:29:23 +0100 Subject: [PATCH 2/4] Allow rolling forward --- global.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/global.json b/global.json index 20f482a..f15a959 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,6 @@ { "sdk": { - "version": "9.0.100" + "version": "9.0.100", + "rollForward": "latestMinor" } } \ No newline at end of file From 88c2db02d09ee61ded74ca816b26c1702cd66498 Mon Sep 17 00:00:00 2001 From: "fabien.menager" Date: Wed, 9 Apr 2025 21:14:39 +0200 Subject: [PATCH 3/4] Handle global query filters (Fixes #81) --- .../Internal/CustomConventionSetPlugin.cs | 14 ++++++++++ ...rojectablesExpandQueryFiltersConvention.cs | 26 +++++++++++++++++++ .../Internal/ProjectionOptionsExtension.cs | 4 +++ ...odelTests.ProjectQueryFilters.verified.txt | 15 +++++++++++ .../ComplexModelTests.cs | 12 +++++++++ ...ampleUserWithGlobalQueryFilterDbContext.cs | 21 +++++++++++++++ 6 files changed, 92 insertions(+) create mode 100644 src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs create mode 100644 src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt create mode 100644 tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs new file mode 100644 index 0000000..4bb5def --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/CustomConventionSetPlugin.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore.Metadata.Conventions; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class CustomConventionSetPlugin : IConventionSetPlugin +{ + public ConventionSet ModifyConventions(ConventionSet conventionSet) + { + conventionSet.ModelFinalizingConventions.Add(new ProjectablesExpandQueryFiltersConvention()); + + return conventionSet; + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs new file mode 100644 index 0000000..0b788e3 --- /dev/null +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectablesExpandQueryFiltersConvention.cs @@ -0,0 +1,26 @@ +using System.Linq.Expressions; +using EntityFrameworkCore.Projectables.Extensions; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Metadata.Conventions; + +namespace EntityFrameworkCore.Projectables.Infrastructure.Internal; + +public class ProjectablesExpandQueryFiltersConvention : IModelFinalizingConvention +{ + + /// + public void ProcessModelFinalizing( + IConventionModelBuilder modelBuilder, + IConventionContext context) + { + foreach (var entityType in modelBuilder.Metadata.GetEntityTypes()) + { + var queryFilter = entityType.GetQueryFilter(); + if (queryFilter != null) + { + // Expands query filters + entityType.SetQueryFilter(queryFilter.ExpandProjectables() as LambdaExpression); + } + } + } +} \ No newline at end of file diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs index ac593a9..29121fe 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -13,6 +13,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; namespace EntityFrameworkCore.Projectables.Infrastructure.Internal { @@ -54,6 +55,9 @@ static object CreateTargetInstance(IServiceProvider services, ServiceDescriptor return ActivatorUtilities.GetServiceOrCreateInstance(services, descriptor.ImplementationType!); } + // Custom convention to handle global query filters, etc + services.AddScoped(); + if (_compatibilityMode is CompatibilityMode.Full) { var targetDescriptor = services.FirstOrDefault(x => x.ServiceType == typeof(IQueryCompiler)); diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt new file mode 100644 index 0000000..1739868 --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.ProjectQueryFilters.verified.txt @@ -0,0 +1,15 @@ +SELECT [t0].[RecordDate] +FROM [User] AS [u] +INNER JOIN ( + SELECT [t].[RecordDate], [t].[UserId] + FROM ( + SELECT [o0].[RecordDate], [o0].[UserId], ROW_NUMBER() OVER(PARTITION BY [o0].[UserId] ORDER BY [o0].[RecordDate] DESC) AS [row] + FROM [Order] AS [o0] + ) AS [t] + WHERE [t].[row] <= 2 +) AS [t0] ON [u].[Id] = [t0].[UserId] +WHERE ( + SELECT TOP(1) [o].[Id] + FROM [Order] AS [o] + WHERE [u].[Id] = [o].[UserId] + ORDER BY [o].[RecordDate] DESC) > 100 \ No newline at end of file diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs index 6ce61b3..a87da15 100644 --- a/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/ComplexModelTests.cs @@ -86,5 +86,17 @@ public Task ProjectOverMethodTakingDbContext() return Verifier.Verify(query.ToQueryString()); } + + [Fact] + public Task ProjectQueryFilters() + { + using var dbContext = new SampleUserWithGlobalQueryFilterDbContext(); + + var query = dbContext.Set() + .SelectMany(x => x.Last2Orders) + .Select(x => x.RecordDate); + + return Verifier.Verify(query.ToQueryString()); + } } } diff --git a/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs b/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs new file mode 100644 index 0000000..b8d284a --- /dev/null +++ b/tests/EntityFrameworkCore.Projectables.FunctionalTests/Helpers/SampleUserWithGlobalQueryFilterDbContext.cs @@ -0,0 +1,21 @@ +using EntityFrameworkCore.Projectables.Infrastructure; +using Microsoft.EntityFrameworkCore; + +namespace EntityFrameworkCore.Projectables.FunctionalTests.Helpers +{ + public class SampleUserWithGlobalQueryFilterDbContext : SampleDbContext + { + public SampleUserWithGlobalQueryFilterDbContext(CompatibilityMode compatibilityMode = CompatibilityMode.Full) : base(compatibilityMode) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => { + b.HasQueryFilter(u => u.LastOrder.Id > 100); + }); + } + } +} From b204527db6b2096eafd31814fe77b5407150fc52 Mon Sep 17 00:00:00 2001 From: Koen Date: Sun, 4 May 2025 20:36:50 +0100 Subject: [PATCH 4/4] Removed duplicated using directive --- .../Infrastructure/Internal/ProjectionOptionsExtension.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs index 29121fe..fbfa4be 100644 --- a/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs +++ b/src/EntityFrameworkCore.Projectables/Infrastructure/Internal/ProjectionOptionsExtension.cs @@ -13,7 +13,6 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure; namespace EntityFrameworkCore.Projectables.Infrastructure.Internal {