根據執行階段狀態查詢

在大部分的 LINQ 查詢中,查詢的一般型式是以程式碼設定。 您可以使用 where 子句篩選項目、使用 orderby 排序輸出集合、群組項目,或執行某些計算。 您的程式碼可能會提供篩選參數、排序索引鍵或其他屬於查詢一部分的運算式。 不過,查詢的整體型式無法變更。 在本文中,您將了解如何使用 System.Linq.IQueryable<T> 介面和實作的型別,以在執行階段修改查詢的型式。

您可以使用這些技術在執行階段建置查詢,其中某些使用者輸入或執行階段狀態會變更您想要作為查詢一部分使用的查詢方法。 您要新增、移除或修改查詢子句來編輯查詢。

注意

請確定您在 .cs 檔案頂端新增 using System.Linq.Expressions;using static System.Linq.Expressions.Expression;

考慮針對資料來源定義 IQueryableIQueryable<T> 的程式碼:

string[] companyNames = [
    "Consolidated Messenger", "Alpine Ski House", "Southridge Video",
    "City Power & Light", "Coho Winery", "Wide World Importers",
    "Graphic Design Institute", "Adventure Works", "Humongous Insurance",
    "Woodgrove Bank", "Margie's Travel", "Northwind Traders",
    "Blue Yonder Airlines", "Trey Research", "The Phone Company",
    "Wingtip Toys", "Lucerne Publishing", "Fourth Coffee"
];

// Use an in-memory array as the data source, but the IQueryable could have come
// from anywhere -- an ORM backed by a database, a web request, or any other LINQ provider.
IQueryable<string> companyNamesSource = companyNames.AsQueryable();
var fixedQry = companyNames.OrderBy(x => x);

每次執行上述程式碼時,都會執行相同的確切查詢。 讓我們了解如何修改查詢來加以擴充或修改。 基本上,IQueryable 有兩個元件:

  • Expression:目前查詢元件中無從驗證語言與無從驗證資料來源的表示法 (以運算式樹狀架構的形式)。
  • Provider:LINQ 提供者的執行個體,其知道如何將目前的查詢具體化為一個值或一組值。

在動態查詢的內容中,提供者通常會維持不變;查詢的運算式樹狀架構則會因各種不同的查詢而異。

運算式樹狀架構為不可變;若您想要一個不同的運算式樹狀架構 (因此有一個不同的查詢),您必須將現有運算式樹狀架構轉換成一個新的運算式樹狀架構。 下列各節描述以不同方式查詢來回應執行階段狀態的特定技術:

  • 從運算式樹狀架構中使用執行階段狀態
  • 呼叫更多 LINQ 方法
  • 改變傳遞至 LINQ 方法的運算式樹狀架構
  • 使用 Expression 的 Factory 方法建構 Expression<TDelegate> 運算式樹狀架構
  • 將方法呼叫節點新增至 IQueryable 的運算式樹狀架構
  • 建構字串,並使用動態 LINQ 程式庫 (英文)

每種技術都能使用更多功能,但代價是複雜度增加。

從運算式樹狀架構中使用執行階段狀態

動態查詢的最簡單方法是透過封閉式變數 (如下面程式碼範例中的 length) 直接在查詢中參考執行階段狀態:

var length = 1;
var qry = companyNamesSource
    .Select(x => x.Substring(0, length))
    .Distinct();

Console.WriteLine(string.Join(",", qry));
// prints: C, A, S, W, G, H, M, N, B, T, L, F

length = 2;
Console.WriteLine(string.Join(",", qry));
// prints: Co, Al, So, Ci, Wi, Gr, Ad, Hu, Wo, Ma, No, Bl, Tr, Th, Lu, Fo

未修改內部運算式樹狀架構 (以及查詢);查詢僅因為 length 的值變更而傳回不同值。

呼叫更多 LINQ 方法

一般來說,Queryable內建 LINQ 方法會執行兩個步驟:

您可以用 System.Linq.IQueryable<T> 傳回方法的結果取代原始查詢,以取得新的查詢。 您可以使用執行階段狀態,如下列範例所示:

// bool sortByLength = /* ... */;

var qry = companyNamesSource;
if (sortByLength)
{
    qry = qry.OrderBy(x => x.Length);
}

改變傳遞至 LINQ 方法的運算式樹狀架構

您可以依據執行階段狀態,將不同的運算式傳入 LINQ 方法:

// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>> expr = (startsWith, endsWith) switch
{
    ("" or null, "" or null) => x => true,
    (_, "" or null) => x => x.StartsWith(startsWith),
    ("" or null, _) => x => x.EndsWith(endsWith),
    (_, _) => x => x.StartsWith(startsWith) || x.EndsWith(endsWith)
};

var qry = companyNamesSource.Where(expr);

您可能也想要使用其他程式庫來撰寫各種子運算式,例如 LinqKitPredicateBuilder

// This is functionally equivalent to the previous example.

// using LinqKit;
// string? startsWith = /* ... */;
// string? endsWith = /* ... */;

Expression<Func<string, bool>>? expr = PredicateBuilder.New<string>(false);
var original = expr;
if (!string.IsNullOrEmpty(startsWith))
{
    expr = expr.Or(x => x.StartsWith(startsWith));
}
if (!string.IsNullOrEmpty(endsWith))
{
    expr = expr.Or(x => x.EndsWith(endsWith));
}
if (expr == original)
{
    expr = x => true;
}

var qry = companyNamesSource.Where(expr);

使用 Factory 方法建構運算式樹狀架構與查詢

在目前為止的所有範例中,您知道編譯時間的元素類型 (string) 及查詢的類型 (IQueryable<string>)。 您可以將元件新增至任何元素類型的查詢,或依據元素類型來新增不同的元件。 您可以從頭開始建立運算式樹狀架構 (使用 System.Linq.Expressions.Expression 的 Factory 方法),進而在執行階段將運算式訂製為特定的元素類型。

建構 Expression<TDelegate>

當您建構運算式以傳入其中一個 LINQ 方法時,您實際上是在建構 System.Linq.Expressions.Expression<TDelegate> 的執行個體,其中 TDelegate 是某種委派類型,例如 Func<string, bool>Action,或自訂委派類型。

System.Linq.Expressions.Expression<TDelegate> 繼承自 LambdaExpression,其代表如下列範例所示的完整 Lambda 運算式:

Expression<Func<string, bool>> expr = x => x.StartsWith("a");

LambdaExpression 有兩個元件:

  1. 參數清單 ((string x)) 由 Parameters 屬性表示。
  2. 主體 (x.StartsWith("a")) 由 Body 屬性表示。

建構 Expression<TDelegate> 的基本步驟如下:

  1. 使用 Parameter Factory 方法,為 Lambda 運算式中的每個參數 (若有的話) 定義 ParameterExpression 物件。
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. 使用定義的 ParameterExpression,以及 Expression 的 Factory 方法,建構 LambdaExpression 的主體。 例如,表示 x.StartsWith("a") 的運算可以像這樣建構:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. 使用適當的 Lambda Factory 方法多載,將參數與主體包裝在 compile-time-typed 的 Expression<TDelegate> 中:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

下列各節說明您可能想要建構 Expression<TDelegate> 以傳入 LINQ 方法的案例。 它提供如何使用 Factory 方法執行此作業的完整範例。

在執行階段建構完整查詢

您要撰寫使用多個實體類型的查詢:

record Person(string LastName, string FirstName, DateTime DateOfBirth);
record Car(string Model, int Year);

針對這些實體類型中的任何一種,您想要篩選並只傳回那些在其中一個 string 欄位中具有指定文字的實體。 對於 Person,您想要搜尋 FirstNameLastName 屬性:

string term = /* ... */;
var personsQry = new List<Person>()
    .AsQueryable()
    .Where(x => x.FirstName.Contains(term) || x.LastName.Contains(term));

但對於 Car,您只想要搜尋 Model 屬性:

string term = /* ... */;
var carsQry = new List<Car>()
    .AsQueryable()
    .Where(x => x.Model.Contains(term));

雖然您可以為 IQueryable<Person> 撰寫一個自訂函式,並為 IQueryable<Car> 撰寫另一個自訂函式,但下列函式會將此篩選新增至任何現有的查詢 (不論特定元素類型為何)。

// using static System.Linq.Expressions.Expression;

IQueryable<T> TextFilter<T>(IQueryable<T> source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    // T is a compile-time placeholder for the element type of the query.
    Type elementType = typeof(T);

    // Get all the string properties on this specific type.
    PropertyInfo[] stringProperties = elementType
        .GetProperties()
        .Where(x => x.PropertyType == typeof(string))
        .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Get the right overload of String.Contains
    MethodInfo containsMethod = typeof(string).GetMethod("Contains", [typeof(string)])!;

    // Create a parameter for the expression tree:
    // the 'x' in 'x => x.PropertyName.Contains("term")'
    // The type of this parameter is the query's element type
    ParameterExpression prm = Parameter(elementType);

    // Map each property to an expression tree node
    IEnumerable<Expression> expressions = stringProperties
        .Select(prp =>
            // For each property, we have to construct an expression tree node like x.PropertyName.Contains("term")
            Call(                  // .Contains(...) 
                Property(          // .PropertyName
                    prm,           // x 
                    prp
                ),
                containsMethod,
                Constant(term)     // "term" 
            )
        );

    // Combine all the resultant expression nodes using ||
    Expression body = expressions
        .Aggregate((prev, current) => Or(prev, current));

    // Wrap the expression body in a compile-time-typed lambda expression
    Expression<Func<T, bool>> lambda = Lambda<Func<T, bool>>(body, prm);

    // Because the lambda is compile-time-typed (albeit with a generic parameter), we can use it with the Where method
    return source.Where(lambda);
}

因為 TextFilter 函式會採用並傳回 IQueryable<T> (而不只是 IQueryable),所以您可以在文字篩選之後新增進階的編譯時間類型查詢元素。

var qry = TextFilter(
        new List<Person>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.DateOfBirth < new DateTime(2001, 1, 1));

var qry1 = TextFilter(
        new List<Car>().AsQueryable(),
        "abcd"
    )
    .Where(x => x.Year == 2010);

將方法呼叫節點新增至 IQueryable<TDelegate> 的運算式樹狀架構

若您有 IQueryable,而不是 IQueryable<T>,則無法直接呼叫泛型 LINQ 方法。 其中一個替代方式是建置內部運算式樹狀架構 (如前一個範例所示),並使用反映以在傳入運算式樹狀架構時,叫用適當的 LINQ 方法。

您也可以複製 LINQ 方法的功能 (藉由將整個樹狀結構包裝在表示對 LINQ 方法呼叫的 MethodCallExpression 中):

IQueryable TextFilter_Untyped(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }
    Type elementType = source.ElementType;

    // The logic for building the ParameterExpression and the LambdaExpression's body is the same as in the previous example,
    // but has been refactored into the constructBody function.
    (Expression? body, ParameterExpression? prm) = constructBody(elementType, term);
    if (body is null) { return source; }

    Expression filteredTree = Call(
        typeof(Queryable),
        "Where",
        [elementType],
        source.Expression,
        Lambda(body, prm!)
    );

    return source.Provider.CreateQuery(filteredTree);
}

在此情況下,因為您沒有編譯時間 T 泛型預留位置,所以要使用不需要編譯時間類型資訊的 Lambda 多載,其會產生 LambdaExpression,而不是 Expression<TDelegate>

動態 LINQ 程式庫

使用 Factory 方法建構運算式樹狀架構相對複雜;編寫字串比較容易。 動態 LINQ 程式庫 (英文) 會在 IQueryable 上公開一組擴充方法,其對應至 Queryable 中的標準 LINQ 方法,且其接受特殊語法 (英文) 中的字串,而不是運算式樹狀架構。 該程式庫可從字串產生適當的運算式樹狀架構,且可傳回已轉譯的結果 IQueryable

例如,先前的範例可以重寫,如下所示:

// using System.Linq.Dynamic.Core

IQueryable TextFilter_Strings(IQueryable source, string term)
{
    if (string.IsNullOrEmpty(term)) { return source; }

    var elementType = source.ElementType;

    // Get all the string property names on this specific type.
    var stringProperties =
        elementType.GetProperties()
            .Where(x => x.PropertyType == typeof(string))
            .ToArray();
    if (!stringProperties.Any()) { return source; }

    // Build the string expression
    string filterExpr = string.Join(
        " || ",
        stringProperties.Select(prp => $"{prp.Name}.Contains(@0)")
    );

    return source.Where(filterExpr, term);
}