Gravar consultas LINQ em C# para consultar dados

A maioria das consultas na documentação introdutória do LINQ (Consulta Integrada à Linguagem) é escrita com o uso da sintaxe de consulta declarativa do LINQ. No entanto, a sintaxe de consulta deve ser convertida em chamadas de método para o CLR (Common Language Runtime) do .NET quando o código for compilado. Essas chamadas de método invocam os operadores de consulta padrão, que têm nomes como Where, Select, GroupBy, Join, Max e Average. Você pode chamá-los diretamente usando a sintaxe de método em vez da sintaxe de consulta.

A sintaxe de consulta e a sintaxe do método são semanticamente idênticas, mas a sintaxe de consulta geralmente é mais simples e fácil de ler. Algumas consultas devem ser expressadas como chamadas de método. Por exemplo, você deve usar uma chamada de método para expressar uma consulta que recupera o número de elementos que correspondem a uma condição especificada. Você também deve usar uma chamada de método para uma consulta que recupera o elemento que tem o valor máximo em uma sequência de origem. A documentação de referência para os operadores de consulta padrão no namespace System.Linq geralmente usa a sintaxe de método. Você deve se familiarizar com como usar a sintaxe do método em consultas e nas próprias expressões de consulta.

Métodos de extensão do operador de consulta padrão

O exemplo a seguir mostra uma expressão de consulta simples e a consulta semanticamente equivalente escrita como uma consulta baseada em método.

int[] numbers = [ 5, 10, 8, 3, 6, 12 ];

//Query syntax:
IEnumerable<int> numQuery1 =
    from num in numbers
    where num % 2 == 0
    orderby num
    select num;

//Method syntax:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

foreach (int i in numQuery1)
{
    Console.Write(i + " ");
}
Console.WriteLine(System.Environment.NewLine);
foreach (int i in numQuery2)
{
    Console.Write(i + " ");
}

A saída dos dois exemplos é idêntica. Você pode ver que o tipo da variável de consulta é o mesmo em ambas as formas: IEnumerable<T>.

Para entender a consulta baseada em método, vamos examiná-la melhor. No lado direito da expressão, observe que a cláusula where agora é expressa como um método de instância no objeto numbers, que tem um tipo de IEnumerable<int>. Se você estiver familiarizado com a interface genérica IEnumerable<T>, você saberá que ela não possui um método Where. No entanto, se você invocar a lista de conclusão do IntelliSense no IDE do Visual Studio, verá não apenas um método Where, mas muitos outros métodos, como Select, SelectMany, Join e Orderby. Esses métodos implementam os operadores de consulta padrão.

Captura de tela que mostra todos os operadores de consulta padrão no IntelliSense.

Embora pareça que IEnumerable<T> inclui mais métodos, ele não inclui. Os operadores de consulta padrão são implementados como métodos de extensão. Métodos de extensão "estendem" um tipo existente, eles podem ser chamados como se fossem métodos de instância no tipo. Os operadores de consulta padrão estendem IEnumerable<T> e que é por esse motivo que você pode escrever numbers.Where(...).

Para usar métodos de extensão, você os coloca no escopo com diretivas using. Do ponto de vista do aplicativo, um método de extensão e um método de instância normal são iguais.

Para obter mais informações sobre os métodos de extensão, consulte Métodos de extensão. Para obter mais informações sobre os operadores de consulta padrão, consulte Visão geral de operadores de consulta padrão (C#). Alguns provedores LINQ, como Entity Framework e LINQ to XML, implementam seus próprios operadores de consulta padrão e métodos de extensão para outros tipos além de IEnumerable<T>.

Expressões lambda

No exemplo anterior, observe que a expressão condicional (num % 2 == 0) é passada como um argumento em linha para o método Enumerable.Where: Where(num => num % 2 == 0). essa expressão embutida é uma expressão lambda. É uma maneira conveniente de escrever código que, de outra forma, teria que ser escrito de forma mais complicada. O num à esquerda do operador é a variável de entrada, que corresponde a num na expressão de consulta. O compilador pode inferir o tipo de num porque ele sabe que numbers é um tipo IEnumerable<T> genérico. O corpo do lambda é igual à expressão na sintaxe da consulta ou em qualquer outra expressão ou instrução C#. Ele pode incluir chamadas de método e outras lógicas complexas. O valor retornado é apenas o resultado da expressão. Determinadas consultas só podem ser expressas na sintaxe do método e algumas delas exigem expressões lambda. As expressões lambda são uma ferramenta poderosa e flexível em sua caixa de ferramentas LINQ.

Possibilidade de composição das consultas

No exemplo de código anterior, o método Enumerable.OrderBy é invocado usando o operador de ponto na chamada para Where. Where produz uma sequência filtrada e, em seguida, Orderby classifica a sequência produzida por Where. Como as consultas retornam uma IEnumerable, você pode escrevê-las na sintaxe de método encadeando as chamadas de método. O compilador faz essa composição quando você escreve consultas usando a sintaxe de consulta. Como uma variável de consulta não armazena os resultados da consulta, você pode modificá-la ou usá-la como base para uma nova consulta a qualquer momento, mesmo depois de executá-la.

Os exemplos a seguir demonstram algumas consultas LINQ simples usando cada abordagem listada anteriormente.

Observação

Essas consultas funcionam em coleções na memória simples, no entanto, a sintaxe básica é idêntica àquela usada no LINQ to Entities e no LINQ to XML.

Exemplo – sintaxe de consulta

Você escreve a maioria das consultas com sintaxe de consulta para criar expressões de consulta. O exemplo a seguir mostra três expressões de consulta. A primeira expressão de consulta demonstra como filtrar ou restringir os resultados aplicando condições com uma cláusula where. Ela retorna todos os elementos na sequência de origem cujos valores são maiores que 7 ou menores que 3. A segunda expressão demonstra como ordenar os resultados retornados. A terceira expressão demonstra como agrupar resultados de acordo com uma chave. Esta consulta retorna dois grupos com base na primeira letra da palavra.

List<int> numbers = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];

// The query variables can also be implicitly typed by using var

// Query #1.
IEnumerable<int> filteringQuery =
    from num in numbers
    where num is < 3 or > 7
    select num;

// Query #2.
IEnumerable<int> orderingQuery =
    from num in numbers
    where num is < 3 or > 7
    orderby num ascending
    select num;

// Query #3.
string[] groupingQuery = ["carrots", "cabbage", "broccoli", "beans", "barley"];
IEnumerable<IGrouping<char, string>> queryFoodGroups =
    from item in groupingQuery
    group item by item[0];

O tipo das consultas é IEnumerable<T>. Todas essas consultas poderiam ser escritas usando var conforme mostrado no exemplo a seguir:

var query = from num in numbers...

Em cada exemplo anterior, as consultas não são realmente executadas até que você itere sobre a variável de consulta em uma instrução foreach ou outra instrução.

Exemplo – sintaxe de método

Algumas operações de consulta devem ser expressas como uma chamada de método. Os métodos mais comuns são aqueles que retornam valores numéricos singleton, como Sum, Max, Min, Average, e assim por diante. Esses métodos sempre devem ser chamados por último em qualquer consulta porque retornam um único valor e não podem servir como a origem de uma operação de consulta extra. O exemplo a seguir mostra uma chamada de método em uma expressão de consulta:

List<int> numbers1 = [5, 4, 1, 3, 9, 8, 6, 7, 2, 0];
List<int> numbers2 = [15, 14, 11, 13, 19, 18, 16, 17, 12, 10];

// Query #4.
double average = numbers1.Average();

// Query #5.
IEnumerable<int> concatenationQuery = numbers1.Concat(numbers2);

Se o método tiver parâmetros System.Action ou System.Func<TResult>, esses argumentos serão fornecidos na forma de uma expressão lambda, conforme mostrado no exemplo a seguir:

// Query #6.
IEnumerable<int> largeNumbersQuery = numbers2.Where(c => c > 15);

Nas consultas anteriores, somente a Consulta nº 4 é executada imediatamente, pois retorna um único valor e não uma coleção genérica IEnumerable<T>. O método em si usa foreach ou código semelhante para calcular seu valor.

Cada uma das consultas anteriores pode ser escrita usando a tipagem implícita com var, conforme mostrado nos seguintes exemplos:

// var is used for convenience in these queries
double average = numbers1.Average();
var concatenationQuery = numbers1.Concat(numbers2);
var largeNumbersQuery = numbers2.Where(c => c > 15);

Exemplo – sintaxe mista de consulta e do método

Este exemplo mostra como usar a sintaxe do método nos resultados de uma cláusula de consulta. Simplesmente coloque a expressão de consulta entre parênteses e, em seguida, aplique o operador de ponto e chame o método. No exemplo a seguir, a Query #7 retorna uma contagem dos números cujo valor está entre 3 e 7. Em geral, no entanto, é melhor usar uma segunda variável para armazenar o resultado da chamada de método. Dessa forma, é menos provável que a consulta seja confundida com os resultados da consulta.

// Query #7.

// Using a query expression with method syntax
var numCount1 = (
    from num in numbers1
    where num is > 3 and < 7
    select num
).Count();

// Better: Create a new variable to store
// the method call result
IEnumerable<int> numbersQuery =
    from num in numbers1
    where num is > 3 and < 7
    select num;

var numCount2 = numbersQuery.Count();

Como a Query #7 retorna um único valor e não uma coleção, a consulta é executada imediatamente.

A consulta anterior pode ser escrita usando a tipagem implícita com var, da seguinte maneira:

var numCount = (from num in numbers...

Ela pode ser escrita na sintaxe de método da seguinte maneira:

var numCount = numbers.Count(n => n is > 3 and < 7);

Ela pode ser escrita usando a tipagem explícita da seguinte maneira:

int numCount = numbers.Count(n => n is > 3 and < 7);

Especificar filtros de predicado dinamicamente em tempo de execução

Em alguns casos, você não sabe até o tempo de execução quantos predicados precisa aplicar aos elementos de origem na cláusula where. Uma maneira de especificar dinamicamente vários filtros de predicados é usar o método Contains, conforme mostrado no exemplo a seguir. A consulta retorna resultados diferentes com base no valor de id quando a consulta é executada.

int[] ids = [111, 114, 112];

var queryNames =
    from student in students
    where ids.Contains(student.ID)
    select new
    {
        student.LastName,
        student.ID
    };

foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Garcia: 114
    O'Donnell: 112
    Omelchenko: 111
 */

// Change the ids.
ids = [122, 117, 120, 115];

// The query will now return different results
foreach (var name in queryNames)
{
    Console.WriteLine($"{name.LastName}: {name.ID}");
}

/* Output:
    Adams: 120
    Feng: 117
    Garcia: 115
    Tucker: 122
 */

Você pode usar instruções de fluxo de controle, como if... else ou switch, para selecionar entre consultas alternativas predeterminadas. No exemplo a seguir, studentQuery usará outra cláusula where se o valor do runtime de oddYear é true ou false.

void FilterByYearType(bool oddYear)
{
    IEnumerable<Student> studentQuery = oddYear
        ? (from student in students
           where student.Year is GradeLevel.FirstYear or GradeLevel.ThirdYear
           select student)
        : (from student in students
           where student.Year is GradeLevel.SecondYear or GradeLevel.FourthYear
           select student);
    var descr = oddYear ? "odd" : "even";
    Console.WriteLine($"The following students are at an {descr} year level:");
    foreach (Student name in studentQuery)
    {
        Console.WriteLine($"{name.LastName}: {name.ID}");
    }
}

FilterByYearType(true);

/* Output:
    The following students are at an odd year level:
    Fakhouri: 116
    Feng: 117
    Garcia: 115
    Mortensen: 113
    Tucker: 119
    Tucker: 122
 */

FilterByYearType(false);

/* Output:
    The following students are at an even year level:
    Adams: 120
    Garcia: 114
    Garcia: 118
    O'Donnell: 112
    Omelchenko: 111
    Zabokritski: 121
 */

Manipular valores nulos em expressões de consulta

Este exemplo mostra como tratar os possíveis valores nulos em coleções de origem. Uma coleção de objetos, tal como uma IEnumerable<T>, pode conter elementos cujo valor é null. Se uma coleção de origem for null ou contiver um elemento cujo valor seja null e a consulta não lidar com valores null, uma NullReferenceException será gerada ao executar a consulta.

Você pode escrever o código defensivamente para evitar uma exceção de referência nula conforme mostrado no exemplo a seguir:

var query1 =
    from c in categories
    where c != null
    join p in products on c.ID equals p?.CategoryID
    select new
    {
        Category = c.Name,
        Name = p.Name
    };

No exemplo anterior, a cláusula where filtra todos os elementos nulos na sequência de categorias. Essa técnica é independente da verificação de nulos na cláusula join. A expressão condicional com null nesse exemplo funciona porque Products.CategoryID é do tipo int?, que é uma abreviação para Nullable<int>.

Em uma cláusula join, se apenas uma das chaves de comparação for um tipo de valor anulável que, você pode converter a outra para um tipo de valor anulável na expressão de consulta. No exemplo a seguir, suponha que EmployeeID é uma coluna que contém os valores do tipo int?:

var query =
    from o in db.Orders
    join e in db.Employees
        on o.EmployeeID equals (int?)e.EmployeeID
    select new { o.OrderID, e.FirstName };

Em cada um dos exemplos, a palavra-chave de consulta equals é usada. Use também padrões correspondentes, que incluem padrões para is null e is not null. Esses padrões não são recomendados em consultas LINQ porque os provedores de consulta podem não interpretar a nova sintaxe C# corretamente. Um provedor de consultas é uma biblioteca que converte expressões de consulta C# em um formato de dados nativo, como o Entity Framework Core. Os provedores de consulta implementam a interface System.Linq.IQueryProvider para criar fontes de dados que implementam a interface System.Linq.IQueryable<T>.

Tratar exceções em expressões de consulta

É possível chamar qualquer método no contexto de uma expressão de consulta. Não chame qualquer método em uma expressão de consulta que possa criar um efeito colateral, como modificar o conteúdo da fonte de dados ou gerar uma exceção. Este exemplo mostra como evitar exceções ao chamar métodos em uma expressão de consulta, sem violar as diretrizes gerais sobre tratamento de exceção do .NET. Essas diretrizes declaram que é aceitável capturar uma exceção específica quando você entende por que ela é gerada em um determinado contexto. Para obter mais informações, consulte Melhores práticas para exceções.

O último exemplo mostra como tratar os casos em que é necessário lançar uma exceção durante a execução de uma consulta.

O exemplo a seguir mostra como mover o código de tratamento de exceção para fora de uma expressão de consulta. Essa refatoração só é possível quando o método não depende de nenhuma variável que seja local para a consulta. É mais fácil lidar com exceções fora da expressão de consulta.

// A data source that is very likely to throw an exception!
IEnumerable<int> GetData() => throw new InvalidOperationException();

// DO THIS with a datasource that might
// throw an exception.
IEnumerable<int>? dataSource = null;
try
{
    dataSource = GetData();
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation");
}

if (dataSource is not null)
{
    // If we get here, it is safe to proceed.
    var query =
        from i in dataSource
        select i * i;

    foreach (var i in query)
    {
        Console.WriteLine(i.ToString());
    }
}

No bloco catch (InvalidOperationException) no exemplo anterior, manipule (ou não manipule) a exceção da maneira apropriada para seu aplicativo.

Em alguns casos, a melhor resposta para uma exceção que é lançada de dentro de uma consulta poderá ser a interrupção imediata da execução da consulta. O exemplo a seguir mostra como tratar exceções que podem ser geradas de dentro de um corpo de consulta. Suponha que SomeMethodThatMightThrow possa causar uma exceção que exija que a execução da consulta seja interrompida.

O bloco try inclui o loop foreach e não a própria consulta. O loop foreach é o ponto em que a consulta é executada. As exceções de runtime são gerada quando a consulta for executada. Portanto, elas devem ser manipuladas no loop foreach.

// Not very useful as a general purpose method.
string SomeMethodThatMightThrow(string s) =>
    s[4] == 'C' ?
        throw new InvalidOperationException() :
        @"C:\newFolder\" + s;

// Data source.
string[] files = ["fileA.txt", "fileB.txt", "fileC.txt"];

// Demonstration query that throws.
var exceptionDemoQuery =
    from file in files
    let n = SomeMethodThatMightThrow(file)
    select n;

try
{
    foreach (var item in exceptionDemoQuery)
    {
        Console.WriteLine($"Processing {item}");
    }
}
catch (InvalidOperationException e)
{
    Console.WriteLine(e.Message);
}

/* Output:
    Processing C:\newFolder\fileA.txt
    Processing C:\newFolder\fileB.txt
    Operation is not valid due to the current state of the object.
 */

Lembre-se de capturar qualquer exceção que você espera gerar e/ou fazer qualquer limpeza necessária em um bloco finally.

Confira também