From dea7d7bd2683e15f7737ad27c7a4847116dd7ded Mon Sep 17 00:00:00 2001 From: Trevor Pilley Date: Sun, 10 May 2020 16:14:31 +0100 Subject: [PATCH] For #15 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 --- .../Linq/ODataQueryOptionsExtensionsTests.cs | 108 ++++++++++++++++-- .../Linq/ODataQueryOptionsExtensions.cs | 64 ++++++++++- 2 files changed, 163 insertions(+), 9 deletions(-) diff --git a/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs b/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs index b1d0cd7..250cc5a 100644 --- a/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs +++ b/Net.Http.OData.Tests/Linq/ODataQueryOptionsExtensionsTests.cs @@ -21,19 +21,19 @@ 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) }, @@ -41,6 +41,98 @@ public ODataQueryOptionsExtensionsTests() }; } + [Fact] + public void ApplyTo_OrderBy_NotDeclared() + { + TestHelper.EnsureEDM(); + + var queryOptions = new ODataQueryOptions( + "?", + EntityDataModel.Current.EntitySets["Products"], + Mock.Of()); + + IList 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()); + + IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + + Assert.Equal(_products.Count, results.Count); + + IOrderedEnumerable 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()); + + IList results = queryOptions.ApplyTo(_products.AsQueryable()).ToList(); + + Assert.Equal(_products.Count, results.Count); + + IOrderedEnumerable 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()); + + IList 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()); + + IList 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() { @@ -93,7 +185,7 @@ public void ApplyTo_Select_Properties() } [Fact] - public void ApplyTo_Select_Properties_PlusPropertyPath() + public void ApplyTo_Select_Properties_IncludingPropertyPath() { TestHelper.EnsureEDM(); diff --git a/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs b/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs index a1f1d02..4968c68 100644 --- a/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs +++ b/Net.Http.OData/Linq/ODataQueryOptionsExtensions.cs @@ -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; @@ -50,9 +51,60 @@ public static IEnumerable 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 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); } @@ -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"; + } } }