3atv精品不卡视频,97人人超碰国产精品最新,中文字幕av一区二区三区人妻少妇,久久久精品波多野结衣,日韩一区二区三区精品

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

如何在 .NET 中构建一个好用的动态查询生成器

發(fā)布時間:2025/6/17 编程问答 27 如意码农
生活随笔 收集整理的這篇文章主要介紹了 如何在 .NET 中构建一个好用的动态查询生成器 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

自從.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.CoreLinqKitSystem.Linq.Dynamic.Core使用字符串定義表達式,并在內部轉換成ExpressionLinqKit則是使用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.CoreLinqKit的劣勢,相比較可能唯一的不便之處是代碼量稍大,等價的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.CoreLinqKit等方案,分析總結了他們的特點和優(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 中构建一个好用的动态查询生成器的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

精品国产一区二区三区四区 | 人人妻人人澡人人爽欧美精品 | 成熟人妻av无码专区 | 亚洲码国产精品高潮在线 | 亚洲精品鲁一鲁一区二区三区 | 欧美精品一区二区精品久久 | 亚洲狠狠婷婷综合久久 | 纯爱无遮挡h肉动漫在线播放 | 亚洲国产成人av在线观看 | 思思久久99热只有频精品66 | 国产黄在线观看免费观看不卡 | 国产成人无码a区在线观看视频app | 牛和人交xxxx欧美 | 色偷偷人人澡人人爽人人模 | 国产一精品一av一免费 | 男人的天堂2018无码 | 日韩人妻无码一区二区三区久久99 | 丰满人妻精品国产99aⅴ | 日日夜夜撸啊撸 | 亚洲第一无码av无码专区 | 2019nv天堂香蕉在线观看 | 无码帝国www无码专区色综合 | 久久综合激激的五月天 | 小sao货水好多真紧h无码视频 | 国产亚洲美女精品久久久2020 | 人人澡人人妻人人爽人人蜜桃 | 曰本女人与公拘交酡免费视频 | 国内丰满熟女出轨videos | 国产人妻精品一区二区三区不卡 | 成人精品天堂一区二区三区 | 国产高清av在线播放 | 国产在线精品一区二区三区直播 | 蜜臀aⅴ国产精品久久久国产老师 | 午夜精品久久久内射近拍高清 | 久久精品人人做人人综合试看 | 亚洲va中文字幕无码久久不卡 | 国产精品第一区揄拍无码 | 色综合久久久无码中文字幕 | 老子影院午夜伦不卡 | 久久久精品成人免费观看 | 国产午夜福利100集发布 | 国产婷婷色一区二区三区在线 | 中文字幕 人妻熟女 | 亚洲s码欧洲m码国产av | 日本乱偷人妻中文字幕 | ass日本丰满熟妇pics | а√天堂www在线天堂小说 | 精品少妇爆乳无码av无码专区 | 国产激情无码一区二区app | 国产免费久久精品国产传媒 | 少妇性荡欲午夜性开放视频剧场 | 亚洲精品一区二区三区四区五区 | 一本久久伊人热热精品中文字幕 | 欧美 亚洲 国产 另类 | 国产精品无码一区二区三区不卡 | 色一情一乱一伦一区二区三欧美 | 日韩无套无码精品 | 亚洲综合在线一区二区三区 | 欧美兽交xxxx×视频 | 中文毛片无遮挡高清免费 | 一本久久伊人热热精品中文字幕 | 午夜精品久久久内射近拍高清 | 97久久超碰中文字幕 | а√资源新版在线天堂 | 精品乱码久久久久久久 | 国产乱人伦app精品久久 国产在线无码精品电影网 国产国产精品人在线视 | 亚洲午夜久久久影院 | 天天摸天天透天天添 | 日本大香伊一区二区三区 | 国产偷国产偷精品高清尤物 | 99在线 | 亚洲 | 日韩无套无码精品 | 国产 精品 自在自线 | 俺去俺来也www色官网 | 九一九色国产 | 婷婷综合久久中文字幕蜜桃三电影 | 少妇邻居内射在线 | 人妻无码久久精品人妻 | 久久精品无码一区二区三区 | 99久久精品午夜一区二区 | 国产精品久久久久9999小说 | 白嫩日本少妇做爰 | 无套内射视频囯产 | 久久久久免费看成人影片 | 99国产精品白浆在线观看免费 | 扒开双腿吃奶呻吟做受视频 | 精品无码av一区二区三区 | 中文字幕人妻丝袜二区 | 99久久精品午夜一区二区 | 精品国产aⅴ无码一区二区 | 亚洲欧美日韩国产精品一区二区 | 人妻少妇精品无码专区二区 | 无码中文字幕色专区 | 国产成人无码av在线影院 | 精品无码国产自产拍在线观看蜜 | 国产午夜无码精品免费看 | 欧美精品一区二区精品久久 | 色综合天天综合狠狠爱 | 国产成人一区二区三区在线观看 | 亚洲欧洲日本无在线码 | 少妇厨房愉情理9仑片视频 | 无人区乱码一区二区三区 | 欧美人与牲动交xxxx | 少妇激情av一区二区 | 日本爽爽爽爽爽爽在线观看免 | 日韩在线不卡免费视频一区 | 日本在线高清不卡免费播放 | 在线亚洲高清揄拍自拍一品区 | 99麻豆久久久国产精品免费 | 国产成人一区二区三区在线观看 | 久久精品中文闷骚内射 | 老子影院午夜伦不卡 | 东京热一精品无码av | 波多野结衣一区二区三区av免费 | 日本精品少妇一区二区三区 | а√资源新版在线天堂 | 久久精品人妻少妇一区二区三区 | 亚洲色欲色欲天天天www | 国色天香社区在线视频 | 爆乳一区二区三区无码 | 欧美日韩色另类综合 | 好屌草这里只有精品 | 精品亚洲成av人在线观看 | 色婷婷综合激情综在线播放 | 国产人妻人伦精品1国产丝袜 | 欧美国产日产一区二区 | 丰满人妻翻云覆雨呻吟视频 | 色噜噜亚洲男人的天堂 | 国产麻豆精品一区二区三区v视界 | 久久人妻内射无码一区三区 | 人人妻人人澡人人爽精品欧美 | 少妇无码av无码专区在线观看 | 国产偷抇久久精品a片69 | 久久久婷婷五月亚洲97号色 | 丰满人妻精品国产99aⅴ | 久久人人爽人人爽人人片av高清 | 国产av无码专区亚洲a∨毛片 | 初尝人妻少妇中文字幕 | 玩弄中年熟妇正在播放 | 熟妇女人妻丰满少妇中文字幕 | 久久精品一区二区三区四区 | 巨爆乳无码视频在线观看 | 久久久久亚洲精品男人的天堂 | 久久久精品成人免费观看 | 日本饥渴人妻欲求不满 | 久久精品人人做人人综合 | 大胆欧美熟妇xx | 亚洲理论电影在线观看 | 亚洲精品国偷拍自产在线麻豆 | 亚洲乱码国产乱码精品精 | 老熟妇仑乱视频一区二区 | 亚洲中文字幕成人无码 | 亚洲日本va中文字幕 | 国产国产精品人在线视 | 99久久久无码国产aaa精品 | 正在播放东北夫妻内射 | 熟妇人妻无码xxx视频 | 国内精品一区二区三区不卡 | 伊在人天堂亚洲香蕉精品区 | 欧美国产日产一区二区 | 久久久国产精品无码免费专区 | 亚洲综合无码一区二区三区 | 亚洲aⅴ无码成人网站国产app | 亚洲无人区一区二区三区 | 亚洲成a人一区二区三区 | 久久成人a毛片免费观看网站 | 狠狠噜狠狠狠狠丁香五月 | 色五月丁香五月综合五月 | 久久精品国产99久久6动漫 | 国产激情无码一区二区 | 国产精品无套呻吟在线 | 97久久精品无码一区二区 | 精品一区二区三区波多野结衣 | 国产特级毛片aaaaaa高潮流水 | 国产精品人人爽人人做我的可爱 | 无码中文字幕色专区 | 久久久久久久女国产乱让韩 | 亚洲日韩乱码中文无码蜜桃臀网站 | 在线观看国产一区二区三区 | 久久国产精品偷任你爽任你 | 中文字幕中文有码在线 | 成人精品一区二区三区中文字幕 | 无码中文字幕色专区 | 欧美性猛交xxxx富婆 | 天天摸天天碰天天添 | 男人和女人高潮免费网站 | 无码人妻精品一区二区三区下载 | 欧美性生交xxxxx久久久 | 国模大胆一区二区三区 | 精品无码国产自产拍在线观看蜜 | 性做久久久久久久久 | 小鲜肉自慰网站xnxx | 欧美一区二区三区 | 一本无码人妻在中文字幕免费 | 久久综合激激的五月天 | 国产欧美亚洲精品a | 久久综合色之久久综合 | 亚洲中文字幕va福利 | 成 人影片 免费观看 | 国产绳艺sm调教室论坛 | 国产亚洲视频中文字幕97精品 | 久久久精品国产sm最大网站 | 亚洲综合在线一区二区三区 | 婷婷六月久久综合丁香 | 欧美日韩一区二区三区自拍 | 欧洲vodafone精品性 | 人妻无码久久精品人妻 | 无码福利日韩神码福利片 | 欧美黑人巨大xxxxx | 国产亚洲精品精品国产亚洲综合 | 好男人社区资源 | 成人毛片一区二区 | 一本久久伊人热热精品中文字幕 | 无码纯肉视频在线观看 | 人妻尝试又大又粗久久 | 亚洲欧美精品aaaaaa片 | 精品熟女少妇av免费观看 | 久久亚洲精品中文字幕无男同 | 欧美人与善在线com | 亚洲国产精品美女久久久久 | 亚洲伊人久久精品影院 | 国产精品高潮呻吟av久久4虎 | 无码中文字幕色专区 | 亚洲国产精品成人久久蜜臀 | 成人免费视频一区二区 | 久久久久成人精品免费播放动漫 | 亚洲国产精品一区二区第一页 | 精品一区二区三区无码免费视频 | 性色av无码免费一区二区三区 | 无码人妻久久一区二区三区不卡 | 成年美女黄网站色大免费全看 | 少女韩国电视剧在线观看完整 | 亚洲小说春色综合另类 | 成熟妇人a片免费看网站 | 我要看www免费看插插视频 | 无码乱肉视频免费大全合集 | 色综合视频一区二区三区 | 麻豆av传媒蜜桃天美传媒 | 精品国产一区av天美传媒 | 欧美日韩视频无码一区二区三 | 亚洲色欲色欲天天天www | 亚洲日韩av一区二区三区四区 | 国产尤物精品视频 | 国产精品18久久久久久麻辣 | 久久久久亚洲精品中文字幕 | 丁香花在线影院观看在线播放 | 精品成在人线av无码免费看 | 精品无码av一区二区三区 | 国产免费久久久久久无码 | 一本大道久久东京热无码av | 日韩在线不卡免费视频一区 | 国产精品99爱免费视频 | 熟妇女人妻丰满少妇中文字幕 | 亚洲а∨天堂久久精品2021 | 女人被男人躁得好爽免费视频 | 亚洲中文字幕无码中文字在线 | 麻花豆传媒剧国产免费mv在线 | 欧美三级a做爰在线观看 | 丰满肥臀大屁股熟妇激情视频 | 日韩人妻系列无码专区 | 少妇太爽了在线观看 | 国产精品毛片一区二区 | aⅴ亚洲 日韩 色 图网站 播放 | 亚洲国产精品一区二区第一页 | 国产精品亚洲一区二区三区喷水 | 综合网日日天干夜夜久久 | 亚洲小说图区综合在线 | 国产成人无码av在线影院 | 亚洲男女内射在线播放 | 日本乱人伦片中文三区 | 国产成人无码av一区二区 | 天堂久久天堂av色综合 | 欧美freesex黑人又粗又大 | 99久久婷婷国产综合精品青草免费 | 黑人巨大精品欧美黑寡妇 | 人妻天天爽夜夜爽一区二区 | 久久综合色之久久综合 | 成 人 网 站国产免费观看 | 亚洲国产一区二区三区在线观看 | 成 人 网 站国产免费观看 | 成年美女黄网站色大免费全看 | 日本乱人伦片中文三区 | 日本一区二区三区免费高清 | 欧美日韩一区二区免费视频 | 亚洲а∨天堂久久精品2021 | 老子影院午夜精品无码 | 日韩无套无码精品 | 国产成人无码av一区二区 | 久久国产精品萌白酱免费 | 麻豆果冻传媒2021精品传媒一区下载 | 牲欲强的熟妇农村老妇女视频 | 精品无码国产自产拍在线观看蜜 | 久久久成人毛片无码 | 欧美第一黄网免费网站 | 色婷婷综合中文久久一本 | 午夜福利不卡在线视频 | 任你躁国产自任一区二区三区 | 少妇久久久久久人妻无码 | 亚洲国产精品久久人人爱 | 性做久久久久久久免费看 | 亚洲成a人片在线观看日本 | 午夜精品久久久久久久 | 国精品人妻无码一区二区三区蜜柚 | 国产精品亚洲lv粉色 | 亚洲啪av永久无码精品放毛片 | 台湾无码一区二区 | 内射老妇bbwx0c0ck | 天堂无码人妻精品一区二区三区 | 99久久99久久免费精品蜜桃 | 偷窥日本少妇撒尿chinese | 欧美日韩综合一区二区三区 | 99精品久久毛片a片 | 少妇一晚三次一区二区三区 | 国产莉萝无码av在线播放 | 爱做久久久久久 | 久久综合给久久狠狠97色 | 亚洲精品国产精品乱码视色 | 国产福利视频一区二区 | 欧美亚洲国产一区二区三区 | 午夜成人1000部免费视频 | 无码午夜成人1000部免费视频 | 国产亚洲日韩欧美另类第八页 | 欧美性黑人极品hd | 久久午夜无码鲁丝片秋霞 | 奇米影视888欧美在线观看 | 熟妇女人妻丰满少妇中文字幕 | 国产亚洲视频中文字幕97精品 | 欧洲美熟女乱又伦 | 免费看男女做好爽好硬视频 | 亚洲国产成人av在线观看 | 乱码午夜-极国产极内射 | 国产日产欧产精品精品app | 免费观看又污又黄的网站 | 精品夜夜澡人妻无码av蜜桃 | 福利一区二区三区视频在线观看 | 99国产精品白浆在线观看免费 | 在线观看国产午夜福利片 | 午夜福利一区二区三区在线观看 | ass日本丰满熟妇pics | 国产精品毛多多水多 | 亚洲熟熟妇xxxx | 亚洲精品中文字幕乱码 | 老熟女重囗味hdxx69 | 永久免费观看美女裸体的网站 | 国产 精品 自在自线 | 日日躁夜夜躁狠狠躁 | 国产一精品一av一免费 | 欧美 亚洲 国产 另类 | 女人和拘做爰正片视频 | 色爱情人网站 | 国产精品久久久av久久久 | 久久久久亚洲精品男人的天堂 | 小鲜肉自慰网站xnxx | 久久精品人人做人人综合试看 | 精品久久久无码人妻字幂 | 永久免费观看美女裸体的网站 | 伊人久久婷婷五月综合97色 | 国产成人综合在线女婷五月99播放 | 76少妇精品导航 | 综合网日日天干夜夜久久 | 色婷婷久久一区二区三区麻豆 | 成人无码视频在线观看网站 | 国产亲子乱弄免费视频 | 扒开双腿吃奶呻吟做受视频 | 无码人妻少妇伦在线电影 | а√天堂www在线天堂小说 | 俺去俺来也www色官网 | 国产精品国产三级国产专播 | 久久国内精品自在自线 | 日本成熟视频免费视频 | 乱人伦人妻中文字幕无码久久网 | 2020久久香蕉国产线看观看 | 久久久久99精品国产片 | 天堂亚洲免费视频 | 无人区乱码一区二区三区 | 日日躁夜夜躁狠狠躁 | 国产精华av午夜在线观看 | 久久精品无码一区二区三区 | 香蕉久久久久久av成人 | 丰满人妻翻云覆雨呻吟视频 | 国产成人无码av片在线观看不卡 | 亚洲中文字幕成人无码 | 国产 浪潮av性色四虎 | 少妇邻居内射在线 | 日日麻批免费40分钟无码 | 色五月丁香五月综合五月 | 久久www免费人成人片 | 久久久久亚洲精品中文字幕 | 亚无码乱人伦一区二区 | 精品 日韩 国产 欧美 视频 | 久久久成人毛片无码 | 精品一区二区三区波多野结衣 | 无码av免费一区二区三区试看 | 亚洲色欲色欲欲www在线 | 亚洲一区二区三区国产精华液 | 国产激情无码一区二区app | 久9re热视频这里只有精品 | 亚洲色欲色欲天天天www | 久久视频在线观看精品 | 精品国产福利一区二区 | 国产精品.xx视频.xxtv | 亚洲成在人网站无码天堂 | 亚洲精品国产品国语在线观看 | 成人三级无码视频在线观看 | 欧美成人午夜精品久久久 | 粉嫩少妇内射浓精videos | 久久国产精品偷任你爽任你 | 黑森林福利视频导航 | 色噜噜亚洲男人的天堂 | 国产无套粉嫩白浆在线 | 中国大陆精品视频xxxx | 久久久久免费精品国产 | 永久免费精品精品永久-夜色 | 日本va欧美va欧美va精品 | 成人精品视频一区二区 | 日本精品人妻无码77777 天堂一区人妻无码 | 日韩亚洲欧美中文高清在线 | 综合人妻久久一区二区精品 | 小泽玛莉亚一区二区视频在线 | 国产suv精品一区二区五 | 日本又色又爽又黄的a片18禁 | 久久午夜无码鲁丝片 | 欧美三级不卡在线观看 | 欧美熟妇另类久久久久久多毛 | 国产xxx69麻豆国语对白 | 久久伊人色av天堂九九小黄鸭 | 人人妻人人澡人人爽欧美一区九九 | 亚洲精品无码人妻无码 | 性生交大片免费看l | 又大又硬又黄的免费视频 | 亚洲中文无码av永久不收费 | 国产人妻精品午夜福利免费 | 美女极度色诱视频国产 | 人妻尝试又大又粗久久 | 亚洲 另类 在线 欧美 制服 | 国内精品九九久久久精品 | 少妇一晚三次一区二区三区 | 欧美真人作爱免费视频 | 国内精品久久毛片一区二区 | 亚洲精品中文字幕乱码 | 欧美性生交xxxxx久久久 | 午夜福利一区二区三区在线观看 | 国产精品视频免费播放 | 中文字幕日韩精品一区二区三区 | 秋霞成人午夜鲁丝一区二区三区 | 国产亚洲美女精品久久久2020 | 婷婷丁香六月激情综合啪 | 中文字幕av无码一区二区三区电影 | 鲁鲁鲁爽爽爽在线视频观看 | 高清国产亚洲精品自在久久 | 日日鲁鲁鲁夜夜爽爽狠狠 | 精品国产青草久久久久福利 | 欧美日韩视频无码一区二区三 | 99精品国产综合久久久久五月天 | 国产精品无套呻吟在线 | 午夜福利试看120秒体验区 | 任你躁在线精品免费 | 国产成人无码区免费内射一片色欲 | 131美女爱做视频 | 亚洲男人av天堂午夜在 | 97久久国产亚洲精品超碰热 | 亚洲中文无码av永久不收费 | 十八禁真人啪啪免费网站 | 全黄性性激高免费视频 | 日韩av无码一区二区三区 | 国产成人人人97超碰超爽8 | 18禁黄网站男男禁片免费观看 | 99麻豆久久久国产精品免费 | 国产69精品久久久久app下载 | 扒开双腿吃奶呻吟做受视频 | 精品无码一区二区三区的天堂 | 亚洲国产成人av在线观看 | 亚洲人成无码网www | 欧美日韩一区二区综合 | 亚洲中文字幕在线无码一区二区 | 国产精品怡红院永久免费 | 天堂亚洲2017在线观看 | www国产亚洲精品久久久日本 | 亚洲七七久久桃花影院 | 欧美xxxx黑人又粗又长 | 性色欲网站人妻丰满中文久久不卡 | 装睡被陌生人摸出水好爽 | 亚洲天堂2017无码 | 精品日本一区二区三区在线观看 | 最近中文2019字幕第二页 | 国产婷婷色一区二区三区在线 | 久在线观看福利视频 | 高清国产亚洲精品自在久久 | 人人妻人人澡人人爽欧美一区 | 十八禁视频网站在线观看 | 最新国产麻豆aⅴ精品无码 | 国产人成高清在线视频99最全资源 | 性史性农村dvd毛片 | 国产精品a成v人在线播放 | 亚洲狠狠色丁香婷婷综合 | 亚洲综合在线一区二区三区 | 日日干夜夜干 | 乱人伦人妻中文字幕无码 | 国产精品毛多多水多 | 正在播放东北夫妻内射 | 无码人妻精品一区二区三区不卡 | 丰满少妇人妻久久久久久 | 亚洲精品午夜无码电影网 | 四虎永久在线精品免费网址 | 2020最新国产自产精品 | 久久精品人妻少妇一区二区三区 | 99re在线播放 | 无码福利日韩神码福利片 | 精品一区二区三区无码免费视频 | 丰腴饱满的极品熟妇 | 国产精品多人p群无码 | 亚洲人成人无码网www国产 | 国产农村妇女高潮大叫 | 日韩人妻无码中文字幕视频 | 2020久久香蕉国产线看观看 | 亚洲а∨天堂久久精品2021 | 亚洲人交乣女bbw | 国产熟妇另类久久久久 | 国产无套内射久久久国产 | 国产乱人偷精品人妻a片 | 澳门永久av免费网站 | 婷婷综合久久中文字幕蜜桃三电影 | 丰满护士巨好爽好大乳 | 麻豆蜜桃av蜜臀av色欲av | 少妇一晚三次一区二区三区 | 亚洲人成网站免费播放 | 国产卡一卡二卡三 | 亚洲成色在线综合网站 | 成人影院yy111111在线观看 | 欧美 丝袜 自拍 制服 另类 | 国产成人一区二区三区在线观看 | 天海翼激烈高潮到腰振不止 | 最近免费中文字幕中文高清百度 | 小sao货水好多真紧h无码视频 | 色综合久久久无码网中文 | 人人妻人人澡人人爽精品欧美 | 日本爽爽爽爽爽爽在线观看免 | 欧美日韩一区二区综合 | 亚洲 激情 小说 另类 欧美 | 国产精品va在线观看无码 | 中文字幕乱码人妻二区三区 | 国产精品爱久久久久久久 | 国产色xx群视频射精 | 久久精品女人的天堂av | 中文字幕无码免费久久99 | 国精产品一区二区三区 | 激情内射亚州一区二区三区爱妻 | 亚洲欧美精品伊人久久 | 久久久中文字幕日本无吗 | 国产性生交xxxxx无码 | 国产精品99爱免费视频 | 亚洲欧美色中文字幕在线 | 国产深夜福利视频在线 | 久久久久久久女国产乱让韩 | 亚洲а∨天堂久久精品2021 | 欧洲精品码一区二区三区免费看 | 亚洲一区二区三区在线观看网站 | 久久亚洲精品中文字幕无男同 | 日本精品少妇一区二区三区 | 亚洲经典千人经典日产 | 精品午夜福利在线观看 | 天天做天天爱天天爽综合网 | 国产 精品 自在自线 | 色综合久久久久综合一本到桃花网 | 性色欲网站人妻丰满中文久久不卡 | 西西人体www44rt大胆高清 | 久久久精品欧美一区二区免费 | 国产特级毛片aaaaaa高潮流水 | 色综合久久88色综合天天 | 黑人玩弄人妻中文在线 | 久久国产精品精品国产色婷婷 | 欧洲极品少妇 | 亚洲爆乳大丰满无码专区 | 任你躁在线精品免费 | 免费观看激色视频网站 | 99久久精品日本一区二区免费 | 一本久久a久久精品vr综合 | 妺妺窝人体色www在线小说 | 免费网站看v片在线18禁无码 | 一本大道伊人av久久综合 | 中文字幕av伊人av无码av | 亚洲综合久久一区二区 | 亚洲综合伊人久久大杳蕉 | 狠狠色噜噜狠狠狠7777奇米 | 精品水蜜桃久久久久久久 | 亚洲小说图区综合在线 | 亚洲成av人在线观看网址 | 精品国产一区二区三区四区在线看 | 国产一区二区三区影院 | 国产乱子伦视频在线播放 | 2020久久香蕉国产线看观看 | 久久久精品人妻久久影视 | av无码不卡在线观看免费 | 精品成人av一区二区三区 | 久久久精品人妻久久影视 | 国产无遮挡又黄又爽又色 | 中文字幕无码人妻少妇免费 | 国产精品多人p群无码 | 大肉大捧一进一出好爽视频 | 精品少妇爆乳无码av无码专区 | 2019nv天堂香蕉在线观看 | 色狠狠av一区二区三区 | 人妻插b视频一区二区三区 | 小鲜肉自慰网站xnxx | 中文字幕av无码一区二区三区电影 | 日本熟妇大屁股人妻 | 午夜性刺激在线视频免费 | 高清国产亚洲精品自在久久 | 中文字幕 亚洲精品 第1页 | 亚洲中文字幕va福利 | 日日麻批免费40分钟无码 | 亚洲经典千人经典日产 | 亚洲小说春色综合另类 | 青青久在线视频免费观看 | 无码一区二区三区在线观看 | 精品成人av一区二区三区 | 67194成是人免费无码 | 麻豆蜜桃av蜜臀av色欲av | 日韩在线不卡免费视频一区 | 欧美激情综合亚洲一二区 | 国产精品美女久久久网av | 中文字幕无码av波多野吉衣 | 无码国产色欲xxxxx视频 | 国产人妻大战黑人第1集 | 乌克兰少妇xxxx做受 | √8天堂资源地址中文在线 | 人妻夜夜爽天天爽三区 | 精品无码一区二区三区爱欲 | 1000部夫妻午夜免费 | 国产精品无套呻吟在线 | 成人欧美一区二区三区 | 一本久久伊人热热精品中文字幕 | 伊人久久婷婷五月综合97色 | 97se亚洲精品一区 | 少妇被粗大的猛进出69影院 | 国产成人无码av一区二区 | 久久人人爽人人人人片 | 精品久久久久香蕉网 | 精品偷拍一区二区三区在线看 | 午夜无码人妻av大片色欲 | 国产无遮挡又黄又爽免费视频 | 露脸叫床粗话东北少妇 | 少妇性荡欲午夜性开放视频剧场 | 国产免费久久精品国产传媒 | 又色又爽又黄的美女裸体网站 | 性色欲情网站iwww九文堂 | 久久久精品欧美一区二区免费 | 99久久精品无码一区二区毛片 | 国产精品久久久 | 精品久久久无码中文字幕 | 精品无人区无码乱码毛片国产 | 性欧美大战久久久久久久 | 国产97色在线 | 免 | 国产精品亚洲一区二区三区喷水 | 色情久久久av熟女人妻网站 | 丝袜美腿亚洲一区二区 | 久久zyz资源站无码中文动漫 | 亚洲精品国产品国语在线观看 | 天堂在线观看www | 对白脏话肉麻粗话av | 日本欧美一区二区三区乱码 | 日韩精品乱码av一区二区 | 国产在热线精品视频 | 日本乱人伦片中文三区 | 国产香蕉97碰碰久久人人 | 国产乱子伦视频在线播放 | 国产激情艳情在线看视频 | 久久综合给久久狠狠97色 | 熟妇人妻无乱码中文字幕 | 国产成人午夜福利在线播放 | 中文字幕日产无线码一区 | 伊在人天堂亚洲香蕉精品区 | 国产精品视频免费播放 | 欧美精品一区二区精品久久 | av在线亚洲欧洲日产一区二区 | 久久亚洲精品中文字幕无男同 | 久久久久免费精品国产 | 九九久久精品国产免费看小说 | 国产凸凹视频一区二区 | 亚洲日韩精品欧美一区二区 | 欧美日韩综合一区二区三区 | 久久亚洲日韩精品一区二区三区 | 色综合久久久久综合一本到桃花网 | 中文字幕人妻无码一夲道 | 亚洲精品成人福利网站 | 狠狠躁日日躁夜夜躁2020 | 国产免费观看黄av片 | 国产午夜无码视频在线观看 | 精品成人av一区二区三区 | 午夜无码人妻av大片色欲 | 色综合久久中文娱乐网 | 人人妻人人澡人人爽人人精品浪潮 | 熟女少妇在线视频播放 | 亚洲欧洲日本综合aⅴ在线 | 久久成人a毛片免费观看网站 | 夜精品a片一区二区三区无码白浆 | 国产亚洲tv在线观看 | 蜜桃臀无码内射一区二区三区 | 久久久精品欧美一区二区免费 | 精品久久久中文字幕人妻 | 中文字幕乱码人妻无码久久 | 国产猛烈高潮尖叫视频免费 | 人人妻人人澡人人爽人人精品浪潮 | 漂亮人妻洗澡被公强 日日躁 | 在线播放免费人成毛片乱码 | 亚洲日本va午夜在线电影 | 国产两女互慰高潮视频在线观看 | 宝宝好涨水快流出来免费视频 | 久久五月精品中文字幕 | 国产在热线精品视频 | 7777奇米四色成人眼影 | 亚洲aⅴ无码成人网站国产app | 人人妻人人澡人人爽欧美一区 | 免费网站看v片在线18禁无码 | 国产手机在线αⅴ片无码观看 | 18禁黄网站男男禁片免费观看 | 特黄特色大片免费播放器图片 | 国色天香社区在线视频 | 性做久久久久久久免费看 | 成人精品天堂一区二区三区 | 特黄特色大片免费播放器图片 | 亚洲国产成人a精品不卡在线 | 欧美zoozzooz性欧美 | 久久久精品456亚洲影院 | 亚洲爆乳无码专区 | 亚洲精品久久久久中文第一幕 | 国产亚洲精品久久久ai换 | 又色又爽又黄的美女裸体网站 | 亚洲精品久久久久avwww潮水 | 日本精品人妻无码免费大全 | 日韩少妇白浆无码系列 | 少妇无码一区二区二三区 | 精品午夜福利在线观看 | 四虎永久在线精品免费网址 | 久久人人97超碰a片精品 | 国产国产精品人在线视 | 人妻中文无码久热丝袜 | 1000部啪啪未满十八勿入下载 | 日韩精品久久久肉伦网站 | 俺去俺来也在线www色官网 | 成熟女人特级毛片www免费 | 国产九九九九九九九a片 | 无码午夜成人1000部免费视频 | 国产亚洲精品久久久久久久久动漫 | 久久人妻内射无码一区三区 | 国产9 9在线 | 中文 | 亚欧洲精品在线视频免费观看 | 成人综合网亚洲伊人 | 国内综合精品午夜久久资源 | 久久久久免费精品国产 | 丁香啪啪综合成人亚洲 | 国产精品多人p群无码 | 性色欲情网站iwww九文堂 | 国产av久久久久精东av | 午夜福利试看120秒体验区 | 青草视频在线播放 | 色综合久久久无码网中文 | 亚洲精品综合五月久久小说 | 一本久久a久久精品亚洲 | 在线观看国产午夜福利片 | 欧美乱妇无乱码大黄a片 | 国产三级久久久精品麻豆三级 | 人妻aⅴ无码一区二区三区 | 2020最新国产自产精品 | 人妻少妇被猛烈进入中文字幕 | 日日鲁鲁鲁夜夜爽爽狠狠 | 久久精品人人做人人综合试看 | √8天堂资源地址中文在线 | 国产人妻精品午夜福利免费 | 欧美怡红院免费全部视频 | 少妇太爽了在线观看 | 国产猛烈高潮尖叫视频免费 | 色噜噜亚洲男人的天堂 | 亚洲а∨天堂久久精品2021 | 国产成人一区二区三区在线观看 | 亚洲理论电影在线观看 | 国产精品久久久av久久久 | 日韩人妻系列无码专区 | 无码午夜成人1000部免费视频 | 国产性生大片免费观看性 | 亚洲无人区午夜福利码高清完整版 | 久久精品无码一区二区三区 | 波多野结衣av一区二区全免费观看 | 在线欧美精品一区二区三区 | 久久久久亚洲精品中文字幕 | 国产激情无码一区二区app | 中文字幕+乱码+中文字幕一区 | 丰满少妇女裸体bbw | 中文字幕人妻丝袜二区 | 领导边摸边吃奶边做爽在线观看 | 日本乱偷人妻中文字幕 | 亚洲成a人片在线观看无码 | 波多野42部无码喷潮在线 | 国产成人无码专区 | 三上悠亚人妻中文字幕在线 | 99久久婷婷国产综合精品青草免费 | 久久亚洲日韩精品一区二区三区 | 国产绳艺sm调教室论坛 | 大地资源中文第3页 | 精品国偷自产在线 | 国精品人妻无码一区二区三区蜜柚 | 中文字幕亚洲情99在线 | 中文字幕av无码一区二区三区电影 | 性欧美牲交xxxxx视频 | 狠狠综合久久久久综合网 | 无码人妻精品一区二区三区下载 | 无码毛片视频一区二区本码 | 久久综合给久久狠狠97色 | 男女猛烈xx00免费视频试看 | 无码人妻丰满熟妇区五十路百度 | 午夜福利一区二区三区在线观看 | 国产精品无码永久免费888 | 成人三级无码视频在线观看 | 图片区 小说区 区 亚洲五月 | 成年女人永久免费看片 | 日日摸日日碰夜夜爽av | 国产色视频一区二区三区 | 亚洲人成影院在线无码按摩店 | 成人片黄网站色大片免费观看 | 又大又黄又粗又爽的免费视频 | 无码人妻精品一区二区三区下载 | 久久国内精品自在自线 | 久久精品中文字幕一区 | 狂野欧美性猛xxxx乱大交 | 国产精品美女久久久网av | 欧美丰满少妇xxxx性 | 国产精品国产自线拍免费软件 | 亚洲综合无码一区二区三区 | 在线a亚洲视频播放在线观看 | 亚洲中文字幕无码中文字在线 | 人人妻人人澡人人爽欧美一区九九 | 亚洲精品无码人妻无码 | 国产九九九九九九九a片 | 大屁股大乳丰满人妻 | 人妻天天爽夜夜爽一区二区 | 亚洲欧美综合区丁香五月小说 | 午夜精品久久久内射近拍高清 | 亚洲日韩中文字幕在线播放 | 天干天干啦夜天干天2017 | 国产亲子乱弄免费视频 | 中文无码成人免费视频在线观看 | 无码人妻久久一区二区三区不卡 | 亚洲精品综合一区二区三区在线 | 成人免费视频视频在线观看 免费 | 久久精品国产日本波多野结衣 | 动漫av一区二区在线观看 | 久久精品国产99精品亚洲 | 99久久久国产精品无码免费 | 国产精品久久精品三级 | 日本va欧美va欧美va精品 | 日韩精品无码一本二本三本色 | 色欲综合久久中文字幕网 | 亚洲精品一区二区三区大桥未久 | 国产精品久久国产精品99 | 无码毛片视频一区二区本码 | 精品久久久无码人妻字幂 | 高清国产亚洲精品自在久久 | 国产精品福利视频导航 | 欧美亚洲日韩国产人成在线播放 | 中文字幕av日韩精品一区二区 | 国产猛烈高潮尖叫视频免费 | 国产性猛交╳xxx乱大交 国产精品久久久久久无码 欧洲欧美人成视频在线 | 亚洲成av人片在线观看无码不卡 | 一本色道婷婷久久欧美 | 亚洲精品中文字幕 | 亚洲小说春色综合另类 | 日本精品高清一区二区 | 熟女体下毛毛黑森林 | 中文字幕 亚洲精品 第1页 | 国内精品久久久久久中文字幕 | 鲁鲁鲁爽爽爽在线视频观看 | 久久天天躁狠狠躁夜夜免费观看 | 国产精品人人妻人人爽 | 国产亚洲欧美在线专区 | 1000部夫妻午夜免费 | 亚洲国产av美女网站 | 久久亚洲精品中文字幕无男同 | 欧美熟妇另类久久久久久不卡 | 久久精品一区二区三区四区 | 在线播放无码字幕亚洲 | 老熟妇仑乱视频一区二区 | 久久天天躁夜夜躁狠狠 | 亚洲日韩av一区二区三区四区 | 国产综合色产在线精品 | 动漫av网站免费观看 | 大乳丰满人妻中文字幕日本 | 宝宝好涨水快流出来免费视频 | 久久人人爽人人爽人人片ⅴ | 久久久亚洲欧洲日产国码αv | 亚洲国产精品成人久久蜜臀 | 精品国产成人一区二区三区 | 亚洲男女内射在线播放 | 三级4级全黄60分钟 | 日本高清一区免费中文视频 | 国产两女互慰高潮视频在线观看 | 日韩人妻系列无码专区 | 全黄性性激高免费视频 | 1000部啪啪未满十八勿入下载 | 日韩少妇内射免费播放 | 久久国内精品自在自线 | 色综合久久久无码中文字幕 | 中文无码成人免费视频在线观看 | 国内综合精品午夜久久资源 | 18禁止看的免费污网站 | 无遮挡啪啪摇乳动态图 | 欧美 日韩 亚洲 在线 | 牛和人交xxxx欧美 | 无码国模国产在线观看 | 黑人玩弄人妻中文在线 | 久久亚洲国产成人精品性色 | 久久久无码中文字幕久... | 曰韩无码二三区中文字幕 | 国产又爽又猛又粗的视频a片 | 久久zyz资源站无码中文动漫 | 亚洲国产精品成人久久蜜臀 | 亚洲精品午夜国产va久久成人 | 亚洲中文字幕乱码av波多ji | 亚洲区小说区激情区图片区 | 丰满少妇人妻久久久久久 | 日韩无码专区 | 中文字幕无码免费久久9一区9 | 性生交片免费无码看人 | 色一情一乱一伦一区二区三欧美 | 3d动漫精品啪啪一区二区中 | 131美女爱做视频 | 女人和拘做爰正片视频 | 国产又爽又猛又粗的视频a片 | 国产高潮视频在线观看 | 5858s亚洲色大成网站www | 亚洲国产精品久久久久久 | 妺妺窝人体色www婷婷 | 青草视频在线播放 | 国产精品国产三级国产专播 | 亚洲春色在线视频 | 永久黄网站色视频免费直播 | 日日碰狠狠丁香久燥 | 亚洲人成影院在线无码按摩店 | 欧美熟妇另类久久久久久多毛 | 亚洲热妇无码av在线播放 | 久青草影院在线观看国产 | 亚洲国产高清在线观看视频 | 精品 日韩 国产 欧美 视频 | 又大又硬又黄的免费视频 | 欧美成人午夜精品久久久 | 欧美人与动性行为视频 | 无码国内精品人妻少妇 | 扒开双腿疯狂进出爽爽爽视频 | 强开小婷嫩苞又嫩又紧视频 | 欧美变态另类xxxx | 欧美日韩在线亚洲综合国产人 | 人妻插b视频一区二区三区 | 精品久久综合1区2区3区激情 | 久久婷婷五月综合色国产香蕉 | 日本精品少妇一区二区三区 | 午夜嘿嘿嘿影院 | 大肉大捧一进一出视频出来呀 | 国产午夜手机精彩视频 | 国产精品手机免费 | 男女猛烈xx00免费视频试看 | 亚洲熟妇色xxxxx欧美老妇 | 99国产精品白浆在线观看免费 | 亚洲精品久久久久avwww潮水 | 色综合久久久无码中文字幕 | 亚洲va欧美va天堂v国产综合 | 成人片黄网站色大片免费观看 | 欧美老妇与禽交 | ass日本丰满熟妇pics | 天天躁日日躁狠狠躁免费麻豆 | 波多野结衣aⅴ在线 | 久久天天躁夜夜躁狠狠 | 午夜无码人妻av大片色欲 | 久在线观看福利视频 | 影音先锋中文字幕无码 | 成人欧美一区二区三区 | 香蕉久久久久久av成人 | 天天爽夜夜爽夜夜爽 | 中文字幕日韩精品一区二区三区 | 真人与拘做受免费视频 | 国产三级精品三级男人的天堂 | 日日干夜夜干 | 日产精品高潮呻吟av久久 | 久久亚洲a片com人成 | 亚洲精品成人福利网站 | 亚洲综合在线一区二区三区 | 国产极品视觉盛宴 | 亚洲精品综合五月久久小说 | 久久99久久99精品中文字幕 | 熟妇人妻无乱码中文字幕 | 亚无码乱人伦一区二区 | 亚洲色欲色欲欲www在线 | 东京一本一道一二三区 | 最近的中文字幕在线看视频 | 无套内谢老熟女 | 一个人看的www免费视频在线观看 | 亚洲人成无码网www | 国产成人无码a区在线观看视频app | 精品欧洲av无码一区二区三区 | 国产在线精品一区二区高清不卡 | 国产在线aaa片一区二区99 | 毛片内射-百度 | 国产黑色丝袜在线播放 | 日本精品人妻无码77777 天堂一区人妻无码 | 少妇被黑人到高潮喷出白浆 | 精品偷拍一区二区三区在线看 | 欧美xxxx黑人又粗又长 | 国产美女极度色诱视频www | 久久久久亚洲精品中文字幕 | 免费人成网站视频在线观看 | 成 人 网 站国产免费观看 | 日本xxxx色视频在线观看免费 | 亚洲一区二区三区四区 | 日本免费一区二区三区最新 | 中文字幕无码日韩欧毛 | 国产亚洲tv在线观看 | 亚洲精品国偷拍自产在线观看蜜桃 | 精品国产乱码久久久久乱码 | 日产精品高潮呻吟av久久 | 76少妇精品导航 | 国产特级毛片aaaaaa高潮流水 | a片在线免费观看 | 国产乱人无码伦av在线a | 欧美人与动性行为视频 | av小次郎收藏 | 国产激情艳情在线看视频 | 久久国产精品精品国产色婷婷 | 国产精品亚洲а∨无码播放麻豆 | 国产美女精品一区二区三区 | 亚洲人成影院在线无码按摩店 | 高潮喷水的毛片 | 色 综合 欧美 亚洲 国产 | 无码人妻久久一区二区三区不卡 | 丝袜人妻一区二区三区 | 久久久久久亚洲精品a片成人 | 无码精品人妻一区二区三区av | 精品人妻人人做人人爽 | 色综合久久久无码网中文 | 国产9 9在线 | 中文 | 少妇性俱乐部纵欲狂欢电影 | 亚洲一区二区三区国产精华液 | av香港经典三级级 在线 | 国内精品久久毛片一区二区 | 人妻aⅴ无码一区二区三区 | 午夜理论片yy44880影院 | 美女张开腿让人桶 | 国产女主播喷水视频在线观看 | 久久久久人妻一区精品色欧美 | 国产特级毛片aaaaaa高潮流水 | 久久久久se色偷偷亚洲精品av | 久久99久久99精品中文字幕 | 中文字幕无线码免费人妻 | 一二三四社区在线中文视频 | 国产午夜精品一区二区三区嫩草 | 欧洲精品码一区二区三区免费看 | 欧美丰满少妇xxxx性 | 77777熟女视频在线观看 а天堂中文在线官网 | 午夜福利试看120秒体验区 | 少妇久久久久久人妻无码 | 亚洲熟熟妇xxxx | 欧美 日韩 人妻 高清 中文 | 日本在线高清不卡免费播放 | 女人被男人爽到呻吟的视频 | 中文字幕无码视频专区 | 国内少妇偷人精品视频免费 | 天堂а√在线地址中文在线 | 18黄暴禁片在线观看 | 国产精品人人爽人人做我的可爱 | 国产成人精品必看 | 久久这里只有精品视频9 | 亚洲中文字幕va福利 | 亚洲色欲色欲天天天www | 欧美国产日韩久久mv | 中文字幕无码av波多野吉衣 | 四十如虎的丰满熟妇啪啪 | 欧美变态另类xxxx | 亚洲呦女专区 | 影音先锋中文字幕无码 | 日韩少妇内射免费播放 | 少妇无码一区二区二三区 | 少妇无码av无码专区在线观看 | 高清不卡一区二区三区 | 欧美肥老太牲交大战 | 九九久久精品国产免费看小说 | 最新版天堂资源中文官网 | 国产婷婷色一区二区三区在线 | 欧美一区二区三区 | 国产成人无码a区在线观看视频app | 荫蒂添的好舒服视频囗交 | 国产精品久免费的黄网站 | 超碰97人人射妻 | 国产熟妇高潮叫床视频播放 | 2020最新国产自产精品 | 亚洲精品无码人妻无码 | 国内精品一区二区三区不卡 | 福利一区二区三区视频在线观看 | 亚洲国产精品无码久久久久高潮 | 精品午夜福利在线观看 | 少妇激情av一区二区 | 国产小呦泬泬99精品 | 日韩精品无码一区二区中文字幕 | 老头边吃奶边弄进去呻吟 | 一本久久伊人热热精品中文字幕 | 久久精品人人做人人综合试看 | 欧美性黑人极品hd | 天天做天天爱天天爽综合网 | 香港三级日本三级妇三级 | 国产精品久久久一区二区三区 | 国产精品久免费的黄网站 | 亚洲狠狠色丁香婷婷综合 | 性生交片免费无码看人 | 日韩av无码一区二区三区不卡 | 久久 国产 尿 小便 嘘嘘 | 精品国精品国产自在久国产87 | 欧美35页视频在线观看 | 亚洲日本va中文字幕 | 国产麻豆精品一区二区三区v视界 | 日本免费一区二区三区最新 | 天堂亚洲2017在线观看 | 在线看片无码永久免费视频 | 国产97色在线 | 免 | 成人三级无码视频在线观看 | 理论片87福利理论电影 | 少妇一晚三次一区二区三区 | 波多野结衣aⅴ在线 | 大乳丰满人妻中文字幕日本 | 76少妇精品导航 | 国产熟妇高潮叫床视频播放 | 亚洲精品午夜无码电影网 | 精品久久久无码人妻字幂 | 天堂无码人妻精品一区二区三区 | 日本精品人妻无码77777 天堂一区人妻无码 | 久久久av男人的天堂 | 国产成人久久精品流白浆 | 国产网红无码精品视频 | 内射欧美老妇wbb | 久久国产36精品色熟妇 | 午夜不卡av免费 一本久久a久久精品vr综合 | 一区二区三区高清视频一 | 久久久久99精品成人片 | 色五月五月丁香亚洲综合网 | 国产另类ts人妖一区二区 | 久久zyz资源站无码中文动漫 | 玩弄中年熟妇正在播放 | 国产人妻久久精品二区三区老狼 | 熟妇人妻无码xxx视频 | 亚洲综合在线一区二区三区 | 未满小14洗澡无码视频网站 | 乌克兰少妇xxxx做受 | 99久久久无码国产aaa精品 | 亚洲自偷自偷在线制服 | 亚洲精品午夜国产va久久成人 | 特大黑人娇小亚洲女 | 精品一区二区不卡无码av | 丁香啪啪综合成人亚洲 | 久激情内射婷内射蜜桃人妖 | 国产亚洲精品精品国产亚洲综合 | 亚洲小说图区综合在线 | 欧美日韩综合一区二区三区 | 精品国精品国产自在久国产87 | 亚洲色大成网站www | 国产成人综合在线女婷五月99播放 | 最新国产麻豆aⅴ精品无码 | 国产无遮挡吃胸膜奶免费看 | 超碰97人人做人人爱少妇 | 成人免费视频视频在线观看 免费 | 国产va免费精品观看 | 99久久人妻精品免费二区 | 亚洲熟妇色xxxxx亚洲 | 久久zyz资源站无码中文动漫 | 欧洲精品码一区二区三区免费看 | 亚洲精品鲁一鲁一区二区三区 | 日本一卡二卡不卡视频查询 | 无人区乱码一区二区三区 | 亚洲一区av无码专区在线观看 | 亚洲综合无码一区二区三区 | √天堂中文官网8在线 | 玩弄少妇高潮ⅹxxxyw | 亚洲中文字幕乱码av波多ji | 国产真实夫妇视频 | 亚洲春色在线视频 | 草草网站影院白丝内射 | 麻豆国产人妻欲求不满 | 日韩精品无码一本二本三本色 | 乱码av麻豆丝袜熟女系列 | www国产亚洲精品久久久日本 | 无码毛片视频一区二区本码 | 国产人妻精品午夜福利免费 | 1000部啪啪未满十八勿入下载 | 国产亚洲精品精品国产亚洲综合 | 国产精品无码mv在线观看 | 一本色道婷婷久久欧美 | 色一情一乱一伦一区二区三欧美 | 自拍偷自拍亚洲精品被多人伦好爽 | 久久久久久国产精品无码下载 | 中文字幕人妻丝袜二区 | 午夜肉伦伦影院 | 麻豆精品国产精华精华液好用吗 | √天堂中文官网8在线 | 亚洲精品一区二区三区四区五区 | 乱人伦中文视频在线观看 | 欧美国产亚洲日韩在线二区 | 在线播放免费人成毛片乱码 | 夫妻免费无码v看片 | 激情内射日本一区二区三区 | 一本色道久久综合狠狠躁 | 国产午夜亚洲精品不卡下载 | 国产午夜福利亚洲第一 | 久久精品女人天堂av免费观看 | 成人精品天堂一区二区三区 | 偷窥日本少妇撒尿chinese | 国产精品高潮呻吟av久久 | 国产一精品一av一免费 | 亚洲精品一区三区三区在线观看 | 伦伦影院午夜理论片 | 免费看男女做好爽好硬视频 | 午夜福利一区二区三区在线观看 | 国产麻豆精品一区二区三区v视界 | 扒开双腿吃奶呻吟做受视频 | 无码国产色欲xxxxx视频 | 日日噜噜噜噜夜夜爽亚洲精品 | 97se亚洲精品一区 | 亚洲aⅴ无码成人网站国产app | 久久精品国产精品国产精品污 | 丰满少妇熟乱xxxxx视频 | 亚洲国产精品美女久久久久 | 午夜理论片yy44880影院 | 久久精品无码一区二区三区 | 国产农村妇女高潮大叫 | 一二三四在线观看免费视频 | 精品久久综合1区2区3区激情 | 亚洲成色www久久网站 | 久久午夜无码鲁丝片午夜精品 | 无人区乱码一区二区三区 | 国产精品亚洲lv粉色 | 牛和人交xxxx欧美 | 一个人免费观看的www视频 | 国产成人一区二区三区别 | 无码吃奶揉捏奶头高潮视频 | 国精品人妻无码一区二区三区蜜柚 | 熟妇激情内射com | 国精产品一品二品国精品69xx | 国产亚洲美女精品久久久2020 | 又紧又大又爽精品一区二区 | 免费国产黄网站在线观看 | 国产免费久久精品国产传媒 | 帮老师解开蕾丝奶罩吸乳网站 | 国产一区二区三区精品视频 | 国产精品久久福利网站 | 欧美人妻一区二区三区 | 国产亲子乱弄免费视频 | 成人aaa片一区国产精品 | 国产亚洲欧美日韩亚洲中文色 | 国产亚洲欧美日韩亚洲中文色 | 风流少妇按摩来高潮 | 国内丰满熟女出轨videos | 成人欧美一区二区三区黑人免费 | 国产色xx群视频射精 | 亚洲精品一区二区三区四区五区 | 4hu四虎永久在线观看 | 精品国产av色一区二区深夜久久 | 国产精品美女久久久久av爽李琼 | 国产精品久久久久7777 | 狠狠噜狠狠狠狠丁香五月 | 亚洲中文字幕无码中字 | 国产免费无码一区二区视频 | 日韩欧美中文字幕在线三区 | 国产成人无码区免费内射一片色欲 | 午夜男女很黄的视频 | 中文字幕av伊人av无码av | 亚洲欧美精品aaaaaa片 | 特大黑人娇小亚洲女 | 久久久国产精品无码免费专区 | 中文字幕无线码 | 99久久精品无码一区二区毛片 | 丰满人妻一区二区三区免费视频 | 日韩成人一区二区三区在线观看 | 在线 国产 欧美 亚洲 天堂 | 欧美熟妇另类久久久久久多毛 | 国产免费久久精品国产传媒 | 欧美成人高清在线播放 | a片在线免费观看 | 又色又爽又黄的美女裸体网站 | 乱人伦人妻中文字幕无码 | 人人超人人超碰超国产 | 国产精品内射视频免费 | 国产香蕉尹人综合在线观看 | 免费乱码人妻系列无码专区 | 天海翼激烈高潮到腰振不止 | 欧美精品免费观看二区 | 国产国语老龄妇女a片 | 波多野结衣aⅴ在线 | 国产精品-区区久久久狼 | 亚洲第一网站男人都懂 | 国产在线精品一区二区高清不卡 | 狠狠色丁香久久婷婷综合五月 | 欧美日韩视频无码一区二区三 | 欧美黑人性暴力猛交喷水 | 国产无遮挡吃胸膜奶免费看 | 午夜时刻免费入口 | 正在播放老肥熟妇露脸 | 久久精品中文字幕一区 | 人人妻人人澡人人爽欧美一区 | 免费无码午夜福利片69 | 国产精品无码一区二区桃花视频 | 久久精品人妻少妇一区二区三区 | 日韩 欧美 动漫 国产 制服 | 亚洲国产精品无码一区二区三区 | 欧美人与物videos另类 | 欧美自拍另类欧美综合图片区 | 性史性农村dvd毛片 | 欧美怡红院免费全部视频 | 亚洲娇小与黑人巨大交 | 麻豆蜜桃av蜜臀av色欲av | 亚洲熟女一区二区三区 | 最近免费中文字幕中文高清百度 | 午夜福利一区二区三区在线观看 | 精品国产国产综合精品 | 亚洲男人av天堂午夜在 | 精品 日韩 国产 欧美 视频 | 久久精品国产大片免费观看 | 亚洲一区二区观看播放 | 久久精品中文字幕大胸 | 四虎国产精品一区二区 | 东京热无码av男人的天堂 | 久久午夜无码鲁丝片 | 亚洲一区二区三区在线观看网站 | 国产精品无码mv在线观看 | 欧美乱妇无乱码大黄a片 | yw尤物av无码国产在线观看 | 欧美黑人性暴力猛交喷水 | 沈阳熟女露脸对白视频 | 国产无套粉嫩白浆在线 | 天海翼激烈高潮到腰振不止 | yw尤物av无码国产在线观看 | 国内丰满熟女出轨videos | 永久免费观看国产裸体美女 | 波多野结衣av在线观看 | 蜜桃av蜜臀av色欲av麻 999久久久国产精品消防器材 | 人人妻人人藻人人爽欧美一区 | 少妇性l交大片 | 欧美三级a做爰在线观看 | 丝袜人妻一区二区三区 | 综合激情五月综合激情五月激情1 | 国产成人午夜福利在线播放 | 久久精品无码一区二区三区 | 亚洲午夜福利在线观看 | 老熟女重囗味hdxx69 | 成在人线av无码免观看麻豆 | 亚洲 日韩 欧美 成人 在线观看 | 色 综合 欧美 亚洲 国产 | 午夜免费福利小电影 | 丝袜美腿亚洲一区二区 | 国产午夜亚洲精品不卡下载 | 狂野欧美激情性xxxx | 日本乱人伦片中文三区 | 免费男性肉肉影院 | 亚洲一区二区三区国产精华液 | 欧美国产日韩久久mv | 成人av无码一区二区三区 | 野狼第一精品社区 | 精品久久久久久人妻无码中文字幕 | 精品国产青草久久久久福利 | 在线播放亚洲第一字幕 | 男女爱爱好爽视频免费看 | 无套内射视频囯产 | 自拍偷自拍亚洲精品10p | 奇米综合四色77777久久 东京无码熟妇人妻av在线网址 | 国内精品久久毛片一区二区 | 性欧美熟妇videofreesex | 亚洲精品欧美二区三区中文字幕 | 波多野结衣 黑人 | 国产精品久久久久无码av色戒 | 欧美肥老太牲交大战 | 装睡被陌生人摸出水好爽 | 国产精品久久久久久久9999 | 欧美阿v高清资源不卡在线播放 | 高清国产亚洲精品自在久久 | 欧美激情一区二区三区成人 | 国产精品沙发午睡系列 | 国色天香社区在线视频 | 亚洲精品www久久久 | 国精品人妻无码一区二区三区蜜柚 | 国产明星裸体无码xxxx视频 | 特大黑人娇小亚洲女 | 国产精品久久久久影院嫩草 | 女人被男人躁得好爽免费视频 | 免费观看的无遮挡av | av无码不卡在线观看免费 | 激情综合激情五月俺也去 | 国产综合色产在线精品 | 思思久久99热只有频精品66 | 99久久精品日本一区二区免费 | 国产精品-区区久久久狼 | 亚欧洲精品在线视频免费观看 | av在线亚洲欧洲日产一区二区 | 国产在线无码精品电影网 | 老司机亚洲精品影院 | 欧美老妇交乱视频在线观看 | 好男人www社区 | 亚洲国产精品一区二区第一页 | 无码人妻丰满熟妇区五十路百度 | 中文字幕无线码免费人妻 | 丰满妇女强制高潮18xxxx | 少妇高潮一区二区三区99 | 欧美激情内射喷水高潮 | 欧美老人巨大xxxx做受 | 欧美性生交活xxxxxdddd | 亚洲精品国偷拍自产在线麻豆 | 国产免费久久久久久无码 | 久久久久99精品成人片 | 欧美猛少妇色xxxxx | 久久综合给久久狠狠97色 | 18无码粉嫩小泬无套在线观看 | 99久久精品午夜一区二区 | 无码人妻黑人中文字幕 | 动漫av一区二区在线观看 | 成人精品视频一区二区三区尤物 | 久久综合九色综合97网 | 国模大胆一区二区三区 | 亚洲小说图区综合在线 | av在线亚洲欧洲日产一区二区 | 欧美性生交活xxxxxdddd | 俄罗斯老熟妇色xxxx | 少妇高潮一区二区三区99 | 国产午夜亚洲精品不卡下载 | 国产成人精品视频ⅴa片软件竹菊 | 强辱丰满人妻hd中文字幕 | 爽爽影院免费观看 | 国产香蕉尹人综合在线观看 | 男女猛烈xx00免费视频试看 | 无码人妻丰满熟妇区毛片18 | 无码国产色欲xxxxx视频 | 亚洲精品久久久久久久久久久 | 高中生自慰www网站 | 亚洲日韩中文字幕在线播放 | 性欧美大战久久久久久久 | 麻豆精产国品 | 午夜无码区在线观看 | 无码av最新清无码专区吞精 | 理论片87福利理论电影 | 玩弄少妇高潮ⅹxxxyw | 国产高潮视频在线观看 | 中文字幕亚洲情99在线 | 国产精品美女久久久久av爽李琼 | 亚洲中文字幕无码一久久区 | 在线看片无码永久免费视频 | 国产明星裸体无码xxxx视频 | 九九热爱视频精品 | 亚洲大尺度无码无码专区 | 免费无码av一区二区 | 99精品久久毛片a片 | 最新国产麻豆aⅴ精品无码 | 国产精品久久久久7777 | www国产精品内射老师 | 久久亚洲中文字幕无码 | 国产精品无套呻吟在线 | 国产精品久久国产精品99 | 九九热爱视频精品 | 男人的天堂av网站 | 久久综合九色综合欧美狠狠 | 女人被男人躁得好爽免费视频 | 午夜福利试看120秒体验区 | 国产精品a成v人在线播放 | 98国产精品综合一区二区三区 | 牲欲强的熟妇农村老妇女视频 | 亚欧洲精品在线视频免费观看 | 永久免费观看美女裸体的网站 | 性史性农村dvd毛片 | 久久99精品国产.久久久久 | 国产无套粉嫩白浆在线 | 亚洲综合无码久久精品综合 | 一本久久伊人热热精品中文字幕 | 国产亚洲精品久久久久久大师 | 天天做天天爱天天爽综合网 | 妺妺窝人体色www在线小说 | 亚洲国产精品毛片av不卡在线 | 久久久久99精品国产片 | 精品国产麻豆免费人成网站 | 欧美成人午夜精品久久久 | 国产成人人人97超碰超爽8 | 亚洲精品久久久久avwww潮水 | ass日本丰满熟妇pics | 亚洲s码欧洲m码国产av | 一本无码人妻在中文字幕免费 | 日韩欧美中文字幕在线三区 | 白嫩日本少妇做爰 | 国产熟女一区二区三区四区五区 | 免费人成网站视频在线观看 | 秋霞特色aa大片 | 午夜精品久久久内射近拍高清 | 无码人妻久久一区二区三区不卡 | 久久综合狠狠综合久久综合88 | 一本一道久久综合久久 | 久久99国产综合精品 | 欧美性色19p | 一本色道久久综合亚洲精品不卡 | 国内精品九九久久久精品 | 国产va免费精品观看 | 四虎国产精品免费久久 | 久久久精品人妻久久影视 | 国产精品无码一区二区桃花视频 | 久久国产精品_国产精品 | 久久精品视频在线看15 | 精品 日韩 国产 欧美 视频 | 亚洲精品午夜国产va久久成人 | 三上悠亚人妻中文字幕在线 | 一个人免费观看的www视频 | 2019午夜福利不卡片在线 | 欧美人与牲动交xxxx | 久久久久se色偷偷亚洲精品av | 日韩精品成人一区二区三区 | 日本va欧美va欧美va精品 | 亚洲中文字幕无码一久久区 | 免费无码一区二区三区蜜桃大 | 亚洲 高清 成人 动漫 | 久久久中文久久久无码 | 久久精品99久久香蕉国产色戒 | 国产成人久久精品流白浆 | 欧美老人巨大xxxx做受 | 在线精品亚洲一区二区 | 国产人妻人伦精品1国产丝袜 | 在线精品国产一区二区三区 | 激情五月综合色婷婷一区二区 | 无码一区二区三区在线观看 | 激情国产av做激情国产爱 | 黑人巨大精品欧美一区二区 | 欧美怡红院免费全部视频 | 人人妻人人藻人人爽欧美一区 | 亚洲色偷偷偷综合网 | 亚洲人成无码网www | 久久久精品成人免费观看 | 2020久久香蕉国产线看观看 | 亚洲精品国产第一综合99久久 | 国产av一区二区三区最新精品 | 久久99精品久久久久婷婷 | а√资源新版在线天堂 | 成 人 免费观看网站 | 国产精品美女久久久久av爽李琼 | 动漫av一区二区在线观看 | 欧美成人家庭影院 | 中文字幕无码热在线视频 | 亚洲а∨天堂久久精品2021 | 女人被男人躁得好爽免费视频 | 国内精品久久久久久中文字幕 | 亚洲精品国偷拍自产在线观看蜜桃 | 在线天堂新版最新版在线8 | 色欲人妻aaaaaaa无码 | 国产精品第一区揄拍无码 | 国内精品久久毛片一区二区 | 精品少妇爆乳无码av无码专区 | 沈阳熟女露脸对白视频 | 国产成人无码区免费内射一片色欲 | 一本色道久久综合狠狠躁 | 国产真实伦对白全集 | 欧美老妇交乱视频在线观看 | 亚洲欧美日韩国产精品一区二区 | 东京热男人av天堂 | 亚洲爆乳大丰满无码专区 | 永久免费观看美女裸体的网站 | 国产精品久久国产精品99 | 丝袜足控一区二区三区 | 香港三级日本三级妇三级 | 香蕉久久久久久av成人 | 亚洲国产精品毛片av不卡在线 | 少妇被黑人到高潮喷出白浆 | 国产一区二区三区四区五区加勒比 | 亚洲成a人一区二区三区 | 国产精品亚洲一区二区三区喷水 | 免费乱码人妻系列无码专区 | 亚洲无人区午夜福利码高清完整版 | 青青草原综合久久大伊人精品 | 欧美日本免费一区二区三区 | 久9re热视频这里只有精品 | www国产亚洲精品久久网站 | 99精品久久毛片a片 | 国产精品成人av在线观看 | 狂野欧美性猛交免费视频 | 久久精品无码一区二区三区 | 狠狠综合久久久久综合网 | 超碰97人人做人人爱少妇 | 大乳丰满人妻中文字幕日本 | 久久久久久九九精品久 | а天堂中文在线官网 | 亚洲aⅴ无码成人网站国产app | 日日鲁鲁鲁夜夜爽爽狠狠 | 中文字幕无码视频专区 | 天天做天天爱天天爽综合网 | 女人高潮内射99精品 | 99精品视频在线观看免费 | 欧美日本精品一区二区三区 | 无码精品人妻一区二区三区av | 人人爽人人爽人人片av亚洲 | 亚洲精品国产精品乱码不卡 | 国产猛烈高潮尖叫视频免费 | 中文字幕无码免费久久9一区9 | 久久精品成人欧美大片 | 欧美乱妇无乱码大黄a片 | 中文字幕乱码人妻无码久久 | 国产免费无码一区二区视频 | 丰满少妇高潮惨叫视频 | 东京无码熟妇人妻av在线网址 | 精品国产麻豆免费人成网站 | 久久国产精品_国产精品 | 欧美日韩综合一区二区三区 | 少妇高潮一区二区三区99 | 国产精品久久久午夜夜伦鲁鲁 | 噜噜噜亚洲色成人网站 | 永久黄网站色视频免费直播 | 欧美刺激性大交 | 波多野结衣aⅴ在线 | 亚洲va中文字幕无码久久不卡 | 精品欧洲av无码一区二区三区 | 国产疯狂伦交大片 | 国产人妻大战黑人第1集 | 久久精品国产大片免费观看 | 久青草影院在线观看国产 | 久久久www成人免费毛片 | 成人精品一区二区三区中文字幕 | 国产精品美女久久久网av | 日本精品久久久久中文字幕 | 午夜性刺激在线视频免费 | 亚洲欧美色中文字幕在线 | 欧美一区二区三区视频在线观看 | 内射欧美老妇wbb | 久久久久久久人妻无码中文字幕爆 | 18黄暴禁片在线观看 | 乌克兰少妇xxxx做受 | 捆绑白丝粉色jk震动捧喷白浆 | 国产亲子乱弄免费视频 | 国产亚洲欧美在线专区 | 老熟妇仑乱视频一区二区 | 欧美日韩一区二区免费视频 | 亚洲国产av精品一区二区蜜芽 | 内射巨臀欧美在线视频 | 丰满少妇熟乱xxxxx视频 | 俺去俺来也在线www色官网 | 亲嘴扒胸摸屁股激烈网站 | 日韩视频 中文字幕 视频一区 | 狂野欧美性猛交免费视频 | 国产人妻人伦精品 | 东京热男人av天堂 | 在线а√天堂中文官网 | 动漫av一区二区在线观看 | 东京无码熟妇人妻av在线网址 | 亚洲色无码一区二区三区 | 六十路熟妇乱子伦 | 亚洲人成影院在线无码按摩店 | 久久97精品久久久久久久不卡 | 国产在线精品一区二区三区直播 | 亚洲aⅴ无码成人网站国产app | 国产99久久精品一区二区 | 午夜精品久久久久久久久 | 久久国语露脸国产精品电影 | 国产麻豆精品精东影业av网站 | 丰满岳乱妇在线观看中字无码 | 久久午夜无码鲁丝片 | 国产农村妇女aaaaa视频 撕开奶罩揉吮奶头视频 | a国产一区二区免费入口 | 无码人妻久久一区二区三区不卡 | 国产精品美女久久久久av爽李琼 | 国语精品一区二区三区 | 国产精品第一区揄拍无码 | 桃花色综合影院 | 欧美 丝袜 自拍 制服 另类 | 无码纯肉视频在线观看 | 日本一本二本三区免费 | 亚洲国产日韩a在线播放 | 国产香蕉尹人综合在线观看 | 无码av岛国片在线播放 | 色欲人妻aaaaaaa无码 | 中文字幕久久久久人妻 | 男人的天堂2018无码 | 亚洲人成影院在线无码按摩店 | 成在人线av无码免费 | 婷婷丁香五月天综合东京热 | 无码av免费一区二区三区试看 | 麻豆国产人妻欲求不满谁演的 |