Falando sobre expressões de consulta LINQ introduzidas na linguagem de programação C#

O artigo aborda os detalhes das expressões de consulta LINQ introduzidas na linguagem de programação C# 3.0. Todas as cláusulas de expressões de consulta da linguagem C# serão demonstradas com exemplos práticos de consultas realizadas no sistema de arquivos Windows.

A finalidade do artigo é demonstrar o poder da LINQ para fazer consultas complexas de forma simples. O leitor terá a oportunidade de utilizar os seus conhecimentos em SQL, linguagem usada em bancos de dados relacionais, para fazer consultas com a linguagem C# em fontes de dados variadas.

O tema é útil no desenvolvimento de praticamente qualquer sistema em .NET. O domínio das expressões de consulta na linguagem C# é importante no uso dos providers LINQ disponíveis, como: LINQ to Objects, LINQ to XML, LINQ to DataSet, LINQ to SQL e LINQ to Entities.

Resumindo o LINQ

Após uma breve introdução à LINQ e algumas de suas diversas implementações para diversas fontes de dados, o leitor terá a oportunidade de conhecer os Standard Query Operators e a sintaxe das expressões de consulta com explicações de todas as cláusulas introduzidas na linguagem C# 3.0. Depois, o leitor será orientado na criação de um projeto de aplicação ASP.NET Web Forms para demonstrar todas as cláusulas das expressões de consulta LINQ, desde exemplos mais básicos até exemplos relativamente complexos.

O objetivo da Microsoft com a criação da Language-Integrated Query (LINQ) foi aproximar o mundo dos objetos do mundo dos dados. O LINQ corresponde a uma sintaxe unificada, inicialmente incorporada às linguagens C# e Visual Basic, para consultas em fontes de dados variadas. A sintaxe de consulta da LINQ foi inspirada na da Structured Query Language (SQL), que é uma linguagem padrão para comunicação com bancos de dados relacionais. Assim como na linguagem SQL, as expressões de consulta LINQ permitem a construção de instruções variadas para extração de informações.

Tradicionalmente, as consultas a bancos de dados em aplicações eram expressas por strings sem verificação de sintaxe nem em tempo de projeto e nem em tempo de compilação, além de não haver suporte a IntelliSense. Erros de sintaxe na consulta somente eram identificados em tempo de execução. Além disto, o desenvolvedor precisava aprender linguagens de consulta específicas para cada tipo de fonte de dados, como: bancos de dados relacionais, coleções de objetos na memória, documentos XML, dentre outros tipos de fontes de dados.

A ideia com a LINQ foi tornar as consultas como um recurso de primeira classe nas linguagens de programação da plataforma .NET, sendo incorporado inicialmente nas linguagens C# e Visual Basic. Este artigo trata somente das expressões de consulta na linguagem C#.

Introdução do LINQ na plataforma .NET

A LINQ foi introduzida nas linguagens Visual Basic 9.0 (ou Visual Basic 2008) e C# 3.0 (Visual C# 2008), em novembro de 2007, com o .NET Framework 3.5 e o Visual Studio 2008.

Diversos novos recursos foram introduzidos na versão 3.0 da linguagem de programação C#, sendo que grande parte deles voltados para suportar a inclusão da LINQ na linguagem. Vemos a seguir uma lista dos recursos que estão diretamente ligados com o suporte à LINQ:

  • Iniciadores de objetos
  • Tipos implícitos em variáveis locais (var)
  • Métodos de extensão
  • Expressões lambda
  • Árvores de expressão
  • Tipos anônimos
  • Expressões de consulta

É fundamental dominar estes recursos, além de outros das versões 2.0 e 1.0 da linguagem C#, para entender completamente o funcionamento interno da LINQ.

LINQ Providers

O padrão LINQ torna simples a consulta em fontes de dados para as quais o LINQ esteja habilitado, uma vez que a sintaxe e o padrão de consultas não muda.

Um LINQ Provider para uma determinada fonte de dados permite não somente a operação de consulta, mas também operações de inclusão, atualização e exclusão, além de mapeamento para tipos definidos pelo usuário.

O .NET Framework suporta os seguintes LINQ Providers:

LINQ to Objects
Componente, introduzido no .NET Framework 3.5, que permite consultar coleções de objetos do tipo IEnumerable diretamente.
LINQ to XML
Componente, introduzido no .NET Framework 3.5, que fornece uma interface de manipulação de XML em memória. Corresponde a uma interface de programação muito mais avançada e simples de manipulação XML na memória que a interface do padrão W3C Document Object Model (DOM).
LINQ to DataSet
Componente, introduzido no .NET Framework 3.5, que possibilita a consulta de objetos do tipo DataSet na memória.
LINQ to SQL
Componente, introduzido no .NET Framework 3.5, que fornece infraestrutura para gerenciar dados relacionais como objetos. Este componente permite fazer mapeamento objeto-relacional (ORM – Object-Relational Mapping), ou seja, permite mapear um modelo de dados de um banco de dados relacional para um modelo de objetos. Atualmente, a LINQ to SQL somente contém um provider para o servidor de banco de dados SQL Server, desenvolvido pela Microsoft. Como o LINQ to SQL não fornece uma boa abstração do banco de dados, não houve interesse no desenvolvimento de providers para outros bancos de dados.
LINQ to Entities
que é parte do ADO.NET Entity Framework O ADO.NET Entity Framework é um componente do .NET Framework, introduzido no .NET Framework 3.5 SP1, que, assim como a LINQ to SQL, fornece infraestrutura para gerenciar dados relacionais como objetos. Porém, ao contrário da LINQ to SQL, permite trabalhar com dados num alto nível de abstração do engine de armazenamento de dados e do esquema relacional.

O Entity Framework suporta o Entity Data Model (EDM) para definição dos dados num nível conceitual. Há uma separação clara entre as informações do modelo conceitual, do modelo de armazenamento e do mapeamento entre estes dois modelos.

Consultas ao modelo conceitual usando as linguagens C# e Visual Basic. Atualmente, o ADO .NET Entity Framework está na versão 4.0 e corresponde à principal tecnologia de mapeamento objeto-relacional suportada pela Microsoft.

Standard Query Operators

Os Standard Query Operators são métodos de extensão que formam o padrão LINQ. Eles fornecem capacidades de consulta incluindo projeção, filtragem, ordenação, agregação e outras.

Existem dois conjuntos de Standard Query Operators LINQ: os que atuam em objetos do tipo System.Collections.Generic.IEnumerable e os que atuam em objetos do tipo System.Linq.IQueryable. Os métodos de extensão que constituem cada conjunto são definidos nas classes estáticas System.Linq.Enumerable e System.Linq.Queryable, respectivamente.

Alguns dos Standard Query Operators usados mais frequentemente possuem palavras-chaves na sintaxe da linguagem C# para permitir que eles sejam chamados como parte de uma expressão de consulta.

Principalmente na linguagem C#, diversos métodos de extensão importantes dos Standard Query Operators não possuem palavras-chaves equivalentes nas expressões de consulta LINQ. Deste modo, é muito importante o domínio destes métodos de extensão para ultrapassar as limitações das expressões de consulta. Na seção de Links do final do artigo há o endereço, no site da Microsoft, com informações detalhadas dos Standard Query Operators.

Expressões de consulta LINQ

Para um desenvolvedor que escreve consultas LINQ, a parte mais visível da integração com a linguagem C# fornecida pela LINQ são as expressões de consulta. Expressões de consulta são escritas com uma sintaxe declarativa que foi introduzida na versão 3.0 da linguagem C#. A sintaxe de consulta permite realizar, com um mínimo de código, diversas operações em dados, como:

  • filtragem;
  • projeção;
  • ordenação;
  • agrupamento;
  • junção e outras.

As mesmas expressões de consulta LINQ são usadas para consultar coleções de objetos na memória (LINQ to Objects), dados em bancos de dados relacionais (LINQ to SQL e LINQ to Entities), objetos DataSet na memória (LINQ to DataSet), documentos XML na memória (LINQ to XML) ou, ainda, qualquer outra fonte de dados que tenha um LINQ Provider disponível.

Sintaxe das expressões de consulta LINQ


        from id in fonteDados
        { from id in fonteDados |
        join id in fonteDados on expressão equals expressão [ into id ] |
        let id = expressão |
        where condição |
        orderby id1, id2, … [ascending | descending] }
        select expressão | group expressão by chave
        [ into id ]
        

As expressões de consulta devem iniciar com from, depois podem ter um ou mais from, join, let, where ou orderby e deve terminar com select ou group by.

Podemos conferir na Tabela 1 as descrições das funções das palavras-chaves, ou cláusulas, para expressões de consulta LINQ introduzidas na versão 3.0 da linguagem de programação C#.

Tabela 1. Palavras-chaves (cláusulas) de C# para consultas LINQ
Cláusula Descrição
from Especifica a fonte de dados e uma variável de série (similar a uma variável de iteração num laço).
where Filtra elementos da fonte de dados baseada em uma ou mais expressões booleanas.
select Faz projeções, permitindo especificar o tipo e o formato dos elementos do resultado da consulta. Frequentemente usada em conjunto com o recurso de tipos anônimos.
join Junta duas fontes de dados baseado em comparações de igualdade entre dois elementos de comparação especificados.
in

Palavra-chave contextual usada numa cláusula join.
on Palavra-chave contextual usada numa cláusula join.
equals Palavra-chave contextual usada numa cláusula join. Observe que se deve usar a palavra-chave equals ao invés do operador == na comparação da cláusula join.
group Agrupa os resultados de uma consulta de acordo com valores específicos de uma chave.
by Palavra-chave contextual usada numa cláusula group.
into Fornece um identificador para servir de referência para os resultados de uma cláusula de junção (join), agrupamento (group) ou projeção (select).
orderby Classifica os resultados em ordem ascendente ou descendente.
ascending Palavra-chave contextual usada numa cláusula orderby para determinar a classificação em ordem ascendente, que é a classificação padrão em caso de omissão.
descending Palavra-chave contextual usada numa cláusula orderby para determinar a classificação em ordem descendente.
let Introduz uma variável para armazenar resultados de expressões intermediárias numa expressão de consulta. Deste modo, o resultado armazenado na variável pode ser reutilizado na consulta.

Criação do projeto de demonstração

Este artigo tem como objetivo detalhar os recursos das expressões de consulta LINQ. Deste modo, para não perder o foco, escolheu-se desenvolver um ASP.NET Web Site (ASP.NET Web Forms) para utilizar as funcionalidades do controle Web de servidor GridView para apresentar os resultados das consultas de forma automática e com um mínimo de codificação. O controle GridView possui a propriedade booleana AutoGenerateColumns, com valor true por padrão, que indica quando as colunas são geradas automaticamente para cada campo da fonte de dados consumida. Internamente, o controle percorre todos os elementos da fonte de dados e utiliza reflexão para recuperar os nomes das suas propriedades/campos e seus valores. Deste modo, o controle pode ser usado para apresentar os resultados até mesmo de coleções de tipos anônimos gerados automaticamente em projeções.

No Visual Studio 2008, 2008 SP1 ou 2010, crie um novo Web Site vazio (ASP.NET Empty Web Site) em Visual C# numa pasta nomeada ExpressoesConsultaLinq. Adicione um novo item Web Form Default.aspx com o código C# num arquivo separado (code-behind). A página deve conter um controle DropDownList nomeado DropDownListConsulta e um controle GridView nomeado GridViewResultadoConsulta. A interface da página Default.aspx deve ser desenhada de modo similar à Figura 1.

Interface da página Default.aspx do Web Site
de demonstração
Figura 1. Interface da página Default.aspx do Web Site de demonstração

Selecione o controle DropDownList, defina o valor da propriedade AutoPostBack para true e acrescente um item em branco (sem texto e nem valor). Depois, dê um duplo clique no DropDownList para gerar um método de manipulação do evento SelectedIndexChanged, que é o evento padrão do controle. A Listagem 1 apresenta o modelo de código C# do arquivo Default.aspx.cs conforme será usado nos exemplos de expressões de consulta LINQ apresentados no decorrer do artigo. Observe que todos os rótulos da instrução switch terminam com a instrução break. Nas expressões de consulta LINQ mais complexas poderá haver mais instruções num rótulo "case :" do que as apresentadas neste modelo.

Listagem 1. Modelo de código C# do arquivo Default.aspx.cs

        public partial class _Default : System.Web.UI.Page
        {
        protected void Page_Load(object sender, EventArgs e)
        {

        }
        protected void DropDownListConsulta_SelectedIndexChanged(object sender, EventArgs e)
        {
        switch (DropDownListConsulta.SelectedIndex)
        {
        case 1:
        GridViewResultadoConsulta.DataSource =
        expressãoConsultaLinq
        break;
        case 2:
        GridViewResultadoConsulta.DataSource =
        expressãoConsultaLinq
        break;
        case 3:
        GridViewResultadoConsulta.DataSource =
        expressãoConsultaLinq
        break;
        ...
        default:
        GridViewResultadoConsulta.DataSource = null;
        break;
        }
        GridViewResultadoConsulta.DataBind();
        }
        }

Cláusulas from e select

A cláusula from é usada para especificar a fonte de dados e uma variável de série usada para referenciar cada elemento da fonte de dados.

A cláusula select é usada para fazer projeções, permitindo especificar o tipo e o formato dos elementos do resultado da consulta.

Acrescente um segundo item no DropDownListConsulta, após o item em branco, com o seguinte texto: "Consulta dos nomes completos dos arquivos na pasta C:\Windows\System32". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 1:" e atribua a expressão de consulta LINQ da Listagem 2 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 2. Consulta simples com as cláusulas from e select
from arquivo in Directory.GetFiles(@"C:\Windows\System32")
        select arquivo; 

O método estático GetFiles(string pasta) da classe System.IO.Directory retorna um array de strings com os nomes completos dos arquivos, incluindo os seus caminhos, na pasta especificada.

Observe, no resultado da consulta LINQ anterior, que são apresentados os nomes completos dos arquivos, incluindo o caminho. Por exemplo: "C:\Windows\System32\net.exe". Para apresentar somente o nome do arquivo, sem o caminho, e mais a informação da extensão, pode-se fazer uma projeção.

O recurso dos tipos anônimos, acrescentado na versão 3.0 da linguagem C#, permite fazer uma projeção do resultado de uma consulta LINQ sem a necessidade de se criar um novo tipo manualmente.

Acrescente um terceiro item no DropDownListConsulta com o seguinte texto: "Consulta dos nomes dos arquivos na pasta C:\Windows\System32 e de suas extensões". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 2:" e atribua a expressão de consulta LINQ da Listagem 3 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 3. Consulta com uso de tipo anônimo na cláusula select (projeção)
from arquivo in Directory.GetFiles(@"C:\Windows\System32")
        select new {
        NomeArquivo = Path.GetFileName(arquivo),
        Extensao = Path.GetExtension(arquivo)
        };

O método estático GetFileName(string arquivo) da classe System.IO.Path retorna o nome do arquivo com a extensão a partir do nome completo do arquivo, enquanto o método GetExtension(string arquivo) da mesma classe retorna a extensão do arquivo.

Cláusulas let, orderby, ascending e descending

A cláusula let permite introduzir uma variável para armazenar resultados de expressões intermediárias numa expressão de consulta. Deste modo, o resultado armazenado na variável pode ser reutilizado na consulta.

A cláusula orderby classifica os resultados em ordem ascendente, definido pelo uso opcional da cláusula ascending, ou descendente, definido pelo uso obrigatório da cláusula descending. As comparações entre os elementos são baseadas na implementação da interface System.Collections.Generic.IComparer para o tipo de dado dos elementos.

A cláusula orderby pode ser usada para classificar o resultado da consulta da Listagem 3 em ordem crescente de extensão (opcional o uso da cláusula ascending) e decrescente de nome de arquivo (obrigatório o uso da cláusula descending). A cláusula let pode ser usada para evitar a repetição das operações de extração do nome de arquivo e da extensão dos nomes completos dos arquivos.

Acrescente um quarto item no DropDownListConsulta com o seguinte texto: "Consulta dos nomes dos arquivos na pasta C:\Windows\System32 e de suas extensões com ordenação". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 3:" e atribua a expressão de consulta LINQ da Listagem 4 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 4. Consulta com uso de variáveis e ordenação
from arquivo in Directory.GetFiles(@"C:\Windows\System32")
        let nomeArquivo = Path.GetFileName(arquivo)
        let extensao = Path.GetExtension(arquivo).ToUpper()
        orderby extensao, nomeArquivo descending
        select new
        {
        NomeArquivo = nomeArquivo,
        Extensao = extensao
        };

Observe que foram criadas duas variáveis na consulta LINQ da Listagem 4 (nomeArquivo e extensao) porque os resultados das expressões foram usados na cláusula orderby para ordenação e na cláusula select para projeção. Observe também o uso do método ToUpper(), da classe System.String, para converter a extensão para letras maiúsculas, uma vez que há distinção entre letras maiúsculas e minúsculas na ordenação de strings.

Cláusula where

A cláusula where permite filtrar elementos da fonte de dados baseada em uma ou mais expressões booleanas separadas pelos operadores lógicos && (e) ou || (ou).

Por exemplo, vamos supor que desejamos obter uma relação com todos os arquivos executáveis da pasta C:\Windows\System32 com tamanho superior a 1 MB, classificados em ordem crescente do tamanho do arquivo.

Acrescente um quinto item no DropDownListConsulta com o seguinte texto: "Consulta dos arquivos executáveis na pasta C:\Windows\System32 com tamanho superior a 1 MB". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 4:" e atribua a expressão de consulta LINQ da Listagem 5 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta, que deve ser algo semelhante ao demonstrado na Figura 2.

Listagem 5. Consulta com duas condições de filtragem do resultado
from arquivo in Directory.GetFiles(@"C:\Windows\System32")
        let infoArquivo = new FileInfo(arquivo)
        let tamanhoArquivoMB = infoArquivo.Length / 1024M / 1024M
        where tamanhoArquivoMB > 1M &&
        infoArquivo.Extension.ToUpper() == ".EXE"
        orderby tamanhoArquivoMB
        select new
        {
        Nome = infoArquivo.Name,
        TamanhoMB = tamanhoArquivoMB
        };

A classe System.IO.FileInfo fornece informações sobre um arquivo no sistema de arquivos. Além disto, a classe contém métodos de instância para criar, copiar, excluir e abrir arquivos. O construtor FileInfo(string arquivo) inicializa uma instância da classe FileInfo como um invólucro para o arquivo. A propriedade Length obtém o tamanho do arquivo, em bytes, a propriedade Extension obtém a extensão do arquivo e a propriedade Name obtém o nome de arquivo do arquivo.

Resultado da consulta da Listagem 5 no browser
Google Chrome
Figura 2. Resultado da consulta da Listagem 5 no browser Google Chrome

Cláusulas group, by e into

A cláusula group agrupa os resultados de uma consulta de acordo com valores específicos de chaves. Ela é usada em conjunto com as palavras-chave by e into.

A cláusula group retorna uma sequência de objetos do tipo System.Linq.IGrouping, TElement>, que contém zero ou mais itens que correspondem ao valor da chave para o grupo. A Listagem 6 mostra a declaração da interface genérica IGrouping. O compilador deduz os tipos da chave e do elemento usando o recurso de variáveis locais tipadas implicitamente, que foi introduzido na versão 3.0 da linguagem C#.

Listagem 6. Interface System.Linq.IGrouping
public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>, IEnumerable
        {
        TKey Key { get; }
        }

Observe, na declaração da interface IGrouping, a definição de um único membro: a propriedade Key do tipo genérico TKey. Esta propriedade fornece acesso à chave de agrupamento após o uso da cláusula group numa expressão de consulta LINQ.

O LINQ fornece uma série de Standard Query Operators para executar operações de agregação em dados agrupados, assim como ocorrem com o SQL. A Tabela 2 apresenta um resumo das operações de agregação disponíveis, expostas por meio de métodos de extensão definidos nas classes estáticas System.Linq.Enumerable e System.Linq.Queryable.

Tabela 2. Standard Query Operators de operações de agregação
Método de extensão Descrição
Aggregate Realiza uma operação de agregação personalizada nos valores de uma coleção.
Average Calcula a média aritmética simples dos valores de uma coleção.
Count Conta os elementos de uma coleção. Opcionalmente, pode ser usada para contar somente os elementos que satisfazem uma determinada condição (predicado).
LongCount Conta os elementos de uma grande coleção, que ultrapassa o limite de armazenamento do tipo int. Opcionalmente, pode ser usada para contar somente os elementos que satisfazem uma determinada condição (predicado).
Max Determina o valor máximo em uma coleção.
Min Determina o valor mínimo em uma coleção.
Sum Calcula a soma dos valores em uma coleção.

Vamos supor que desejamos obter uma relação com informações agregadas por extensão dos arquivos na pasta C:\Windows\System32.

Acrescente um sexto item no DropDownListConsulta com o seguinte texto: "Consulta de informações agregadas por extensão dos arquivos na pasta C:\Windows\System32". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 5:" e atribua a expressão de consulta LINQ da Listagem 7 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 7. Consulta com uso de agrupamento de dados
from arquivo in Directory.GetFiles(@"C:\Windows\System32")
        let infoArquivo = new FileInfo(arquivo)
        group infoArquivo by infoArquivo.Extension.ToUpper() into g
        let extensao = g.Key
        orderby extensao
        select new
        {
        Extensao = extensao,
        NumeroArquivos = g.Count(),
        TamanhoTotalArquivosKB = g.Sum(fi => fi.Length) / 1024M,
        TamanhoMedioArquivosKB = g.Average(fi => fi.Length) / 1024D,
        TamanhoMenorArquivoKB = g.Min(fi => fi.Length) / 1024M,
        TamanhoMaiorArquivoKB = g.Max(fi => fi.Length) / 1024M
        };

Observe a linha com a cláusula group na consulta da Listagem 7. Entre a cláusula group e a palavra-chave by deve-se colocar os objetos que serão agrupados, que neste caso são objetos do tipo System.IO.FileInfo. Entre as palavras-chaves by e into deve-se colocar a chave de agrupamento, que neste caso são as extensões dos arquivos em letras maiúsculas. Finalmente, após a palavra-chave into deve-se colocar um identificador para receber os dados agrupados, que neste caso foi definido como “g”. Observe que as operações de agregação na cláusula select foram feitas por meio de métodos de extensão aplicados nos agrupamentos representados pela variável “g . O resultado da consulta é apresentado na Figura 3.

Browser
Apple Safari
Figura 3. Resultado da consulta da Listagem 7 no browser Apple Safari

Agora, vamos supor que exista a necessidade de se gerar um relatório bem mais complexo com informações agregadas de todos os arquivos abaixo de uma dada pasta. Por exemplo, a partir de uma pasta base é necessário gerar um relatório com informações de todos os arquivos na própria pasta base e em todos os níveis de subpastas. Este relatório deve estar agrupado por pasta e por extensão, portanto trata-se de agrupamento com chave composta, ao invés de um agrupamento com chave simples como na Listagem 7. Para cada conjunto de pasta e de extensão, se devem fornecer as informações da quantidade e do tamanho total dos arquivos, em KB, em cada pasta e de cada extensão. Os dados devem estar classificados em ordem crescente de nome das pastas e em ordem decrescente do tamanho total dos arquivos.

Na classe _Default do arquivo Default.aspx.cs, crie um novo método estático privado nomeado obterTodosArquivosDotNet4() com o código apresentado na Listagem 8. Este método retorna os nomes completos de todos os arquivos presentes na pasta do .NET Framework 4.0.

Listagem 8. Método que retorna os arquivos na pasta do .NET Framework 4.0 e em todas subpastas
private static string[] obterTodosArquivosDotNet4()
        {
        return
        Directory.GetFiles(
        @"C:\Windows\Microsoft.NET\Framework\v4.0.30319",
        "*",
        SearchOption.AllDirectories
        );
        }
        

O método estático GetFiles(string pasta, string padraoBusca, SearchOption opcao) da classe System.IO.Directory retorna um array de strings com os nomes completos dos arquivos, incluindo os seus caminhos, na pasta definida, satisfazendo o padrão de busca especificado e de acordo com a opção selecionada (SearchOption.TopDirectoryOnly: arquivos somente na pasta especificada ou SearchOption.AllDirectories: arquivos na pasta especificada e em todas as suas subpastas).

Acrescente um sétimo item no DropDownListConsulta com o seguinte texto: "Consulta de informações agregadas por pasta e extensão dos arquivos na do .NET Framework 4.0". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 6:" e atribua a expressão de consulta LINQ da Listagem 9 à propriedade DataSource do controle GridViewResultadoConsulta. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 9. Consulta com uso de agrupamento com chave composta
from arquivo in obterTodosArquivosDotNet4()
        let infoArquivo = new FileInfo(arquivo)
        group infoArquivo
        by new
        {
        Pasta = infoArquivo.DirectoryName,
        Extensao = infoArquivo.Extension.ToUpper()
        }
        into infoArquivosPorPastaExtensao
        let tamanhoKB =
        infoArquivosPorPastaExtensao.Sum(ia => ia.Length) / 1024M
        orderby infoArquivosPorPastaExtensao.Key.Pasta,
        tamanhoKB descending
        select new
        {
        infoArquivosPorPastaExtensao.Key.Pasta,
        infoArquivosPorPastaExtensao.Key.Extensao,
        NumeroArquivos = infoArquivosPorPastaExtensao.Count(),
        TamanhoKB = tamanhoKB
        };

Observe que para se realizar um agrupamento com uma chave composta pode-se definir um tipo anônimo após a palavra-chave “by” com propriedades definidas para cada integrante da chave.

Em bancos de dados relacionais, o SQL permite criar apelidos para expressões na lista da cláusula SELECT e estes apelidos podem ser usados nas cláusulas ORDER BY. Porém, por uma limitação da linguagem, os apelidos não podem ser usados na cláusula GROUP BY. Neste último caso, somos obrigados a copiar uma expressão para usá-la novamente na lista de agrupamento, ao invés de usar somente o apelido. Certamente, a introdução de uma cláusula LET no SQL, similar à usada nas expressões de consulta LINQ, seria muito útil.

Cláusulas join, in, on e equals

A cláusula join junta duas fontes de dados baseada em comparações de igualdade entre dois critérios de comparação especificados. A comparação de igualdade é feita com a palavra-chave equals, ao invés do operador de igualdade (==).

Para fechar com chave de ouro, vamos acrescentar uma complexidade a mais na consulta da Listagem 9. Deve-se acrescentar mais uma informação na projeção final da consulta: a porcentagem do tamanho ocupado pelos arquivos de uma dada extensão em uma dada pasta em relação ao tamanho total de todos os arquivos nesta pasta, independente da extensão.

Uma possível solução para a consulta proposta no parágrafo anterior é por meio da geração de outra consulta auxiliar que posteriormente pode ser juntada à consulta da Listagem 9 com uso da cláusula join.

Acrescente um oitavo item no DropDownListConsulta com o seguinte texto: "Consulta de informações dos arquivos na pasta do .NET Framework 4.0, incluindo porcentagem". Na instrução switch do arquivo Default.aspx.cs, acrescente um rótulo "case 7:". Neste rótulo, declare uma variável local nomeada arquivosDotNet4, do tipo array de strings, e atribua a ela o valor de retorno do método estático obterTodosArquivosDotNet4(), como mostra a Listagem 10. Isto porque este mesmo array será utilizado em duas consultas LINQ.

Listagem 10. Declaração de uma variável local recebendo todos os arquivos do .NET Framework 4.0
string[] arquivosDotNet4 = obterTodosArquivosDotNet4();

Após a declaração da variável local da Listagem 10, declare outra variável local nomeada tamanhosTotaisPorPasta, indicando uma dedução implícita de tipo com a palavra-chave var, e atribua a ela a expressão de consulta LINQ da Listagem 11. Esta consulta traz todas as pastas com os tamanhos totais dos arquivos que cada uma delas contém diretamente.

Listagem 11. Consulta com uso de agrupamento com chave simples
from arquivo in arquivosDotNet4
        let infoArquivo = new FileInfo(arquivo)
        group infoArquivo by infoArquivo.DirectoryName into g
        select new
        {
        Pasta = g.Key,
        TamanhoTotalKB = g.Sum(ia => ia.Length) / 1024M
        };

Finalmente, após a consulta LINQ anterior, atribua a expressão de consulta LINQ da Listagem 12 à propriedade DataSource do controle GridViewResultadoConsulta. Esta consulta consiste em uma modificação daquela apresentada na Listagem 9 para apresentação de uma coluna adicional com a porcentagem. Os trechos novos ou modificados estão destacados em negrito. Execute a aplicação Web e veja o resultado desta consulta.

Listagem 12. Consulta com uso de agrupamento com chave composta e junção
from arquivo in arquivosDotNet4
        let infoArquivo = new FileInfo(arquivo)
        group infoArquivo
        by new
        {
        Pasta = infoArquivo.DirectoryName,
        Extensao = infoArquivo.Extension.ToUpper()
        }
        into infoArquivosPorPastaExtensao
        join tamanhoTotalPorPasta in tamanhosTotaisPorPasta
        on infoArquivosPorPastaExtensao.Key.Pasta equals tamanhoTotalPorPasta.Pasta
        into juncaoComTamanhoTotalKB
        let tamanhoTotalKB = juncaoComTamanhoTotalKB.Single().TamanhoTotalKB
        let tamanhoKB = infoArquivosPorPastaExtensao.Sum(ia => ia.Length) / 1024M
        orderby infoArquivosPorPastaExtensao.Key.Pasta,
        tamanhoKB descending
        select new
        {
        infoArquivosPorPastaExtensao.Key.Pasta,
        infoArquivosPorPastaExtensao.Key.Extensao,
        NumeroArquivos = infoArquivosPorPastaExtensao.Count(),
        TamanhoKB = tamanhoKB,
        Porcentagem = 100 * (tamanhoKB / tamanhoTotalKB)
        };

Na Listagem 12, entre a cláusula “join” e a palavra-chave “in” há um identificador (tamanhoTotalPorPasta) que representa cada elemento da fonte de dados (tamanhosTotaisPorPasta). Em seguida, a palavra-chave “on” identifica a condição de junção, com uso obrigatório da palavra-chave equals para realizar a comparação de igualdade. Observe que a junção é realizada pela comparação das pastas em ambas as coleções. Finalmente, o resultado da junção é armazenado na variável cujo identificador é definido após a palavra-chave into (juncaoComTamanhoTotalKB). Neste caso, a junção sempre retorna um único elemento correspondente da fonte de dados tamanhosTotaisPorPasta. Porém, o resultado sempre é armazenado numa coleção, uma vez que a comparação poderia trazer vários elementos. Sendo assim, foi usado o Standard Query Operator Single() para extrair o elemento único e acessar a sua propriedade com o tamanho total em KB (tamanhoTotalKB).

Conclusão

O acréscimo do recurso LINQ à linguagem C# permite ao desenvolvedor fazer consultas complexas de uma forma bem mais simples, permitindo que ele foque no resultado desejado sem se preocupar em como os dados estão sendo extraídos nos bastidores. O recurso permite usar um raciocínio similar ao fornecido pela linguagem SQL, em bancos de dados relacionais, para fazer consultas em fontes de dados variadas diretamente na linguagem C#, a partir da versão 3.0.

As demonstrações realizadas no decorrer do artigo foram todas relacionadas com informações do sistema de arquivos obtidas de classes do namespace System.IO. Diversas destas ideias podem ser usadas para se criar uma aplicação Web de monitoramento do espaço utilizado em servidores de arquivos, que pode ser extremamente útil no gerenciamento de recursos de infraestrutura numa empresa. Neste caso, o LINQ to Objects permite a geração de relatórios complexos com a simplicidade fornecida pelas expressões de consultas LINQ.

Links Úteis
Saiba mais sobre LINQ ;)