Skip to content

Commit 05a0068

Browse files
authoredNov 14, 2024··
Fixes Issue #219: Filtering on members absent in $select. (#220)
1 parent 675de06 commit 05a0068

File tree

10 files changed

+186
-21
lines changed

10 files changed

+186
-21
lines changed
 

‎AutoMapper.AspNetCore.OData.EF6/AutoMapper.AspNetCore.OData.EF6.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\Visitors\MethodAppender.cs" Link="Visitors\MethodAppender.cs" />
4949
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\Visitors\ProjectionVisitor.cs" Link="Visitors\ProjectionVisitor.cs" />
5050
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\Visitors\ParameterReplacer.cs" Link="Visitors\ParameterReplacer.cs" />
51+
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\Visitors\ReplaceExpressionVisitor.cs" Link="Visitors\ReplaceExpressionVisitor.cs" />
5152
</ItemGroup>
5253

5354
<ItemGroup>
@@ -57,7 +58,7 @@
5758
<ItemGroup>
5859
<PackageReference Include="AutoMapper.Extensions.ExpressionMapping" Version="[6.0.4,7.0.0)" />
5960
<PackageReference Include="EntityFramework" Version="6.4.4" />
60-
<PackageReference Include="LogicBuilder.Expressions.Utils" Version="[6.0.0,7.0.0)" />
61+
<PackageReference Include="LogicBuilder.Expressions.Utils" Version="[6.0.3,7.0.0)" />
6162
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.4" />
6263
<PackageReference Include="MinVer" Version="4.3.0">
6364
<PrivateAssets>all</PrivateAssets>

‎AutoMapper.AspNetCore.OData.EF6/QueryableExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
197197
.BuildIncludes<TModel>(options.SelectExpand.GetSelects())
198198
.ToList(),
199199
querySettings?.ProjectionSettings
200-
).UpdateQueryableExpression(expansions, options.Context);
200+
).UpdateQueryableExpression(expansions, options.Context, mapper);
201201
}
202202

203203
private static IQueryable<TModel> GetQuery<TModel, TData>(this IQueryable<TData> query,

‎AutoMapper.AspNetCore.OData.EFCore/AutoMapper.AspNetCore.OData.EFCore.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@
2828
</ItemGroup>
2929

3030
<ItemGroup>
31-
<PackageReference Include="AutoMapper.Extensions.ExpressionMapping" Version="[7.0.0,8.0.0)" />
32-
<PackageReference Include="LogicBuilder.Expressions.Utils" Version="[6.0.0,7.0.0)" />
31+
<PackageReference Include="AutoMapper.Extensions.ExpressionMapping" Version="[7.0.2,8.0.0)" />
32+
<PackageReference Include="LogicBuilder.Expressions.Utils" Version="[6.0.3,7.0.0)" />
3333
<PackageReference Include="Microsoft.AspNetCore.OData" Version="8.2.4" />
3434
<PackageReference Include="MinVer" Version="4.3.0">
3535
<PrivateAssets>all</PrivateAssets>

‎AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs

+4-3
Original file line numberDiff line numberDiff line change
@@ -630,7 +630,7 @@ private static Expression<Func<TSource, object>> BuildSelectorExpression<TSource
630630
}
631631

632632
internal static IQueryable<TModel> UpdateQueryableExpression<TModel>(
633-
this IQueryable<TModel> query, List<List<ODataExpansionOptions>> expansions, ODataQueryContext context)
633+
this IQueryable<TModel> query, List<List<ODataExpansionOptions>> expansions, ODataQueryContext context, IMapper mapper)
634634
{
635635
List<List<ODataExpansionOptions>> filters = GetFilters();
636636
List<List<ODataExpansionOptions>> methods = GetQueryMethods();
@@ -643,7 +643,7 @@ internal static IQueryable<TModel> UpdateQueryableExpression<TModel>(
643643
if (methods.Any())
644644
expression = UpdateProjectionMethodExpression(expression);
645645

646-
if (filters.Any())//do filter last so it runs before a Skip or Take call.
646+
if (filters.Any())
647647
expression = UpdateProjectionFilterExpression(expression);
648648

649649
return query.Provider.CreateQuery<TModel>(expression);
@@ -656,7 +656,8 @@ Expression UpdateProjectionFilterExpression(Expression projectionExpression)
656656
(
657657
projectionExpression,
658658
filterList,
659-
context
659+
context,
660+
mapper
660661
)
661662
);
662663

‎AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
143143
.BuildIncludes<TModel>(options.SelectExpand.GetSelects())
144144
.ToList(),
145145
querySettings?.ProjectionSettings
146-
).UpdateQueryableExpression(expansions, options.Context);
146+
).UpdateQueryableExpression(expansions, options.Context, mapper);
147147
}
148148

149149
private static IQueryable<TModel> GetQuery<TModel, TData>(this IQueryable<TData> query,

‎AutoMapper.AspNetCore.OData.EFCore/Visitors/ChildCollectionFilterUpdater.cs

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,32 @@ namespace AutoMapper.AspNet.OData.Visitors
88
{
99
internal class ChildCollectionFilterUpdater : ProjectionVisitor
1010
{
11-
public ChildCollectionFilterUpdater(List<ODataExpansionOptions> expansions, ODataQueryContext context) : base(expansions)
11+
public ChildCollectionFilterUpdater(List<ODataExpansionOptions> expansions, ODataQueryContext context, IMapper mapper) : base(expansions)
1212
{
1313
this.context = context;
14+
this.mapper = mapper;
1415
}
1516

1617
private readonly ODataQueryContext context;
18+
private readonly IMapper mapper;
1719

18-
public static Expression UpdaterExpansion(Expression expression, List<ODataExpansionOptions> expansions, ODataQueryContext context)
19-
=> new ChildCollectionFilterUpdater(expansions, context).Visit(expression);
20+
public static Expression UpdaterExpansion(Expression expression, List<ODataExpansionOptions> expansions, ODataQueryContext context, IMapper mapper)
21+
=> new ChildCollectionFilterUpdater(expansions, context, mapper).Visit(expression);
2022

2123
protected override Expression GetBindingExpression(MemberAssignment binding, ODataExpansionOptions expansion)
2224
{
2325
if (expansion.FilterOptions != null)
2426
{
25-
return FilterAppender.AppendFilter(binding.Expression, expansion, context);
27+
return FilterAppender.AppendFilter(binding.Expression, expansion, context, mapper);
2628
}
2729
else if (expansions.Count > 1) //Mutually exclusive with expansion.Filter != null.
2830
{ //There can be only one filter in the list. See the GetFilters() method in QueryableExtensions.UpdateQueryable.
2931
return UpdaterExpansion
3032
(
3133
binding.Expression,
3234
expansions.Skip(1).ToList(),
33-
context
35+
context,
36+
mapper
3437
);
3538
}
3639
else

‎AutoMapper.AspNetCore.OData.EFCore/Visitors/FilterAppender.cs

+70-8
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,31 @@
1-
using LogicBuilder.Expressions.Utils;
1+
using AutoMapper.Extensions.ExpressionMapping;
2+
using AutoMapper.Internal;
3+
using LogicBuilder.Expressions.Utils;
24
using Microsoft.AspNetCore.OData.Query;
35
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
48
using System.Linq.Expressions;
59

610
namespace AutoMapper.AspNet.OData.Visitors
711
{
812
internal class FilterAppender : ExpressionVisitor
913
{
10-
public FilterAppender(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context)
14+
public FilterAppender(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context, IMapper mapper)
1115
{
1216
this.expansion = expansion;
1317
this.expression = expression;
1418
this.context = context;
19+
this.mapper = mapper;
1520
}
1621

1722
private readonly ODataExpansionOptions expansion;
1823
private readonly Expression expression;
1924
private readonly ODataQueryContext context;
25+
private readonly IMapper mapper;
2026

21-
public static Expression AppendFilter(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context)
22-
=> new FilterAppender(expression, expansion, context).Visit(expression);
27+
public static Expression AppendFilter(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context, IMapper mapper)
28+
=> new FilterAppender(expression, expansion, context, mapper).Visit(expression);
2329

2430
protected override Expression VisitMethodCall(MethodCallExpression node)
2531
{
@@ -28,14 +34,70 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
2834
&& elementType == node.Type.GetUnderlyingElementType()
2935
&& this.expression.ToString().StartsWith(node.ToString()))//makes sure we're not updating some nested "Select"
3036
{
37+
Type parentUnderlyingType = node.Arguments[0].Type.GetUnderlyingElementType();
38+
Type nodeUnderlyingType = elementType;
39+
LambdaExpression filter = GetFilterExpression();
40+
var replacedParent = GetNewParentExpression();
41+
var listOfArgumentsForNewMethod = GetArgumentsForNewMethod();
42+
3143
return Expression.Call
3244
(
3345
node.Method.DeclaringType,
34-
"Where",
35-
new Type[] { node.GetUnderlyingElementType() },
36-
node,
37-
expansion.FilterOptions.FilterClause.GetFilterExpression(elementType, context)
46+
node.Method.Name,
47+
node.Method.GetGenericArguments(),
48+
listOfArgumentsForNewMethod
3849
);
50+
51+
LambdaExpression GetFilterExpression()
52+
{
53+
LambdaExpression filterExpression = expansion.FilterOptions.FilterClause.GetFilterExpression(elementType, context);
54+
55+
if (parentUnderlyingType != nodeUnderlyingType)
56+
{
57+
var typeMap = mapper.ConfigurationProvider.Internal().ResolveTypeMap(sourceType: parentUnderlyingType, destinationType: nodeUnderlyingType);
58+
if (typeMap != null)
59+
{
60+
Type sourceType = typeof(Func<,>).MakeGenericType(nodeUnderlyingType, typeof(bool));
61+
Type destType = typeof(Func<,>).MakeGenericType(parentUnderlyingType, typeof(bool));
62+
Type sourceExpressionype = typeof(Expression<>).MakeGenericType(sourceType);
63+
Type destExpressionType = typeof(Expression<>).MakeGenericType(destType);
64+
filterExpression = mapper.MapExpression(filterExpression, sourceExpressionype, destExpressionType);
65+
}
66+
}
67+
68+
return filterExpression;
69+
}
70+
71+
Expression GetNewParentExpression()
72+
{
73+
return new ReplaceExpressionVisitor
74+
(
75+
node.Arguments[0],
76+
Expression.Call
77+
(
78+
node.Method.DeclaringType,
79+
"Where",
80+
[parentUnderlyingType],
81+
node.Arguments[0],
82+
filter
83+
)
84+
).Visit(node.Arguments[0]);
85+
}
86+
87+
Expression[] GetArgumentsForNewMethod()
88+
{
89+
return
90+
[
91+
.. node.Arguments.Aggregate(new List<Expression>(), (lst, next) =>
92+
{
93+
if (next == node.Arguments[0])
94+
lst.Add(replacedParent);
95+
else
96+
lst.Add(next);
97+
return lst;
98+
})
99+
];
100+
}
39101
}
40102

41103
return base.VisitMethodCall(node);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Linq.Expressions;
2+
3+
namespace AutoMapper.AspNet.OData.Visitors
4+
{
5+
internal class ReplaceExpressionVisitor : ExpressionVisitor
6+
{
7+
private readonly Expression _oldExpression;
8+
private readonly Expression _newExpression;
9+
10+
public ReplaceExpressionVisitor(Expression oldExpression, Expression newExpression)
11+
{
12+
_oldExpression = oldExpression;
13+
_newExpression = newExpression;
14+
}
15+
16+
public override Expression Visit(Expression node)
17+
{
18+
if (_oldExpression == node)
19+
return _newExpression;
20+
21+
return base.Visit(node);
22+
}
23+
}
24+
}

‎AutoMapper.OData.EF6.Tests/GetQueryTests.cs

+37
Original file line numberDiff line numberDiff line change
@@ -1095,6 +1095,43 @@ static void Test(ICollection<CategoryModel> collection)
10951095
}
10961096
}
10971097

1098+
[Fact]
1099+
public async void Filtering_On_Members_Not_Selected_In_Chiled_Collections()
1100+
{
1101+
string query = "/CategoryModel?$top=5&$expand=Products($select=ProductID;$filter=ProductName ne '';$top=1;$expand=AlternateAddresses($select=State;$filter=City ne ''))&$filter=CategoryName ne ''";
1102+
Test
1103+
(
1104+
Get<CategoryModel, Category>
1105+
(
1106+
query,
1107+
GetCategories()
1108+
)
1109+
);
1110+
Test
1111+
(
1112+
await GetAsync<CategoryModel, Category>
1113+
(
1114+
query,
1115+
GetCategories()
1116+
)
1117+
);
1118+
Test
1119+
(
1120+
await GetUsingCustomNameSpace<CategoryModel, Category>
1121+
(
1122+
query,
1123+
GetCategories()
1124+
)
1125+
);
1126+
1127+
static void Test(ICollection<CategoryModel> collection)
1128+
{
1129+
Assert.Equal(2, collection.Count);
1130+
Assert.Single(collection.First().Products);
1131+
Assert.Equal(2, collection.First().Products.First().AlternateAddresses.Count());
1132+
}
1133+
}
1134+
10981135
[Fact]
10991136
public async void FilteringOnRoot_ChildCollection_AndChildCollectionOfChildCollection_WithNoMatches_SortRoot_AndChildCollection_AndChildCollectionOfChildCollection()
11001137
{

‎AutoMapper.OData.EFCore.Tests/GetQueryTests.cs

+37
Original file line numberDiff line numberDiff line change
@@ -1148,6 +1148,43 @@ static void Test(ICollection<CategoryModel> collection)
11481148
}
11491149
}
11501150

1151+
[Fact]
1152+
public async void Filtering_On_Members_Not_Selected_In_Chiled_Collections()
1153+
{
1154+
string query = "/CategoryModel?$top=5&$expand=Products($select=ProductID;$filter=ProductName ne '';$top=1;$expand=AlternateAddresses($select=State;$filter=City ne ''))&$filter=CategoryName ne ''";
1155+
Test
1156+
(
1157+
Get<CategoryModel, Category>
1158+
(
1159+
query,
1160+
GetCategories()
1161+
)
1162+
);
1163+
Test
1164+
(
1165+
await GetAsync<CategoryModel, Category>
1166+
(
1167+
query,
1168+
GetCategories()
1169+
)
1170+
);
1171+
Test
1172+
(
1173+
await GetUsingCustomNameSpace<CategoryModel, Category>
1174+
(
1175+
query,
1176+
GetCategories()
1177+
)
1178+
);
1179+
1180+
static void Test(ICollection<CategoryModel> collection)
1181+
{
1182+
Assert.Equal(2, collection.Count);
1183+
Assert.Single(collection.First().Products);
1184+
Assert.Equal(2, collection.First().Products.First().AlternateAddresses.Count());
1185+
}
1186+
}
1187+
11511188
[Fact]
11521189
public async void FilteringOnRoot_ChildCollection_AndChildCollectionOfChildCollection_WithNoMatches_SortRoot_AndChildCollection_AndChildCollectionOfChildCollection()
11531190
{

0 commit comments

Comments
 (0)
Please sign in to comment.