From 234522519b2392b1ad43f1976a4615d4518ea774 Mon Sep 17 00:00:00 2001 From: Amir Emil Date: Sun, 16 Nov 2025 13:41:33 +0100 Subject: [PATCH 1/2] feature: add option apply filtering after projection --- .../ProjectionSettings.cs | 1 + .../QueryableExtensions.cs | 27 +++++-- .../GetQueryTests.cs | 78 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/AutoMapper.AspNetCore.OData.EFCore/ProjectionSettings.cs b/AutoMapper.AspNetCore.OData.EFCore/ProjectionSettings.cs index e5c606a..80c6a27 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/ProjectionSettings.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/ProjectionSettings.cs @@ -10,5 +10,6 @@ namespace AutoMapper.AspNet.OData public class ProjectionSettings { public object Parameters { get; set; } + public bool ApplyFilterAfterProjection { get; set; } } } diff --git a/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs b/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs index fcd6f4a..ed0d86b 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs @@ -154,19 +154,36 @@ private static IQueryable GetQuery(this IQueryable IEnumerable>> includeProperties = null, ProjectionSettings projectionSettings = null) { - Expression> f = mapper.MapExpression>>(filter); + var applyFilterAfterProjection = projectionSettings?.ApplyFilterAfterProjection ?? false; + Func, IQueryable> mappedQueryFunc = mapper.MapExpression, IQueryable>>>(queryFunc)?.Compile(); - if (filter != null) - query = query.Where(f); + if (applyFilterAfterProjection) + { + Expression> f = mapper.MapExpression>>(filter); - return mappedQueryFunc != null + var projectedQuery = mappedQueryFunc != null ? mapper.ProjectTo(mappedQueryFunc(query), projectionSettings?.Parameters, GetIncludes()) : mapper.ProjectTo(query, projectionSettings?.Parameters, GetIncludes()); + return filter != null ? projectedQuery.Where(f) : projectedQuery; + } + else + { + Expression> f = mapper.MapExpression>>(filter); + + if (filter != null) + query = query.Where(f); + + return mappedQueryFunc != null + ? mapper.ProjectTo(mappedQueryFunc(query), projectionSettings?.Parameters, GetIncludes()) + : mapper.ProjectTo(query, projectionSettings?.Parameters, GetIncludes()); + + } + Expression>[] GetIncludes() => includeProperties?.ToArray() ?? new Expression>[] { }; } - + private static void ApplyOptions(this IQueryable query, IMapper mapper, Expression> filter, ODataQueryOptions options, QuerySettings querySettings) { ApplyOptions(options, querySettings); diff --git a/AutoMapper.OData.EFCore.Tests/GetQueryTests.cs b/AutoMapper.OData.EFCore.Tests/GetQueryTests.cs index 8d4203b..e241efd 100644 --- a/AutoMapper.OData.EFCore.Tests/GetQueryTests.cs +++ b/AutoMapper.OData.EFCore.Tests/GetQueryTests.cs @@ -416,7 +416,85 @@ void Test(ICollection collection) Assert.Equal("Two", collection.First().Name); } } + [Fact] + public async Task FilterByDynamicParameterValueAfterProjectionReturnExpectedResults() + { + // Arrange + string buildingParameterValue = Guid.NewGuid().ToString(); + + var parameters = new { buildingParameter = buildingParameterValue }; + + var projectionSettings = new ProjectionSettings + { + Parameters = parameters, + //if false test will fail + ApplyFilterAfterProjection = true + }; + + var odataSettings = new ODataSettings + { + HandleNullPropagation = HandleNullPropagationOption.False + }; + + string query = $"/corebuilding?$filter=Parameter eq '{buildingParameterValue}'"; + + // Act + var result = await GetAsync( + query, + null, + new QuerySettings + { + ProjectionSettings = projectionSettings, + ODataSettings = odataSettings + }); + + // Assert + if (result.Count == 0) + { + throw new Xunit.Sdk.XunitException( + "Expected at least one CoreBuilding with matching Parameter value, " + + "but none were returned. This replicates the case where a runtime parameter " + + "(like CurrentUserId) was not propagated during OData filter translation." + ); + } + Assert.All(result, b => Assert.Equal(buildingParameterValue, b.Parameter)); + } + + [Fact] + public async Task FilterByDynamicParameterValueBeforeProjectionReturnZeroResults() + { + // Arrange + string buildingParameterValue = Guid.NewGuid().ToString(); + + var parameters = new { buildingParameter = buildingParameterValue }; + + var projectionSettings = new ProjectionSettings + { + Parameters = parameters, + ApplyFilterAfterProjection = false + }; + + var odataSettings = new ODataSettings + { + HandleNullPropagation = HandleNullPropagationOption.False + }; + + string query = $"/corebuilding?$filter=Parameter eq '{buildingParameterValue}'"; + + // Act + var result = await GetAsync( + query, + null, + new QuerySettings + { + ProjectionSettings = projectionSettings, + ODataSettings = odataSettings + }); + + // Assert + Assert.Empty(result); + } [Fact] public async Task BuildingExpandBuilderTenantFilterEqAndOrderByWithParameter() { From 6f1d497e95b5ad78888e5b8ab5a2f2e2ad95ec73 Mon Sep 17 00:00:00 2001 From: Blaise Taylor Date: Wed, 19 Nov 2025 17:57:00 -0500 Subject: [PATCH 2/2] No need to map the filter for projections. --- .../QueryableExtensions.cs | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs b/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs index ed0d86b..665fc35 100644 --- a/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs +++ b/AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs @@ -154,33 +154,21 @@ private static IQueryable GetQuery(this IQueryable IEnumerable>> includeProperties = null, ProjectionSettings projectionSettings = null) { - var applyFilterAfterProjection = projectionSettings?.ApplyFilterAfterProjection ?? false; - Func, IQueryable> mappedQueryFunc = mapper.MapExpression, IQueryable>>>(queryFunc)?.Compile(); - if (applyFilterAfterProjection) - { - Expression> f = mapper.MapExpression>>(filter); + if (filter != null && !FilterAfterProjection()) + query = query.Where(mapper.MapExpression>>(filter)); - var projectedQuery = mappedQueryFunc != null + var projectedQuery = mappedQueryFunc != null ? mapper.ProjectTo(mappedQueryFunc(query), projectionSettings?.Parameters, GetIncludes()) : mapper.ProjectTo(query, projectionSettings?.Parameters, GetIncludes()); - return filter != null ? projectedQuery.Where(f) : projectedQuery; - } - else - { - Expression> f = mapper.MapExpression>>(filter); - - if (filter != null) - query = query.Where(f); - - return mappedQueryFunc != null - ? mapper.ProjectTo(mappedQueryFunc(query), projectionSettings?.Parameters, GetIncludes()) - : mapper.ProjectTo(query, projectionSettings?.Parameters, GetIncludes()); + if (filter != null && FilterAfterProjection()) + projectedQuery = projectedQuery.Where(filter); - } + return projectedQuery; + bool FilterAfterProjection() => projectionSettings?.ApplyFilterAfterProjection ?? false; Expression>[] GetIncludes() => includeProperties?.ToArray() ?? new Expression>[] { }; }