Utilizando Let no LINQ

O let no LINQ permite criar variáveis dentro de uma consulta para guardar o resultado de um cálculo e reaproveitá-lo nas cláusulas seguintes. Com o LINQ, você consulta listas de objetos de forma simples e prática, e o let ajuda a deixar essas consultas mais legíveis e enxutas.

Neste artigo, vamos revisar os fundamentos do LINQ, passar por orderby e group by e, por fim, explorar o let com um exemplo completo. Além disso, você vai ver como a cláusula funciona por baixo dos panos e quais cuidados tomar ao usá-la.

O que é LINQ

LINQ, sigla de Language Integrated Query (consulta integrada à linguagem), permite executar consultas sobre coleções de objetos, de forma parecida com o que fazemos no SQL. A sintaxe mais simples possível seleciona todos os elementos de uma lista:

var data = from c in customers select c;

A diferença é que, aqui, a consulta roda sobre uma lista de objetos, e não sobre uma tabela. Além disso, você ganha a vantagem da tipagem forte e da verificação em tempo de compilação.

Em outras palavras, se você digitar o nome de uma propriedade errada, o compilador acusa o erro antes mesmo de a aplicação rodar. Isso torna o LINQ muito mais seguro do que montar consultas como texto.

A documentação oficial do LINQ apresenta duas formas de escrever consultas. A primeira é a sintaxe de consulta, usada neste artigo, e a segunda é a sintaxe de métodos, com chamadas como Where e Select.

Essa distinção importa, porque o let no LINQ pertence à sintaxe de consulta. Mais adiante, você verá como o compilador traduz essa cláusula para a sintaxe de métodos.

LINQ com Entity Framework

Outro ponto interessante aparece quando você usa o LINQ junto ao Entity Framework. Nesse caso, o provider traduz a consulta para a linguagem SQL do banco de dados.

Por exemplo, se o from percorrer um DbSet do seu contexto, o resultado vira uma instrução SQL:

var data = from c in context.Customers select c;
SELECT * FROM [Customer]

O SQL acima é uma simplificação didática. Na prática, o EF Core lista explicitamente cada coluna mapeada da entidade, em vez de usar o asterisco.

O LINQ é extremamente poderoso e permite compor consultas como fazemos no SQL. Por exemplo, você pode gerar novos objetos a partir do select:

var data =
    from c in context.Customers
    select new { c.Name, c.Email };

Nesse caso, o SQL gerado seleciona apenas os campos informados no select new. Assim, o provider sempre acompanha a consulta LINQ, tentando fazer a tradução mais fiel possível:

SELECT [Name], [Email] FROM [Customer]

Essa tradução depende do tipo da coleção consultada, e o mesmo vale para o let no LINQ, como veremos no fim do artigo. Se quiser entender melhor essa diferença, o artigo sobre IEnumerable vs IQueryable explica como cada interface impacta o SQL gerado.

Mais recursos do LINQ

O LINQ também oferece recursos equivalentes a vários comandos do SQL, como ordenação, soma e agrupamento. De fato, ele é tão expressivo que você consegue escrever quase qualquer consulta SQL com ele, embora isso nem sempre seja uma boa ideia.

As duas cláusulas a seguir aparecem com frequência ao lado do let no LINQ. Por isso, vale revisá-las antes de chegar ao tema principal.

Ordenando com orderby

O uso do orderby é simples. Basta informar a palavra reservada, seguida do campo e do sentido da ordenação, ascending ou descending:

var data =
    from c in context.Customers
    orderby c.Name descending
    select new
    {
        c.Name,
        c.Email
    };

Repare que a palavra reservada é orderby, toda em minúsculas. O C# diferencia maiúsculas de minúsculas, então orderBy não compila.

Quando você omite o sentido, a ordenação é crescente por padrão. Além disso, você pode ordenar por vários campos separando-os por vírgula, como em orderby c.Name, c.Email.

Agrupando com group by

O group by permite agrupar conjuntos de dados. Por exemplo, podemos agrupar pedidos pela quantidade de itens que cada um possui:

var ordersGroupedByItemCount =
    from order in orders
    group order by order.OrderLines.Count into itemCountGroup
    orderby itemCountGroup.Key
    select itemCountGroup;

Cada grupo retornado é um IGrouping<TKey, TElement>. Nele, a propriedade Key guarda a chave do agrupamento, ou seja, a quantidade de itens, e o próprio grupo enumera os pedidos com aquela quantidade.

Por esse motivo, usamos orderby itemCountGroup.Key antes do select. Assim, os grupos aparecem em ordem crescente de quantidade de itens.

Note também a palavra into. Ela cria uma continuação da consulta, e a partir dela você trabalha com o grupo, e não mais com o pedido individual.

Como usar let no LINQ

Todas essas explicações serviram para chegar a um recurso muito útil e pouco lembrado: a possibilidade de criar variáveis durante a consulta. Para isso, o LINQ oferece a palavra reservada let.

Apesar de passar despercebido por muita gente, o let no LINQ não é novidade. Ele existe desde o C# 3.0, a mesma versão que introduziu as expressões de consulta.

Segundo a referência da cláusula let, ela cria uma nova variável de intervalo e a inicializa com o resultado de uma expressão. Depois disso, você não pode atribuir outro valor a ela.

Na prática, o let armazena valores como o total de itens ou a soma de determinados campos. Em seguida, você usa essas variáveis no where, no orderby, no select ou mesmo no group by.

Preparando o exemplo de let no LINQ

Para exemplificar, vamos criar dois records, um para o pedido e outro para o item do pedido:

public record Order(string Number, List<OrderLine> OrderLines);

public record OrderLine(string Product, int Quantity, decimal Price);

Com os tipos criados, vamos popular uma lista com alguns dados. O método GenerateOrderLines gera itens de pedido de forma sequencial, mas os dados em si não importam tanto aqui:

var orders = new List<Order>
{
    new("Order 1 (fewer than 10 items)", GenerateOrderLines(9)),
    new("Order 2 (more than 10 items)", GenerateOrderLines(12)),
    new("Order 3 (more than 10 items)", GenerateOrderLines(18)),
    new("Order 4 (more than 10 items)", GenerateOrderLines(22))
};

List<OrderLine> GenerateOrderLines(int quantity)
{
    var result = new List<OrderLine>();

    // Cada item recebe o índice como quantidade e como preço
    for (var i = 0; i < quantity; i++)
        result.Add(new OrderLine($"Product {i}", i, i));

    return result;
}

Como o preço de cada item é igual ao seu índice, o total de cada pedido é fácil de calcular. Por exemplo, o pedido com 12 itens soma 66, enquanto o pedido com 18 itens soma 153.

Filtrando pedidos com let no LINQ

Agora, considere o seguinte cenário: queremos obter todos os pedidos com mais de 10 itens e cujo total seja maior que 100 reais. Como vimos, o where filtra os dados, mas o pedido não expõe o total de itens nem a soma dos valores.

Uma opção seria criar essas propriedades no Order. Contudo, para deixar o cenário mais desafiador, vamos resolver tudo apenas com LINQ:

var data =
    from order in orders
    let totalItems = order.OrderLines.Count
    let total = order.OrderLines.Sum(x => x.Price)
    where
        totalItems > 10 &&
        total > 100
    select order;

No exemplo acima, criamos duas variáveis, totalItems e total. Elas armazenam, respectivamente, a quantidade de itens e o valor total de cada pedido.

Você pode fazer qualquer tipo de cálculo nessas variáveis. Depois, elas ficam disponíveis para as cláusulas seguintes da consulta.

Com os dados do exemplo, apenas os pedidos 3 e 4 atendem às duas condições. O pedido 2 tem mais de 10 itens, porém soma apenas 66.

Usando as variáveis do let no select

As variáveis criadas com o let no LINQ também podem compor o resultado final. Por exemplo, podemos devolver um objeto anônimo com o número do pedido, o total e a quantidade de itens:

var data =
    from order in orders
    let totalItems = order.OrderLines.Count
    let total = order.OrderLines.Sum(x => x.Price)
    where
        totalItems > 10 &&
        total > 100
    select new
    {
        order.Number,
        Total = total,
        TotalItems = totalItems
    };

Assim, os valores calculados uma única vez servem tanto para o filtro quanto para a projeção. Para exibir os resultados, basta percorrer a consulta:

foreach (var item in data)
    Console.WriteLine($"{item.Number} - {item.Total}");

Como o objeto anônimo já carrega o total, não precisamos recalcular a soma no foreach. A saída mostra os dois pedidos filtrados:

Order 3 (more than 10 items) - 153
Order 4 (more than 10 items) - 231

Se quiser se aprofundar nesse tipo de objeto, o artigo sobre tipos anônimos no C# mostra como eles funcionam e quais são as suas limitações.

Let no LINQ com consultas aninhadas

O let também pode guardar uma coleção, e não apenas um valor simples. Nesse caso, a variável vira uma nova fonte de dados, e você pode consultá-la com outro from.

A documentação oficial mostra um exemplo clássico com frases e palavras, adaptado abaixo:

string[] sentences =
[
    "A penny saved is a penny earned.",
    "The early bird catches the worm.",
    "The pen is mightier than the sword."
];

var wordsStartingWithVowel =
    from sentence in sentences
    let words = sentence.Split(' ')
    from word in words
    // Converte para minúsculas uma única vez por palavra
    let lowerWord = word.ToLower()
    where "aeiou".Contains(lowerWord[0])
    select word;

Primeiro, o let no LINQ guarda o array de palavras de cada frase, e o segundo from percorre esse array. Em seguida, outro let chama ToLower uma única vez por palavra.

Sem o let, você precisaria repetir word.ToLower() em cada comparação do where. Portanto, a cláusula evita processamento repetido e deixa a condição mais limpa.

Combinando let no LINQ com group by

As variáveis do let também servem como chave de agrupamento. Por exemplo, podemos separar os pedidos entre os de alto valor e os demais, usando o total calculado:

var ordersByValue =
    from order in orders
    let total = order.OrderLines.Sum(x => x.Price)
    group order by total > 100 into valueGroup
    select new
    {
        IsHighValue = valueGroup.Key,
        Count = valueGroup.Count()
    };

Nesse caso, a chave do grupo é um bool. Com os dados do exemplo, dois pedidos ficam no grupo true, com totais de 153 e 231, e dois ficam no grupo false, com 36 e 66.

Assim, o mesmo cálculo que antes alimentava o filtro agora define o agrupamento. Esse é um bom exemplo de como o let no LINQ reduz a repetição de expressões em consultas maiores.

Como o let no LINQ funciona por baixo dos panos

A sintaxe de consulta é açúcar sintático. Ou seja, o compilador converte a consulta em chamadas de métodos como Select, Where e OrderBy.

No caso do let no LINQ, o compilador cria um Select intermediário que carrega o elemento original e a nova variável juntos, em um tipo anônimo. A especificação chama esse recurso de identificador transparente.

A consulta de filtro do exemplo fica, de forma aproximada, equivalente ao código abaixo:

var data = orders
    .Select(order => new { order, totalItems = order.OrderLines.Count })
    .Select(x => new { x, total = x.order.OrderLines.Sum(l => l.Price) })
    // Cada let adiciona uma camada de projeção
    .Where(y => y.x.totalItems > 10 && y.total > 100)
    .Select(y => y.x.order);

Repare como a versão com métodos fica mais verbosa. Por isso, consultas com vários cálculos intermediários costumam ficar mais legíveis na sintaxe de consulta com let.

Por outro lado, cada let adiciona uma projeção extra. Em coleções em memória, esse custo é pequeno, mas vale saber que ele existe ao escrever código muito sensível a desempenho.

Let no LINQ com Entity Framework Core

Como o let vira apenas uma projeção, ele também funciona em consultas sobre um DbSet. Nesse caso, o EF Core tenta traduzir a expressão inteira para SQL, inclusive os cálculos guardados nas variáveis.

Expressões simples, como Count e Sum sobre uma navegação, normalmente viram subconsultas no SQL. Entretanto, se a variável chamar um método que o provider não conhece, a tradução falha em tempo de execução.

Portanto, ao combinar let no LINQ com EF Core, confira o SQL gerado durante o desenvolvimento. Dessa forma, você garante que o filtro roda no banco, e não na memória da aplicação.

Se você quiser revisar os conceitos do EF Core antes disso, o artigo Dapper vs Entity Framework Core traz uma boa comparação entre as duas abordagens de acesso a dados.

Let no LINQ e a execução adiada

Muitos operadores do LINQ usam execução adiada. Em outras palavras, a consulta apenas descreve o resultado, e a leitura dos dados acontece quando o código percorre a sequência, por exemplo, em um foreach.

Com o let no LINQ, isso continua valendo. Ou seja, os cálculos de totalItems e total só rodam durante a enumeração, e não no momento em que você declara a consulta.

Por isso, se você percorrer a mesma consulta duas vezes, os cálculos rodam duas vezes. Nesse caso, materialize o resultado com ToList quando precisar reutilizá-lo.

Boas práticas ao usar let no LINQ

Primeiro, use let quando um mesmo cálculo aparece mais de uma vez na consulta. Assim, você evita repetir a expressão no where e no select.

Em seguida, dê nomes claros às variáveis. Como elas funcionam como parte da consulta, um nome como total ou totalItems documenta a intenção melhor do que um comentário.

Também evite transformar a consulta em um programa inteiro. Se o let começar a guardar lógica de negócio complexa, considere mover essa lógica para um método ou para o próprio tipo.

Por fim, lembre que a variável do let é somente leitura. Se você precisar de um valor que muda durante a iteração, um laço tradicional provavelmente será mais adequado.

Conclusão

O LINQ é uma ferramenta poderosa que permite consultas simples e complexas em coleções de objetos, inclusive com tradução para SQL. Ainda assim, isso não significa que você deve fazer tudo com ele.

Nesse contexto, o let no LINQ ajuda a manter as consultas legíveis, porque guarda cálculos intermediários e os reaproveita no filtro, na ordenação e na projeção. Com isso, você escreve menos código repetido e deixa a intenção da consulta mais clara.

Em resumo, use let sempre que um valor calculado aparecer mais de uma vez. Desse modo, suas consultas ficam mais fáceis de ler, testar e manter.

FAQ

O let no LINQ funciona com a sintaxe de métodos?

Não diretamente. A palavra let existe apenas na sintaxe de consulta, mas você obtém o mesmo efeito com um Select que projeta um tipo anônimo.

Posso alterar o valor de uma variável criada com let?

Não. Depois de inicializada, a variável de intervalo não aceita outro valor, embora você possa consultá-la se ela guardar uma coleção.

O let no LINQ é um recurso novo?

Não. A cláusula existe desde o C# 3.0, versão que introduziu as expressões de consulta.

O EF Core traduz consultas com let para SQL?

Sim, quando as expressões guardadas nas variáveis são traduzíveis. Caso contrário, a consulta falha em tempo de execução.

Quantas cláusulas let posso usar em uma consulta?

Não há um limite prático, e cada let adiciona uma projeção. Use quantas forem necessárias para manter a consulta legível.