Consulta genérica com expressão Lambda

Renato Haddad

Dn807151.060DE5057573180CEC6D227C6D3E2207(pt-br,MSDN.10).png

Setembro, 2014

Quando pensamos em qualquer tipo de consulta de dados, logo pensamos em como montar os critérios das pesquisas. E se a fonte de dados for um array, uma coleção, um json, um xml ou ainda, um banco de dados, qual sintaxe utilizar? Diante deste cenário comum em aplicações, resolvi compartilhar com todos os leitores o conhecimento de C# envolvendo expressão Lambda para realizar um código genérico. Portanto, o objetivo deste artigo é criar apenas uma linha de código que sirva para a maioria das consultas com critérios dinâmicos.

Os pré-requisitos para este artigo são o Visual Studio .NET 2013 Update 2, conhecimento do Entity Framework e da linguagem C# avançada.

Projeto de Console

Para focar na linguagem C# nada melhor que criar um projeto de Console Application, assim mostro desde o início. Portanto, abra o VS 2013 e crie um projeto deste tipo em C#. Em seguida, abra o Nuget (Package Manager Console) e instale o Entity Framework através da seguinte linha de comando:

PM> Install-Package EntityFramework

Isto fará com que o VS pesquise a versão mais recente e instale no projeto com todas as referências necessárias. O resultado é apresentado a seguir, ou seja, instalado com sucesso.

Installing 'EntityFramework 6.1.1'.

You are downloading EntityFramework from Microsoft, the license agreement to which is available at http://go.microsoft.com/fwlink/?LinkID=320539. Check the package for additional dependencies, which may come with their own license agreement(s). Your use of the package and dependencies constitutes your acceptance of their license agreements. If you do not accept the license agreement(s), then delete the relevant components from your device.

Successfully installed 'EntityFramework 6.1.1'.

Adding 'EntityFramework 6.1.1' to ArtigoConsultaGenericaLambda.

Successfully added 'EntityFramework 6.1.1' to ArtigoConsultaGenericaLambda.

Agora precisamos de uma fonte de dados, e nada mais fácil que usar o famoso banco de dados Northwind do SQL Server (download em codeplex.com). Adicione um novo item do tipo Data / ADO.NET Entity Data Model chamado ModeloNwind, conforme a figura 1.

Dn807151.6BC2D732FDC5A58C47ED4AE5381F1D97(pt-br,MSDN.10).png

Figura 1 – Novo Data Model

Clique no botão Add e aparecerá uma nova janela com diversas opções, algumas novas se você tiver instalado o Update 2 do VS 2013. Selecione “EF Designer from database”, conforme a figura 2. Isto fará com que o modelo de dados seja criado a partir de um banco existente.

Dn807151.0255597B77367B4C8230C1BDF2C8B897(pt-br,MSDN.10).png

Figura 2 – Modelo a partir do banco de dados

Clique no botão Next, aponte a conexão para o seu servidor e o banco de dados Northwind. Na lista de entidades, selecione Categories, Products e Employees. Ao final, terá um modelo com as 3 entidades, conforme a figura 3, as quais servirão de base para as consultas.

Dn807151.5A321692593F170582398E83B307D9F6(pt-br,MSDN.10).png

Figura 3 – Modelo de dados

Definição da Interface da Classe

A seguir, crie uma interface chamada IPesquisas para generalizarmos a implementação da classe. Toda interface representa um contrato da classe, e como quero padronizar qualquer implementação ou customização de uma classe de pesquisa, resolvi criar esta interface. Veja no código a seguir a lista de usings necessários. Na declaração da interface, informe o parâmetro chamado <TEntity> e atribua que o mesmo trata-se de uma classe (where TEntity : class).

Esta interface contém apenas um método Get declarado, porém muito importante. Antes de mais nada, observe que o retorno do método será uma IQueryable de TEntity, ou seja, você retorna uma lista tipada de TEntity, onde futuramente é possível aplicar filtros, ordenações, etc.

Agora, veja o parâmetro do Get, o qual é uma Func que recebe TEntity e dispara um predicate (delegate), o qual avalia cada condição passada dinamicamente, assim como cada um dos registros. Toda Func obrigatoriamente tem um retorno, e neste caso é um booleano, ou seja, você aplica critérios a serem avaliados, e a cada registro o retorno será verdadeiro ou falso. Se verdadeiro, o mesmo é incluído automaticamente na lista de itens do IQueryable tipado. Caso contrário, é descartado.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ArtigoConsultaGenericaLambda
{
    // interface declara TEntity onde TEntity é uma classe
    // o qual será ex: Produtos, Clientes
    interface IPesquisas<TEntity> where TEntity : class
    {
        // IQueryable de TEntity é o retorno no método Get, o qual será
        // aplicado um filtro dinâmico no predicate
        // p => p.Preco > 40
        // c= > c.NomeCliente.Contains("a")
        IQueryable<TEntity> Get(Func<TEntity, bool> predicate);
    }
}

Para que você possa entender os bastidores do Func é preciso conhecimento de C# avançado e do conceito de predicate. Primeiro, tenha em mente que predicate será um delegate criado dinamicamente em tempo de execução. Em seguida, o delegate terá critério(s) a ser(em) avaliado(s) pelo Func de acordo com a condição dinâmica declarada na expressão Lambda. E, ao final, o Func sempre retorna algo booleano, atendendo ou não aos critérios.

Confesso que lendo o parágrafo anterior dá vontade de abandonar o entendimento, mas no momento em que formos consumir isto, você ficará mais tranquilo, espero eu!

Veja que nos comentários da interface eu já coloquei duas expressões Lambda para você começar a absorver o entendimento.

Pergunta: Renatão, você disse que o delegate será dinâmico, então o método não existe fisicamente? Exatamente isto, tudo é dinâmico criado em tempo de execução. Costumo brincar que o compilador chama o desenvolvedor indiano para criar esta parte dinâmica, e é isto mesmo. Fisicamente você não tem absolutamente nada, não existe um método chamado GetClientes(critério), GetProdutos(critério), cada um com critérios específicos, ou seja, o indiano criará de acordo com a condição passada.

Implementação da Classe Genérica

Uma vez definida a interface, crie uma classe chamada PesquisaGenerica, conforme a listagem a seguir. Note que o parâmetro é o <TEntity>, que herda de IDisposable para que esta classe possa ter o Dispose(), e implementa a interface IPesquisas com o <TEntity> declarado como uma classe. Pense da seguinte forma: desejo fazer uma pesquisa na entidade Produto, a qual implementa a interface de Produto, e posso aplicar qualquer critério que tenha na entidade Produto através de uma expressão Lambda. É isto que esta classe faz.

Quando você escrever o “IPesquisas<TEntity> where TEntity : class”, este ficará destacado pelo Visual Studio, informando que o mesmo precisa ser implementado, ou seja, deixe o cursor no texto IPesquisas e dê um CTRL + . (ponto). Isto irá implementar automaticamente o método Get.

Em seguida, precisamos informar que o modelo de dados está contido no NORTHWNDEntities, então, a variável ctx contém todo o contexto, ou seja, o DbContext que faz as referências do DbSet para as 3 entidades do modelo. Isto foi criado automaticamente pelo modelo, você nem precisa se preocupar, só não erre no nome do contexto NORTHWNDEntities. Na dúvida, pesquise qual classe herda DbContext.

Vale destacar que a interface apenas declara o contrato do método Get, mas quem implementa o código em si é esta classe PesquisaGenerica. Observe o conteúdo do método Get, o qual contém apenas uma linha 100% dinâmica. O Set faz referência a entidade TEntity, passada em tempo de execução (Produto, Categoria, etc). O critério é aplicado ao método de extensão Where através de uma expressão Lambda declarada no predicate. E, após a execução a lista é convertida para AsQueryable porque o retorno é do tipo IQueryable.

Existe ainda o método Dispose somente para dar o Dispose() no contexto ctx.

using System;
using System.Linq;

namespace ArtigoConsultaGenericaLambda
{
    // TEntity é uma classe (Where)
    public class PesquisaGenerica<TEntity> : IDisposable,
        IPesquisas<TEntity> where TEntity : class
    {
        NORTHWNDEntities ctx = new NORTHWNDEntities();

        // Func recebe a entidade/classe (ex produto)
        // a ser usada na pesquisa, portanto, é dinâmica
        public IQueryable<TEntity> Get(Func<TEntity, bool> predicate)
        {
            //TEntity = é uma classe, ex Produtos, Clientes
            // predicate = é a expressão de filtro
            // p => p.Preco > 10
            // AsQueryable = converte para uma lista consultável
            // .Set<> referencia a entidade dinamicamente
            return ctx.Set<TEntity>().Where(predicate).AsQueryable();
        }

        public void Dispose()
        {
            ctx.Dispose();
        }

    }
}

Consumo da Consulta Genérica

Agora vem o melhor de tudo, consumir esta classe genérica que criamos de forma totalmente dinâmica. Com certeza você irá compreender melhor os códigos agora, pois estamos sempre acostumados apenas consumir códigos prontos.

Abra o Program.cs e vamos aos exemplos. O primeiro código deverá retornar todos os produtos da entidade Products, sendo que o critério são todos os produtos que não estão descontinuados, ou seja, em estoque. Note que a variável pg instancia o PesquisaGenerica<Products>, ou seja, é tipado, Products é o nome da classe que representa a entidade Products no banco de dados. Já o critério é aplicado quando se invoca o método Get, através da expressão Lambda (p => !p.Discontinued). Em seguida é feito um looping com o foreach para mostrar todos os produtos que atendem a esta condição.

Console.WriteLine("consulta de produtos");
var pg = new PesquisaGenerica<Products>();
var dados = pg.Get(p => !p.Discontinued);
foreach (var item in dados)
{
    Console.WriteLine("ID {0} {1} preço: {2:n2} estoque: {3:n0}",
        item.ProductID, item.ProductName, 
        item.UnitPrice, item.UnitsInStock);
}

Console.ReadLine();

Agora, que tal escrever este código anterior desta forma usando o ForEach com uma Action:

var pg = new PesquisaGenerica<Products>();
pg.Get(p => !p.Discontinued).ToList()
.ForEach(x =>
{
    Console.WriteLine("ID {0} {1} preço: {2:n2} estoque: {3:n0}",
        x.ProductID, x.ProductName,
        x.UnitPrice, x.UnitsInStock);
});

Outro exemplo é aplicar um critério para duas propriedades na expressão Lambda:

pg.Get(p => p.UnitsInStock >=80 && p.UnitPrice <= 20).ToList()
.ForEach(x =>
{
    Console.WriteLine("ID {0} {1} preço: {2:n2} estoque: {3:n0}",
        x.ProductID, x.ProductName,
        x.UnitPrice, x.UnitsInStock);
});

Veja este outro exemplo onde aplico ao Get duas condições, no entanto, crio com o Select dois tipos anônimos (descricao e valorEstoque), uso o OrderBy para classificar pelo tipo anônimo criado descrição que representa o nome do produto. Veja o resultado na figura 4.

pg.Get(p => p.UnitsInStock >= 80 && p.UnitPrice <= 20)
    .Select(p => new {
            descricao = p.ProductName,
            valorEstoque = p.UnitPrice * p.UnitsInStock
            })
    .OrderBy(p => p.descricao)
    .ToList()
.ForEach(x =>
{
    Console.WriteLine("{0} - valor estoque: {1:n2}",
        x.descricao, x.valorEstoque );
});

Dn807151.8C788AC686CD8B79641796F0DEB12C64(pt-br,MSDN.10).png

Figura 4 – Resultado do tipo anônimo

Veja um exemplo trocando para a entidade Categories, o qual retorna a quantidade de categorias cadastradas que contém a letra A. Esta consulta genérica permite aplicar qualquer método de extensão de agregação que você quiser.

var pg_cat = new PesquisaGenerica<Categories>();
Console.WriteLine("Exitem {0} categorias cadastradas que contém a letra A",
    pg_cat.Get(c => c.CategoryName.Contains("a")).Count());
Console.ReadLine();

E agora, um exemplo usando a entidade Empregados que moram na cidade de London:

var pg_emp = new PesquisaGenerica<Employees>();
pg_emp.Get(e => e.City == "London").ToList()
    .ForEach(x => 
    Console.WriteLine("{0} | telefone: {1} | endereço: {2}",
    x.FirstName, x.HomePhone, x.Address)
    );

No caso de consultas com critérios de relacionamentos entre entidades, se você fez o trabalho de casa corretamente no banco de dados, o modelo de dados irá expressar o relacionamento pai e filho de forma que seja possível a navegação entre entidades através das propriedades de vínculo. E, no Entity Framework, se você declarar a propriedade de vínculo como virtual, você terá total acesso ao relacionamento, ou seja, consegue navegar entre as entidades. Veja um exemplo de pesquisa de produtos que pertencem a categoria chamada “Beverages”, sendo que o texto está na entidade Categories, e não na entidade Products.

pg.Get(p => p.Categories.CategoryName == "Beverages").ToList()
.ForEach(x =>
{
    Console.WriteLine("{0} | {1} | preço: {2:n2} | estoque: {3:n0}",
        x.ProductName, x.Categories.CategoryName,
        x.UnitPrice, x.UnitsInStock);
});

O resultado está na figura 4, veja que mostrei o nome da categoria filtrada para termos certeza do retorno da pesquisa.

Dn807151.5DED82BC1FCFD218E900848DFECD7B6D(pt-br,MSDN.10).png

Figura 4 – Resultado da pesquisa

Caso esteja curioso para saber o que acontece nos bastidores, coloque um breakpoint no Get e execute o código. Quando a execução para no return do Get, abra a janela de Locals e veja que o delegate é criado em tempo de execução, conforme a figura 5.

Dn807151.068819F1576504AB26DB727F3D734CBB(pt-br,MSDN.10).png

Figura 5 – Execução com breakpoint

Conclusão

Escrever códigos dinâmicos é uma tarefa que pode mudar completamente a forma de programar, organizar a estrutura da aplicação, ter produtividade, ter controle sobre a execução, e principalmente padronizar a execução. Por isto que sempre incentivo o estudo e o aprendizado da sua linguagem, não se contente em apenas escrever códigos repetitivos ou similares, procure saber os bastidores, e ao final a recompensa é gratificante.

Sugiro fortemente que você leia este meu artigo http://msdn.microsoft.com/pt-br/library/dn630213.aspx sobre Repositório de Dados dinâmico, onde mostro uma aplicação prática deste conceito dinâmico.

Agradeço a oportunidade de poder compartilhar o conhecimento deste artigo. Qualquer dúvida e preparação de times de desenvolvimento, por favor me contate.

Sobre o Autor

Renato Haddad (rehaddad@msn.comwww.renatohaddad.com ) é MVP, MCT, MCPD e MCTS, palestrante em eventos da Microsoft em diversos países, ministra treinamentos focados em produtividade com o VS.NET 2012/2013, ASP.NET 4/5, ASP.NET MVC, Entity Framework, Reporting Services, Windows Phone e Windows 8. Visite o blog http://weblogs.asp.net/renatohaddad.

Mostrar: