Partilhar via


Consulta baseada no estado de tempo de execução

Na maioria das consultas LINQ, a forma geral da consulta é definida em código. Você pode filtrar itens usando uma where cláusula, classificar a coleção de saída usando orderby, agrupar itens ou executar algum cálculo. Seu código pode fornecer parâmetros para o filtro, a chave de classificação ou outras expressões que fazem parte da consulta. No entanto, a forma geral da consulta não pode ser alterada. Neste artigo, você aprenderá técnicas para usar System.Linq.IQueryable<T> a interface e os tipos que a implementam para modificar a forma de uma consulta em tempo de execução.

Você usa essas técnicas para criar consultas em tempo de execução, onde alguma entrada do usuário ou estado de tempo de execução altera os métodos de consulta que você deseja usar como parte da consulta. Você deseja editar a consulta adicionando, removendo ou modificando cláusulas de consulta.

Nota

Certifique-se de adicionar using System.Linq.Expressions; e using static System.Linq.Expressions.Expression; na parte superior do seu arquivo .cs .

Considere o código que define uma IQueryable ou uma fonte de IQueryable<T> dados:

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

Toda vez que você executa o código anterior, a mesma consulta exata é executada. Vamos aprender como modificar a consulta, estendê-la ou modificá-la. Fundamentalmente, um IQueryable tem duas componentes:

  • Expression—uma representação agnóstica de linguagem e de fonte de dados dos componentes da consulta atual, na forma de uma árvore de expressão.
  • Provider—uma instância de um provedor LINQ, que sabe como materializar a consulta atual em um valor ou conjunto de valores.

No contexto de consultas dinâmicas, o provedor geralmente permanece o mesmo; A árvore de expressões da consulta difere de consulta para consulta.

As árvores de expressão são imutáveis; Se você quiser uma árvore de expressão diferente — e, portanto, uma consulta diferente — precisará traduzir a árvore de expressões existente para uma nova. As seções a seguir descrevem técnicas específicas para consultar de forma diferente em resposta ao estado de tempo de execução:

  • Usar o estado em tempo de execução de dentro da árvore de expressão
  • Ligue para mais métodos LINQ
  • Variar a árvore de expressão passada para os métodos LINQ
  • Construa uma árvore de Expression<TDelegate> expressão usando os métodos de fábrica em Expression
  • Adicionar nós de chamada de método a uma IQueryableárvore de expressão do
  • Construa cadeias de caracteres e use a biblioteca Dynamic LINQ

Cada uma das técnicas permite mais capacidades, mas a um custo de maior complexidade.

Usar o estado em tempo de execução de dentro da árvore de expressão

A maneira mais simples de consultar dinamicamente é fazer referência ao estado de tempo de execução diretamente na consulta por meio de uma variável fechada, como length no exemplo de código a seguir:

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

A árvore de expressão interna — e, portanto, a consulta — não é modificada; A consulta retorna valores diferentes somente porque o valor de length Changed.

Ligue para mais métodos LINQ

Geralmente, os métodos LINQ internos executam Queryable duas etapas:

  • Envolva a árvore de expressão atual em uma MethodCallExpression chamada que represente o método.
  • Passe a árvore de expressão encapsulada de volta para o provedor, seja para retornar um valor por meio do método do IQueryProvider.Execute provedor ou para retornar um objeto de consulta traduzido por meio do IQueryProvider.CreateQuery método.

Você pode substituir a consulta original pelo resultado de um System.Linq.IQueryable<T>método -returning, para obter uma nova consulta. Você pode usar o estado de tempo de execução, como no exemplo a seguir:

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

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

Variar a árvore de expressão passada para os métodos LINQ

Você pode passar expressões diferentes para os métodos LINQ, dependendo do estado em tempo de execução:

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

Você também pode querer compor as várias subexpressões usando outra biblioteca, como o PredicateBuilder do LinqKit:

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

Construir árvores de expressão e consultas usando métodos de fábrica

Em todos os exemplos até este ponto, você sabe o tipo de elemento em tempo de compilação —string e, portanto, o tipo da consulta —IQueryable<string>. Você pode adicionar componentes a uma consulta de qualquer tipo de elemento ou adicionar componentes diferentes, dependendo do tipo de elemento. Você pode criar árvores de expressão do zero, usando os métodos de fábrica em System.Linq.Expressions.Expression, e, assim, adaptar a expressão em tempo de execução para um tipo de elemento específico.

Construindo um TDelegate de Expressão<>

Quando você constrói uma expressão para passar para um dos métodos LINQ, na verdade está construindo uma instância de , onde TDelegate é algum tipo de delegado, como Func<string, bool>, Actionou um tipo de System.Linq.Expressions.Expression<TDelegate>delegado personalizado.

System.Linq.Expressions.Expression<TDelegate> Herda de LambdaExpression, que representa uma expressão lambda completa como o exemplo a seguir:

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

A LambdaExpression tem duas componentes:

  1. Uma lista de parâmetros—(string x)—representada pela Parameters propriedade.
  2. Um corpo—x.StartsWith("a")—representado pela Body propriedade.

As etapas básicas na construção de um Expression<TDelegate> são as seguintes:

  1. Defina ParameterExpression objetos para cada um dos parâmetros (se houver) na expressão lambda, usando o Parameter método factory.
    ParameterExpression x = Parameter(typeof(string), "x");
    
  2. Construa o corpo do seu LambdaExpression, usando os ParameterExpression métodos definidos e de fábrica em Expression. Por exemplo, uma expressão que representa x.StartsWith("a") poderia ser construída assim:
    Expression body = Call(
        x,
        typeof(string).GetMethod("StartsWith", [typeof(string)])!,
        Constant("a")
    );
    
  3. Envolva os parâmetros e o corpo em um Expression<TDelegate> digitado em tempo de compilação, usando a sobrecarga de método de fábrica apropriadaLambda:
    Expression<Func<string, bool>> expr = Lambda<Func<string, bool>>(body, x);
    

As seções a seguir descrevem um cenário no qual você pode querer construir um Expression<TDelegate> para passar para um método LINQ. Ele fornece um exemplo completo de como fazer isso usando os métodos de fábrica.

Construir uma consulta completa em tempo de execução

Você deseja escrever consultas que funcionam com vários tipos de entidade:

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

Para qualquer um desses tipos de entidade, você deseja filtrar e retornar apenas as entidades que têm um determinado texto dentro de um de seus string campos. Para Persono , você deseja pesquisar as FirstName propriedades e LastName :

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

Mas para Caro , você deve pesquisar apenas o Model estabelecimento:

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

Embora você possa escrever uma função personalizada para IQueryable<Person> e outra para IQueryable<Car>, a função a seguir adiciona essa filtragem a qualquer consulta existente, independentemente do tipo de elemento específico.

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

Como a TextFilter função usa e retorna um IQueryable<T> (e não apenas um IQueryable), você pode adicionar mais elementos de consulta digitados em tempo de compilação após o filtro de texto.

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

Adicionar nós de chamada de método à árvore de expressão do IQueryable<TDelegate>

Se você tiver um IQueryable em vez de um IQueryable<T>, não poderá chamar diretamente os métodos LINQ genéricos. Uma alternativa é construir a árvore de expressão interna, como mostrado no exemplo anterior, e usar reflexão para invocar o método LINQ apropriado ao passar na árvore de expressão.

Você também pode duplicar a funcionalidade do método LINQ, encapsulando toda a árvore em um MethodCallExpression que representa uma chamada para o método LINQ:

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

Nesse caso, você não tem um espaço reservado genérico em tempo T de compilação, portanto, usa a Lambda sobrecarga que não requer informações de tipo em tempo de compilação e que produz um LambdaExpression em vez de um Expression<TDelegate>arquivo .

A biblioteca Dynamic LINQ

A construção de árvores de expressão usando métodos de fábrica é relativamente complexa; é mais fácil compor cordas. A biblioteca Dynamic LINQ expõe um conjunto de métodos de extensão correspondentes IQueryable aos métodos LINQ padrão em Queryable, e que aceitam cadeias de caracteres em uma sintaxe especial em vez de árvores de expressão. A biblioteca gera a árvore de expressão apropriada a partir da cadeia de caracteres e pode retornar o resultado traduzido IQueryable.

Por exemplo, o exemplo anterior poderia ser reescrito da seguinte forma:

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