Skip to content

Commit

Permalink
Merge pull request #7 from MONQDL/feature/SMCORE-13358_add_sort_by_mu…
Browse files Browse the repository at this point in the history
…ltiple_cols

[SMCORE-13358] Added a sorting by multiple columns.
  • Loading branch information
nazarov23892 authored Nov 21, 2023
2 parents 4b4fb3a + 2fce080 commit d8a7456
Show file tree
Hide file tree
Showing 5 changed files with 336 additions and 13 deletions.
257 changes: 257 additions & 0 deletions src/Monq.Core.Paging.Tests/MultipleColumnPagingSortingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,257 @@
using Microsoft.AspNetCore.Http;
using Monq.Core.Paging.Extensions;
using Monq.Core.Paging.Models;
using System.Collections.Generic;
using System.Linq;
using Xunit;

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<TestEntity> SeedData()
{
var data = new List<TestEntity>
{
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; }
}
30 changes: 30 additions & 0 deletions src/Monq.Core.Paging/Extensions/ExpressionHelperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

/// <summary>
/// Отсортировать по возрастанию.
/// </summary>
Expand All @@ -38,6 +48,26 @@ public static IQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> sour
public static IQueryable<TSource> OrderByDescending<TSource>(this IQueryable<TSource> source, LambdaExpression lambda)
=> _orderByDescMethod.Call<IQueryable<TSource>>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda);

/// <summary>
/// Performs a subsequent ordering in a sequence in ascending order.
/// </summary>
/// <typeparam name="TSource">The type of the source.</typeparam>
/// <param name="source">The source.</param>
/// <param name="lambda">The lambda.</param>
/// <returns></returns>
public static IQueryable<TSource> ThenBy<TSource>(this IQueryable<TSource> source, LambdaExpression lambda)
=> _thenByMethod.Call<IQueryable<TSource>>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda);

/// <summary>
/// Performs a subsequent ordering in a sequence in descending order.
/// </summary>
/// <typeparam name="TSource">The type of the source.</typeparam>
/// <param name="source">The source.</param>
/// <param name="lambda">The lambda.</param>
/// <returns></returns>
public static IQueryable<TSource> ThenByDescending<TSource>(this IQueryable<TSource> source, LambdaExpression lambda)
=> _thenByDescMethod.Call<IQueryable<TSource>>(new[] { typeof(TSource), lambda.ReturnType }, source, lambda);

/// <summary>
/// Вызвать метод с указанными параметрами.
/// </summary>
Expand Down
46 changes: 38 additions & 8 deletions src/Monq.Core.Paging/Extensions/PagingExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,16 +338,21 @@ static IQueryable<TSource> ApplySortSearchAndPageFilter<TSource, TSortKey>(
if (!string.IsNullOrWhiteSpace(paging.Search) && searchExpression is not null)
filteredData = data.Where(searchExpression);

IQueryable<TSource> sortedAndFilteredData;
var sortCol = typeof(TSource).GetValidPropertyName(paging.SortCol);
if (!string.IsNullOrEmpty(sortCol) && sortCol.Length == paging.SortCol.Trim().Length)
List<SortColParameter> multipleSortParameters = new List<SortColParameter>();
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<TSource> sortedAndFilteredData = GetOrderedBy(filteredData, multipleSortParameters, defaultOrder);

var totalItemsCount = data.Count();
var filteredItemsCount = data == filteredData ? totalItemsCount : sortedAndFilteredData.Count();
Expand All @@ -358,6 +363,31 @@ static IQueryable<TSource> ApplySortSearchAndPageFilter<TSource, TSortKey>(
return sortedAndFilteredData;
}

static IQueryable<TSource> GetOrderedBy<TSource, TSortKey>(
IQueryable<TSource> query,
IEnumerable<SortColParameter> sortOptions,
Expression<Func<TSource, TSortKey>>? 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<TSource> ApplySkipTakeAndGetPagination<TSource>(
this IQueryable<TSource> sorteredAndFilteredData,
PagingModel paging,
Expand Down
14 changes: 10 additions & 4 deletions src/Monq.Core.Paging/Extensions/QueryableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public static class QueryableExtensions
/// <param name="data">The data.</param>
/// <param name="propertyName">Name of the field.</param>
/// <param name="dir">The dir.</param>
public static IQueryable<TSource> OrderByProperty<TSource>(this IQueryable<TSource> data, string propertyName, string? dir)
/// <param name="isSubsequent">The option whether sorting is a subsequent.</param>
public static IQueryable<TSource> OrderByProperty<TSource>(this IQueryable<TSource> data, string propertyName, string? dir, bool isSubsequent = false)
{
var par = Expression.Parameter(typeof(TSource), "col");
var propType = typeof(TSource).GetPropertyType(propertyName);
Expand All @@ -30,9 +31,14 @@ public static IQueryable<TSource> OrderByProperty<TSource>(this IQueryable<TSour
// Декомпилируем свойства помеченные как Computed, чтобы EF мог их правильно воспринимать.
var lambda = Expression.Lambda(propExpr.Decompile().ExpressionCallsToConstants(), par);

Check warning on line 32 in src/Monq.Core.Paging/Extensions/QueryableExtensions.cs

View workflow job for this annotation

GitHub Actions / Build and Publish Library

Possible null reference argument for parameter 'expr' in 'Expression ExpressionHelperExtensions.Decompile(Expression expr)'.

Check warning on line 32 in src/Monq.Core.Paging/Extensions/QueryableExtensions.cs

View workflow job for this annotation

GitHub Actions / Build and Publish Library

Possible null reference argument for parameter 'expr' in 'Expression ExpressionHelperExtensions.Decompile(Expression expr)'.

if (string.IsNullOrEmpty(dir) || dir == "asc")
return data.OrderBy(lambda);
return data.OrderByDescending(lambda);
if (string.IsNullOrEmpty(dir) || dir.ToLower() == "asc")
return !isSubsequent
? data.OrderBy(lambda)
: data.ThenBy(lambda);

return !isSubsequent
? data.OrderByDescending(lambda)
: data.ThenByDescending(lambda);
}

/// <summary>
Expand Down
Loading

0 comments on commit d8a7456

Please sign in to comment.