Skip to content
This repository was archived by the owner on Feb 17, 2025. It is now read-only.

Commit fbcad8e

Browse files
author
Ihar Yakimush
committed
support order by
1 parent fc15581 commit fbcad8e

File tree

4 files changed

+250
-76
lines changed

4 files changed

+250
-76
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Linq;
2+
using Community.OData.Linq.xTests.SampleData;
3+
using Xunit;
4+
5+
namespace Community.OData.Linq.xTests
6+
{
7+
public class OrderByTests
8+
{
9+
[Fact]
10+
public void OrderByIdAsc()
11+
{
12+
var result = SimpleClass.CreateQuery().OData().OrderBy("Id asc").First();
13+
14+
Assert.Equal(1, result.Id);
15+
}
16+
17+
[Fact]
18+
public void OrderByIdDesc()
19+
{
20+
var result = SimpleClass.CreateQuery().Take(2).OData().OrderBy("Id desc").First();
21+
22+
Assert.Equal(2, result.Id);
23+
}
24+
25+
[Fact]
26+
public void OrderByIdDefault()
27+
{
28+
var result = SimpleClass.CreateQuery().OData().OrderBy("Id,Name").First();
29+
30+
Assert.Equal(1, result.Id);
31+
}
32+
}
33+
}
Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,12 @@
11
namespace Community.OData.Linq
22
{
33
using System;
4-
using System.Collections;
5-
using System.Collections.Generic;
64
using System.Linq;
7-
using System.Linq.Expressions;
85

9-
using Microsoft.Extensions.DependencyInjection;
10-
using Microsoft.OData.Edm;
11-
12-
public class ODataQuery<T> : IQueryable<T>, IQueryable
6+
public class ODataQueryOrdered<T> : ODataQuery<T>
137
{
14-
public ODataQuery(IQueryable inner, IServiceProvider serviceProvider)
15-
{
16-
this.Inner = inner ?? throw new ArgumentNullException(nameof(inner));
17-
this.ServiceProvider = serviceProvider;
18-
}
19-
20-
public IEdmModel EdmModel => this.ServiceProvider.GetRequiredService<IEdmModel>();
21-
22-
public Type ElementType => this.Inner.ElementType;
23-
24-
public Expression Expression => this.Inner.Expression;
25-
26-
public IQueryProvider Provider => this.Inner.Provider;
27-
28-
public IQueryable Inner { get; }
29-
public IServiceProvider ServiceProvider { get; }
30-
31-
public IEnumerator GetEnumerator()
32-
{
33-
return this.Inner.GetEnumerator();
34-
}
35-
36-
IEnumerator<T> IEnumerable<T>.GetEnumerator()
8+
public ODataQueryOrdered(IOrderedQueryable inner, IServiceProvider serviceProvider):base(inner,serviceProvider)
379
{
38-
return (IEnumerator<T>)this.Inner.GetEnumerator();
39-
}
10+
}
4011
}
4112
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
namespace Community.OData.Linq
2+
{
3+
using System;
4+
using System.Collections;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Linq.Expressions;
8+
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.OData.Edm;
11+
12+
public class ODataQuery<T> : IQueryable<T>, IOrderedQueryable<T>
13+
{
14+
public ODataQuery(IQueryable inner, IServiceProvider serviceProvider)
15+
{
16+
this.Inner = inner ?? throw new ArgumentNullException(nameof(inner));
17+
this.ServiceProvider = serviceProvider;
18+
}
19+
20+
public IEdmModel EdmModel => this.ServiceProvider.GetRequiredService<IEdmModel>();
21+
22+
public Type ElementType => this.Inner.ElementType;
23+
24+
public Expression Expression => this.Inner.Expression;
25+
26+
public IQueryProvider Provider => this.Inner.Provider;
27+
28+
public IQueryable Inner { get; }
29+
public IServiceProvider ServiceProvider { get; }
30+
31+
public IEnumerator GetEnumerator()
32+
{
33+
return this.Inner.GetEnumerator();
34+
}
35+
36+
IEnumerator<T> IEnumerable<T>.GetEnumerator()
37+
{
38+
return (IEnumerator<T>)this.Inner.GetEnumerator();
39+
}
40+
}
41+
}

Community.Data.OData.Linq/OdataLinqExtensions.cs

Lines changed: 173 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using Community.OData.Linq.Builder.Validators;
2+
using Community.OData.Linq.Common;
3+
using Community.OData.Linq.Properties;
24

35
namespace Community.OData.Linq
46
{
@@ -21,56 +23,14 @@ namespace Community.OData.Linq
2123

2224
public static class ODataLinqExtensions
2325
{
24-
private static readonly FilterQueryValidator Validator =
26+
private static readonly FilterQueryValidator FilterValidator =
2527
new FilterQueryValidator(new DefaultQuerySettings {EnableFilter = true});
2628

2729
/// <summary>
2830
/// The simplified options.
2931
/// </summary>
3032
private static readonly ODataSimplifiedOptions SimplifiedOptions = new ODataSimplifiedOptions();
3133

32-
public static ODataQuery<T> Filter<T>(this ODataQuery<T> query, string filterText, string entitySetName = null)
33-
{
34-
IEdmModel edmModel = query.EdmModel;
35-
36-
if (entitySetName == null)
37-
{
38-
entitySetName = typeof(T).Name;
39-
}
40-
41-
IEdmEntityContainer container =
42-
(IEdmEntityContainer)edmModel.SchemaElements.Single(
43-
e => e.SchemaElementKind == EdmSchemaElementKind.EntityContainer);
44-
45-
IEdmEntitySet entitySet = container.FindEntitySet(entitySetName);
46-
ODataPath path = new ODataPath(new EntitySetSegment(entitySet));
47-
48-
ODataQueryOptionParser queryOptionParser = new ODataQueryOptionParser(
49-
edmModel,
50-
path,
51-
new Dictionary<string, string> { { "$filter", filterText } },
52-
query.ServiceProvider);
53-
54-
ODataSettings settings = query.ServiceProvider.GetRequiredService<ODataSettings>();
55-
56-
// Workaround for strange behavior in QueryOptionsParserConfiguration constructor which set it to false always
57-
queryOptionParser.Resolver.EnableCaseInsensitive = settings.EnableCaseInsensitive;
58-
59-
FilterClause filterClause = queryOptionParser.ParseFilter();
60-
SingleValueNode filterExpression = filterClause.Expression.Accept(
61-
new ParameterAliasNodeTranslator(queryOptionParser.ParameterAliasNodes)) as SingleValueNode;
62-
filterExpression = filterExpression ?? new ConstantNode(null);
63-
filterClause = new FilterClause(filterExpression, filterClause.RangeVariable);
64-
Contract.Assert(filterClause != null);
65-
66-
Validator.Validate(filterClause, settings.ValidationSettings, edmModel);
67-
68-
Expression filter = FilterBinder.Bind(query, filterClause, typeof(T), query.ServiceProvider);
69-
var result = ExpressionHelpers.Where(query, filter, typeof(T));
70-
71-
return new ODataQuery<T>(result, query.ServiceProvider);
72-
}
73-
7434
/// <summary>
7535
/// Enable applying OData specific functions to query
7636
/// </summary>
@@ -101,7 +61,7 @@ public static ODataQuery<T> OData<T>(this IQueryable<T> query, Action<ODataSetti
10161

10262
ODataSettings settings = new ODataSettings();
10363
configuration?.Invoke(settings);
104-
64+
10565
ServiceContainer container = new ServiceContainer();
10666
container.AddService(typeof(IEdmModel), edmModel);
10767
container.AddService(typeof(ODataQuerySettings), settings.QuerySettings);
@@ -116,5 +76,174 @@ public static ODataQuery<T> OData<T>(this IQueryable<T> query, Action<ODataSetti
11676

11777
return dataQuery;
11878
}
79+
80+
public static ODataQuery<T> Filter<T>(this ODataQuery<T> query, string filterText, string entitySetName = null)
81+
{
82+
IEdmModel edmModel = query.EdmModel;
83+
84+
ODataQueryOptionParser queryOptionParser = GetParser(query, entitySetName,
85+
new Dictionary<string, string> {{"$filter", filterText}});
86+
87+
ODataSettings settings = query.ServiceProvider.GetRequiredService<ODataSettings>();
88+
89+
// Workaround for strange behavior in QueryOptionsParserConfiguration constructor which set it to false always
90+
queryOptionParser.Resolver.EnableCaseInsensitive = settings.EnableCaseInsensitive;
91+
92+
FilterClause filterClause = queryOptionParser.ParseFilter();
93+
SingleValueNode filterExpression = filterClause.Expression.Accept(
94+
new ParameterAliasNodeTranslator(queryOptionParser.ParameterAliasNodes)) as SingleValueNode;
95+
filterExpression = filterExpression ?? new ConstantNode(null);
96+
filterClause = new FilterClause(filterExpression, filterClause.RangeVariable);
97+
Contract.Assert(filterClause != null);
98+
99+
FilterValidator.Validate(filterClause, settings.ValidationSettings, edmModel);
100+
101+
Expression filter = FilterBinder.Bind(query, filterClause, typeof(T), query.ServiceProvider);
102+
var result = ExpressionHelpers.Where(query, filter, typeof(T));
103+
104+
return new ODataQuery<T>(result, query.ServiceProvider);
105+
}
106+
107+
public static IOrderedQueryable<T> OrderBy<T>(this ODataQuery<T> query, string orderbyText, string entitySetName = null)
108+
{
109+
IEdmModel edmModel = query.EdmModel;
110+
111+
ODataQueryOptionParser queryOptionParser = GetParser(query, entitySetName,
112+
new Dictionary<string, string> { { "$orderby", orderbyText } });
113+
114+
var orderByClause = queryOptionParser.ParseOrderBy();
115+
116+
orderByClause = TranslateParameterAlias(orderByClause, queryOptionParser);
117+
118+
ODataSettings settings = query.ServiceProvider.GetRequiredService<ODataSettings>();
119+
120+
IOrderedQueryable<T> result = (IOrderedQueryable<T>) OrderApplyToCore<T>(query, settings.QuerySettings, orderByClause, edmModel);
121+
122+
return new ODataQueryOrdered<T>(result,query.ServiceProvider);
123+
}
124+
125+
private static IOrderedQueryable OrderApplyToCore<T>(ODataQuery<T> query, ODataQuerySettings querySettings, OrderByClause orderByClause, IEdmModel model)
126+
{
127+
ICollection<OrderByNode> nodes = OrderByNode.CreateCollection(orderByClause);
128+
129+
bool alreadyOrdered = false;
130+
IQueryable querySoFar = query;
131+
132+
HashSet<object> propertiesSoFar = new HashSet<object>();
133+
HashSet<string> openPropertiesSoFar = new HashSet<string>();
134+
bool orderByItSeen = false;
135+
136+
foreach (OrderByNode node in nodes)
137+
{
138+
OrderByPropertyNode propertyNode = node as OrderByPropertyNode;
139+
OrderByOpenPropertyNode openPropertyNode = node as OrderByOpenPropertyNode;
140+
141+
if (propertyNode != null)
142+
{
143+
// Use autonomy class to achieve value equality for HasSet.
144+
var edmPropertyWithPath = new { propertyNode.Property, propertyNode.PropertyPath };
145+
OrderByDirection direction = propertyNode.Direction;
146+
147+
// This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows
148+
if (propertiesSoFar.Contains(edmPropertyWithPath))
149+
{
150+
throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, edmPropertyWithPath.PropertyPath));
151+
}
152+
153+
propertiesSoFar.Add(edmPropertyWithPath);
154+
155+
if (propertyNode.OrderByClause != null)
156+
{
157+
querySoFar = AddOrderByQueryForProperty(query, querySettings, propertyNode.OrderByClause, querySoFar, direction, alreadyOrdered);
158+
}
159+
else
160+
{
161+
querySoFar = ExpressionHelpers.OrderByProperty(querySoFar, model, edmPropertyWithPath.Property, direction, typeof(T), alreadyOrdered);
162+
}
163+
164+
alreadyOrdered = true;
165+
}
166+
else if (openPropertyNode != null)
167+
{
168+
// This check prevents queries with duplicate properties (e.g. $orderby=Id,Id,Id,Id...) from causing stack overflows
169+
if (openPropertiesSoFar.Contains(openPropertyNode.PropertyName))
170+
{
171+
throw new ODataException(Error.Format(SRResources.OrderByDuplicateProperty, openPropertyNode.PropertyPath));
172+
}
173+
174+
openPropertiesSoFar.Add(openPropertyNode.PropertyName);
175+
Contract.Assert(openPropertyNode.OrderByClause != null);
176+
querySoFar = AddOrderByQueryForProperty(query, querySettings, openPropertyNode.OrderByClause, querySoFar, openPropertyNode.Direction, alreadyOrdered);
177+
alreadyOrdered = true;
178+
}
179+
else
180+
{
181+
// This check prevents queries with duplicate nodes (e.g. $orderby=$it,$it,$it,$it...) from causing stack overflows
182+
if (orderByItSeen)
183+
{
184+
throw new ODataException(Error.Format(SRResources.OrderByDuplicateIt));
185+
}
186+
187+
querySoFar = ExpressionHelpers.OrderByIt(querySoFar, node.Direction, typeof(T), alreadyOrdered);
188+
alreadyOrdered = true;
189+
orderByItSeen = true;
190+
}
191+
}
192+
193+
return querySoFar as IOrderedQueryable;
194+
}
195+
196+
private static IQueryable AddOrderByQueryForProperty<T>(ODataQuery<T> query, ODataQuerySettings querySettings,
197+
OrderByClause orderbyClause, IQueryable querySoFar, OrderByDirection direction, bool alreadyOrdered)
198+
{
199+
//Context.UpdateQuerySettings(querySettings, query);
200+
201+
LambdaExpression orderByExpression =
202+
FilterBinder.Bind(query, orderbyClause, typeof(T), query.ServiceProvider);
203+
querySoFar = ExpressionHelpers.OrderBy(querySoFar, orderByExpression, direction, typeof(T),
204+
alreadyOrdered);
205+
return querySoFar;
206+
}
207+
208+
private static OrderByClause TranslateParameterAlias(OrderByClause orderBy, ODataQueryOptionParser queryOptionParser)
209+
{
210+
if (orderBy == null)
211+
{
212+
return null;
213+
}
214+
215+
SingleValueNode orderByExpression = orderBy.Expression.Accept(
216+
new ParameterAliasNodeTranslator(queryOptionParser.ParameterAliasNodes)) as SingleValueNode;
217+
orderByExpression = orderByExpression ?? new ConstantNode(null, "null");
218+
219+
return new OrderByClause(
220+
TranslateParameterAlias(orderBy.ThenBy, queryOptionParser),
221+
orderByExpression,
222+
orderBy.Direction,
223+
orderBy.RangeVariable);
224+
}
225+
226+
private static ODataQueryOptionParser GetParser<T>(ODataQuery<T> query,string entitySetName, Dictionary<string, string> raws)
227+
{
228+
IEdmModel edmModel = query.EdmModel;
229+
230+
if (entitySetName == null)
231+
{
232+
entitySetName = typeof(T).Name;
233+
}
234+
235+
IEdmEntityContainer container =
236+
(IEdmEntityContainer)edmModel.SchemaElements.Single(
237+
e => e.SchemaElementKind == EdmSchemaElementKind.EntityContainer);
238+
239+
IEdmEntitySet entitySet = container.FindEntitySet(entitySetName);
240+
ODataPath path = new ODataPath(new EntitySetSegment(entitySet));
241+
242+
return new ODataQueryOptionParser(
243+
edmModel,
244+
path,
245+
raws,
246+
query.ServiceProvider);
247+
}
119248
}
120249
}

0 commit comments

Comments
 (0)