C# 多欄位動態排序示範

在之前的 C# LINQ 排序介紹 (OrderBy, ThenBy) 文章中有介紹到要將清單排序可以使用 OrderBy 做第一層排序,然後再使用 ThenBy 做第二、三、四層和後面的排序。

雖然用起來很簡單,但是只能寫死(hard-coding),並且在特殊情境下很不方便。例如之前某個專案的其中一個搜尋功能客戶要求有四種排序方式,第一種是 A、B、C 欄位都遞增或遞減排序,第二種是 D、A 欄位都遞增或遞減排續...,算了直接看程式碼:
    
/// <summary>
/// 客戶要求的特殊查詢排序
/// </summary>
private IQueryable<MyDto> SortBySearchSortItem(
    IQueryable<MyDto> queryable,
    SearchSortItem sortItem,
    SortType sortType
) => sortItem switch
{
    SearchSortItem.A => sortType == SortType.Asc
        ? queryable
            .OrderBy(x => x.A)
            .ThenBy(x => x.B)
            .ThenBy(x => x.C)
        : queryable
            .OrderByDescending(x => x.A)
            .ThenByDescending(x => x.B)
            .ThenByDescending(x => x.C),
    SearchSortItem.B => sortType == SortType.Asc
        ? queryable
            .OrderBy(x => x.D)
            .ThenBy(x => x.A)
        : queryable
            .OrderByDescending(x => x.D)
            .ThenByDescending(x => x.A),
    SearchSortItem.C => sortType == SortType.Asc
        ? queryable
            .OrderBy(x => x.E)
            .ThenBy(x => x.A)
            .ThenBy(x => x.B)
        : queryable
            .OrderByDescending(x => x.E)
            .ThenByDescending(x => x.A)
            .ThenByDescending(x => x.B),
    SearchSortItem.D => sortType == SortType.Asc
        ? queryable
            .OrderBy(x => x.F)
            .ThenBy(x => x.A)
            .ThenBy(x => x.B)
        : queryable
            .OrderByDescending(x => x.F)
            .ThenByDescending(x => x.A)
            .ThenByDescending(x => x.B),
    _ => queryable
};
    

在上面的程式碼中會將原始查詢結果經過這個 Method 排序完再輸出,這樣寫看起來是正確的沒錯,但是就是超級躼躼等(冗長)

現在夜深人靜,正好是提升自己的最佳時間,上面都是寫死的,那有沒有辦法改為動態的呢?

單欄位動態排序

範例物件:
    
public class UserDto
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}
    

範例清單:
    
List<UserDto> list = new()
{
    new() { Id = 1, Name = "小美", Age = 18, },
    new() { Id = 2, Name = "小明", Age = 20, },
    new() { Id = 3, Name = "大頭", Age = 20, },
    new() { Id = 4, Name = "小華", Age = 18, },
};
    

    
string sortField = "Id";
string sortOrder = "desc";

var propertyInfo = typeof(UserDto).GetProperty(sortField);

if (sortOrder.ToLower() == "asc")
{
    list = list.OrderBy(x => propertyInfo?.GetValue(x, null)).ToList();
}
else
{
    list = list.OrderByDescending(x => propertyInfo?.GetValue(x, null)).ToList();
}

foreach (var user in list)
{
    Console.WriteLine($"Id: {user.Id}, Name: {user.Name}, Age: {user.Age}");
}

/*
Id: 4, Name: 小華, Age: 18
Id: 3, Name: 大頭, Age: 20
Id: 2, Name: 小明, Age: 20
Id: 1, Name: 小美, Age: 18
*/
    

多欄位動態排序

一樣是上面的範例,如果想要能夠動態的依照 Age 遞增、Id 遞減 該怎麼做?最好還是能夠讓任何類型複用(這樣後端開好後就可以直接讓前端指定要排序的欄位,省去很多事情,不過還是要限制可以排序的欄位)。再經過一翻調整後程式碼如下:
    
public enum SortType
{
    /// <summary>
    /// 遞增
    /// </summary>
    Asc,

    /// <summary>
    /// 遞減
    /// </summary>
    Desc
}
    

    
public class SortCriterion
{
    /// <summary>
    /// 排序的欄位名稱
    /// </summary>
    public string Field { get; set; } = null!;

    /// <summary>
    /// 排序方式
    /// </summary>
    public SortType SortType { get; set; } = SortType.Desc;
}
    

    
/// <summary>
/// 排序工具類
/// </summary>
/// <typeparam name="T">要排序的類型</typeparam>
public static class SortUtility<T>
{
    /// <summary>
    /// 依照排序條件動態排序
    /// </summary>
    /// <param name="list">來源資料</param>
    /// <param name="sortCriteria">排序條件</param>
    /// <returns></returns>
    public static List<T> SortListByCriteria(List<T> list, List<SortCriterion> sortCriteria)
    {
        IOrderedEnumerable<T>? orderedQuery = null;

        foreach (var sortCriterion in sortCriteria)
        {
            var propertyInfo = typeof(T).GetProperty(sortCriterion.Field);
            if (propertyInfo == null) continue;


            if (orderedQuery == null)
            {
                orderedQuery = sortCriterion.SortType == SortType.Asc
                    ? list.OrderBy(x => propertyInfo.GetValue(x, null))
                    : list.OrderByDescending(x => propertyInfo.GetValue(x, null));
            }
            else
            {
                orderedQuery = sortCriterion.SortType == SortType.Asc
                    ? orderedQuery.ThenBy(x => propertyInfo.GetValue(x, null))
                    : orderedQuery.ThenByDescending(x => propertyInfo.GetValue(x, null));
            }
        }

        return orderedQuery == null ? list : orderedQuery.ToList();
    }
}
    

可惜泛型不能寫成擴充方法,不然使用起來會更簡單。使用範例:
    
List<SortCriterion> sortCriteria = new()
{
    new() { Field = "Age", SortType = SortType.Asc },
    new() { Field = "Id", SortType = SortType.Desc },
};

var result = SortUtility<UserDto>.SortListByCriteria(list, sortCriteria);
foreach (var user in result)
{
    Console.WriteLine($"Id: {user.Id}, Name: {user.Name}, Age: {user.Age}");
}

/*
Id: 4, Name: 小華, Age: 18
Id: 1, Name: 小美, Age: 18
Id: 3, Name: 大頭, Age: 20
Id: 2, Name: 小明, Age: 20
*/
    

如果找不到屬性名稱就會略過排序,所以再寫死排序方式時我們也可以使用 nameof 指定欄位名稱,並免屬性名稱寫錯:
    
List<SortCriterion> sortCriteria = new()
{
    new() { Field = nameof(UserDto.Age), SortType = SortType.Asc },
    new() { Field = nameof(UserDto.Id), SortType = SortType.Desc },
};
    

回到一開始的問題,客戶的特殊排序經過上面的動態排序工具類別簡化後可以變成這樣:
    
/// <summary>
/// 客戶要求的特殊查詢排序
/// </summary>
private IQueryable<MyDto> SortBySearchSortItem(
    IQueryable<MyDto> queryable,
    SearchSortItem sortItem,
    SortType sortType
)
{
    return sortItem switch
    {
        SearchSortItem.A =>
            SortUtility<MyDto>.SortListByCriteria(queryable.ToList(),
                new()
                {
                    new() { Field = nameof(MyDto.A), SortType = sortType },
                    new() { Field = nameof(MyDto.B), SortType = sortType },
                    new() { Field = nameof(MyDto.C), SortType = sortType },
                }
            ).AsQueryable(),

        SearchSortItem.B =>
            SortUtility<MyDto>.SortListByCriteria(queryable.ToList(),
                new()
                {
                    new() { Field = nameof(MyDto.D), SortType = sortType },
                    new() { Field = nameof(MyDto.A), SortType = sortType },
                }
            ).AsQueryable(),
        SearchSortItem.C =>
            SortUtility<MyDto>.SortListByCriteria(queryable.ToList(),
                new()
                {
                    new() { Field = nameof(MyDto.E), SortType = sortType },
                    new() { Field = nameof(MyDto.A), SortType = sortType },
                    new() { Field = nameof(MyDto.B), SortType = sortType },
                }
            ).AsQueryable(),
        SearchSortItem.D =>
            SortUtility<MyDto>.SortListByCriteria(queryable.ToList(),
                new()
                {
                    new() { Field = nameof(MyDto.F), SortType = sortType },
                    new() { Field = nameof(MyDto.A), SortType = sortType },
                    new() { Field = nameof(MyDto.B), SortType = sortType },
                }
            ).AsQueryable(),
        _ => queryable
    };
}
    

ㄜ...直接換成動態後好像沒有少多少程式碼,不過還可以再簡化:
    
/// <summary>
/// 客戶要求的特殊查詢排序
/// </summary>
private IQueryable<MyDto> SortBySearchSortItem(
    IQueryable<MyDto> queryable,
    SearchSortItem sortItem,
    SortType sortType
)
{
    IQueryable<MyDto> Sort(List<SortCriterion> sortCriteria) =>
        SortUtility<MyDto>.SortListByCriteria(queryable.ToList(), sortCriteria).AsQueryable();


    return sortItem switch
    {
        SearchSortItem.A => Sort(new()
        {
            new() { Field = nameof(MyDto.A), SortType = sortType },
            new() { Field = nameof(MyDto.B), SortType = sortType },
            new() { Field = nameof(MyDto.C), SortType = sortType },
        }),

        SearchSortItem.B => Sort(new()
            {
                new() { Field = nameof(MyDto.D), SortType = sortType },
                new() { Field = nameof(MyDto.A), SortType = sortType },
            }
        ),
        SearchSortItem.C => Sort(new()
            {
                new() { Field = nameof(MyDto.E), SortType = sortType },
                new() { Field = nameof(MyDto.A), SortType = sortType },
                new() { Field = nameof(MyDto.B), SortType = sortType },
            }
        ),
        SearchSortItem.D => Sort(new()
            {
                new() { Field = nameof(MyDto.F), SortType = sortType },
                new() { Field = nameof(MyDto.A), SortType = sortType },
                new() { Field = nameof(MyDto.B), SortType = sortType },
            }
        ),
        _ => queryable
    };
}
    

還可以再簡化:
    
/// <summary>
/// 客戶要求的特殊查詢排序
/// </summary>
private IQueryable<MyDto> SortBySearchSortItem(
    IQueryable<MyDto> queryable,
    SearchSortItem sortItem,
    SortType sortType
)
{
    IQueryable<MyDto> Sort(string[] sortFields) =>
        SortUtility<MyDto>.SortListByCriteria(
            queryable.ToList(),
            sortFields.Select(x => new SortCriterion { Field = x, SortType = sortType }).ToList()
        ).AsQueryable();

    return sortItem switch
    {
        SearchSortItem.A => Sort(new[] { nameof(MyDto.A), nameof(MyDto.B), nameof(MyDto.C) }),
        SearchSortItem.B => Sort(new[] { nameof(MyDto.D), nameof(MyDto.A) }),
        SearchSortItem.C => Sort(new[] { nameof(MyDto.E), nameof(MyDto.A), nameof(MyDto.B) }),
        SearchSortItem.D => Sort(new[] { nameof(MyDto.F), nameof(MyDto.A), nameof(MyDto.B) }),
        _ => queryable
    };
}
    

使用剛剛的動態排序工具類再經過簡化後真的少了很多程式碼,能夠專注在功能的實現,而且可以很方便的複用,很推薦這種寫法。

延伸閱讀: C# LINQ 從 0 到 1 基礎教學

留言