如何在 .NET 中构建一个好用的动态查询生成器
前言
自從.NET Framework 3.5提供了LINQ之后,集合數(shù)據查詢基本被LINQ統(tǒng)一了。這大幅提高了編寫數(shù)據查詢代碼的效率和質量,但是在需要編寫動態(tài)查詢的時候反而很困難,特別是最常用的where和order by子句,他們的參數(shù)是Expression。編寫靜態(tài)查詢的時候編譯器會自動把代碼轉換成等價的表達式,而動態(tài)查詢無法借助編譯器完成表達式構建,只能手動拼接。想要正確拼接一個描述低級代碼結構的表達式對開發(fā)者的功力提出了較高的要求,哪怕是這方面的高手也容易翻車。
為了簡化查詢表達式的動態(tài)構建,社區(qū)出現(xiàn)了很多表達式生成輔助庫。其中最知名當屬System.Linq.Dynamic.Core和LinqKit。System.Linq.Dynamic.Core使用字符串定義表達式,并在內部轉換成Expression,LinqKit則是使用PredicateBuilder<T>把復雜表達式拆分成多個片段的組合。但是他們也存在一些不便之處,System.Linq.Dynamic.Core犧牲了代碼的靜態(tài)檢查能力,只有在運行時才知道表達式是否正確。如果把表達式作為允許前端填寫的參數(shù),不僅需要讓前端開發(fā)人員多學習一套表達式定義語法,還會產生安全漏洞。如果想提前檢查表達式的安全性,就需要對字符串進行分析。分析字符串生成表達式會成為一個流行庫的原因之一就是分析這個字符串很難,這樣一來相當于把外包出去的困難任務又拿回來了。LinqKit則是對前端不友好,這種類型無法序列化傳輸,如果想通過前端配合使用,還是需要再想辦法寫一套轉換代碼和配套的可序列化數(shù)據結構。
這兩個庫在傳輸序列化和動態(tài)拼接簡化方面各有顯著優(yōu)勢,也各有明顯不足。因此筆者開始思考是否有辦法開發(fā)一個便于序列化傳輸,安全性能得到靜態(tài)檢查保證,對于復雜表達式的拼接也能良好支持的表達式生成器。經過多次摸索,終有一些心得,在此分享給大家。
新書宣傳
有關新書的更多介紹歡迎查看《C#與.NET6 開發(fā)從入門到實踐》上市,作者親自來打廣告了!
雖然本書是基于.NET 6編寫的,但是其中大多數(shù)內容依然可用,仍然具有一定的參考價值。
正文
提煉基本概念
在安全的前提下提高靈活性
想要保證構建的查詢安全性,勢必要限制能夠查詢的屬性,最好能讓生成器中只出現(xiàn)可以查詢的屬性。為了避免底層屬性改名導致查詢出錯,或是隱藏代碼中的屬性名,暴露給生成器的名稱應該和真實屬性名解耦,兩者能獨立調整。對于查詢器中可能出現(xiàn)的特殊自定義條件提供自定義擴展點。最好支持靜態(tài)編譯檢查和基于自動重構的自動代碼調整。
/// <summary>
/// 查詢條件構造器接口
/// </summary>
public interface IFilterPredicateBuilder<T>
{
/// <summary>
/// 獲取查詢條件
/// </summary>
/// <returns>生成的查詢條件</returns>
Expression<Func<T, bool>>? GetWherePredicate();
}
基于以上假設,筆者提煉出了這個基本接口,用于生成器表示支持生成謂詞表達式。接口沒有任何額外內容以允許最大程度的自定義擴展。
為復雜的嵌套查詢提供支持
一個完備的表達式生成一定會面臨嵌套對象屬性的情況,這其中的問題在于,對象類型無窮無盡,相同類型的對象也可能出現(xiàn)在各種地方。如何訪問到需要的對象屬性并應用篩選條件就是一個需要仔細考慮的問題。在筆者看來,這個問題可以分解為兩個子問題,訪問屬性和應用條件。將這兩個部分分離開,條件就可以只針對最終類型開發(fā),屬性的訪問則交由外部決定。這樣一來,針對某種類型開發(fā)的條件就可以在任何地方的屬性上使用。
/// <summary>
/// 可組合的查詢條件構造器接口
/// </summary>
public interface IComposableFilterPredicateBuilder<T>
{
/// <summary>
/// 獲取查詢條件,并把條件應用到<typeparamref name="TOwner"/>類型的對象所擁有的<typeparamref name="T"/>類型的成員上。
/// </summary>
/// <typeparam name="TOwner">擁有<typeparamref name="T"/>類型的成員的類型</typeparam>
/// <param name="memberAccesser">成員訪問器</param>
/// <returns>已應用到成員的查詢條件</returns>
Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser);
}
/// <summary>
/// 值類型可組合的查詢條件構造器接口
/// </summary>
public interface IStructComposableFilterPredicateBuilder<T> : IComposableFilterPredicateBuilder<T>
where T : struct
{
/// <summary>
/// 獲取查詢條件,并把條件應用到<typeparamref name="TOwner"/>類型的對象所擁有的<typeparamref name="T"/>類型的成員上。
/// </summary>
/// <typeparam name="TOwner">擁有<typeparamref name="T"/>類型的成員的類型</typeparam>
/// <param name="memberAccesser">成員訪問器</param>
/// <returns>已應用到成員的查詢條件</returns>
Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccesser);
}
基于以上假設,可以再次提煉出一個接口。通過參數(shù)由外部決定屬性如何訪問,并返回最終拼合條件。值類型需要特殊處理。
為集合類型的查詢提供支持
有時要查詢的屬性可能是集合類型,這種查詢和普通的單值查詢有區(qū)別,需要單獨處理。
/// <summary>
/// 集合可組合的查詢條件構造器接口
/// </summary>
public interface ICollectionComposableFilterPredicateBuilder<T>
{
/// <summary>
/// 獲取查詢條件,并把條件應用到<typeparamref name="TOwner"/>類型的對象所擁有的<typeparamref name="T"/>類型的集合的成員上。
/// </summary>
/// <typeparam name="TOwner">擁有<typeparamref name="T"/>類型的集合的成員的類型</typeparam>
/// <param name="memberAccesser">成員訪問器</param>
/// <returns>已應用到成員的查詢條件</returns>
Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccesser);
}
這表示專門用于集合類型的查詢,IQueryable<T>實現(xiàn)了IEnumerable<T>,不需要單獨定義。
條件反轉
一鍵支持條件反轉是個非常有用的功能,如果一個條件有多個子條件,且條件之間混合了各種加了括號的且或非連接,想要正確反轉這樣條件非常容易困難。
/// <summary>
/// 可反轉條件接口
/// </summary>
public interface IPredicateReversible
{
/// <summary>
/// 是否反轉條件
/// </summary>
bool Reverse { get; }
}
使用一個bool標記反轉條件,由查詢生成器自動處理反轉是合理的選擇。
序列化傳輸支持
到此為止,一個完備的表達式生成器所需的基本接口就提煉完成了。但是這些接口所表達的概念并不支持序列化傳輸,接下來就要解決這問題。
序列化傳輸查詢條件意味著要分離出條件中可以序列化的部分。例如:Foo.Bar > 1,此處需要傳輸?shù)牟糠质菍傩裕容^方式,比較參數(shù)。屬性的話由于需要支持靜態(tài)檢查,需要單獨處理。對于比較方式,辦法比較多,筆者選擇使用枚舉來表達。關鍵字一般是各種基礎類型,應該天然支持序列化。
/// <summary>
/// 引用類型搜索關鍵字接口
/// </summary>
/// <typeparam name="T">關鍵字類型</typeparam>
public interface ISearchFilterClassKey<T> where T : class
{
/// <summary>
/// 搜索關鍵字
/// </summary>
ImmutableList<T?> Keys { get; }
}
/// <summary>
/// 值類型搜索關鍵字接口
/// </summary>
/// <typeparam name="T">關鍵字類型</typeparam>
public interface ISearchFilterStructKey<T> where T : struct
{
/// <summary>
/// 搜索關鍵字
/// </summary>
ImmutableList<T?> Keys { get; }
}
/// <summary>
/// 搜索操作符接口
/// </summary>
/// <typeparam name="TOperator"></typeparam>
public interface ISearchFilterOperator<TOperator> where TOperator : struct, Enum
{
/// <summary>
/// 搜索操作符
/// </summary>
TOperator Operator { get; }
}
基于以上假設,可以提煉出以上接口。
為概念接口提供實現(xiàn)
這些接口表達了查詢生成器所需的各種概念,但是讓開發(fā)者自行實現(xiàn)并不是好主意,這些接口對于開發(fā)者來說應該是用做泛型約束的。筆者勢必要為此提供一套最常見情形的實現(xiàn)。
/// <summary>
/// 查詢構造器基類
/// </summary>
/// <typeparam name="T">查詢的數(shù)據類型</typeparam>
/// <param name="CombineType">條件謂詞組合方式。Json屬性名用 combine 減少字數(shù)。</param>
/// <inheritdoc cref="IFilterPredicateBuilder{T}"/>
public abstract record QueryBuilderBase<T>(
[EnumDataType(typeof(PredicateCombineKind))]
PredicateCombineKind? CombineType = PredicateCombineKind.And)
: IFilterPredicateBuilder<T>, IComposableFilterPredicateBuilder<T>
{
private static readonly MethodInfo _logicallyDeletePredicateOfT = typeof(AuditableQueryPredicateExtensions)
.GetMethod(
nameof(AuditableQueryPredicateExtensions.GetLogicallyDeleteQueryPredicate),
BindingFlags.Public | BindingFlags.Static
)!;
private static readonly MethodInfo _DependencylogicallyDeletePredicateOfT = typeof(AuditableQueryPredicateExtensions)
.GetMethod(
nameof(AuditableQueryPredicateExtensions.GetDependencyLogicallyDeleteQueryPredicate),
BindingFlags.Public | BindingFlags.Static
)!;
/// <inheritdoc/>
public Expression<Func<T, bool>>? GetWherePredicate()
{
var where = BuildWherePredicate();
if (this is IPredicateReversible reversible) where = reversible.ApplyReversiblePredicate(where);
return where;
}
/// <summary>
/// 構造查詢條件
/// </summary>
/// <returns>獲得的查詢條件</returns>
/// <remarks>
/// 派生類重寫時請只負責構造自身的條件,
/// 最后使用<see cref="CombinePredicates"/>合并來自基類的條件后再返回。
/// 不要在這里進行條件反轉。
/// </remarks>
protected virtual Expression<Func<T, bool>>? BuildWherePredicate()
{
return null;
}
/// <summary>
/// 組合查詢條件
/// </summary>
/// <param name="predicates">待組合的子條件</param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
{
var predicate = predicates?.FirstOrDefault();
if (predicates?.Any() is true)
{
predicate = CombineType switch
{
PredicateCombineKind.And => predicates?.AndAlsoAll(),
PredicateCombineKind.Or => predicates?.OrElseAll(),
_ => throw new NotSupportedException(CombineType.ToString()),
};
}
return predicate;
}
/// <inheritdoc/>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccesser)
{
ArgumentNullException.ThrowIfNull(memberAccesser);
var where = GetWherePredicate();
if (where is null) return null;
MemberReplaceExpressionVisitor visitor = new();
var result = visitor.ReplaceMember(memberAccesser, where);
return result;
}
/// <summary>
/// 成員訪問表達式替換訪問器
/// </summary>
private sealed class MemberReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Lock _lock = new();
private volatile bool _calledFromReplaceMember = false;
private LambdaExpression? _memberAccesser;
/// <summary>
/// 把指定表達式的成員訪問替換為新的成員訪問。
/// </summary>
/// <typeparam name="TOwner">擁有<typeparamref name="TMember"/>類型的成員的類型</typeparam>
/// <typeparam name="TMember">用于替換成員訪問的類型</typeparam>
/// <typeparam name="TResult">返回值類型</typeparam>
/// <param name="memberAccessor">替換用的新成員訪問表達式。</param>
/// <param name="resultAccessor">要替換成員訪問的表達式。</param>
/// <returns>已替換成員訪問的表達式。</returns>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="ArgumentNullException"></exception>
public Expression<Func<TOwner, TResult>> ReplaceMember<TOwner, TMember, TResult>(
Expression<Func<TOwner, TMember>> memberAccessor,
Expression<Func<TMember, TResult>> resultAccessor)
{
ArgumentNullException.ThrowIfNull(resultAccessor);
ArgumentNullException.ThrowIfNull(memberAccessor);
lock (_lock)
{
try
{
_calledFromReplaceMember = true;
_memberAccesser = memberAccessor;
var newLambda = (LambdaExpression)Visit(resultAccessor);
return Expression.Lambda<Func<TOwner, TResult>>(newLambda.Body, memberAccessor.Parameters);
}
catch
{
throw;
}
finally
{
_calledFromReplaceMember = false;
_memberAccesser = null;
}
}
}
/// <inheritdoc/>
[return: NotNullIfNotNull(nameof(node))]
public override Expression? Visit(Expression? node)
{
if (!_calledFromReplaceMember) throw new InvalidOperationException($"Don't call directly, call {nameof(ReplaceMember)} instead.");
return base.Visit(node);
}
/// <inheritdoc/>
protected override Expression VisitMember(MemberExpression node)
{
if (node.Expression is ParameterExpression)
{
return Expression.PropertyOrField(_memberAccesser!.Body, node.Member.Name);
}
return base.VisitMember(node);
}
}
}
/// <summary>
/// 條件謂詞組合方式
/// </summary>
public enum PredicateCombineKind
{
/// <summary>
/// 且
/// </summary>
And = 1,
/// <summary>
/// 或
/// </summary>
Or = 2
}
/// <summary>
/// 可反轉謂詞接口擴展
/// </summary>
public static class PredicateReversibleExtensions
{
/// <summary>
/// 應用可反轉的謂詞
/// </summary>
/// <typeparam name="T">謂詞表達式的參數(shù)類型</typeparam>
/// <param name="reversible">可反轉謂詞接口的實例</param>
/// <param name="predicate">謂詞表達式</param>
/// <returns></returns>
public static Expression<Func<T, bool>>? ApplyReversiblePredicate<T>(
this IPredicateReversible reversible,
Expression<Func<T, bool>>? predicate)
{
return !reversible.Reverse ? predicate : predicate?.Not();
}
}
在接口的GetWherePredicate()方法之外,增加一個內部的BuildWherePredicate()方法,把生成基本條件和反轉條件隔離開并統(tǒng)一處理,確保反轉條件只會在最后進行一次。
實現(xiàn)通用的基本類型過濾器表達式
定義操作類型
/// <summary>
/// 基本搜索操作
/// </summary>
public enum BaseSearchOperator : uint
{
/// <summary>
/// 等于
/// </summary>
Equal = 1 << 0,
/// <summary>
/// 是候選項之一
/// </summary>
In = 1 << 1,
}
/// <summary>
/// 字符串搜索操作
/// </summary>
public enum StringSearchOperator : uint
{
/// <summary>
/// 等于
/// </summary>
Equal = 1 << 0,
/// <summary>
/// 是候選項之一
/// </summary>
In = 1 << 1,
/// <summary>
/// 包含
/// </summary>
Contains = 1 << 2,
/// <summary>
/// 包含全部候選項
/// </summary>
EqualContains = Equal | Contains,
/// <summary>
/// 包含候選項之一
/// </summary>
InContains = In | Contains,
/// <summary>
/// 開頭是
/// </summary>
StartsWith = 1 << 3,
/// <summary>
/// 開頭是候選項之一
/// </summary>
InStartsWith = In | StartsWith,
/// <summary>
/// 結尾是
/// </summary>
EndsWith = 1 << 4,
/// <summary>
/// 結尾是候選項之一
/// </summary>
InEndsWith = In | EndsWith,
}
/// <summary>
/// 可排序數(shù)字搜索操作
/// </summary>
public enum ComparableNumberSearchOperator : uint
{
/// <summary>
/// 等于
/// </summary>
Equal = 1 << 0,
/// <summary>
/// 是候選項之一
/// </summary>
In = 1 << 1,
/// <summary>
/// 小于
/// </summary>
LessThan = 1 << 2,
/// <summary>
/// 小于等于
/// </summary>
LessThanOrEqual = LessThan | Equal,
/// <summary>
/// 大于
/// </summary>
GreaterThan = 1 << 3,
/// <summary>
/// 大于等于
/// </summary>
GreaterThanOrEqual = GreaterThan | Equal,
/// <summary>
/// 介于兩個值之間,但不包含兩邊的邊界值
/// </summary>
BetweenOpen = 1 << 4,
/// <summary>
/// 是多組介于兩個值之間,但不包含兩邊的邊界值的候選區(qū)間之一
/// </summary>
InBetweenOpen = In | BetweenOpen,
/// <summary>
/// 介于兩個值之間,包含左邊界值,但不包含右邊界值
/// </summary>
BetweenLeftClosed = 1 << 5,
/// <summary>
/// 是多組介于兩個值之間,包含左邊界值,但不包含右邊界值的候選區(qū)間之一
/// </summary>
InBetweenLeftClosed = In | BetweenLeftClosed,
/// <summary>
/// 介于兩個值之間,包含右邊界值,但不包含左邊界值
/// </summary>
BetweenRightClosed = 1 << 6,
/// <summary>
/// 是多組介于兩個值之間,包含右邊界值,但不包含左邊界值的候選區(qū)間之一
/// </summary>
InBetweenRightClosed = In | BetweenRightClosed,
/// <summary>
/// 介于兩個值之間,同時包含兩邊的邊界值
/// </summary>
BetweenClosed = BetweenOpen | BetweenLeftClosed | BetweenRightClosed,
/// <summary>
/// 是多組介于兩個值之間,同時包含兩邊的邊界值的候選區(qū)間之一
/// </summary>
InBetweenClosed = In | BetweenClosed,
}
類似不等于這種操作使用等于和反轉條件的組合來表示。同時這些操作使用位枚舉讓每個位都能用于表達操作所具有的特征。
具體實現(xiàn)
/// <summary>
/// 值類型基本搜索過濾器
/// </summary>
/// <typeparam name="T">要搜索的值類型</typeparam>
public record StructSearchFilter<T>
: ISearchFilterStructKey<T>
, ISearchFilterOperator<BaseSearchOperator>
, IStructComposableFilterPredicateBuilder<T>
, IPredicateReversible
where T : struct
{
private static readonly Type _baseType = typeof(T);
private static readonly Type _nullableType = typeof(T?);
private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
.GetMethods()
.Where(static m => m.Name is nameof(Enumerable.Contains))
.Single(static m => m.GetParameters().Length is 2)
.MakeGenericMethod([_baseType]);
private static readonly MethodInfo _enumerableNullableContains = typeof(Enumerable)
.GetMethods()
.Where(static m => m.Name is nameof(Enumerable.Contains))
.Single(static m => m.GetParameters().Length is 2)
.MakeGenericMethod([_nullableType]);
/// <summary>
/// 初始化一個新實例
/// </summary>
/// <param name="keys">搜索關鍵字</param>
/// <param name="operator">搜索操作符</param>
/// <param name="reverse">是否反轉條件</param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidEnumArgumentException"></exception>
public StructSearchFilter(
ImmutableList<T?> keys,
[EnumDataType(typeof(BaseSearchOperator))]
BaseSearchOperator @operator = BaseSearchOperator.Equal,
bool reverse = false)
{
ArgumentNullException.ThrowIfNull(nameof(keys));
if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
if (@operator is BaseSearchOperator.In && keys is null or { Count: < 2 })
{
throw new ArgumentException($"當 {nameof(@operator)} 的值為 {@operator} 時必須設置多個元素。", nameof(keys));
}
else if (@operator is not BaseSearchOperator.In && keys is { Count: > 1 })
{
throw new ArgumentException($"當 {nameof(@operator)} 的值為 {@operator} 時必須設置一個元素。", nameof(keys));
}
else if (@operator is not (BaseSearchOperator.In or BaseSearchOperator.Equal) && keys.Any(static n => Equals(n, null)))
{
throw new ArgumentException($"當 {nameof(@operator)} 的值為 {@operator} 時元素的值不能為空。", nameof(keys));
}
Keys = keys;
Operator = @operator;
Reverse = reverse;
}
/// <inheritdoc/>
public virtual ImmutableList<T?> Keys { get; }
/// <inheritdoc/>
public virtual BaseSearchOperator Operator { get; }
/// <inheritdoc/>
public virtual bool Reverse { get; }
/// <inheritdoc/>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T>> memberAccessor)
{
if (Keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值為空的元素搜索值不能為空的成員。");
Expression newBody = Operator switch
{
BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _baseType)),
BaseSearchOperator.In => Expression.Call(null, _enumerableContains, [Expression.Constant(Keys.Cast<T>().ToList(), typeof(IEnumerable<T>)), memberAccessor.Body]),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
if (Reverse) newBody = Expression.Not(newBody);
return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
}
/// <inheritdoc/>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, T?>> memberAccessor)
{
Expression newBody = Operator switch
{
BaseSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(Keys.First(), _nullableType)),
BaseSearchOperator.In => Expression.Call(null, _enumerableNullableContains, [Expression.Constant(Keys, typeof(IEnumerable<T?>)), memberAccessor.Body]),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
if (Reverse) newBody = Expression.Not(newBody);
return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
}
}
/// <summary>
/// 布爾搜索過濾器
/// </summary>
public record BoolSearchFilter : StructSearchFilter<bool>
{
/// <inheritdoc/>
public BoolSearchFilter(
ImmutableList<bool?> keys,
[EnumDataType(typeof(BaseSearchOperator))]
BaseSearchOperator @operator = BaseSearchOperator.Equal,
bool reversePredicate = false) : base(keys, @operator, reversePredicate)
{
}
}
/// <summary>
/// 可排序數(shù)字搜索過濾器
/// </summary>
/// <typeparam name="TNumber">數(shù)字的類型</typeparam>
public record NumberSearchFilter<TNumber>
: IStructComposableFilterPredicateBuilder<TNumber>
, ISearchFilterStructKey<TNumber>
, ISearchFilterOperator<ComparableNumberSearchOperator>
, IPredicateReversible
where TNumber : struct, IComparisonOperators<TNumber, TNumber, bool>
{
/// <summary>
/// 初始化一個新實例
/// </summary>
/// <param name="keys">搜索關鍵字</param>
/// <param name="operator">搜索操作符</param>
/// <param name="reverse">是否反轉條件</param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidEnumArgumentException"></exception>
public NumberSearchFilter(
ImmutableList<TNumber?> keys,
[EnumDataType(typeof(ComparableNumberSearchOperator))]
ComparableNumberSearchOperator @operator = ComparableNumberSearchOperator.Equal,
bool reverse = false)
{
ArgumentNullException.ThrowIfNull(nameof(keys));
if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
string? message = GetKeysCheckMessage(keys, @operator);
if (message is not null) throw new ArgumentException(message, nameof(keys));
Keys = keys;
Operator = @operator;
Reverse = reverse;
}
/// <inheritdoc/>
public virtual ImmutableList<TNumber?> Keys { get; }
/// <inheritdoc/>
public virtual ComparableNumberSearchOperator Operator { get; }
/// <inheritdoc/>
public virtual bool Reverse { get; }
/// <inheritdoc/>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber>> memberAccessor)
{
NullKeyCheck(Keys);
var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
if (Reverse) where = where.Not();
return where;
}
/// <inheritdoc/>
/// <exception cref="InvalidOperationException"></exception>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, TNumber?>> memberAccessor)
{
var where = GetWherePredicateExtension(Keys, Operator, memberAccessor);
if (Reverse) where = where.Not();
return where;
}
}
/// <summary>
/// 字符串搜索過濾器
/// </summary>
public record StringSearchFilter
: IComposableFilterPredicateBuilder<string>,
ISearchFilterOperator<StringSearchOperator>,
ISearchFilterClassKey<string>,
IPredicateReversible
{
private static readonly MethodInfo _contains = typeof(string)
.GetMethod(
nameof(string.Contains),
BindingFlags.Public | BindingFlags.Instance,
[typeof(string)]
)!;
private static readonly MethodInfo _startsWith = typeof(string)
.GetMethod(
nameof(string.StartsWith),
BindingFlags.Public | BindingFlags.Instance,
[typeof(string)]
)!;
private static readonly MethodInfo _endsWith = typeof(string)
.GetMethod(
nameof(string.EndsWith),
BindingFlags.Public | BindingFlags.Instance,
[typeof(string)]
)!;
private static readonly MethodInfo _equals = typeof(string)
.GetMethod(
nameof(string.Equals),
BindingFlags.Public | BindingFlags.Instance,
[typeof(string)]
)!;
private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
.GetMethods()
.Where(static m => m.Name is nameof(Enumerable.Contains))
.Single(static m => m.GetParameters().Length is 2)
.MakeGenericMethod([typeof(string)]);
/// <summary>
/// 初始化一個新實例
/// </summary>
/// <param name="keys">搜索關鍵字</param>
/// <param name="operator">搜索操作符</param>
/// <param name="reverse">是否反轉條件</param>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidEnumArgumentException"></exception>
public StringSearchFilter(
ImmutableList<string?> keys,
[EnumDataType(typeof(StringSearchOperator))]
StringSearchOperator @operator = StringSearchOperator.Contains,
bool reverse = false)
{
ArgumentNullException.ThrowIfNull(nameof(keys));
if (keys is null or { Count: 0 }) throw new ArgumentException("不能是空集。", nameof(keys));
if (!Enum.IsDefined(@operator)) throw new InvalidEnumArgumentException(nameof(@operator), (int)@operator, @operator.GetType());
string? exceptionHint = null;
switch (@operator)
{
case StringSearchOperator.Equal:
if (keys is { Count: > 1 })
{
exceptionHint = $"必須設置一個元素。";
goto default;
}
break;
case StringSearchOperator.In:
if (keys is { Count: < 2 })
{
exceptionHint = $"必須設置多個元素。";
goto default;
}
break;
case StringSearchOperator.Contains:
goto case StringSearchOperator.Equal;
case StringSearchOperator.EqualContains:
if (keys is { Count: < 2 })
{
exceptionHint = $"必須設置多個元素。";
goto default;
}
else if (keys.Any(static key => key is null))
{
exceptionHint = $"元素不能為空。";
goto default;
}
break;
case StringSearchOperator.InContains:
goto case StringSearchOperator.EqualContains;
case StringSearchOperator.StartsWith:
if (keys is { Count: > 2 })
{
exceptionHint = $"必須設置一個元素。";
goto default;
}
else if (keys.Any(static key => key is null))
{
exceptionHint = $"元素不能為空。";
goto default;
}
break;
case StringSearchOperator.InStartsWith:
goto case StringSearchOperator.EqualContains;
case StringSearchOperator.EndsWith:
goto case StringSearchOperator.StartsWith;
case StringSearchOperator.InEndsWith:
goto case StringSearchOperator.EqualContains;
default:
exceptionHint ??= "的元素數(shù)量錯誤。";
throw new ArgumentException($"當 {nameof(@operator)} 的值為 {@operator} 時{exceptionHint}", nameof(keys));
};
Keys = keys;
Operator = @operator;
Reverse = reverse;
}
/// <inheritdoc/>
public virtual ImmutableList<string?> Keys { get; }
/// <inheritdoc/>
public virtual StringSearchOperator Operator { get; }
/// <inheritdoc/>
public virtual bool Reverse { get; }
/// <inheritdoc/>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>> GetWherePredicate<TOwner>(Expression<Func<TOwner, string>> memberAccessor)
{
(MethodInfo method, object? value, Type type) = Operator switch
{
StringSearchOperator.Equal => (_equals, (object?)Keys![0], typeof(string)),
StringSearchOperator.In => (_enumerableContains, Keys, typeof(IEnumerable<string?>)),
StringSearchOperator.Contains => (_contains, Keys![0], typeof(string)),
StringSearchOperator.EqualContains => (_contains, Keys, typeof(string)),
StringSearchOperator.InContains => (_contains, Keys, typeof(string)),
StringSearchOperator.StartsWith => (_startsWith, Keys![0], typeof(string)),
StringSearchOperator.InStartsWith => (_startsWith, Keys, typeof(string)),
StringSearchOperator.EndsWith => (_endsWith, Keys![0], typeof(string)),
StringSearchOperator.InEndsWith => (_endsWith, Keys, typeof(string)),
_ => throw new InvalidDataException(nameof(StringSearchOperator))
};
Expression newBody;
switch (Operator)
{
case StringSearchOperator.Equal:
newBody = Expression.Call(memberAccessor.Body, method, Expression.Constant(value, type));
break;
case StringSearchOperator.In:
newBody = Expression.Call(null, method, [Expression.Constant(value, type), memberAccessor.Body]);
break;
case StringSearchOperator.Contains:
goto case StringSearchOperator.Equal;
case StringSearchOperator.EqualContains:
newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.And);
break;
case StringSearchOperator.InContains:
newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
break;
case StringSearchOperator.StartsWith:
goto case StringSearchOperator.Equal;
case StringSearchOperator.InStartsWith:
newBody = CombineIn((IReadOnlyList<string?>)value!, memberAccessor.Body, method, type, PredicateCombineKind.Or);
break;
case StringSearchOperator.EndsWith:
goto case StringSearchOperator.Equal;
case StringSearchOperator.InEndsWith:
goto case StringSearchOperator.InStartsWith;
default:
throw new InvalidDataException(nameof(StringSearchOperator));
}
if (Reverse) newBody = Expression.Not(newBody);
return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
static Expression CombineIn(IReadOnlyList<string?> keys, Expression instance, MethodInfo method, Type type, PredicateCombineKind combineKind)
{
Expression expr = Expression.Call(instance, method, Expression.Constant(keys[0], type));
foreach (var key in keys.Skip(1))
{
expr = combineKind switch
{
PredicateCombineKind.And => Expression.AndAlso(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
PredicateCombineKind.Or => Expression.OrElse(expr, Expression.Call(instance, method, Expression.Constant(key, type))),
_ => throw new NotImplementedException(),
};
}
return expr;
}
}
}
此處展示了一部分基礎數(shù)據類型的過濾器定義。如果將來有自定義基本類型也可以照葫蘆畫瓢。
集合類型屬性的實現(xiàn)
/// <summary>
/// 復雜類型的集合屬性搜索過濾器
/// </summary>
/// <typeparam name="TQueryBuilder">要搜索的集合元素類型的查詢類型</typeparam>
/// <typeparam name="T">要搜索的元素類型</typeparam>
public record CollectionMemberSearchFilter<TQueryBuilder, T>
: ICollectionComposableFilterPredicateBuilder<T>
, IPredicateReversible
where TQueryBuilder : IFilterPredicateBuilder<T>
{
private static readonly MethodInfo _asQueryableOfT = typeof(Queryable)
.GetMethods()
.Single(static m => m.Name is nameof(Queryable.AsQueryable) && m.IsGenericMethod);
private static readonly MethodInfo _queryableWhereOfT = typeof(Queryable)
.GetMethods()
.Single(static m =>
{
return m.Name is nameof(Queryable.Where)
&& m.GetParameters()[1]
.ParameterType
.GenericTypeArguments[0]
.GenericTypeArguments
.Length is 2;
});
private static readonly MethodInfo _queryableCountOfT = typeof(Queryable)
.GetMethods()
.Single(static m => m.Name is nameof(Queryable.Count) && m.GetParameters().Length is 1);
private static readonly MethodInfo _queryableAnyOfT = typeof(Queryable)
.GetMethods()
.Single(static m => m.Name is nameof(Queryable.Any) && m.GetParameters().Length is 1);
private static readonly MethodInfo _enumerableContains = typeof(Enumerable)
.GetMethods()
.Where(static m => m.Name is nameof(Enumerable.Contains))
.Single(static m => m.GetParameters().Length is 2)
.MakeGenericMethod([typeof(T)]);
/// <summary>
/// 元素的查詢
/// </summary>
public TQueryBuilder? Query { get; }
/// <summary>
/// 計數(shù)搜索過濾器
/// </summary>
/// <remarks>和<see cref="Percent"/>只能存在一個。</remarks>
public NumberSearchFilter<int>? Count { get; }
/// <summary>
/// 比例搜索過濾器
/// </summary>
/// <remarks>
/// 和<see cref="Count"/>只能存在一個。
/// 如果存在,則<see cref="Query"/>也必須同時存在。
/// </remarks>
public NumberSearchFilter<double>? Percent { get; }
/// <inheritdoc/>
public bool Reverse { get; }
/// <summary>
/// 初始化新的實例
/// </summary>
/// <param name="query">元素的查詢</param>
/// <param name="count">計數(shù)搜索過濾器</param>
/// <exception cref="ArgumentNullException"></exception>
public CollectionMemberSearchFilter(
TQueryBuilder? query,
NumberSearchFilter<int> count,
bool reverse = false) : this(query, count, null, reverse)
{
}
/// <summary>
/// 初始化新的實例
/// </summary>
/// <param name="query">元素的查詢</param>
/// <param name="percent">比例搜索過濾器</param>
/// <exception cref="ArgumentNullException"></exception>
public CollectionMemberSearchFilter(
TQueryBuilder query,
NumberSearchFilter<double> percent,
bool reverse = false) : this(query, null, percent, reverse)
{
}
/// <summary>
/// 初始化新的實例
/// </summary>
/// <param name="query">元素的查詢</param>
/// <param name="count">計數(shù)搜索過濾器</param>
/// <param name="percent">比例搜索過濾器</param>
/// <exception cref="ArgumentException"></exception>
[JsonConstructor]
public CollectionMemberSearchFilter(
TQueryBuilder? query,
NumberSearchFilter<int>? count = null,
NumberSearchFilter<double>? percent = null,
bool reverse = false)
{
if (count is null && percent is null || count is not null && percent is not null)
{
throw new ArgumentException($"{nameof(count)} 和 {nameof(percent)} 必須設置且只能設置其中一個。");
}
if (percent is not null && query is null)
{
throw new ArgumentException($"{nameof(percent)} 和 {nameof(query)} 必須同時設置。");
}
Count = count;
Percent = percent;
Reverse = reverse;
}
/// <inheritdoc/>
/// <exception cref="ArgumentException"></exception>
/// <exception cref="InvalidDataException"></exception>
public Expression<Func<TOwner, bool>>? GetWherePredicate<TOwner>(Expression<Func<TOwner, IEnumerable<T>>> memberAccessor)
{
ArgumentNullException.ThrowIfNull(memberAccessor);
var asQueryable = _asQueryableOfT.MakeGenericMethod(typeof(T));
Expression queryable = Expression.Call(null, asQueryable, [memberAccessor.Body]);
Expression originalQueryable = queryable;
var queryCount = _queryableCountOfT.MakeGenericMethod(typeof(T));
Expression allCount = Expression.Call(null, queryCount, queryable);
Expression? whereCount = null;
var where = Query?.GetWherePredicate();
if (where != null)
{
var queryableWhere = _queryableWhereOfT.MakeGenericMethod(typeof(T));
queryable = Expression.Call(null, queryableWhere, [queryable, where]);
whereCount = Expression.Call(null, queryCount, queryable);
}
Expression? resultBody = null;
if (Count is not null)
{
var usedCount = whereCount ?? allCount;
resultBody = Count.Operator switch
{
ComparableNumberSearchOperator.Equal => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
? anyCall
: Expression.Equal(usedCount, Expression.Constant(Count.Keys[0])),
ComparableNumberSearchOperator.In =>
Expression.Call(
null,
_enumerableContains,
[Expression.Constant(Count.Keys, typeof(IEnumerable<int>)), usedCount]
),
ComparableNumberSearchOperator.LessThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
? anyCall
: Expression.LessThan(usedCount, Expression.Constant(Count.Keys[0])),
ComparableNumberSearchOperator.LessThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
? anyCall
: Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
ComparableNumberSearchOperator.GreaterThan => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
? anyCall
: Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
ComparableNumberSearchOperator.GreaterThanOrEqual => TryUseAnyCall(Count.Operator, Count.Keys[0], queryable, out var anyCall)
? anyCall
: Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
ComparableNumberSearchOperator.BetweenOpen =>
Expression.AndAlso(
Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
),
ComparableNumberSearchOperator.BetweenLeftClosed =>
Expression.AndAlso(
Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
Expression.LessThan(usedCount, Expression.Constant(Count.Keys[1]))
),
ComparableNumberSearchOperator.BetweenRightClosed =>
Expression.AndAlso(
Expression.GreaterThan(usedCount, Expression.Constant(Count.Keys[0])),
Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
),
ComparableNumberSearchOperator.BetweenClosed =>
Expression.AndAlso(
Expression.GreaterThanOrEqual(usedCount, Expression.Constant(Count.Keys[0])),
Expression.LessThanOrEqual(usedCount, Expression.Constant(Count.Keys[1]))
),
_ => throw new InvalidDataException(nameof(Count.Operator)),
};
if (Count.Reverse) resultBody = Expression.Not(resultBody);
}
else if (Percent is not null)
{
Debug.Assert(whereCount is not null);
Expression doubleAllCount = Expression.Convert(allCount, typeof(double));
whereCount = Expression.Convert(whereCount, typeof(double));
Expression usedPercent = Expression.Divide(whereCount, doubleAllCount);
var queryableAny = _queryableAnyOfT.MakeGenericMethod(typeof(T));
usedPercent = Expression.Condition(Expression.Not(Expression.Call(null, queryableAny, originalQueryable)), Expression.Constant(0.0), usedPercent);
resultBody = Percent.Operator switch
{
ComparableNumberSearchOperator.Equal => Expression.Equal(usedPercent, Expression.Constant(Percent.Keys[0])),
ComparableNumberSearchOperator.In =>
Expression.Call(
null,
_enumerableContains,
[Expression.Constant(Percent.Keys, typeof(IEnumerable<double>)), usedPercent]
),
ComparableNumberSearchOperator.LessThan => Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[0])),
ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
ComparableNumberSearchOperator.BetweenOpen =>
Expression.AndAlso(
Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
),
ComparableNumberSearchOperator.BetweenLeftClosed =>
Expression.AndAlso(
Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
Expression.LessThan(usedPercent, Expression.Constant(Percent.Keys[1]))
),
ComparableNumberSearchOperator.BetweenRightClosed =>
Expression.AndAlso(
Expression.GreaterThan(usedPercent, Expression.Constant(Percent.Keys[0])),
Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
),
ComparableNumberSearchOperator.BetweenClosed =>
Expression.AndAlso(
Expression.GreaterThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[0])),
Expression.LessThanOrEqual(usedPercent, Expression.Constant(Percent.Keys[1]))
),
_ => throw new InvalidDataException(nameof(Percent.Operator)),
};
if (Percent.Reverse) resultBody = Expression.Not(resultBody);
}
Debug.Assert(resultBody is not null);
var result = Expression.Lambda<Func<TOwner, bool>>(resultBody, memberAccessor.Parameters);
return this.ApplyReversiblePredicate(result);
static bool TryUseAnyCall(
ComparableNumberSearchOperator @operator,
int? key,
Expression toCallAny,
[NotNullWhen(true)] out Expression? result)
{
ArgumentNullException.ThrowIfNull(toCallAny);
(bool shouldUseAny, bool shouldReverseAny) = (@operator, key) switch
{
(ComparableNumberSearchOperator.Equal, 0) => (true, true),
(ComparableNumberSearchOperator.LessThan, 1) => (true, true),
(ComparableNumberSearchOperator.LessThanOrEqual, 0) => (true, true),
(ComparableNumberSearchOperator.GreaterThan, 0) => (true, false),
(ComparableNumberSearchOperator.GreaterThanOrEqual, 1) => (true, false),
_ => (false, false),
};
result = null;
if (shouldUseAny)
{
result = Expression.Call(
null,
_queryableAnyOfT.MakeGenericMethod(typeof(T)),
[toCallAny]
);
if (shouldReverseAny)
{
result = Expression.Not(result);
}
return true;
}
return false;
}
}
}
對于集合類型的屬性,筆者實現(xiàn)了計數(shù)和比例比較,用于篩選符合條件的元素數(shù)量或占比是否符合條件。如果需要,各種聚合條件也應該可以實現(xiàn),此處不再列舉。TryUseAnyCall()方法對計數(shù)條件嘗試使用Any()替換Count(),例如Count > 0等價于Any(),這可以在EF Core中生成更高效的SQL(據說EF Core準備在內部添加這個優(yōu)化)。
輔助類型
internal static class ScalarSearchFilterExtensions
{
private static readonly MethodInfo _enumerableContainsOfT = typeof(Enumerable)
.GetMethods()
.Where(static m => m.Name is nameof(Enumerable.Contains))
.Single(static m => m.GetParameters().Length is 2);
internal static string? GetKeysCheckMessage<T>(ICollection<T> keys, ComparableNumberSearchOperator @operator)
{
string? exceptionHint = null;
switch (@operator)
{
case ComparableNumberSearchOperator.Equal:
if (keys is { Count: > 1 })
{
exceptionHint = $"必須設置一個元素。";
}
break;
case ComparableNumberSearchOperator.In:
if (keys is { Count: < 2 })
{
exceptionHint = $"必須設置多個元素。";
}
break;
case ComparableNumberSearchOperator.LessThan:
goto case ComparableNumberSearchOperator.Equal;
case ComparableNumberSearchOperator.LessThanOrEqual:
goto case ComparableNumberSearchOperator.Equal;
case ComparableNumberSearchOperator.GreaterThan:
goto case ComparableNumberSearchOperator.Equal;
case ComparableNumberSearchOperator.GreaterThanOrEqual:
goto case ComparableNumberSearchOperator.Equal;
case ComparableNumberSearchOperator.BetweenOpen:
goto case ComparableNumberSearchOperator.BetweenClosed;
case ComparableNumberSearchOperator.BetweenLeftClosed:
goto case ComparableNumberSearchOperator.BetweenClosed;
case ComparableNumberSearchOperator.BetweenRightClosed:
goto case ComparableNumberSearchOperator.BetweenClosed;
case ComparableNumberSearchOperator.BetweenClosed:
if (keys is { Count: not 2 })
{
exceptionHint = $"必須設置兩個元素。";
}
break;
case ComparableNumberSearchOperator.InBetweenOpen:
if (keys is { Count: < 4 } || keys.Count % 2 != 0)
{
exceptionHint = $"必須設置不少于四個的偶數(shù)個元素。";
}
break;
case ComparableNumberSearchOperator.InBetweenLeftClosed:
goto case ComparableNumberSearchOperator.InBetweenOpen;
case ComparableNumberSearchOperator.InBetweenRightClosed:
goto case ComparableNumberSearchOperator.InBetweenOpen;
case ComparableNumberSearchOperator.InBetweenClosed:
goto case ComparableNumberSearchOperator.InBetweenOpen;
default:
exceptionHint = "的元素數(shù)量錯誤。";
break;
};
if (exceptionHint is not null) return $"當 {nameof(@operator)} 的值為 {@operator} 時{exceptionHint}";
else return null;
}
internal static void NullKeyCheck<T>(IReadOnlyList<T?> keys)
where T : struct
{
if (keys.Any(static n => n is null)) throw new InvalidOperationException("不能使用值為空的元素搜索值不能為空的成員。");
}
internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
IReadOnlyList<TNumber?> keys,
ComparableNumberSearchOperator @operator,
Expression<Func<TOwner, TNumber>> memberAccessor)
where TNumber : struct
{
var typeOfNumber = typeof(TNumber);
var _enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNumber]) : null;
Expression newBody = @operator switch
{
ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
ComparableNumberSearchOperator.In => Expression.Call(null, _enumerableContains!, [Expression.Constant(keys.Cast<TNumber>().ToList(), typeof(IEnumerable<TNumber>)), memberAccessor.Body]),
ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)),
ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNumber))),
ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
}
internal static Expression<Func<TOwner, bool>> GetWherePredicateExtension<TOwner, TNumber>(
IReadOnlyList<TNumber?> keys,
ComparableNumberSearchOperator @operator,
Expression<Func<TOwner, TNumber?>> memberAccessor)
where TNumber : struct
{
var typeOfNullableNumber = typeof(TNumber?);
var enumerableContains = @operator is ComparableNumberSearchOperator.In ? _enumerableContainsOfT.MakeGenericMethod([typeOfNullableNumber]) : null;
Expression newBody = @operator switch
{
ComparableNumberSearchOperator.Equal => Expression.Equal(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
ComparableNumberSearchOperator.In => Expression.Call(null, enumerableContains!, [Expression.Constant(keys, typeof(IEnumerable<TNumber?>)), memberAccessor.Body]),
ComparableNumberSearchOperator.LessThan => Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
ComparableNumberSearchOperator.LessThanOrEqual => Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
ComparableNumberSearchOperator.GreaterThan => Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
ComparableNumberSearchOperator.GreaterThanOrEqual => Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)),
ComparableNumberSearchOperator.BetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.BetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.BetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.BetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keys[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.InBetweenOpen => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenLeftClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenRightClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
ComparableNumberSearchOperator.InBetweenClosed => CombineInBetweenBody(keys, @operator, memberAccessor),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
return Expression.Lambda<Func<TOwner, bool>>(newBody, memberAccessor.Parameters);
}
private static Expression CombineInBetweenBody<TOwner, TNumber>(
IReadOnlyList<TNumber?> keys,
ComparableNumberSearchOperator @operator,
Expression<Func<TOwner, TNumber>> memberAccessor)
where TNumber : struct
{
var typeOfNumber = typeof(TNumber);
var keysGroups = keys.Chunk(2);
Expression newBody = @operator switch
{
ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNumber))),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
foreach (var inKeys in keysGroups.Skip(1))
{
newBody = @operator switch
{
ComparableNumberSearchOperator.InBetweenOpen => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
ComparableNumberSearchOperator.InBetweenRightClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
ComparableNumberSearchOperator.InBetweenClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNumber)))),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
}
return newBody;
}
private static Expression CombineInBetweenBody<TOwner, TNumber>(
IReadOnlyList<TNumber?> keys,
ComparableNumberSearchOperator @operator,
Expression<Func<TOwner, TNumber?>> memberAccessor)
where TNumber : struct
{
var typeOfNullableNumber = typeof(TNumber?);
var keysGroups = keys.Chunk(2);
Expression newBody = @operator switch
{
ComparableNumberSearchOperator.InBetweenOpen => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.InBetweenRightClosed => Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
ComparableNumberSearchOperator.InBetweenClosed => Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(keysGroups.First()[1], typeOfNullableNumber))),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
foreach (var inKeys in keysGroups.Skip(1))
{
newBody = @operator switch
{
ComparableNumberSearchOperator.InBetweenOpen => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
ComparableNumberSearchOperator.InBetweenLeftClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThan(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
ComparableNumberSearchOperator.InBetweenRightClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThan(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
ComparableNumberSearchOperator.InBetweenClosed => Expression.OrElse(newBody, Expression.AndAlso(Expression.GreaterThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[0], typeOfNullableNumber)), Expression.LessThanOrEqual(memberAccessor.Body, Expression.Constant(inKeys[1], typeOfNullableNumber)))),
_ => throw new InvalidDataException(nameof(ComparableNumberSearchOperator))
};
}
return newBody;
}
}
輔助類型用于統(tǒng)一定義表達式的拼接方式等,因為這些拼接對于大多數(shù)基礎數(shù)據類型來說都是通用的。
擴展復雜篩選支持
有時查詢條件可能比較復雜,單個生成器無法表達。因此需要一個用于描述更復雜的條件的生成器,單個生成器無法描述的情況使用多個生成器組合來實現(xiàn)。既然復雜條件是由單個條件組合而來,最好能復用已經定義好的單個篩選器。
/// <summary>
/// 支持復雜條件嵌套的高級查詢條件構造器接口
/// </summary>
/// <typeparam name="TFilterPredicateBuilder">基礎查詢條件構造器</typeparam>
/// <typeparam name="T">要查詢的數(shù)據類型</typeparam>
public interface IAdvancedFilterPredicateBuilder<out TFilterPredicateBuilder, T> : IFilterPredicateBuilder<T>
where TFilterPredicateBuilder : IFilterPredicateBuilder<T>
{
/// <summary>
/// 基礎查詢條件集合
/// </summary>
IReadOnlyList<TFilterPredicateBuilder>? Filters { get; }
/// <summary>
/// 高級查詢條件組集合
/// </summary>
IReadOnlyList<IAdvancedFilterPredicateBuilder<TFilterPredicateBuilder, T>>? FilterGroups { get; }
}
高級查詢接口允許組合和嵌套任意多個篩選器以實現(xiàn)更復雜的條件。
/// <summary>
/// 支持復雜條件嵌套的高級查詢構造器
/// </summary>
/// <typeparam name="TQueryBuilder">基礎查詢構造器</typeparam>
/// <typeparam name="T">要查詢的數(shù)據類型</typeparam>
/// <param name="Queries">基礎查詢條件集合</param>
/// <param name="QueryGroups">高級查詢條件組集合</param>
/// <param name="CombineType">條件組合方式</param>
/// <param name="Reverse">是否反轉條件</param>
public record AdvancedQueryBuilder<TQueryBuilder, T>(
ImmutableList<TQueryBuilder>? Queries = null,
ImmutableList<AdvancedQueryBuilder<TQueryBuilder, T>>? QueryGroups = null,
[EnumDataType(typeof(PredicateCombineKind))]
PredicateCombineKind? CombineType = PredicateCombineKind.And,
bool Reverse = false)
: IAdvancedFilterPredicateBuilder<TQueryBuilder, T>
, IPredicateReversible
where TQueryBuilder : IFilterPredicateBuilder<T>
{
/// <inheritdoc/>
public IReadOnlyList<TQueryBuilder>? Filters => Queries;
/// <inheritdoc/>
public IReadOnlyList<IAdvancedFilterPredicateBuilder<TQueryBuilder, T>>? FilterGroups => QueryGroups;
/// <summary>
/// 獲取查詢條件
/// </summary>
/// <returns>組合完成的查詢條件</returns>
public Expression<Func<T, bool>>? GetWherePredicate()
{
var where = CombinePredicates(Queries?.Select(static q => q.GetWherePredicate())
.Concat(QueryGroups?.Select(static qg => qg.GetWherePredicate()) ?? [])
.Where(static p => p is not null)!);
return this.ApplyReversiblePredicate(where);
}
/// <summary>
/// 組合查詢條件
/// </summary>
/// <param name="predicates">待組合的子條件</param>
/// <returns></returns>
/// <exception cref="NotSupportedException"></exception>
protected Expression<Func<T, bool>>? CombinePredicates(IEnumerable<Expression<Func<T, bool>>>? predicates)
{
var predicate = predicates?.FirstOrDefault();
if (predicates?.Any() is true)
{
predicate = CombineType switch
{
PredicateCombineKind.And => predicates?.AndAlsoAll(),
PredicateCombineKind.Or => predicates?.OrElseAll(),
_ => throw new NotSupportedException(CombineType.ToString()),
};
}
return predicate;
}
}
單個條件可以獲取對應的篩選表達式,那么這些條件的組合也就是對這些表達式進行組合。
添加排序和分頁支持
分頁的前提是排序,否則分頁的結果不穩(wěn)定。對于分頁,通常還想獲得符合條件的數(shù)據總量用于計算頁數(shù),但是排序并不影響總數(shù)計算。如果能在計算總數(shù)時忽略排序,僅在獲取某一頁數(shù)據時使用排序最好。這就要求我們分別存儲和處理篩選條件和排序條件。篩選條件的問題已經在前面解決了,這里只需要關注排序條件的問題。
排序的概念接口
/// <summary>
/// 排序查詢構造器接口
/// </summary>
/// <typeparam name="T">查詢的元素類型</typeparam>
public interface IOrderedQueryBuilder<T>
{
/// <summary>
/// 對查詢應用排序
/// </summary>
/// <param name="query">原始查詢</param>
/// <returns>已排序的查詢</returns>
IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
}
/// <summary>
/// 可排序查詢接口
/// </summary>
/// <typeparam name="T">查詢的元素類型</typeparam>
/// <typeparam name="TOrderKey">可用的排序關鍵字枚舉類型</typeparam>
public interface IKeySelectorOrderedQueryBuilder<T, TOrderKey> : IOrderedQueryBuilder<T>
where TOrderKey : struct, Enum
{
/// <summary>
/// 獲取支持的排序關鍵字選擇器
/// </summary>
IReadOnlyDictionary<TOrderKey, Expression<Func<T, object?>>> GetSupportedOrderKeySelectors();
/// <summary>
/// 排序關鍵字信息
/// </summary>
ImmutableList<OrderInfo<TOrderKey>>? OrderKeys { get; }
}
/// <summary>
/// 排序信息
/// </summary>
/// <typeparam name="T">排序對象的類型</typeparam>
/// <param name="Key">排序關鍵字</param>
/// <param name="OrderKind">排序方式</param>
public record OrderInfo<T>(
T Key,
[EnumDataType(typeof(OrderKind))]
OrderKind OrderKind = OrderKind.Asc)
where T : struct, Enum
{
/// <summary>
/// 排序關鍵字
/// </summary>
public T Key { get; } = CheckOrderKey(Key);
private static T CheckOrderKey(T value)
{
if (!Enum.IsDefined(value)) throw new InvalidEnumArgumentException(nameof(Key), int.Parse(value.ToString()), typeof(T));
return value;
}
}
/// <summary>
/// 排序方式
/// </summary>
public enum OrderKind
{
/// <summary>
/// 升序
/// </summary>
Asc = 1,
/// <summary>
/// 降序
/// </summary>
Desc = 2,
}
同樣的,第一個接口只描述如何為查詢附加排序,第二個接口描述序列化傳輸?shù)姆绞健INQ中的排序方法參數(shù)是一個排序關鍵字屬性訪問表達式,屬性的類型就是表達式的返回值類型,屬性類型千變萬化,因此只能使用返回object的表達式來存儲。表達式關鍵字本身無法序列化傳輸,因此筆者選擇使用枚舉來指代表達式,這也同時限定了可用于排序的屬性,有利于安全。
分頁的概念接口
目前有兩種流行的分頁方式,偏移量分頁和游標分頁。偏移量分頁支持隨機頁碼跳轉,也需要提前計算數(shù)據總數(shù),數(shù)據量大或訪問尾部頁碼時性能會下降。游標分頁則是根據唯一鍵游標排序和來進行,可以是兩個游標之間的數(shù)據或單個游標和獲取數(shù)據量的組合。在返回的結果中需要附帶相鄰頁面的起始游標,因此實際查詢數(shù)據時需要多查一條數(shù)據,多查的這一條數(shù)據只需要游標值,不需要數(shù)據本身。本文以偏移量分頁為例。
/// <summary>
/// 可頁碼分頁查詢接口
/// </summary>
public interface IOffsetPagingSupport
{
/// <summary>
/// 分頁信息
/// </summary>
OffsetPageInfo OffsetPage { get; }
}
/// <summary>
/// 頁碼分頁信息
/// </summary>
/// <param name="PageIndex">頁碼</param>
/// <param name="PageSize">頁面大小</param>
public record OffsetPageInfo(
[Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageIndex = 1,
[Range(1, int.MaxValue, ErrorMessage = DataAnnotationErrorMessageDefaults.Range)] int PageSize = 10
)
{
/// <summary>
/// 跳過的頁數(shù)
/// </summary>
public int SkipedPageCount => PageIndex - 1;
/// <summary>
/// 跳過的元素數(shù)量
/// </summary>
public int SkipedElementCount => SkipedPageCount * PageSize;
}
/// <summary>
/// 頁碼分頁查詢構造擴展
/// </summary>
public static class OffsetPageQueryBuilderExtensions
{
/// <summary>
/// 頁碼分頁
/// </summary>
/// <typeparam name="T">查詢的元素類型</typeparam>
/// <param name="offsetPaging">分頁信息</param>
/// <param name="query">要應用分頁的查詢</param>
/// <returns>已頁碼分頁的查詢</returns>
public static IQueryable<T> OffsetPage<T>(this IOffsetPagingSupport offsetPaging, IQueryable<T> query)
{
ArgumentNullException.ThrowIfNull(offsetPaging);
ArgumentNullException.ThrowIfNull(query);
var paging = offsetPaging.OffsetPage;
return query.Skip(paging.SkipedElementCount).Take(paging.PageSize);
}
}
完整的分頁查詢生成器
/// <summary>
/// 分頁查詢構造器
/// </summary>
/// <typeparam name="TQueryBuilder">查詢構造器類型</typeparam>
/// <typeparam name="T">查詢的數(shù)據類型</typeparam>
/// <param name="Query">基礎查詢</param>
/// <param name="OffsetPage">分頁信息</param>
public abstract record OffsetPagedQueryBuilder<TQueryBuilder, T>(
TQueryBuilder Query,
OffsetPageInfo? OffsetPage = null)
: IFilterPredicateBuilder<T>
, IOrderedQueryBuilder<T>
, IOffsetPagingSupport
where TQueryBuilder : IFilterPredicateBuilder<T>
{
/// <inheritdoc/>
public Expression<Func<T, bool>>? GetWherePredicate() => Query.GetWherePredicate();
/// <inheritdoc/>
public virtual OffsetPageInfo OffsetPage { get; } = OffsetPage ?? new();
/// <inheritdoc/>
public abstract IOrderedQueryable<T> ApplyOrder(IQueryable<T> query);
}
/// <summary>
/// 查詢的排序方法
/// </summary>
public enum QueryableOrderMethod
{
/// <summary>
/// 優(yōu)先升序
/// </summary>
OrderBy = 1,
/// <summary>
/// 優(yōu)先降序
/// </summary>
OrderByDescending,
/// <summary>
/// 次一級升序
/// </summary>
ThenBy,
/// <summary>
/// 次一級降序
/// </summary>
ThenByDescending
}
/// <summary>
/// 關鍵字排序查詢構造器擴展
/// </summary>
public static class KeySelectorOrderQueryBuilderExtensions
{
private static readonly MethodInfo _queryableOederByOfT = typeof(Queryable).GetMethods()
.Single(static m => m.Name is nameof(Queryable.OrderBy) && m.GetParameters().Length is 2);
private static readonly MethodInfo _queryableThenByOfT = typeof(Queryable).GetMethods()
.Single(static m => m.Name is nameof(Queryable.ThenBy) && m.GetParameters().Length is 2);
private static readonly MethodInfo _queryableOrderByDescendingOfT = typeof(Queryable).GetMethods()
.Single(static m => m.Name is nameof(Queryable.OrderByDescending) && m.GetParameters().Length is 2);
private static readonly MethodInfo _queryableThenByDescendingOfT = typeof(Queryable).GetMethods()
.Single(static m => m.Name is nameof(Queryable.ThenByDescending) && m.GetParameters().Length is 2);
/// <summary>
/// 對查詢應用關鍵字排序
/// </summary>
/// <typeparam name="T">查詢的元素類型</typeparam>
/// <typeparam name="TOrderKey">可用的排序關鍵字類型</typeparam>
/// <param name="OrderInfos">排序信息</param>
/// <param name="query">原始查詢</param>
/// <returns>已排序的查詢</returns>
/// <exception cref="InvalidDataException"></exception>
public static IOrderedQueryable<T> ApplyKeyedOrder<T, TOrderKey>(this IKeySelectorOrderedQueryBuilder<T, TOrderKey> OrderInfos, IQueryable<T> query)
where TOrderKey : struct, Enum
{
ArgumentNullException.ThrowIfNull(OrderInfos);
ArgumentNullException.ThrowIfNull(query);
if (OrderInfos.GetSupportedOrderKeySelectors()?.Count > 0 is false) throw new InvalidDataException($"{nameof(OrderInfos.GetSupportedOrderKeySelectors)}");
IOrderedQueryable<T> orderedQuery;
QueryableOrderMethod methodKind;
MethodInfo orderMethod;
Expression<Func<T, object?>> keySelector;
var firstOrder = OrderInfos.OrderKeys?.FirstOrDefault();
if (firstOrder is not null)
{
methodKind = firstOrder.OrderKind switch
{
OrderKind.Asc => QueryableOrderMethod.OrderBy,
OrderKind.Desc => QueryableOrderMethod.OrderByDescending,
_ => throw new InvalidDataException($"{nameof(OrderKind)}"),
};
keySelector = OrderInfos.GetSupportedOrderKeySelectors()[firstOrder.Key];
orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [query, keySelector])!;
}
else
{
keySelector = OrderInfos.GetSupportedOrderKeySelectors().First().Value;
orderedQuery = (IOrderedQueryable<T>)(GetQueryOrderMethod<T>(QueryableOrderMethod.OrderBy, keySelector.ReturnType)
.Invoke(null, [query, keySelector]))!;
}
foreach (var subsequentOrder in OrderInfos.OrderKeys?.Skip(1) ?? [])
{
if (subsequentOrder is not null)
{
methodKind = subsequentOrder.OrderKind switch
{
OrderKind.Asc => QueryableOrderMethod.ThenBy,
OrderKind.Desc => QueryableOrderMethod.ThenByDescending,
_ => throw new InvalidDataException($"{nameof(OrderKind)}"),
};
keySelector = OrderInfos.GetSupportedOrderKeySelectors()[subsequentOrder.Key];
orderMethod = GetQueryOrderMethod<T>(methodKind, keySelector.ReturnType);
orderedQuery = (IOrderedQueryable<T>)orderMethod.Invoke(null, [orderedQuery, keySelector])!;
}
}
return orderedQuery;
}
private static MethodInfo GetQueryOrderMethod<T>(QueryableOrderMethod method, Type orderKeyType)
{
return method switch
{
QueryableOrderMethod.OrderBy => _queryableOederByOfT.MakeGenericMethod(typeof(T), orderKeyType),
QueryableOrderMethod.OrderByDescending => _queryableOrderByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
QueryableOrderMethod.ThenBy => _queryableThenByOfT.MakeGenericMethod(typeof(T), orderKeyType),
QueryableOrderMethod.ThenByDescending => _queryableThenByDescendingOfT.MakeGenericMethod(typeof(T), orderKeyType),
_ => throw new InvalidDataException($"{nameof(method)}"),
};
}
}
此處的抽象基類不實現(xiàn)關鍵字排序是因為無法確定最終查詢是否支持關鍵字排序,有可能是在代碼中定義的靜態(tài)排序規(guī)則。如果確實支持關鍵字排序,在最終類型上實現(xiàn)關鍵字排序接口即可。擴展類型則用于快速實現(xiàn)排序表達式生成。
使用示例
演示用數(shù)據類型
/// <summary>
/// 示例1
/// </summary>
public class Entity1
{
public int Id { get; set; }
public string? Text1 { get; set; }
public Entity2? Entity2 { get; set; }
public List<Entity3> Entities3 { get; set; } = [];
}
/// <summary>
/// 示例2
/// </summary>
public class Entity2
{
public int Id { get; set; }
public string? Text2 { get; set; }
}
/// <summary>
/// 示例3
/// </summary>
public class Entity3
{
public int Id { get; set; }
public string? Text3 { get; set; }
public Entity1? Entity1 { get; set; }
}
基礎查詢定義
/// <summary>
/// Entity1查詢生成器
/// </summary>
/// <param name="Text1">Entity1文本</param>
/// <param name="Id">Entity1的Id</param>
/// <param name="Entity2">Entity1的Entity2篩選</param>
/// <param name="Entities3">Entity1的Entity3集合篩選</param>
/// <param name="Reverse">是否反轉條件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
/// <remarks>
/// 反轉條件由<see cref="QueryBuilderBase{T}"/>在<see cref="QueryBuilderBase{T}.GetWherePredicate()"/>中自動進行,此處不需要處理。
/// </remarks>
public sealed record Entity1QueryBuilder(
NumberSearchFilter<int>? Id = null,
StringSearchFilter? Text1 = null,
Entity2QueryBuilder? Entity2 = null,
CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>? Entities3 = null,
[EnumDataType(typeof(PredicateCombineKind))]
PredicateCombineKind? CombineType = PredicateCombineKind.And,
bool Reverse = false)
: QueryBuilderBase<Entity1>(CombineType)
, IPredicateReversible
{
/// <inheritdoc/>
protected override Expression<Func<Entity1, bool>>? BuildWherePredicate()
{
List<Expression<Func<Entity1, bool>>> predicates = [];
predicates.AddIfNotNull(Id?.GetWherePredicate<Entity1>(e1 => e1.Id));
predicates.AddIfNotNull(Text1?.GetWherePredicate<Entity1>(e1 => e1.Text1!));
predicates.AddIfNotNull(Entity2?.GetWherePredicate<Entity1>(e1 => e1.Entity2!));
predicates.AddIfNotNull(Entities3?.GetWherePredicate<Entity1>(e1 => e1.Entities3));
predicates.AddIfNotNull(base.BuildWherePredicate());
var where = CombinePredicates(predicates);
return where;
}
}
/// <summary>
/// Entity2查詢生成器
/// </summary>
/// <param name="Text2">Entity2文本</param>
/// <param name="Id">Entity2的Id</param>
/// <param name="Reverse">是否反轉條件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
public sealed record Entity2QueryBuilder(
NumberSearchFilter<int>? Id = null,
StringSearchFilter? Text2 = null,
[EnumDataType(typeof(PredicateCombineKind))]
PredicateCombineKind? CombineType = PredicateCombineKind.And,
bool Reverse = false)
: QueryBuilderBase<Entity2>(CombineType)
, IPredicateReversible
{
/// <inheritdoc/>
protected override Expression<Func<Entity2, bool>>? BuildWherePredicate()
{
List<Expression<Func<Entity2, bool>>> predicates = [];
predicates.AddIfNotNull(Id?.GetWherePredicate<Entity2>(e1 => e1.Id));
predicates.AddIfNotNull(Text2?.GetWherePredicate<Entity2>(e2 => e2.Text2!));
predicates.AddIfNotNull(base.BuildWherePredicate());
var where = CombinePredicates(predicates);
return where;
}
}
/// <summary>
/// Entity3查詢生成器
/// </summary>
/// <param name="Text3">Entity3文本</param>
/// <param name="Entity1">Entity3的Entity1篩選</param>
/// <param name="Id">Entity3的Id</param>
/// <param name="Reverse">是否反轉條件</param>
/// <inheritdoc cref="QueryBuilderBase{T}"/>
public sealed record Entity3QueryBuilder(
NumberSearchFilter<int>? Id = null,
StringSearchFilter? Text3 = null,
Entity1QueryBuilder? Entity1 = null,
[EnumDataType(typeof(PredicateCombineKind))]
PredicateCombineKind? CombineType = PredicateCombineKind.And,
bool Reverse = false)
: QueryBuilderBase<Entity3>(CombineType)
, IPredicateReversible
{
/// <inheritdoc/>
protected override Expression<Func<Entity3, bool>>? BuildWherePredicate()
{
List<Expression<Func<Entity3, bool>>> predicates = [];
predicates.AddIfNotNull(Id?.GetWherePredicate<Entity3>(e3 => e3.Id));
predicates.AddIfNotNull(Text3?.GetWherePredicate<Entity3>(e3 => e3.Text3!));
predicates.AddIfNotNull(Entity1?.GetWherePredicate<Entity3>(e3 => e3.Entity1!));
predicates.AddIfNotNull(base.BuildWherePredicate());
var where = CombinePredicates(predicates);
return where;
}
}
public static class CollectionExtensions
{
/// <summary>
/// 如果<paramref name="item"/>不是<see langword="null"/>,把<paramref name="item"/>添加到<paramref name="collection"/>。
/// </summary>
/// <typeparam name="T">集合的元素類型。</typeparam>
/// <param name="collection">待添加元素的集合。</param>
/// <param name="item">要添加的元素。</param>
/// <returns>是否成功把元素添加到集合。</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool AddIfNotNull<T>(this ICollection<T> collection, T? item)
{
ArgumentNullException.ThrowIfNull(collection, nameof(collection));
if (item is not null)
{
collection.Add(item);
return true;
}
return false;
}
}
從示例中可以看出,只需要針對數(shù)據類型的基礎數(shù)據使用標量過濾器類型實現(xiàn)基礎篩選,對于引用的其他數(shù)據類型,可以直接復用引用類型的查詢生成器,并使用由組合查詢生成接口提供的組合方法即可自動把復雜類型的篩選條件嵌套到當前類型的屬性上。
分頁查詢
/// <summary>
/// Entity1分頁查詢生成器
/// </summary>
/// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T2}"/>
public sealed record OffsetPagedEntity1QueryBuilder(
Entity1QueryBuilder Query,
OffsetPageInfo? OffsetPage = null,
ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null)
: OffsetPagedQueryBuilder<Entity1QueryBuilder, Entity1>(Query, OffsetPage)
, IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
{
/// <inheritdoc/>
public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;
/// <inheritdoc/>
public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
}
/// <summary>
/// Entity1排序關鍵字
/// </summary>
public enum Entity1OrderKey : uint
{
/// <summary>
/// Id
/// </summary>
Id = 1,
/// <summary>
/// Text1
/// </summary>
Text1
}
internal static class Entity1OrderKeySelector
{
public static IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> Content { get; } =
FrozenDictionary.ToFrozenDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>>([
new(Entity1OrderKey.Id, e1 => e1.Id),
new(Entity1OrderKey.Text1, e1 => e1.Text1),
]);
}
分頁查詢只要利用之前定義好的泛型基類填充類型參數(shù)即可。由于實例類型需要實現(xiàn)關鍵字排序,因此要實現(xiàn)相關接口。
高級分頁查詢
/// <summary>
/// Entity1分頁高級查詢生成器
/// </summary>
/// <param name="OrderKeys">排序信息</param>
/// <inheritdoc cref="OffsetPagedQueryBuilder{TQueryBuilder, T}"/>
public sealed record OffsetPagedAdvancedEntity1QueryBuilder(
AdvancedQueryBuilder<Entity1QueryBuilder, Entity1> Query,
ImmutableList<OrderInfo<Entity1OrderKey>>? OrderKeys = null,
OffsetPageInfo? OffsetPage = null)
: OffsetPagedQueryBuilder<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>, Entity1>(
Query,
OffsetPage)
, IKeySelectorOrderedQueryBuilder<Entity1, Entity1OrderKey>
{
/// <inheritdoc/>
public IReadOnlyDictionary<Entity1OrderKey, Expression<Func<Entity1, object?>>> GetSupportedOrderKeySelectors() => Entity1OrderKeySelector.Content;
/// <inheritdoc/>
public override IOrderedQueryable<Entity1> ApplyOrder(IQueryable<Entity1> query) => this.ApplyKeyedOrder(query);
}
高級分頁查詢同樣只需要填充預定義泛型基類的類型參數(shù)即可。
實例化查詢生成器對象
OffsetPagedAdvancedEntity1QueryBuilder queryBuilder =
new OffsetPagedAdvancedEntity1QueryBuilder(
Query: new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
Queries: [ // ImmutableList<Entity1QueryBuilder>
new Entity1QueryBuilder (
Id: new NumberSearchFilter<int>([2], ComparableNumberSearchOperator.GreaterThan),
Text1: new StringSearchFilter(["aa"], StringSearchOperator.Contains),
Entity2: new Entity2QueryBuilder(
new NumberSearchFilter<int>([100]),
new StringSearchFilter(["ccc"])
),
Entities3: null,
CombineType: PredicateCombineKind.Or,
Reverse: false
),
new Entity1QueryBuilder(
Id: new NumberSearchFilter<int>([5], ComparableNumberSearchOperator.LessThan)
)
],
QueryGroups: [ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
new AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>(
Queries: [ // ImmutableList<Entity1QueryBuilder>
new Entity1QueryBuilder(
Id: new NumberSearchFilter<int>([20], ComparableNumberSearchOperator.Equal),
Text1: new StringSearchFilter(["bb"], StringSearchOperator.Contains),
Entity2: null,
Entities3: new CollectionMemberSearchFilter<Entity3QueryBuilder, Entity3>(
query: new Entity3QueryBuilder(
Id: null,
Text3: new StringSearchFilter(["fff"], StringSearchOperator.StartsWith)
),
count: new NumberSearchFilter < int >([50]),
percent: null,
reverse: false
),
CombineType: PredicateCombineKind.And,
Reverse: false
)
],
QueryGroups:[ // ImmutableList<AdvancedQueryBuilder<Entity1QueryBuilder, Entity1>>
],
CombineType: PredicateCombineKind.Or,
Reverse: true
)
],
CombineType: PredicateCombineKind.And,
Reverse: true
),
OrderKeys: [ // ImmutableList<OrderInfo<Entity1OrderKey>>
new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Text1, OrderKind.Desc),
new OrderInfo<Entity1OrderKey>(Entity1OrderKey.Id),
],
OffsetPage: new OffsetPageInfo(1,20)
);
在集合中使用生成器
// 準備一個集合
var entity1Arr = new Entity1[
new()
];
// 從生成器中獲取篩選表達式
var where = queryBuilder.GetWherePredicate();
// 把集合轉換為 IQueryable<Entity1> 使用表達式類型的參數(shù)
var query = entity1Arr.AsQueryable().Where(where!);
// 把排序應用到查詢
var ordered = builder.ApplyOrder(query);
// 把分頁應用到查詢
var paged = builder.OffsetPage(ordered);
// 把表達式編譯為委托
var whereFunc = where.Compile();
最終得到的篩選表達式
Expression<Func<Entity1, bool>> exp = e1 =>
!(
(
e1.Id > 2
|| e1.Text1.Contains("aa")
|| (e1.Entity2.Id == 100 && e1.Entity2.Text2.Contains("ccc"))
)
&& e1.Id < 5
&& !(
e1.Id == 20
&& e1.Text1.Contains("bb")
&& e1.Entities3.AsQueryable().Count() == 50
)
);
這個表達式是經過手動去除多余的括號,重新整理縮進后得到的版本。原始表達式像這樣:
在此也推薦這個好用的VS插件:ReadableExpressions.Visualizers。這個插件可以把表達式顯示成代碼編輯器里的樣子,對各種語法要素也會著色,調試動態(tài)拼接的表達式時非常好用。
EF Core生成的SQL(SQL Server)
DECLARE @__p_0 int = 0;
DECLARE @__p_1 int = 20;
SELECT [e].[Id], [e].[Entity2Id], [e].[Text1]
FROM [Entity1] AS [e]
LEFT JOIN [Entity2] AS [e0] ON [e].[Entity2Id] = [e0].[Id]
WHERE ([e].[Id] <= 2 AND ([e].[Text1] NOT LIKE N'%aa%' OR [e].[Text1] IS NULL) AND ([e0].[Id] <> 100 OR [e0].[Id] IS NULL OR [e0].[Text2] NOT LIKE N'%ccc%' OR [e0].[Text2] IS NULL)) OR [e].[Id] >= 5 OR ([e].[Id] = 20 AND [e].[Text1] LIKE N'%bb%' AND (
SELECT COUNT(*)
FROM [Entity3] AS [e1]
WHERE [e].[Id] = [e1].[Entity1Id]) = 50)
ORDER BY [e].[Text1] DESC, [e].[Id]
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY
等價的JSON表示
{
"Query": {
"Queries": [
{
"Id": {
"Keys": [
2
],
"Operator": 8,
"Reverse": false
},
"Text1": {
"Keys": [
"aa"
],
"Operator": 4,
"Reverse": false
},
"Entity2": {
"Id": {
"Keys": [
100
],
"Operator": 1,
"Reverse": false
},
"Text2": {
"Keys": [
"ccc"
],
"Operator": 4,
"Reverse": false
},
"Reverse": false,
"CombineType": 1
},
"Entities3": null,
"Reverse": false,
"CombineType": 2
},
{
"Id": {
"Keys": [
5
],
"Operator": 4,
"Reverse": false
},
"Text1": null,
"Entity2": null,
"Entities3": null,
"Reverse": false,
"CombineType": 1
}
],
"QueryGroups": [
{
"Queries": [
{
"Id": {
"Keys": [
20
],
"Operator": 1,
"Reverse": false
},
"Text1": {
"Keys": [
"bb"
],
"Operator": 4,
"Reverse": false
},
"Entity2": null,
"Entities3": {
"Query": null,
"Count": {
"Keys": [
50
],
"Operator": 1,
"Reverse": false
},
"Percent": null,
"Reverse": false
},
"Reverse": false,
"CombineType": 1
}
],
"QueryGroups": [],
"CombineType": 2,
"Reverse": true,
"Filters": [
{
"Id": {
"Keys": [
20
],
"Operator": 1,
"Reverse": false
},
"Text1": {
"Keys": [
"bb"
],
"Operator": 4,
"Reverse": false
},
"Entity2": null,
"Entities3": {
"Query": null,
"Count": {
"Keys": [
50
],
"Operator": 1,
"Reverse": false
},
"Percent": null,
"Reverse": false
},
"Reverse": false,
"CombineType": 1
}
],
"FilterGroups": []
}
],
"CombineType": 1,
"Reverse": true,
"Filters": [
{
"Id": {
"Keys": [
2
],
"Operator": 8,
"Reverse": false
},
"Text1": {
"Keys": [
"aa"
],
"Operator": 4,
"Reverse": false
},
"Entity2": {
"Id": {
"Keys": [
100
],
"Operator": 1,
"Reverse": false
},
"Text2": {
"Keys": [
"ccc"
],
"Operator": 4,
"Reverse": false
},
"Reverse": false,
"CombineType": 1
},
"Entities3": null,
"Reverse": false,
"CombineType": 2
},
{
"Id": {
"Keys": [
5
],
"Operator": 4,
"Reverse": false
},
"Text1": null,
"Entity2": null,
"Entities3": null,
"Reverse": false,
"CombineType": 1
}
],
"FilterGroups": [
{
"Filters": [
{
"Id": {
"Keys": [
20
],
"Operator": 1,
"Reverse": false
},
"Text1": {
"Keys": [
"bb"
],
"Operator": 4,
"Reverse": false
},
"Entity2": null,
"Entities3": {
"Query": null,
"Count": {
"Keys": [
50
],
"Operator": 1,
"Reverse": false
},
"Percent": null,
"Reverse": false
},
"Reverse": false,
"CombineType": 1
}
],
"FilterGroups": []
}
]
},
"OrderKeys": [
{
"OrderKind": 2,
"Key": 2
},
{
"OrderKind": 1,
"Key": 1
}
],
"OffsetPage": {
"PageIndex": 1,
"PageSize": 20
}
}
JSON中的屬性如果是類型定義時的默認值,可以省略不寫。例如字符串搜索的默認操作是包含子串,條件反轉的默認值是false等。
特點總結
這套查詢生成器是一個完全可組合的結構。從一組內置基礎類型的篩選器開始組合出基層自定義類型的生成器,再通過基礎篩選器和自定義生成器的組合繼續(xù)組合出具有嵌套結構的類型的篩選器,最后通過泛型的分頁和高級查詢生成器組合出完整的查詢生成器。這些查詢生成器也可以獨立使用,自由度很高。例如分頁查詢的總數(shù)計算,就可以只提取其中的篩選表達式部分來用,其中的各種自定義篩選器也都可以當作頂層篩選器來用。
基礎類型的篩選器只實現(xiàn)組合生成器接口,因為基礎類型一定是作為其他類型的屬性來用的,所以針對基礎類型的條件也一定要嫁接到一個屬性訪問表達式上才有意義。對于自定義表達式生成器,當作為頂級類型來使用時,表現(xiàn)為直接生成器,以當前類型為目標生成表達式;當作為其他類型的屬性時,又表現(xiàn)為組合生成器,把生成的條件嫁接到上層對象的屬性上。
篩選器的各個屬性名和作用目標屬性名完全無關,這樣既隔離了內部代碼和外部查詢,使兩邊互不干擾,也能輕松對外部查詢隱藏內部名稱,降低安全風險。由于每個可查詢的屬性都是明確定義的,因此完全不存在惡意攻擊的可能性,如果想對參數(shù)的范圍之類的信息進行審查,結構化的查詢數(shù)據也非常容易操作。從查詢生成的定義中可以看出,查詢的每一個片段都是靜態(tài)表達式,因此生成器的所有部分都完全兼容靜態(tài)編譯檢查和自動重構。
這個查詢生成器解決了System.Linq.Dynamic.Core和LinqKit的劣勢,相比較可能唯一的不便之處是代碼量稍大,等價的JSON表示內容量較大,但是就因此獲得的組合靈活性、序列化傳輸兼容性和靜態(tài)安全性而言,這點代價還是可以接受的。
為了減少復雜查詢的需要,筆者把查詢關鍵字設計為數(shù)組類型,再根據操作檢查具體數(shù)據。例如候選項查詢,如果不直接支持,就只能使用高級查詢生成器的基礎生成器數(shù)組之間的Or連接來模擬。既然候選項查詢必須使用數(shù)組型關鍵字,干脆充分利用這個數(shù)組的特點,直接提供區(qū)間查詢,多個不連續(xù)區(qū)間查詢等功能,最大程度減少對高級查詢生成器的依賴,盡可能在簡單查詢生成器里實現(xiàn)絕大部部分常見條件。
如果直接把表達式編譯成委托來用的話,可能會出現(xiàn)空引用異常,因為表達式不支持空傳播運算符,只能直接訪問。用EF Core生成SQL不會出現(xiàn)問題。
結語
很久以前筆者就思考過,利用LINQ實現(xiàn)動態(tài)表達式生成應該怎么辦。剛開始發(fā)現(xiàn)JqGrid這個表格組件支持嵌套的復雜條件,并以嵌套的JSON結構來表示。后來又驚嘆于了HotChocolate的自動條件參數(shù)生成和架構修改配置。開始思考動態(tài)生成的問題后又先后研究了System.Linq.Dynamic.Core和LinqKit等方案,分析總結了他們的特點和優(yōu)劣。幾經周折終于實現(xiàn)了這個比較滿意表達式生成器。
像普通表達式生成器接口和組合表達式生成器接口就是研究過程中發(fā)現(xiàn)應該是兩個不同的功能和接口才分離出來的。對于基礎類型生成器,一定要嫁接到到其他類型的屬性上才有用。而對于分頁生成器來說又沒有可組合的必要,要分頁就說明應該是以頂級類型的身份來用。對于自定義類型的生成器來說又是兩種都有可能。這樣隨著研究的深入問題逐步清晰的情況經常出現(xiàn),而且構思階段很難發(fā)現(xiàn)。
最開始分頁生成器是沒有通用泛型類的,需要自己繼承,但是用了一段時間發(fā)現(xiàn)這個東西形態(tài)固定,實際上可以用泛型類實現(xiàn)。自動化條件反轉和防止重復反轉也是后來才發(fā)現(xiàn)和解決。
這次研究能順利進行下去的一個關鍵是想到了對于復雜嵌套類型,可以把完整的條件表達式拆分為從頂級類型到目標類型的訪問表達式和針對目標類型的條件表達式作為兩個獨立的部分來處理,然后使用表達式訪問器拼合兩個部分。這樣使得生成器和數(shù)據類型一樣可以自由組合。嵌套的表達式生成問題曾一直困擾著筆者,直到弄懂了表達式訪問器的用法和打通了思路。
經過這次研究,對表達式的使用也更加熟練,收獲頗豐。歡迎園友體驗交流。
QQ群
讀者交流QQ群:540719365
歡迎讀者和廣大朋友一起交流,如發(fā)現(xiàn)本書錯誤也歡迎通過博客園、QQ群等方式告知筆者。
本文地址:如何在 .NET 中構建一個好用的動態(tài)查詢生成器
總結
以上是生活随笔為你收集整理的如何在 .NET 中构建一个好用的动态查询生成器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 首刷是什么意思
- 下一篇: 个人消费贷款和经营性贷款有什么区别