UI 層直接用 DataContext 時,var query = ... 配合匿名型別與延遲執行,動態加 .Where() 極其自然。但分層架構 (如 WCF 服務層) 中:
IQueryable<匿名型別> → 客戶端無法識別型別,智能感應失效。Expression<Func<DTO, bool>>。var query = db.Customers
.Where("City == @0 and Orders.Count >= @1", "London", 10)
.OrderBy("CompanyName")
.Select("New(CompanyName as Name, Phone)");
服務層查詢產生 IQueryable<匿名型別> (含 ProductName, UnitPrice, CompanyName...)。UI 層想用強型別 Expression<Func<Product, bool>> (如 p => p.ProductName.Contains("che")) 來篩選。需將 Expression 參數從 Product → 匿名型別 轉換。
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));
}
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>();
}
}
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) 無法傳遞 Expression | Expression Tree 無法序列化 | 改用規格模式 (Specification Pattern) 或 Dynamic LINQ 字串 |
// 服務層簽名
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);
無法傳遞 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>();
}
ExpressionVisitor、IQueryProvider.CreateQuery、Expression.Call 動態建構 Expression Tree。