Partilhar via


Escrever consultas LINQ em C# para consultar dados

A maioria das consultas na documentação introdutória do Language Integrated Query (LINQ) utiliza a sintaxe declarativa da consulta LINQ. O compilador C# converte a sintaxe de consulta em chamadas de método. Estas chamadas de método implementam os operadores padrão de consulta. Têm nomes como Where, Select, GroupBy, Join, Max, e Average. Chama-os diretamente usando sintaxe de método em vez de sintaxe de 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. Deve expressar algumas consultas 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. Familiarize-se com a utilização da sintaxe dos métodos 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. O tipo da variável de consulta é o mesmo em ambas as formas: IEnumerable<T>.

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 membros de extensão "estendem" um tipo existente; Podem ser chamados como se fossem membros do tipo. Os operadores de consulta padrão estendem IEnumerable<T>, e é por isso que podes escrever numbers.Where(...). Você traz extensões para o escopo com using diretivas antes de chamá-las.

Para mais informações sobre membros da extensão, veja Membros da 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 fornecedores LINQ, como Entity Framework e LINQ para XML, implementam os seus próprios operadores de consulta padrão e membros de extensão para outros tipos além do IEnumerable<T>.

Expressões lambda

No exemplo anterior, a expressão condicional (num % 2 == 0) é passada como um argumento in-line para o método Enumerable.Where: 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 é 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 é o resultado da expressão. Só podes expressar certas consultas na sintaxe do método, e algumas dessas consultas 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 método Enumerable.OrderBy é 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 básicas usando cada abordagem listada anteriormente.

Nota

Estas consultas operam em coleções em memória; no entanto, a sintaxe é idêntica à usada no LINQ para Entidades e no LINQ para XML.

Exemplo - sintaxe de consulta

Escreva 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 estas consultas podem ser escritas usando var como mostrado no seguinte exemplo:

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

Deverá expressar algumas operações de consulta 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. Chame estes métodos por último em qualquer consulta porque devolvem um único valor e não podem servir de fonte para mais operações de consulta. 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>, fornecer estes argumentos sob a forma de uma expressão lambda, como mostrado no seguinte exemplo:

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

Nas consultas anteriores, apenas a Consulta #4 é executada imediatamente porque devolve 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.

Pode escrever cada uma das consultas anteriores usando tipagem implícita com var, como mostrado no seguinte exemplo:

// 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. Inclua a expressão da consulta entre parênteses, depois aplique o operador ponto e chame o método. No exemplo a seguir, a consulta #7 retorna uma contagem dos números cujo valor está entre 3 e 7.

// 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.

Pode escrever a consulta anterior usando a tipagem implícita com var, da seguinte forma:

var numCount = (from num in numbers...

Pode escrevê-lo em sintaxe de método da seguinte forma:

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

Pode escrevê-lo usando a tipagem 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
 */

Nota

Este exemplo usa a seguinte fonte de dados e informações.

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

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.

O exemplo a seguir usa esses tipos e matrizes de dados estáticos:

record Product(string Name, int CategoryID);
record Category(string Name, int ID);
static Category?[] categories =
[
    new ("brass", 1),
    null,
    new ("winds", 2),
    default,
    new ("percussion", 3)
];

static Product?[] products =
[
    new Product("Trumpet", 1),
    new Product("Trombone", 1),
    new Product("French Horn", 1),
    null,
    new Product("Clarinet", 2),
    new Product("Flute", 2),
    null,
    new Product("Cymbal", 3),
    new Product("Drum", 3)
];

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

Pode 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 dizem que é aceitável apanhar uma exceção específica quando se percebe porque é incluída num 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. Só podes refatorar desta forma quando o método não depende de variáveis locais à 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) do exemplo anterior, trate (ou não trata) a exceção da forma que for apropriada para a sua aplicação.

Em alguns casos, a melhor resposta a uma exceção lançada dentro de uma consulta pode ser parar imediatamente a execução da consulta. 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 isso, trate-os do foreach ciclo.

// 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.
 */

Apanhe a exceção que você espera levantar e faça a limpeza necessária num bloco finally.

Consulte também