Escrever consultas LINQ em C# para consultar dados

A maioria das consultas na documentação introdutória Language Integrated Query (LINQ) é escrita usando a sintaxe de consulta declarativa 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 é 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 do método em vez da sintaxe da consulta.

A sintaxe da consulta e a sintaxe do método são semanticamente idênticas, mas a sintaxe da consulta é geralmente mais simples e fácil de ler. Algumas consultas devem ser expressas 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 geralmente usa sintaxe de System.Linq 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 mais atentamente. No lado direito da expressão, observe que a where cláusula agora é expressa como um método de instância no numbers objeto, que tem um tipo de IEnumerable<int>. Se você está familiarizado com a interface genérica IEnumerable<T> , sabe que ela não tem um Where método. No entanto, se você invocar a lista de conclusão do IntelliSense no IDE do Visual Studio, verá não apenas um Where método, mas muitos outros métodos, como Select, SelectMany, Joine Orderby. Esses métodos implementam os operadores de consulta padrão.

Captura de tela mostrando todos os operadores de consulta padrão no Intellisense.

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

Para usar métodos de extensão, coloque-os em escopo com using diretivas. Do ponto de vista do seu aplicativo, um método de extensão e um método de instância regular são os mesmos.

Para obter mais informações sobre métodos de extensão, consulte Métodos de extensão. Para obter mais informações sobre operadores de consulta padrão, consulte Visão geral dos 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 do IEnumerable<T>.

Expressões lambda

No exemplo anterior, observe que a expressão condicional (num % 2 == 0) é passada como um argumento in-line para o Enumerable.Where método: Where(num => num % 2 == 0). Esta 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. A num à esquerda do operador é a variável de entrada, que corresponde à num expressão de consulta. O compilador pode inferir o tipo de num porque sabe que numbers é um tipo genérico IEnumerable<T> . O corpo do lambda é exatamente o mesmo que a expressão na sintaxe de consulta ou em qualquer outra expressão ou instrução C#. Pode incluir chamadas de método e outras lógicas complexas. O valor de retorno é apenas o resultado da expressão. Certas consultas só podem ser expressas em sintaxe de método e algumas delas requerem expressões lambda. As expressões lambda são uma ferramenta poderosa e flexível em sua caixa de ferramentas LINQ.

Composabilidade das consultas

No exemplo de código anterior, o Enumerable.OrderBy método é invocado usando o operador dot 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 um IEnumerable, você as compõe na sintaxe do método encadeando as chamadas de método juntas. O compilador faz essa composição quando você escreve consultas usando 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.

Nota

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

Exemplo - Sintaxe da 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 resultados aplicando condições com uma where cláusula. Ele 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 os 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 de consultas é IEnumerable<T>. Todas essas consultas podem ser escritas usando, como var 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 foreach instrução ou outra instrução.

Exemplo - Sintaxe do 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, , Averagee 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 fonte para 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 System.Action parâmetros OR 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, apenas a Consulta #4 é executada imediatamente, porque retorna um único valor, e não uma coleção genérica IEnumerable<T> . O próprio método usa foreach código semelhante para calcular seu valor.

Cada uma das consultas anteriores pode ser escrita usando digitação implícita com 'var'', como mostrado no exemplo a seguir:

// 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 método

Este exemplo mostra como usar a sintaxe do método nos resultados de uma cláusula de consulta. Basta colocar a expressão de consulta entre parênteses e, em seguida, aplicar o operador de ponto e chamar o método. No exemplo a seguir, a consulta #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 do método. Desta 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 Consulta #7 retorna um único valor e não uma coleção, a consulta é executada imediatamente.

A consulta anterior pode ser escrita usando digitação implícita com var, da seguinte maneira:

var numCount = (from num in numbers...

Ele pode ser escrito na sintaxe do método da seguinte maneira:

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

Ele pode ser escrito usando digitação explícita, da seguinte forma:

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

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

Em alguns casos, você não sabe até o tempo de execução quantos predicados você tem que aplicar aos elementos de origem na where cláusula. Uma maneira de especificar dinamicamente vários filtros de predicados é usar o Contains método, como 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 usa uma cláusula diferente where se o valor de tempo de execução 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 manipular possíveis valores nulos em coleções de origem. Uma coleção de objetos como an IEnumerable<T> pode conter elementos cujo valor é null. Se uma coleção de origem for null ou contiver um elemento cujo valor é null, e sua consulta não manipular null valores, um NullReferenceException será lançado quando você executar a consulta.

Você pode codificar 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 where cláusula filtra todos os elementos nulos na sequência de categorias. Esta técnica é independente da verificação nula na cláusula de junção. A expressão condicional com null neste exemplo funciona porque Products.CategoryID é do tipo int?, que é abreviação Nullable<int>de .

Em uma cláusula join, se apenas uma das chaves de comparação for um tipo de valor anulável, você poderá converter a outra em um tipo de valor anulável na expressão de consulta. No exemplo a seguir, suponha que EmployeeID é uma coluna que contém 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 equals palavra-chave query é usada. Você também pode usar a correspondência de padrões, que inclui 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 consulta é 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 System.Linq.IQueryProvider interface para criar fontes de dados que implementam a System.Linq.IQueryable<T> interface.

Manipular exceções em expressões de consulta

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

O exemplo final mostra como lidar com esses casos em que você deve 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 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());
    }
}

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

Em alguns casos, a melhor resposta a uma exceção lançada de dentro de uma consulta pode ser interromper a execução da consulta imediatamente. O exemplo a seguir mostra como lidar com exceções que podem ser lançadas de dentro de um corpo de consulta. Suponha que isso SomeMethodThatMightThrow pode potencialmente causar uma exceção que requer a execução da consulta para parar.

O try bloco encerra o foreach loop, e não a consulta em si. O foreach loop é o ponto em que a consulta é executada. As exceções em tempo de execução são lançadas quando a consulta é executada. Por conseguinte, devem ser tratados em 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 pegar qualquer exceção que você espera levantar e/ou fazer qualquer limpeza necessária em um finally bloco.

Consulte também