Çalışma zamanı durumunu temel alan sorgu

ÇOĞU LINQ sorgusunda, sorgunun genel şekli kodda ayarlanır. Bir yan tümce kullanarak where öğeleri filtreleyebilirsiniz, kullanarak çıkış koleksiyonunu orderbysıralayabilir, öğeleri gruplandırabilir veya bazı hesaplamalar gerçekleştirebilirsiniz. Kodunuz filtre veya sıralama anahtarı ya da sorgunun parçası olan diğer ifadeler için parametreler sağlayabilir. Ancak, sorgunun genel şekli değiştirilemez. Bu makalede, çalışma zamanında sorgunun şeklini değiştirmek için bunu uygulayan arabirim ve türleri kullanma System.Linq.IQueryable<T> tekniklerini öğreneceksiniz.

Bazı kullanıcı girişlerinin veya çalışma zamanı durumunun sorgunun parçası olarak kullanmak istediğiniz sorgu yöntemlerini değiştirdiği çalışma zamanında sorgular oluşturmak için bu teknikleri kullanırsınız. Sorgu yan tümceleri ekleyerek, kaldırarak veya değiştirerek sorguyu düzenlemek istiyorsunuz.

Not

.cs dosyanızın en üstüne ve using static System.Linq.Expressions.Expression; eklediğinizden using System.Linq.Expressions; emin olun.

Bir veri kaynağına karşı bir IQueryable veya IQueryable<T> tanımlayan kodu göz önünde bulundurun:

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);

Önceki kodu her çalıştırdığınızda tam olarak aynı sorgu yürütülür. Şimdi sorguyu genişletmeyi veya değiştirmeyi öğrenelim. Temelde, bir IQueryable iki bileşene sahiptir:

  • Expression—geçerli sorgu bileşenlerinin ifade ağacı biçiminde dil-bağımsız ve veri kaynağı-agnostik gösterimi.
  • Provider—geçerli sorguyu bir değere veya değer kümesine dönüştürmeyi bilen bir LINQ sağlayıcısı örneği.

Dinamik sorgulama bağlamında sağlayıcı genellikle aynı kalır; sorgunun ifade ağacı sorgudan sorguya farklılık gösterir.

İfade ağaçları sabittir; farklı bir ifade ağacı ve dolayısıyla farklı bir sorgu istiyorsanız, var olan ifade ağacını yeni bir ifade ağacına çevirmeniz gerekir. Aşağıdaki bölümlerde, çalışma zamanı durumuna yanıt olarak farklı sorgulamaya yönelik belirli teknikler açıklanmaktadır:

  • İfade ağacının içinden çalışma zamanı durumunu kullanma
  • Daha fazla LINQ yöntemi çağırma
  • LINQ yöntemlerine geçirilen ifade ağacını değiştirin
  • konumundaki fabrika yöntemlerini kullanarak bir Expression<TDelegate> ifade ağacı oluşturma Expression
  • ' IQueryablenin ifade ağacına yöntem çağrı düğümleri ekleme
  • Dizeler oluşturun ve Dinamik LINQ kitaplığını kullanın

Tekniklerin her biri daha fazla özellik sağlar, ancak karmaşıklığın artmasına neden olur.

İfade ağacının içinden çalışma zamanı durumunu kullanma

Dinamik olarak sorgulamanın en basit yolu, aşağıdaki kod örneğinde olduğu gibi length kapalı bir değişken aracılığıyla doğrudan sorgudaki çalışma zamanı durumuna başvurmaktır:

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

İç ifade ağacı ve dolayısıyla sorgu değiştirilmez; sorgu, yalnızca değeri değiştirildiğinden farklı değerler length döndürür.

Daha fazla LINQ yöntemi çağırma

Genel olarak, içindeki yerleşik LINQ yöntemleriQueryable iki adım gerçekleştirir:

  • Geçerli ifade ağacını yöntem çağrısını temsil eden bir MethodCallExpression içinde sarmalar.
  • Sağlayıcının yöntemi aracılığıyla bir değer döndürmek veya yöntemi aracılığıyla çevrilmiş bir sorgu nesnesi döndürmek için sarmalanmış ifade ağacını sağlayıcıya IQueryProvider.ExecuteIQueryProvider.CreateQuery geri geçirin.

Yeni bir System.Linq.IQueryable<T>sorgu almak için özgün sorguyu -returning yönteminin sonucuyla değiştirebilirsiniz. Aşağıdaki örnekte olduğu gibi çalışma zamanı durumunu kullanabilirsiniz:

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

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

LINQ yöntemlerine geçirilen ifade ağacını değiştirin

Çalışma zamanı durumuna bağlı olarak LINQ yöntemlerine farklı ifadeler geçirebilirsiniz:

// 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);

LinqKit'in PredicateBuilder'ı gibi başka bir kitaplık kullanarak çeşitli alt ifadeleri de oluşturmak isteyebilirsiniz:

// 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);

Fabrika yöntemlerini kullanarak ifade ağaçları ve sorguları oluşturma

Bu noktaya kadar olan tüm örneklerde, derleme zamanındastring öğe türünü ve dolayısıyla sorgununIQueryable<string> türünü bilirsiniz. Herhangi bir öğe türündeki bir sorguya bileşen ekleyebilir veya öğe türüne bağlı olarak farklı bileşenler ekleyebilirsiniz. konumundaki fabrika yöntemlerini System.Linq.Expressions.Expressionkullanarak sıfırdan ifade ağaçları oluşturabilir ve böylece çalışma zamanında ifadeyi belirli bir öğe türüne uyarlayabilirsiniz.

İfade<TDelegate Oluşturma>

LINQ yöntemlerinden birine geçirmek için bir ifade oluşturduğunuzda, örneğini oluşturursunuz; burada TDelegate , Actiongibi Func<string, bool>bir System.Linq.Expressions.Expression<TDelegate>temsilci türü veya özel bir temsilci türüdür.

System.Linq.Expressions.Expression<TDelegate> aşağıdaki örnekte olduğu gibi tam bir lambda ifadesini temsil eden öğesinden LambdaExpressiondevralır:

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

A'nın LambdaExpression iki bileşeni vardır:

  1. Özelliğiyle Parameters temsil edilen bir parametre listesi(string x).
  2. Özelliğiyle Body temsil edilen bir gövdex.StartsWith("a").

oluşturmanın Expression<TDelegate> temel adımları şunlardır:

  1. Factory yöntemini kullanarak Parameter lambda ifadesindeki parametrelerin (varsa) her biri için nesneleri tanımlayınParameterExpression.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. öğesinin gövdesini LambdaExpression, konumundaki ParameterExpression tanımlı ve fabrika yöntemlerini Expressionkullanarak oluşturun. Örneğin, temsil eden x.StartsWith("a") bir ifade şu şekilde oluşturulabilir:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Parametreleri ve gövdeyi uygun Lambda fabrika yöntemi aşırı yüklemesini kullanarak derleme zamanı türünde bir İfade<TDelegate> içinde sarmalar:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

Aşağıdaki bölümlerde, linq yöntemine geçirmek için oluşturmak Expression<TDelegate> isteyebileceğiniz bir senaryo açıklanmaktadır. Fabrika yöntemlerini kullanarak bunun nasıl yapılacağını gösteren eksiksiz bir örnek sağlar.

Çalışma zamanında tam sorgu oluşturma

Birden çok varlık türüyle çalışan sorgular yazmak istiyorsunuz:

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

Bu varlık türlerinden herhangi biri için, yalnızca alanlarından birinde string belirli bir metni olan varlıkları filtrelemek ve döndürmek istiyorsunuz. için Personve LastName özelliklerinde FirstName arama yapmak istersiniz:

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

Ancak için Caryalnızca Model özelliğini aramak istersiniz:

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

için IQueryable<Person> bir özel işlev ve için IQueryable<Car>başka bir özel işlev yazabilirsiniz ancak aşağıdaki işlev, belirli öğe türünden bağımsız olarak bu filtrelemeyi mevcut sorgulara ekler.

// 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);
}

İşlev bir (yalnızca bir IQueryable<T> değil) aldığından TextFilter ve döndürdüğündenIQueryable, metin filtresinden sonra daha fazla derleme zamanı türündeki sorgu öğesi ekleyebilirsiniz.

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'in> ifade ağacına yöntem çağrı düğümleri ekleme

yerine bir IQueryableIQueryable<T>varsa, genel LINQ yöntemlerini doğrudan çağıramazsınız. Bir alternatif, önceki örnekte gösterildiği gibi iç ifade ağacını oluşturmak ve ifade ağacını geçirirken uygun LINQ yöntemini çağırmak için yansımayı kullanmaktır.

LinQ yönteminin işlevselliğini, linq yöntemine yapılan çağrıyı temsil eden bir MethodCallExpression ağaçta sarmalayarak da çoğaltabilirsiniz:

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);
}

Bu durumda, derleme zamanı genel yer tutucunuz yoktur, bu nedenle derleme zamanı T türü bilgileri gerektirmeyen ve yerine Expression<TDelegate>bir LambdaExpression oluşturan aşırı yüklemeyi kullanırsınızLambda.

Dinamik LINQ kitaplığı

fabrika yöntemlerini kullanarak ifade ağaçları oluşturmak nispeten karmaşıktır; dize oluşturmak daha kolaydır. Dinamik LINQ kitaplığı, üzerindeki IQueryable standart LINQ yöntemlerine karşılık gelen ve dizeleri ifade ağaçları yerine özel bir söz diziminde kabul eden bir uzantı yöntemleri Queryablekümesini kullanıma sunar. Kitaplık, dizeden uygun ifade ağacını oluşturur ve sonuç çevirisini IQueryabledöndürebilir.

Örneğin, önceki örnek aşağıdaki gibi yeniden yazılabilir:

// 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);
}