Noções básicas sobre expressões de consulta

Este artigo apresenta os conceitos básicos relacionados a expressões de consulta em C#.

O que é uma consulta e o que ela faz?

Uma consulta é um conjunto de instruções que descreve quais dados recuperar de uma determinada fonte de dados (ou fontes) e que forma e organização os dados retornados devem ter. Uma consulta é diferente dos resultados que ela produz.

Em geral, os dados de origem são organizados de forma lógica como uma sequência de elementos do mesmo tipo. Por exemplo, uma tabela de banco de dados SQL contém uma sequência de linhas. Em um arquivo XML, há uma "sequência" de elementos XML (embora eles estejam organizados hierarquicamente em uma estrutura de árvore). Uma coleção na memória contém uma sequência de objetos.

Do ponto de vista do aplicativo, o tipo específico e a estrutura dos dados de origem original não são importantes. O aplicativo sempre vê os dados de origem como uma coleção de IEnumerable<T> ou de IQueryable<T>. Por exemplo, no LINQ to XML, os dados de origem ficam visíveis como um IEnumerable<XElement>.

Dada essa sequência de origem, uma consulta pode executar uma das três ações:

  • Recuperar um subconjunto dos elementos para produzir uma nova sequência sem modificar os elementos individuais. A consulta pode, em seguida, classificar ou agrupar a sequência retornada de várias maneiras, conforme mostrado no exemplo a seguir (suponha que scores é um int[]):

    IEnumerable<int> highScoresQuery =
        from score in scores
        where score > 80
        orderby score descending
        select score;
    
  • Recuperar uma sequência de elementos, como no exemplo anterior, mas transformá-los em um novo tipo de objeto. Por exemplo, uma consulta pode recuperar apenas os nomes de família de determinados registros de clientes em uma fonte de dados. Ou pode recuperar o registro completo e, em seguida, usá-lo para construir outro tipo de objeto na memória ou até mesmo dados XML antes de gerar a sequência de resultados final. O exemplo a seguir mostra uma projeção de um int para um string. Observe o novo tipo de highScoresQuery.

    IEnumerable<string> highScoresQuery2 =
        from score in scores
        where score > 80
        orderby score descending
        select $"The score is {score}";
    
  • Recuperar um valor singleton sobre os dados de origem, como:

    • O número de elementos que correspondem a uma determinada condição.

    • O elemento que tem o maior ou menor valor.

    • O primeiro elemento que corresponde a uma condição ou a soma dos valores específicos em um conjunto de elementos especificado. Por exemplo, a consulta a seguir retorna o número pontuações maiores que 80 na matriz de inteiros scores:

      var highScoreCount = (
          from score in scores
          where score > 80
          select score
      ).Count();
      

      No exemplo anterior, observe o uso de parênteses ao redor da expressão de consulta antes da chamada para o método Enumerable.Count. Você também pode usar uma nova variável para armazenar o resultado concreto.

      IEnumerable<int> highScoresQuery3 =
          from score in scores
          where score > 80
          select score;
      
      var scoreCount = highScoresQuery3.Count();
      

No exemplo anterior, a consulta é executada na chamada para Count, pois Count deve iterar os resultados para determinar o número de elementos retornados por highScoresQuery.

O que é uma expressão de consulta?

Uma expressão de consulta é uma consulta expressada na sintaxe da consulta. Uma expressão de consulta é um constructo de linguagem de primeira classe. É exatamente como qualquer outra expressão e pode ser usada em qualquer contexto em que uma expressão C# seja válida. Uma expressão de consulta consiste em um conjunto de cláusulas escritas em uma sintaxe declarativa semelhante ao SQL ou XQuery. Cada cláusula, por sua vez, contém uma ou mais expressões C# e essas expressões podem ser ou conter uma expressão de consulta.

Uma expressão de consulta deve começar com uma cláusula from e deve terminar com uma cláusula select ou group. Entre a primeira cláusula from e a última cláusula select ou group, ela pode conter uma ou mais dessas cláusulas opcionais: where, orderby, join, let e até mesmo outras cláusulas from. Você também pode usar a palavra-chave into para permitir que o resultado de uma cláusula join ou group sirva como a fonte para mais cláusulas de consulta na mesma expressão de consulta.

Variável da consulta

Em LINQ, uma variável de consulta é qualquer variável que armazena uma consulta em vez dos resultados de uma consulta. Mais especificamente, uma variável de consulta é sempre um tipo enumerável que produzirá uma sequência de elementos quando for iterada em uma instrução foreach ou uma chamada direta para seu método IEnumerator.MoveNext().

Observação

Os exemplos nesse artigo usam a seguinte fonte de dados e dados de amostra.

record City(string Name, long Population);
record Country(string Name, double Area, long Population, List<City> Cities);
record Product(string Name, string Category);
static readonly City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000),
    new City("Mumbai", 20_412_000),
    new City("Beijing", 20_384_000),
    new City("Cairo", 18_772_000),
    new City("Dhaka", 17_598_000),
    new City("Osaka", 19_281_000),
    new City("New York-Newark", 18_604_000),
    new City("Karachi", 16_094_000),
    new City("Chongqing", 15_872_000),
    new City("Istanbul", 15_029_000),
    new City("Buenos Aires", 15_024_000),
    new City("Kolkata", 14_850_000),
    new City("Lagos", 14_368_000),
    new City("Kinshasa", 14_342_000),
    new City("Manila", 13_923_000),
    new City("Rio de Janeiro", 13_374_000),
    new City("Tianjin", 13_215_000)
];

static readonly Country[] countries = [
    new Country ("Vatican City", 0.44, 526, [new City("Vatican City", 826)]),
    new Country ("Monaco", 2.02, 38_000, [new City("Monte Carlo", 38_000)]),
    new Country ("Nauru", 21, 10_900, [new City("Yaren", 1_100)]),
    new Country ("Tuvalu", 26, 11_600, [new City("Funafuti", 6_200)]),
    new Country ("San Marino", 61, 33_900, [new City("San Marino", 4_500)]),
    new Country ("Liechtenstein", 160, 38_000, [new City("Vaduz", 5_200)]),
    new Country ("Marshall Islands", 181, 58_000, [new City("Majuro", 28_000)]),
    new Country ("Saint Kitts & Nevis", 261, 53_000, [new City("Basseterre", 13_000)])
];

O exemplo de código a seguir mostra uma expressão de consulta simples com uma fonte de dados, uma cláusula de filtragem, uma cláusula de ordenação e nenhuma transformação dos elementos de origem. A cláusula select termina a consulta.

// Data source.
int[] scores = [90, 71, 82, 93, 75, 82];

// Query Expression.
IEnumerable<int> scoreQuery = //query variable
    from score in scores //required
    where score > 80 // optional
    orderby score descending // optional
    select score; //must end with select or group

// Execute the query to produce the results
foreach (var testScore in scoreQuery)
{
    Console.WriteLine(testScore);
}

// Output: 93 90 82 82

No exemplo anterior, scoreQuery é uma variável de consulta, o que às vezes é chamado apenas de uma consulta. A variável de consulta não armazena nenhum dado de resultado real, que é produzido no loop foreach. E quando a instrução foreach é executada, os resultados da consulta não são retornados pela variável de consulta scoreQuery. Em vez disso, eles são retornados pela variável de iteração testScore. A variável scoreQuery pode ser iterada em um segundo loop foreach. Ela produzirá os mesmos resultados contanto que nem ela nem a fonte de dados tenham sido modificadas.

Uma variável de consulta pode armazenar uma consulta que é expressa na sintaxe de consulta ou na sintaxe de método ou uma combinação das duas. Nos exemplos a seguir, queryMajorCities e queryMajorCities2 são variáveis de consulta:

City[] cities = [
    new City("Tokyo", 37_833_000),
    new City("Delhi", 30_290_000),
    new City("Shanghai", 27_110_000),
    new City("São Paulo", 22_043_000)
];

//Query syntax
IEnumerable<City> queryMajorCities =
    from city in cities
    where city.Population > 100000
    select city;

// Execute the query to produce the results
foreach (City city in queryMajorCities)
{
    Console.WriteLine(city);
}

// Output:
// City { Population = 120000 }
// City { Population = 112000 }
// City { Population = 150340 }

// Method-based syntax
IEnumerable<City> queryMajorCities2 = cities.Where(c => c.Population > 100000);

Por outro lado, os dois exemplos a seguir mostram variáveis que não são variáveis de consulta, embora sejam inicializadas com uma consulta. Elas não são variáveis de consulta porque armazenam resultados:

var highestScore = (
    from score in scores
    select score
).Max();

// or split the expression
IEnumerable<int> scoreQuery =
    from score in scores
    select score;

var highScore = scoreQuery.Max();
// the following returns the same result
highScore = scores.Max();
var largeCitiesList = (
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city
).ToList();

// or split the expression
IEnumerable<City> largeCitiesQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;
var largeCitiesList2 = largeCitiesQuery.ToList();

Tipagem explícita e implícita de variáveis de consulta

Esta documentação normalmente fornece o tipo explícito da variável de consulta para mostrar a relação de tipo entre a variável de consulta e a cláusula select. No entanto, você também pode usar a palavra-chave var para instruir o compilador a inferir o tipo de uma variável de consulta (ou qualquer outra variável local) em tempo de compilação. Por exemplo, o exemplo de consulta que foi mostrado anteriormente neste artigo também pode ser expressado usando a tipagem implícita:

var queryCities =
    from city in cities
    where city.Population > 100000
    select city;

No exemplo anterior, o uso de “var” é opcional. queryCities é um tipo IEnumerable<City> digitado implícita ou explicitamente.

Iniciando uma expressão de consulta

Uma expressão de consulta deve começar com uma cláusula from. Especifica uma fonte de dados junto com uma variável de intervalo. A variável de intervalo representa cada elemento sucessivo na sequência de origem como a sequência de origem que está sendo percorrida. A variável de intervalo é fortemente tipada com base no tipo dos elementos na fonte de dados. No exemplo a seguir, como countries é uma matriz de objetos Country, a variável de intervalo também é tipada como Country. Como a variável de intervalo é fortemente tipada, você pode usar o operador ponto para acessar todos os membros disponíveis do tipo.

IEnumerable<Country> countryAreaQuery =
    from country in countries
    where country.Area > 500000 //sq km
    select country;

A variável de intervalo está no escopo até a consulta ser encerrada com ponto e vírgula ou com uma cláusula continuation.

Uma expressão de consulta pode conter várias cláusulas from. Use mais cláusulas from quando cada elemento na sequência de origem for ele próprio uma coleção ou contiver uma coleção. Por exemplo, suponha que você tem uma coleção de objetos Country e cada um dos quais contém uma coleção de objetos City chamada Cities. Para consular os objetos City em cada Country, use duas cláusulas from como mostrado aqui:

IEnumerable<City> cityQuery =
    from country in countries
    from city in country.Cities
    where city.Population > 10000
    select city;

Para obter mais informações, consulte Cláusula from.

Encerrando uma expressão de consulta

Uma expressão de consulta deve ser encerrada com uma cláusula group ou uma cláusula select.

Cláusula group

Use a cláusula group para produzir uma sequência de grupos organizada por uma chave que você especificar. A chave pode ter qualquer tipo de dados. Por exemplo, a consulta a seguir cria uma sequência de grupos que contém um ou mais objetos Country e cuja chave é um tipo char com o valor sendo a primeira letra dos nomes dos países.

var queryCountryGroups =
    from country in countries
    group country by country.Name[0];

Para obter mais informações sobre o agrupamento, consulte Cláusula group.

Cláusula select

Use a cláusula select para produzir todos os outros tipos de sequências. Uma cláusula select simples produz apenas uma sequência do mesmo tipo dos objetos contidos na fonte de dados. Neste exemplo, a fonte de dados contém objetos Country. A cláusula orderby simplesmente classifica os elementos em uma nova ordem e a cláusula select produz uma sequência dos objetos Country reordenados.

IEnumerable<Country> sortedQuery =
    from country in countries
    orderby country.Area
    select country;

A cláusula select pode ser usada para transformar dados de origem em sequências de novos tipos. Essa transformação também é chamada de projeção. No exemplo a seguir, a cláusula selectprojeta uma sequência de tipos anônimos que contém apenas um subconjunto dos campos no elemento original. Os novos objetos são inicializados usando um inicializador de objeto.

var queryNameAndPop =
    from country in countries
    select new
    {
        Name = country.Name,
        Pop = country.Population
    };

Portanto, nesse exemplo, o var é obrigatório porque a consulta produz um tipo anônimo.

Para obter mais informações sobre todas as maneiras que uma cláusula select pode ser usada para transformar os dados de origem, consulte Cláusula select.

Continuações com into

Você pode usar a palavra-chave into em uma cláusula select ou group para criar um identificador temporário que armazena uma consulta. Use a cláusula into quando precisar executar operações de consulta adicionais em uma consulta após a operação de agrupamento ou seleção. No exemplo a seguir, countries são agrupados de acordo com a população em intervalos de 10 milhões. Depois que esses grupos são criados, mais cláusulas filtram alguns grupos e, em seguida, classificam os grupos em ordem crescente. Para executar essas operações extras, a continuação representada por countryGroup é necessária.

// percentileQuery is an IEnumerable<IGrouping<int, Country>>
var percentileQuery =
    from country in countries
    let percentile = (int)country.Population / 10_000_000
    group country by percentile into countryGroup
    where countryGroup.Key >= 20
    orderby countryGroup.Key
    select countryGroup;

// grouping is an IGrouping<int, Country>
foreach (var grouping in percentileQuery)
{
    Console.WriteLine(grouping.Key);
    foreach (var country in grouping)
    {
        Console.WriteLine(country.Name + ":" + country.Population);
    }
}

Para obter mais informações, consulte into.

Filtragem, ordenação e junção

Entre a cláusula from inicial e a cláusula select ou group final, todas as outras cláusulas (where, join, orderby, from, let) são opcionais. Todas as cláusulas opcionais podem ser usadas várias vezes ou nenhuma vez no corpo de uma consulta.

Cláusula where

Use a cláusula where para filtrar os elementos dos dados de origem com base em uma ou mais expressões de predicado. A cláusula where no exemplo a seguir tem um predicado com duas condições.

IEnumerable<City> queryCityPop =
    from city in cities
    where city.Population is < 200000 and > 100000
    select city;

Para obter mais informações, consulte Cláusula where.

Cláusula orderby

Use a cláusula orderby para classificar os resultados em ordem crescente ou decrescente. Você também pode especificar as ordens de classificação secundárias. O exemplo a seguir executa uma classificação primária nos objetos country usando a propriedade Area. Em seguida, ele executa a classificação secundária usando a propriedade Population.

IEnumerable<Country> querySortedCountries =
    from country in countries
    orderby country.Area, country.Population descending
    select country;

A palavra-chave ascending é opcional. Será a ordem de classificação padrão se nenhuma ordem for especificada. Para obter mais informações, consulte Cláusula orderby.

Cláusula join

Use a cláusula join para associar e/ou combinar elementos de uma fonte de dados com elementos de outra fonte de dados com base em uma comparação de igualdade entre as chaves especificadas em cada elemento. Na LINQ, as operações join são executadas em sequências de objetos cujos elementos são de tipos diferentes. Após ter unido duas sequências, você deve usar uma instrução select ou group para especificar qual elemento armazenar na sequência de saída. Você também pode usar um tipo anônimo para combinar propriedades de cada conjunto de elementos associados em um novo tipo para a sequência de saída. O exemplo a seguir associa objetos prod cuja propriedade Category corresponde a uma das categorias na matriz de cadeias de caracteres categories. Produtos cuja Category não corresponda a nenhuma cadeia de caracteres em categories são filtrados. A instrução select projeta um novo tipo cujas propriedades são tiradas de cat e prod.

var categoryQuery =
    from cat in categories
    join prod in products on cat equals prod.Category
    select new
    {
        Category = cat,
        Name = prod.Name
    };

Você também pode executar uma junção de grupo armazenando os resultados da operação join em uma variável temporária usando a palavra-chave into. Para obter mais informações, consulte Cláusula join.

Cláusula let

Use a cláusula let para armazenar o resultado de uma expressão, como uma chamada de método, em uma nova variável de intervalo. No exemplo a seguir, a variável de intervalo firstName armazena o primeiro elemento da matriz de cadeias de caracteres retornado pelo Split.

string[] names = ["Svetlana Omelchenko", "Claire O'Donnell", "Sven Mortensen", "Cesar Garcia"];
IEnumerable<string> queryFirstNames =
    from name in names
    let firstName = name.Split(' ')[0]
    select firstName;

foreach (var s in queryFirstNames)
{
    Console.Write(s + " ");
}

//Output: Svetlana Claire Sven Cesar

Para obter mais informações, consulte Cláusula let.

Subconsultas em uma expressão de consulta

Uma cláusula de consulta pode conter uma expressão de consulta, que às vezes é chamada de subconsulta. Cada subconsulta começa com sua própria cláusula from que não necessariamente aponta para a mesma fonte de dados na primeira cláusula from. Por exemplo, a consulta a seguir mostra uma expressão de consulta que é usada na instrução select para recuperar os resultados de uma operação de agrupamento.

var queryGroupMax =
    from student in students
    group student by student.Year into studentGroup
    select new
    {
        Level = studentGroup.Key,
        HighestScore = (
            from student2 in studentGroup
            select student2.ExamScores.Average()
        ).Max()
    };

Para obter mais informações, confira Executar uma subconsulta em uma operação de agrupamento.

Confira também