首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

LINQ to SQL N 層架構動態查詢 (下):IQueryable 動態 Where 擴展與跨層邏輯復用

     編輯:關於.NET

問題背景:分層後的動態查詢困境

UI 層直接用 DataContext 時,var query = ... 配合匿名型別與延遲執行,動態加 .Where() 極其自然。但分層架構 (如 WCF 服務層) 中:

  • 服務層回傳 IQueryable<匿名型別> → 客戶端無法識別型別,智能感應失效。
  • 若定義具體 DTO,又難以在服務層組裝動態 Expression<Func<DTO, bool>>。

方案一:Dynamic LINQ 字串拼接 (官方範例庫)

var query = db.Customers
    .Where("City == @0 and Orders.Count >= @1", "London", 10)
    .OrderBy("CompanyName")
    .Select("New(CompanyName as Name, Phone)");
  • 優點:支援字串動態組裝 Where/OrderBy/Select。
  • 缺點:**失去編譯期檢查與智能感應**,重構風險高。

方案二:Expression 參數型別替換 —— 核心突破

核心洞見

服務層查詢產生 IQueryable<匿名型別> (含 ProductName, UnitPrice, CompanyName...)。UI 層想用強型別 Expression<Func<Product, bool>> (如 p => p.ProductName.Contains("che")) 來篩選。需將 Expression 參數從 Product → 匿名型別 轉換。

關鍵擴展方法:DynamicWhere

public static IQueryable DynamicWhere<T>(this IQueryable query, Expression<Func<T, bool>> predicate)
{
    if (predicate == null) return query;

    // 關鍵:呼叫 predicate.Replace<T>(query.ElementType) 將參數型別從 T 換成匿名型別
    var newPredicate = predicate.Replace<T>(query.ElementType);

    // 透過 Provider.CreateQuery 動態建構新的 Where CallExpression
    return query.Provider.CreateQuery(
        Expression.Call(
            typeof(Queryable), "Where",
            new Type[] { query.ElementType },
            query.Expression, newPredicate));
}

參數型別替換核心實現 (Replace 方法)

public static Expression<Func<TResult, bool>> Replace<TSource, TResult>(
    this Expression<Func<TSource, bool>> predicate)
{
    var converter = new ParameterConverter(predicate);
    return (Expression<Func<TResult, bool>>)converter.Replace(typeof(TResult));
}

// ParameterConverter 內部繼承 ExpressionVisitor (需引用 System.Linq.Expressions)
// 遍歷 Expression Tree,將所有 ParameterExpression 型別從 TSource 替換為 TResult
// 並同步更新 MemberAccess (屬性存取) 的 DeclaringType
// 完整程式碼參考原文 (約 50 行),核心邏輯為 VisitParameter + VisitMember

實戰使用範例

public List<ProductExt> TestDynamicQuery3()
{
    using (var ctx = new NorthWindDataContext())
    {
        // 服務層建構基礎查詢 (匿名型別)
        IQueryable query = from P in ctx.Products
                           join S in ctx.Suppliers on P.SupplierID equals S.SupplierID
                           select new
                           {
                               P.ProductName,
                               P.UnitPrice,
                               P.QuantityPerUnit,
                               S.CompanyName,
                               S.Address
                           };

        // UI 層以強型別 Expression 動態疊加條件
        query = query.DynamicWhere((Products p) => p.ProductName.Contains("che"))
                     .DynamicWhere((Suppliers s) => s.Address == "P.O. Box 78934")
                     .DynamicWhere((Products p) => p.UnitPrice >= 5 * 4 + 2); // 常數摺疊

        // 最後轉換為具體 DTO 回傳
        return query.ConvertTo<ProductExt>();
    }
}

ConvertTo 擴展方法:匿名型別 → DTO 物件投影

public static List<TResult> ConvertTo<TResult>(this IEnumerable source) where TResult : new()
{
    // 反射比對同名屬性,逐項複製
    // 含快取優化:靜態 Dictionary<Type, PropertyInfo[]> + Dictionary<string, CommonProperty[]>
    // 詳細實現參考原文 (ObjectConverter 類別)
}

已知限制與規避

限制原因規避建議
Expression 中不可使用匿名型別未包含的屬性轉換後 MemberAccess 找不到目標基礎查詢 select 所有可能用到的欄位;或定義介面約束
BinaryExpression 右側不可參考參數Replace 時嘗試常數摺疊失敗改寫為常數或獨立變數:p => p.Price >= threshold
跨進程 (WCF) 無法傳遞 ExpressionExpression Tree 無法序列化改用規格模式 (Specification Pattern) 或 Dynamic LINQ 字串

跨層邏輯復用策略

同一 AppDomain (進程內)

// 服務層簽名
public List<ProductExt> Query(Expression<Func<ProductExt, bool>> predicate)
{
    var query = BuildBaseQuery(); // IQueryable<匿名型別>
    return query.DynamicWhere(predicate).ConvertTo<ProductExt>();
}

// UI 層調用
var predicate = (ProductExt p) => p.ProductName.Contains("che") && p.UnitPrice >= 22;
var result = service.Query(predicate);

跨進程 (WCF/WebAPI)

無法傳遞 Expression,改用 **參數物件 (Query Criteria DTO)**:

public class ProductQueryCriteria
{
    public string ProductName { get; set; }
    public decimal? MinPrice { get; set; }
    public string SupplierAddress { get; set; }
}

// 服務層
public List<ProductExt> Query(ProductQueryCriteria criteria)
{
    var query = BuildBaseQuery();
    if (!string.IsNullOrEmpty(criteria.ProductName))
        query = query.DynamicWhere((Products p) => p.ProductName.Contains(criteria.ProductName));
    if (criteria.MinPrice.HasValue)
        query = query.DynamicWhere((Products p) => p.UnitPrice >= criteria.MinPrice.Value);
    // ...
    return query.ConvertTo<ProductExt>();
}

總結

  1. DynamicWhere + Expression 參數替換 讓強型別動態查詢在分層架構中成為可能。
  2. ConvertTo 解決匿名型別 → DTO 投影,保持服務層封裝。
  3. 跨進程場景退回 **Criteria DTO** 模式,犧牲部分靈活性換取可序列化。
  4. 核心技術點:ExpressionVisitor、IQueryProvider.CreateQuery、Expression.Call 動態建構 Expression Tree。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved