From 84014144da77bf5968f1bac1a662361f87d7d732 Mon Sep 17 00:00:00 2001 From: NazarovMikhail Date: Tue, 21 Nov 2023 11:19:03 +0500 Subject: [PATCH 1/3] Added a sorting by multiple columns able. --- .../MultipleColumnPagingSortingTests.cs | 258 ++++++++++++++++++ .../Extensions/ExpressionHelperExtensions.cs | 30 ++ .../Extensions/PagingExtensions.cs | 46 +++- .../Extensions/QueryableExtensions.cs | 12 +- src/Monq.Core.Paging/Monq.Core.Paging.csproj | 2 +- 5 files changed, 336 insertions(+), 12 deletions(-) create mode 100644 src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs diff --git a/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs b/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs new file mode 100644 index 0000000..f10c61c --- /dev/null +++ b/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs @@ -0,0 +1,258 @@ +using Microsoft.AspNetCore.Http; +using System.Collections.Generic; +using Monq.Core.Paging.Extensions; +using System.Linq; +using Xunit; +using Monq.Core.Paging.Models; + +namespace Monq.Core.Paging.Tests; + + +public class MultipleColumnPagingSortingTests +{ + [Fact(DisplayName = "Проверка сортировки по нескольким полям. Проверка только первого поля (ASC).")] + public void ShouldProperlySortFirstPropertyAsc() + { + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCols = new[] { new SortColParameter { ColName = "FirstCol", Dir = "asc" } }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var data = SeedData(); + var result = data + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(8, result.Count()); + + var entity1 = result.FirstOrDefault(); + var entity2 = result.Skip(1).FirstOrDefault(); + var entity3 = result.Skip(2).FirstOrDefault(); + var entity4 = result.Skip(3).FirstOrDefault(); + var entity5 = result.Skip(4).FirstOrDefault(); + var entity6 = result.Skip(5).FirstOrDefault(); + var entity7 = result.Skip(6).FirstOrDefault(); + var entity8 = result.Skip(7).FirstOrDefault(); + + Assert.Equal("first-01", entity1.FirstCol); + Assert.Equal("first-01", entity2.FirstCol); + Assert.Equal("first-01", entity3.FirstCol); + Assert.Equal("first-01", entity4.FirstCol); + + Assert.Equal("first-02", entity5.FirstCol); + Assert.Equal("first-02", entity6.FirstCol); + Assert.Equal("first-02", entity7.FirstCol); + Assert.Equal("first-02", entity8.FirstCol); + } + + [Fact(DisplayName = "Проверка сортировки по нескольким полям. Проверка только первого поля (DESC).")] + public void ShouldProperlySortFirstPropertyDesc() + { + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCols = new[] { new SortColParameter { ColName = "FirstCol", Dir = "desc" } }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var data = SeedData(); + var result = data + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(8, result.Count()); + + var entity1 = result.FirstOrDefault(); + var entity2 = result.Skip(1).FirstOrDefault(); + var entity3 = result.Skip(2).FirstOrDefault(); + var entity4 = result.Skip(3).FirstOrDefault(); + var entity5 = result.Skip(4).FirstOrDefault(); + var entity6 = result.Skip(5).FirstOrDefault(); + var entity7 = result.Skip(6).FirstOrDefault(); + var entity8 = result.Skip(7).FirstOrDefault(); + + Assert.Equal("first-02", entity1.FirstCol); + Assert.Equal("first-02", entity2.FirstCol); + Assert.Equal("first-02", entity3.FirstCol); + Assert.Equal("first-02", entity4.FirstCol); + + Assert.Equal("first-01", entity5.FirstCol); + Assert.Equal("first-01", entity6.FirstCol); + Assert.Equal("first-01", entity7.FirstCol); + Assert.Equal("first-01", entity8.FirstCol); + } + + [Fact(DisplayName = "Проверка сортировки по нескольким полям. Проверка по трём полям (ASC).")] + public void ShouldProperlySortThreePropertiesAsc() + { + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCols = new[] + { + new SortColParameter { ColName = "FirstCol", Dir = "asc"}, + new SortColParameter { ColName = "SecondCol", Dir = "asc"}, + new SortColParameter { ColName = "ThirdCol", Dir = "asc"}, + }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var data = SeedData(); + var result = data + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(8, result.Count()); + + var entity1 = result.FirstOrDefault(); + var entity2 = result.Skip(1).FirstOrDefault(); + var entity3 = result.Skip(2).FirstOrDefault(); + var entity4 = result.Skip(3).FirstOrDefault(); + var entity5 = result.Skip(4).FirstOrDefault(); + var entity6 = result.Skip(5).FirstOrDefault(); + var entity7 = result.Skip(6).FirstOrDefault(); + var entity8 = result.Skip(7).FirstOrDefault(); + + Assert.Equal(1, entity1.Id); + Assert.Equal(2, entity2.Id); + Assert.Equal(3, entity3.Id); + Assert.Equal(4, entity4.Id); + Assert.Equal(5, entity5.Id); + Assert.Equal(6, entity6.Id); + Assert.Equal(7, entity7.Id); + Assert.Equal(8, entity8.Id); + } + + [Fact(DisplayName = "Проверка сортировки по нескольким полям. Проверка по трём полям (DESC).")] + public void ShouldProperlySortThreePropertiesDesc() + { + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCols = new[] + { + new SortColParameter { ColName = "FirstCol", Dir = "desc"}, + new SortColParameter { ColName = "SecondCol", Dir = "desc"}, + new SortColParameter { ColName = "ThirdCol", Dir = "desc"}, + }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var data = SeedData(); + var result = data + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(8, result.Count()); + + var entity1 = result.FirstOrDefault(); + var entity2 = result.Skip(1).FirstOrDefault(); + var entity3 = result.Skip(2).FirstOrDefault(); + var entity4 = result.Skip(3).FirstOrDefault(); + var entity5 = result.Skip(4).FirstOrDefault(); + var entity6 = result.Skip(5).FirstOrDefault(); + var entity7 = result.Skip(6).FirstOrDefault(); + var entity8 = result.Skip(7).FirstOrDefault(); + + Assert.Equal(8, entity1.Id); + Assert.Equal(7, entity2.Id); + Assert.Equal(6, entity3.Id); + Assert.Equal(5, entity4.Id); + Assert.Equal(4, entity5.Id); + Assert.Equal(3, entity6.Id); + Assert.Equal(2, entity7.Id); + Assert.Equal(1, entity8.Id); + } + + [Fact(DisplayName = "Проверка сортировки по нескольким полям. Первое,второе - (ASC), третье -(DESC).")] + public void ShouldProperlySortThreePropertiesAscDesc() + { + var paging = new Models.PagingModel + { + Page = 1, + PerPage = 10, + SortCols = new[] + { + new SortColParameter { ColName = "FirstCol", Dir = "asc"}, + new SortColParameter { ColName = "SecondCol", Dir = "asc"}, + new SortColParameter { ColName = "ThirdCol", Dir = "desc"}, + }, + }; + + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost", 5005); + httpContext.Request.Path = "/api/levels"; + + var data = SeedData(); + var result = data + .AsQueryable() + .WithPaging(paging, httpContext, x => x.Id); + + Assert.Equal(8, result.Count()); + + var entity1 = result.FirstOrDefault(); + var entity2 = result.Skip(1).FirstOrDefault(); + var entity3 = result.Skip(2).FirstOrDefault(); + var entity4 = result.Skip(3).FirstOrDefault(); + var entity5 = result.Skip(4).FirstOrDefault(); + var entity6 = result.Skip(5).FirstOrDefault(); + var entity7 = result.Skip(6).FirstOrDefault(); + var entity8 = result.Skip(7).FirstOrDefault(); + + Assert.Equal(2, entity1.Id); + Assert.Equal(1, entity2.Id); + Assert.Equal(4, entity3.Id); + Assert.Equal(3, entity4.Id); + Assert.Equal(6, entity5.Id); + Assert.Equal(5, entity6.Id); + Assert.Equal(8, entity7.Id); + Assert.Equal(7, entity8.Id); + } + + static IEnumerable SeedData() + { + var data = new List + { + new () { Id = 7, FirstCol = "first-02", SecondCol = "second-02", ThirdCol = "third-01" }, + new () { Id = 4, FirstCol = "first-01", SecondCol = "second-02", ThirdCol = "third-02" }, + new () { Id = 2, FirstCol = "first-01", SecondCol = "second-01", ThirdCol = "third-02" }, + new () { Id = 3, FirstCol = "first-01", SecondCol = "second-02", ThirdCol = "third-01" }, + new () { Id = 8, FirstCol = "first-02", SecondCol = "second-02", ThirdCol = "third-02" }, + new () { Id = 5, FirstCol = "first-02", SecondCol = "second-01", ThirdCol = "third-01" }, + new () { Id = 1, FirstCol = "first-01", SecondCol = "second-01", ThirdCol = "third-01" }, + new () { Id = 6, FirstCol = "first-02", SecondCol = "second-01", ThirdCol = "third-02" }, + }; + return data; + } +} + +public class TestEntity +{ + public int Id { get; set; } + public string FirstCol { get; set; } + public string SecondCol { get; set; } + public string ThirdCol { get; set; } +} diff --git a/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs b/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs index 73b5f0b..654afd0 100644 --- a/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs @@ -20,6 +20,16 @@ static class ExpressionHelperExtensions .First(method => method.Name == nameof(Queryable.OrderByDescending) && method.GetParameters().Length == 2); + static readonly MethodInfo _thenByMethod = + typeof(Queryable).GetMethods() + .First(method => method.Name == nameof(Queryable.ThenBy) + && method.GetParameters().Length == 2); + + static readonly MethodInfo _thenByDescMethod = + typeof(Queryable).GetMethods() + .First(method => method.Name == nameof(Queryable.ThenByDescending) + && method.GetParameters().Length == 2); + /// /// Отсортировать по возрастанию. /// @@ -38,6 +48,26 @@ public static IQueryable OrderBy(this IQueryable sour public static IQueryable OrderByDescending(this IQueryable source, LambdaExpression lambda) => _orderByDescMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); + /// + /// Performs a subsequent ordering in a sequence in ascending order. + /// + /// The type of the source. + /// The source. + /// The lambda. + /// + public static IQueryable ThenBy(this IQueryable source, LambdaExpression lambda) + => _thenByMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); + + /// + /// Performs a subsequent ordering in a sequence in descending order. + /// + /// The type of the source. + /// The source. + /// The lambda. + /// + public static IQueryable ThenByDescending(this IQueryable source, LambdaExpression lambda) + => _thenByDescMethod.Call>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda); + /// /// Вызвать метод с указанными параметрами. /// diff --git a/src/Monq.Core.Paging/Extensions/PagingExtensions.cs b/src/Monq.Core.Paging/Extensions/PagingExtensions.cs index 5ae35cf..ee37852 100644 --- a/src/Monq.Core.Paging/Extensions/PagingExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/PagingExtensions.cs @@ -338,16 +338,21 @@ static IQueryable ApplySortSearchAndPageFilter( if (!string.IsNullOrWhiteSpace(paging.Search) && searchExpression is not null) filteredData = data.Where(searchExpression); - IQueryable sortedAndFilteredData; - var sortCol = typeof(TSource).GetValidPropertyName(paging.SortCol); - if (!string.IsNullOrEmpty(sortCol) && sortCol.Length == paging.SortCol.Trim().Length) + List multipleSortParameters = new List(); + if (paging.SortCols != null && paging.SortCols.Any()) { - sortedAndFilteredData = filteredData.OrderByProperty(sortCol, paging.SortDir); + multipleSortParameters.AddRange(paging.SortCols); + } + else if (paging.SortCol is not null) + { + multipleSortParameters.Add(new SortColParameter + { + ColName = paging.SortCol, + Dir = paging.SortDir, + }); } - else if (defaultOrder is not null) - sortedAndFilteredData = filteredData.OrderBy(defaultOrder); - else - sortedAndFilteredData = filteredData; + + IQueryable sortedAndFilteredData = GetOrderedBy(filteredData, multipleSortParameters, defaultOrder); var totalItemsCount = data.Count(); var filteredItemsCount = data == filteredData ? totalItemsCount : sortedAndFilteredData.Count(); @@ -358,6 +363,31 @@ static IQueryable ApplySortSearchAndPageFilter( return sortedAndFilteredData; } + static IQueryable GetOrderedBy( + IQueryable query, + IEnumerable sortOptions, + Expression>? defaultOrder) + { + int count = 0; + foreach (var item in sortOptions) + { + var sortCol = typeof(TSource).GetValidPropertyName(item.ColName); + if (string.IsNullOrEmpty(sortCol) || sortCol.Length != item.ColName.Trim().Length) + continue; + + query = query.OrderByProperty( + propertyName: sortCol, + dir: item.Dir, + isSubsequent: count > 0); + count++; + } + + if (count == 0 && defaultOrder is not null) + query = query.OrderBy(defaultOrder); + + return query; + } + static IQueryable ApplySkipTakeAndGetPagination( this IQueryable sorteredAndFilteredData, PagingModel paging, diff --git a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs index 4ae4a46..645a1e1 100644 --- a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs @@ -15,7 +15,8 @@ public static class QueryableExtensions /// The data. /// Name of the field. /// The dir. - public static IQueryable OrderByProperty(this IQueryable data, string propertyName, string? dir) + /// The option whether sorting is a subsequent. + public static IQueryable OrderByProperty(this IQueryable data, string propertyName, string? dir, bool isSubsequent = false) { var par = Expression.Parameter(typeof(TSource), "col"); var propType = typeof(TSource).GetPropertyType(propertyName); @@ -31,8 +32,13 @@ public static IQueryable OrderByProperty(this IQueryable diff --git a/src/Monq.Core.Paging/Monq.Core.Paging.csproj b/src/Monq.Core.Paging/Monq.Core.Paging.csproj index 5471032..71139e3 100644 --- a/src/Monq.Core.Paging/Monq.Core.Paging.csproj +++ b/src/Monq.Core.Paging/Monq.Core.Paging.csproj @@ -27,7 +27,7 @@ - + From c436455aef7535770132a0b7774971910f6929a5 Mon Sep 17 00:00:00 2001 From: NazarovMikhail Date: Tue, 21 Nov 2023 11:23:59 +0500 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8E=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MultipleColumnPagingSortingTests.cs | 9 ++++----- src/Monq.Core.Paging/Extensions/QueryableExtensions.cs | 8 ++++---- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs b/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs index f10c61c..b8236be 100644 --- a/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs +++ b/src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs @@ -1,13 +1,12 @@ using Microsoft.AspNetCore.Http; -using System.Collections.Generic; using Monq.Core.Paging.Extensions; +using Monq.Core.Paging.Models; +using System.Collections.Generic; using System.Linq; using Xunit; -using Monq.Core.Paging.Models; namespace Monq.Core.Paging.Tests; - public class MultipleColumnPagingSortingTests { [Fact(DisplayName = "Проверка сортировки по нескольким полям. Проверка только первого поля (ASC).")] @@ -101,8 +100,8 @@ public void ShouldProperlySortThreePropertiesAsc() { Page = 1, PerPage = 10, - SortCols = new[] - { + SortCols = new[] + { new SortColParameter { ColName = "FirstCol", Dir = "asc"}, new SortColParameter { ColName = "SecondCol", Dir = "asc"}, new SortColParameter { ColName = "ThirdCol", Dir = "asc"}, diff --git a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs index 645a1e1..0838364 100644 --- a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs @@ -32,11 +32,11 @@ public static IQueryable OrderByProperty(this IQueryable Date: Tue, 21 Nov 2023 16:53:30 +0500 Subject: [PATCH 3/3] Fix the problem: case sensitive for a sort direction value. --- src/Monq.Core.Paging/Extensions/QueryableExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs index 0838364..d55be29 100644 --- a/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs +++ b/src/Monq.Core.Paging/Extensions/QueryableExtensions.cs @@ -31,7 +31,7 @@ public static IQueryable OrderByProperty(this IQueryable