Skip to content
This repository has been archived by the owner on Dec 29, 2020. It is now read-only.

Commit

Permalink
For #15
Browse files Browse the repository at this point in the history
Added initial orderby support covering the following possibilities:

* $orderby not specified
* $orderby=Price,Rating desc
* $orderby=Category/Name,Name,Price desc
* $orderby=Rating
* $orderby=Rating desc
  • Loading branch information
TrevorPilley committed Nov 13, 2020
1 parent 1b2bd66 commit 69285f2
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 9 deletions.
108 changes: 100 additions & 8 deletions Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,118 @@ public ODataQueryOptionsExtensionsTests()
_categories = new[]
{
new Category { Name = "Mobile Phones", Description = "The latest mobile phones"},
new Category { Name = "Phone Cases", Description = "Mobile phone cases"},
new Category { Name = "Accessories", Description = "Mobile phone accessories"},
};

_products = new[]
{
new Product { Category = _categories[0], Colour = Colour.Blue, Description = "iPhone SE 64GB Blue", Name = "iPhone SE 64GB Blue", Price = 419.00M, ProductId = 1, Rating = 4.67f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Blue, Description = "iPhone SE 128GB Blue", Name = "iPhone SE 128GB Blue", Price = 469.00M, ProductId = 2, Rating = 4.76f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Blue, Description = "iPhone SE 256GB Blue", Name = "iPhone SE 256GB Blue", Price = 569.00M, ProductId = 3, Rating = 4.43f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Red, Description = "iPhone SE 64GB Red", Name = "iPhone SE 64GB Red", Price = 419.00M, ProductId = 4, Rating = 4.68f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Red, Description = "iPhone SE 64GB Red", Name = "iPhone SE 64GB Red", Price = 419.00M, ProductId = 2, Rating = 4.68f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Green, Description = "iPhone SE 64GB Green", Name = "iPhone SE 64GB Green", Price = 419.00M, ProductId = 3, Rating = 4.25f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Blue, Description = "iPhone SE 128GB Blue", Name = "iPhone SE 128GB Blue", Price = 469.00M, ProductId = 4, Rating = 4.76f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Red, Description = "iPhone SE 128GB Red", Name = "iPhone SE 128GB Red", Price = 469.00M, ProductId = 5, Rating = 4.72f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Red, Description = "iPhone SE 256GB Red", Name = "iPhone SE 256GB Red", Price = 569.00M, ProductId = 6, Rating = 4.41f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Green, Description = "iPhone SE 64GB Green", Name = "iPhone SE 64GB Green", Price = 419.00M, ProductId = 7, Rating = 4.25f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Green, Description = "iPhone SE 128GB Green", Name = "iPhone SE 128GB Green", Price = 469.00M, ProductId = 8, Rating = 4.36f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Green, Description = "iPhone SE 128GB Green", Name = "iPhone SE 128GB Green", Price = 469.00M, ProductId = 6, Rating = 4.36f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Blue, Description = "iPhone SE 256GB Blue", Name = "iPhone SE 256GB Blue", Price = 569.00M, ProductId = 7, Rating = 4.43f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Red, Description = "iPhone SE 256GB Red", Name = "iPhone SE 256GB Red", Price = 569.00M, ProductId = 8, Rating = 4.41f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[0], Colour = Colour.Green, Description = "iPhone SE 256GB Green", Name = "iPhone SE 256GB Green", Price = 569.00M, ProductId = 9, Rating = 4.26f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[1], Colour = Colour.Blue, Description = "iPhone SE Silicone Case - Blue", Name = "iPhone SE Silicone Case - Blue", Price = 39.00M, ProductId = 10, Rating = 3.76f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[1], Colour = Colour.Red, Description = "iPhone SE Silicone Case - Red", Name = "iPhone SE Silicone Case - Red", Price = 39.00M, ProductId = 11, Rating = 3.24f, ReleaseDate = new DateTime(2020, 4, 24) },
new Product { Category = _categories[1], Colour = Colour.Green, Description = "iPhone SE Silicone Case - Green", Name = "iPhone SE Silicone Case - Green", Price = 39.00M, ProductId = 12, Rating = 3.87f, ReleaseDate = new DateTime(2020, 4, 24) },
};
}

[Fact]
public void ApplyTo_OrderBy_NotDeclared()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = queryOptions.ApplyTo(_products.AsQueryable()).ToList();

Assert.Equal(_products.Count, results.Count);

Assert.Equal(_products.Min(x => x.ProductId), ((dynamic)results[0]).ProductId);
Assert.Equal(_products.Max(x => x.ProductId), ((dynamic)results[results.Count - 1]).ProductId);
}

[Fact]
public void ApplyTo_OrderBy_Properties()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$orderby=Price,Rating desc",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = queryOptions.ApplyTo(_products.AsQueryable()).ToList();

Assert.Equal(_products.Count, results.Count);

IOrderedEnumerable<Product> orderedProducts = _products.OrderBy(x => x.Price).ThenByDescending(x => x.Rating);

Assert.Equal(orderedProducts.First().ProductId, ((dynamic)results[0]).ProductId);
Assert.Equal(orderedProducts.Last().ProductId, ((dynamic)results[results.Count - 1]).ProductId);
}

[Fact]
public void ApplyTo_OrderBy_Properties_IncludingPropertyPath()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$orderby=Category/Name,Name,Price desc",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = queryOptions.ApplyTo(_products.AsQueryable()).ToList();

Assert.Equal(_products.Count, results.Count);

IOrderedEnumerable<Product> orderedProducts = _products.OrderBy(x => x.Category.Name).ThenBy(x => x.Name).ThenByDescending(x => x.Price);

Assert.Equal(orderedProducts.First().ProductId, ((dynamic)results[0]).ProductId);
Assert.Equal(orderedProducts.Last().ProductId, ((dynamic)results[results.Count - 1]).ProductId);
}

[Fact]
public void ApplyTo_OrderBy_SingleProperty_Ascending()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$orderby=Rating",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = queryOptions.ApplyTo(_products.AsQueryable()).ToList();

Assert.Equal(_products.Count, results.Count);
Assert.Equal(_products.Min(x => x.Rating), ((dynamic)results[0]).Rating);
Assert.Equal(_products.Max(x => x.Rating), ((dynamic)results[results.Count - 1]).Rating);
}

[Fact]
public void ApplyTo_OrderBy_SingleProperty_Descending()
{
TestHelper.EnsureEDM();

var queryOptions = new ODataQueryOptions(
"?$orderby=Rating desc",
EntityDataModel.Current.EntitySets["Products"],
Mock.Of<IODataQueryOptionsValidator>());

IList<ExpandoObject> results = queryOptions.ApplyTo(_products.AsQueryable()).ToList();

Assert.Equal(_products.Count, results.Count);
Assert.Equal(_products.Max(x => x.Rating), ((dynamic)results[0]).Rating);
Assert.Equal(_products.Min(x => x.Rating), ((dynamic)results[results.Count - 1]).Rating);
}

[Fact]
public void ApplyTo_Select_NotDeclared()
{
Expand Down Expand Up @@ -93,7 +185,7 @@ public void ApplyTo_Select_Properties()
}

[Fact]
public void ApplyTo_Select_Properties_PlusPropertyPath()
public void ApplyTo_Select_Properties_IncludingPropertyPath()
{
TestHelper.EnsureEDM();

Expand Down
64 changes: 63 additions & 1 deletion Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Linq.Expressions;
using Net.Http.OData.Query;
using Net.Http.OData.Query.Expressions;

Expand Down Expand Up @@ -50,9 +51,60 @@ public static IEnumerable<ExpandoObject> ApplyTo(this ODataQueryOptions queryOpt
return ApplyToImpl(queryOptions, queryable);
}

private static IQueryable ApplyOrder(ODataQueryOptions queryOptions, IQueryable queryable)
{
if (queryOptions.OrderBy == null)
{
return queryable;
}

IQueryable q = queryable;

for (int i = 0; i < queryOptions.OrderBy.Properties.Count; i++)
{
OrderByProperty orderByProperty = queryOptions.OrderBy.Properties[i];
PropertyPath path = orderByProperty.PropertyPath;
Type entityType = queryOptions.EntitySet.EdmType.ClrType;
Type propertyType = path.Property.ClrProperty.PropertyType;

// the 'entity' in the lambda expression (entity => entity.Property)
ParameterExpression entityParameterExpression = Expression.Parameter(entityType, "entity");

// the 'property' in the lambda expression (entity => entity.Property)
MemberExpression propertyMemberExpression = Expression.Property(entityParameterExpression, path.Property.Name);

while (path.Next != null)
{
path = path.Next;
propertyMemberExpression = Expression.Property(propertyMemberExpression, path.Property.Name);
propertyType = path.Property.ClrProperty.PropertyType;
}

// Represents the lambda in the method argument "(entity => entity.Property)"
LambdaExpression lambdaExpression = Expression.Lambda(
typeof(Func<,>).MakeGenericType(entityType, propertyType),
propertyMemberExpression,
new ParameterExpression[] { entityParameterExpression });

// Represents the method call itself "OrderBy(entity => entity.Property)"
MethodCallExpression orderByCallExpression = Expression.Call(
typeof(Queryable),
OrderByMethodName(orderByProperty.Direction, i),
new Type[] { entityType, propertyType },
q.Expression,
lambdaExpression);

q = q.Provider.CreateQuery(orderByCallExpression);
}

return q;
}

private static IEnumerable<ExpandoObject> ApplyToImpl(ODataQueryOptions queryOptions, IQueryable queryable)
{
foreach (object entity in queryable)
IQueryable q = ApplyOrder(queryOptions, queryable);

foreach (object entity in q)
{
yield return BuildExpando(queryOptions, entity);
}
Expand Down Expand Up @@ -83,5 +135,15 @@ private static ExpandoObject BuildExpando(ODataQueryOptions queryOptions, object

return expandoObject;
}

private static string OrderByMethodName(OrderByDirection direction, int index)
{
if (direction == OrderByDirection.Ascending)
{
return index == 0 ? "OrderBy" : "ThenBy";
}

return index == 0 ? "OrderByDescending" : "ThenByDescending";
}
}
}

0 comments on commit 69285f2

Please sign in to comment.