Skip to content

Commit dc9f566

Browse files
authored
Adds support for $skip and $top with no order specified (#123)
* Generate stable order when skip used with no orderby * Find first orderable property * Fix unit tests * Add unit tests * Add unit tests * Cache attribute type * Clean up usings * Use context to find type and keys * Refactor * Move type extension to context extension * Refactor * Add guard * Add link to new code files * Add tests * Refactor * Use model to find entity * Rename functions * Throw exception if type has not been registered in EDM
1 parent fcb4eef commit dc9f566

File tree

18 files changed

+543
-63
lines changed

18 files changed

+543
-63
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@
3434
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\EdmTypeStructure.cs" Link="EdmTypeStructure.cs" />
3535
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\FilterHelper.cs" Link="FilterHelper.cs" />
3636
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\LinqExtensions.cs" Link="LinqExtensions.cs" />
37+
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\ODataQueryContextExtentions.cs" Link="ODataQueryContextExtentions.cs" />
3738
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\ODataQueryOptionsExtensions.cs" Link="ODataQueryOptionsExtensions.cs" />
3839
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\ODataSettings.cs" Link="ODataSettings.cs" />
40+
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\OrderBySetting.cs" Link="OrderBySetting.cs" />
3941
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\ProjectionSettings.cs" Link="ProjectionSettings.cs" />
4042
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\Properties\Resources.Designer.cs" Link="Properties\Resources.Designer.cs" />
4143
<Compile Include="..\AutoMapper.AspNetCore.OData.EFCore\QuerySettings.cs" Link="QuerySettings.cs" />

AutoMapper.AspNetCore.OData.EF6/QueryableExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
181181
.BuildIncludes<TModel>(options.SelectExpand.GetSelects())
182182
.ToList(),
183183
querySettings?.ProjectionSettings
184-
).UpdateQueryableExpression(expansions);
184+
).UpdateQueryableExpression(expansions, options.Context);
185185
}
186186

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

AutoMapper.AspNetCore.OData.EFCore/LinqExtensions.cs

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,15 @@ public static Expression<Func<IQueryable<T>, IQueryable<T>>> GetQueryableExpress
121121
);
122122
}
123123

124-
public static Expression GetOrderByMethod<T>(this Expression expression, ODataQueryOptions<T> options, ODataSettings oDataSettings = null)
125-
{
124+
public static Expression GetOrderByMethod<T>(this Expression expression,
125+
ODataQueryOptions<T> options, ODataSettings oDataSettings = null)
126+
{
126127
if (NoQueryableMethod(options, oDataSettings))
127128
return null;
128129

129130
return expression.GetQueryableMethod
130131
(
132+
options.Context,
131133
options.OrderBy?.OrderByClause,
132134
typeof(T),
133135
options.Skip?.Value,
@@ -148,26 +150,25 @@ public static Expression GetOrderByMethod<T>(this Expression expression, ODataQu
148150
? options.Top.Value
149151
: oDataSettings.PageSize;
150152
}
151-
}
152-
153-
private static bool NoQueryableMethod(ODataQueryOptions options, ODataSettings oDataSettings)
154-
=> options.OrderBy == null && options.Top == null && oDataSettings?.PageSize == null;
153+
}
155154

156-
public static Expression GetQueryableMethod(this Expression expression, OrderByClause orderByClause, Type type, int? skip, int? top)
157-
{
158-
if (orderByClause == null && !top.HasValue)
155+
public static Expression GetQueryableMethod(this Expression expression,
156+
ODataQueryContext context, OrderByClause orderByClause, Type type, int? skip, int? top)
157+
{
158+
if (orderByClause is null && skip is null && top is null)
159159
return null;
160160

161-
if (orderByClause == null)
162-
{
163-
return Expression.Call
164-
(
165-
expression.Type.IsIQueryable() ? typeof(Queryable) : typeof(Enumerable),
166-
"Take",
167-
new[] { type },
168-
expression,
169-
Expression.Constant(top.Value)
170-
);
161+
if (orderByClause is null && (skip is not null || top is not null))
162+
{
163+
var orderBySettings = context.FindSortableProperties(type);
164+
165+
if (orderBySettings is null)
166+
return null;
167+
168+
return expression
169+
.GetDefaultOrderByCall(orderBySettings)
170+
.GetSkipCall(skip)
171+
.GetTakeCall(top);
171172
}
172173

173174
return expression
@@ -176,6 +177,33 @@ public static Expression GetQueryableMethod(this Expression expression, OrderByC
176177
.GetTakeCall(top);
177178
}
178179

180+
private static bool NoQueryableMethod(ODataQueryOptions options, ODataSettings oDataSettings)
181+
=> options.OrderBy is null
182+
&& options.Top is null
183+
&& options.Skip is null
184+
&& oDataSettings?.PageSize is null;
185+
186+
187+
private static Expression GetDefaultThenByCall(this Expression expression, OrderBySetting settings)
188+
{
189+
return settings.ThenBy is null
190+
? GetMethodCall()
191+
: GetMethodCall().GetDefaultThenByCall(settings.ThenBy);
192+
193+
Expression GetMethodCall() =>
194+
expression.GetOrderByCall(settings.Name, nameof(Queryable.ThenBy));
195+
}
196+
197+
private static Expression GetDefaultOrderByCall(this Expression expression, OrderBySetting settings)
198+
{
199+
return settings.ThenBy is null
200+
? GetMethodCall()
201+
: GetMethodCall().GetDefaultThenByCall(settings.ThenBy);
202+
203+
Expression GetMethodCall() =>
204+
expression.GetOrderByCall(settings.Name, nameof(Queryable.OrderBy));
205+
}
206+
179207
private static Expression GetOrderByCall(this Expression expression, OrderByClause orderByClause)
180208
{
181209
const string OrderBy = "OrderBy";
@@ -572,7 +600,8 @@ private static Expression<Func<TSource, object>> BuildSelectorExpression<TSource
572600
);
573601
}
574602

575-
internal static IQueryable<TModel> UpdateQueryableExpression<TModel>(this IQueryable<TModel> query, List<List<ODataExpansionOptions>> expansions)
603+
internal static IQueryable<TModel> UpdateQueryableExpression<TModel>(
604+
this IQueryable<TModel> query, List<List<ODataExpansionOptions>> expansions, ODataQueryContext context)
576605
{
577606
List<List<ODataExpansionOptions>> filters = GetFilters();
578607
List<List<ODataExpansionOptions>> methods = GetQueryMethods();
@@ -611,7 +640,8 @@ Expression UpdateProjectionMethodExpression(Expression projectionExpression)
611640
methodList => projectionExpression = ChildCollectionOrderByUpdater.UpdaterExpansion
612641
(
613642
projectionExpression,
614-
methodList
643+
methodList,
644+
context
615645
)
616646
);
617647

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using Microsoft.AspNetCore.OData.Query;
2+
using Microsoft.OData.Edm;
3+
using System;
4+
using System.Linq;
5+
6+
namespace AutoMapper.AspNet.OData
7+
{
8+
internal static class ODataQueryContextExtentions
9+
{
10+
public static OrderBySetting FindSortableProperties(this ODataQueryContext context, Type type)
11+
{
12+
context = context ?? throw new ArgumentNullException(nameof(context));
13+
14+
var entity = context.Model.FindDeclaredType(type.FullName) as IEdmEntityType;
15+
return entity is not null
16+
? FindProperties(entity)
17+
: throw new InvalidOperationException($"The type '{type.FullName}' has not been declared in the entity data model.");
18+
19+
20+
static OrderBySetting FindProperties(IEdmEntityType entity)
21+
{
22+
var propertyNames = entity.Key().Any() switch
23+
{
24+
true => entity.Key().Select(k => k.Name),
25+
false => entity.StructuralProperties()
26+
.Where(p => p.Type.IsPrimitive() && !p.Type.IsStream())
27+
.Select(p => p.Name)
28+
.OrderBy(n => n)
29+
.Take(1)
30+
};
31+
var orderBySettings = new OrderBySetting();
32+
propertyNames.Aggregate(orderBySettings, (settings, name) =>
33+
{
34+
if (settings.Name is null)
35+
{
36+
settings.Name = name;
37+
return settings;
38+
}
39+
settings.ThenBy = new() { Name = name };
40+
return settings.ThenBy;
41+
});
42+
return orderBySettings.Name is null ? null : orderBySettings;
43+
}
44+
45+
}
46+
}
47+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace AutoMapper.AspNet.OData
2+
{
3+
internal class OrderBySetting
4+
{
5+
public string Name { get; set; }
6+
public OrderBySetting ThenBy { get; set; }
7+
}
8+
}

AutoMapper.AspNetCore.OData.EFCore/QueryableExtensions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public static ICollection<TModel> Get<TModel, TData>(this IQueryable<TData> quer
3333

3434
public static async Task<ICollection<TModel>> GetAsync<TModel, TData>(this IQueryable<TData> query, IMapper mapper, ODataQueryOptions<TModel> options, QuerySettings querySettings = null)
3535
where TModel : class
36-
{
36+
{
3737
Expression<Func<TModel, bool>> filter = options.ToFilterExpression<TModel>(
3838
querySettings?.ODataSettings?.HandleNullPropagation ?? HandleNullPropagationOption.False,
3939
querySettings?.ODataSettings?.TimeZone);
@@ -126,6 +126,7 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
126126
Expression<Func<TModel, bool>> filter)
127127
where TModel : class
128128
{
129+
129130
var expansions = options.SelectExpand.GetExpansions(typeof(TModel));
130131

131132
return query.GetQuery
@@ -138,7 +139,7 @@ private static IQueryable<TModel> GetQueryable<TModel, TData>(this IQueryable<TD
138139
.BuildIncludes<TModel>(options.SelectExpand.GetSelects())
139140
.ToList(),
140141
querySettings?.ProjectionSettings
141-
).UpdateQueryableExpression(expansions);
142+
).UpdateQueryableExpression(expansions, options.Context);
142143
}
143144

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

AutoMapper.AspNetCore.OData.EFCore/TypeExtensions.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
using LogicBuilder.Expressions.Utils;
2+
using Microsoft.AspNetCore.OData.Query;
23
using Microsoft.OData.Edm;
34
using System;
45
using System.Collections.Generic;
6+
using System.ComponentModel.DataAnnotations;
7+
using System.Diagnostics;
58
using System.Linq;
69
using System.Reflection;
710

AutoMapper.AspNetCore.OData.EFCore/Visitors/ChildCollectionOrderByUpdater.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,37 @@
1-
using System;
1+
using Microsoft.AspNetCore.OData.Query;
2+
using System;
23
using System.Collections.Generic;
34
using System.Linq;
45
using System.Linq.Expressions;
5-
using System.Text;
66

77
namespace AutoMapper.AspNet.OData.Visitors
88
{
99
internal class ChildCollectionOrderByUpdater : ProjectionVisitor
1010
{
11-
public ChildCollectionOrderByUpdater(List<ODataExpansionOptions> expansions) : base(expansions)
11+
private readonly ODataQueryContext context;
12+
13+
public ChildCollectionOrderByUpdater(List<ODataExpansionOptions> expansions, ODataQueryContext context)
14+
: base(expansions)
1215
{
16+
this.context = context;
1317
}
1418

15-
public static Expression UpdaterExpansion(Expression expression, List<ODataExpansionOptions> expansions)
16-
=> new ChildCollectionOrderByUpdater(expansions).Visit(expression);
19+
public static Expression UpdaterExpansion(Expression expression, List<ODataExpansionOptions> expansions, ODataQueryContext context)
20+
=> new ChildCollectionOrderByUpdater(expansions, context).Visit(expression);
1721

1822
protected override Expression GetBindingExpression(MemberAssignment binding, ODataExpansionOptions expansion)
1923
{
2024
if (expansion.QueryOptions != null)
2125
{
22-
return MethodAppender.AppendQueryMethod(binding.Expression, expansion);
26+
return MethodAppender.AppendQueryMethod(binding.Expression, expansion, context);
2327
}
2428
else if (expansions.Count > 1) //Mutually exclusive with expansion.QueryOptions != null.
2529
{ //There can be only one set of QueryOptions in the list. See the GetQueryMethods() method in QueryableExtensions.UpdateQueryable.
2630
return UpdaterExpansion
27-
(
31+
(
2832
binding.Expression,
29-
expansions.Skip(1).ToList()
33+
expansions.Skip(1).ToList(),
34+
context
3035
);
3136
}
3237
else

AutoMapper.AspNetCore.OData.EFCore/Visitors/MethodAppender.cs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
using LogicBuilder.Expressions.Utils;
2+
using Microsoft.AspNetCore.OData.Query;
23
using System;
3-
using System.Collections.Generic;
44
using System.Linq.Expressions;
5-
using System.Text;
65

76
namespace AutoMapper.AspNet.OData.Visitors
87
{
98
internal class MethodAppender : ExpressionVisitor
109
{
11-
public MethodAppender(Expression expression, ODataExpansionOptions expansion)
10+
private readonly ODataQueryContext context;
11+
12+
public MethodAppender(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context)
1213
{
1314
this.expansion = expansion;
1415
this.expression = expression;
16+
this.context = context;
1517
}
1618

1719
private readonly ODataExpansionOptions expansion;
1820
private readonly Expression expression;
1921

20-
public static Expression AppendQueryMethod(Expression expression, ODataExpansionOptions expansion)
21-
=> new MethodAppender(expression, expansion).Visit(expression);
22+
public static Expression AppendQueryMethod(Expression expression, ODataExpansionOptions expansion, ODataQueryContext context)
23+
=> new MethodAppender(expression, expansion, context).Visit(expression);
2224

2325
protected override Expression VisitMethodCall(MethodCallExpression node)
2426
{
@@ -29,6 +31,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
2931
{
3032
return node.GetQueryableMethod
3133
(
34+
context,
3235
expansion.QueryOptions.OrderByClause,
3336
elementType,
3437
expansion.QueryOptions.Skip,

AutoMapper.OData.EF6.Tests/Data/DataClasses.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,19 @@ public class Category
5858
{
5959
public int CategoryID { get; set; }
6060
public string CategoryName { get; set; }
61-
6261
public Product Product { get; set; }
63-
6462
public ICollection<Product> Products { get; set; }
65-
63+
public ICollection<CompositeKey> CompositeKeys { get; set; }
6664
public IEnumerable<Product> EnumerableProducts { get; set; }
6765
public IQueryable<Product> QueryableProducts { get; set; }
6866
}
6967

68+
public class CompositeKey
69+
{
70+
public int ID1 { get; set; }
71+
public int ID2 { get; set; }
72+
}
73+
7074
public class Address
7175
{
7276
public int AddressID { get; set; }

0 commit comments

Comments
 (0)