在之前的 C# LINQ 排序介紹 (OrderBy, ThenBy) 文章中有介紹到要將清單排序可以使用 OrderBy 做第一層排序,然後再使用 ThenBy 做第二、三、四層和後面的排序。
雖然用起來很簡單,但是只能寫死(hard-coding),並且在特殊情境下很不方便。例如之前某個專案的其中一個搜尋功能客戶要求有四種排序方式,第一種是 A、B、C 欄位都遞增或遞減排序,第二種是 D、A 欄位都遞增或遞減排續...,算了直接看程式碼:
在上面的程式碼中會將原始查詢結果經過這個 Method 排序完再輸出,這樣寫看起來是正確的沒錯,但是就是超級躼躼等(冗長)
現在夜深人靜,正好是提升自己的最佳時間,上面都是寫死的,那有沒有辦法改為動態的呢?
範例清單:
可惜泛型不能寫成擴充方法,不然使用起來會更簡單。使用範例:
如果找不到屬性名稱就會略過排序,所以再寫死排序方式時我們也可以使用 nameof 指定欄位名稱,並免屬性名稱寫錯:
回到一開始的問題,客戶的特殊排序經過上面的動態排序工具類別簡化後可以變成這樣:
ㄜ...直接換成動態後好像沒有少多少程式碼,不過還可以再簡化:
還可以再簡化:
使用剛剛的動態排序工具類再經過簡化後真的少了很多程式碼,能夠專注在功能的實現,而且可以很方便的複用,很推薦這種寫法。
延伸閱讀: C# LINQ 從 0 到 1 基礎教學
雖然用起來很簡單,但是只能寫死(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 基礎教學
留言
張貼留言
如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com