C# - Entendendo IQueryable


Neste artigo vamos entender o que é e como funciona a interface IQueryable em consultas LINQ.

O primeiro ponto importante a destacar é que a interface IQueryable herda de IEnumerable, de forma que tudo que IEnumerable pode fazer, IQueryable também pode.

Acontece que quando usamos consultas em um banco de dados desejamos obter a informação estritamente necessária no menor número de round-trips possíveis e ainda tirar vantagem dos recursos do provedor do banco de dados.

Pensando nisso a LINQ introduziu os seguintes conceitos:

Assim, o LINQ fornece um conjunto de métodos padrão, definidos na classe System.Linq.Queryable. Todos esses métodos estendem IQueryable. Todos eles compartilham nomes idênticos e sintaxe quase idêntica com suas contrapartes no namespace. System.Linq.Enumerable.

Focando apenas na interface IQueryable temos que ela fornece a funcionalidade para avaliar as consultas em uma fonte de dados específica no qual o tipo de dado não foi especificado.

Vamos a seguir ver na prática como IQueryable funciona.

Criando o projeto no VS 2019 com EF Core

Para conhecer o comportamento de IQueryable vamos criar um projeto console do tipo .NET Core e incluir uma referência ao Entity Framework Core 3.14 no projeto.

Vamos incluir também uma referência ao pacote Microsoft.EntityFrameworkCore.Tools para realizar o migrations.

A seguir vamos criar duas entidades no projeto :  Genero e Pessoa e aplicar o Migrations para criar o banco de dados e as tabelas.

    class Genero
    {
        public int GeneroId { get; set; }
        public string Sexo { get; set; }
    }
    class Pessoa
    {
        public int Id { get; set; }
        public string Nome { get; set; }
        public string Sobrenome { get; set; }
        public DateTime Nascimento { get; set; }
        public virtual Genero Genero { get; set; }
    }

Para isso precisamos definir a string de conexão com o banco de dados no arquivo de contexto que iremos definir a seguir:

using Microsoft.EntityFrameworkCore;
namespace CShp_IQueryable1.Model
{
    class AppDbContext : DbContext
    {
        public DbSet<Pessoa> Pessoas { get; set; }
        public DbSet<Genero> Generos { get; set; }
        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Data Source=Macoratti;Initial Catalog=TesteDB;Integrated Security=True");
        }
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Pessoa>()
                  .HasKey(e => e.Id);
        }
    }
}

Agora veja esta consulta:

using CShp_IQueryable1.Model;
using System;
using System.Linq;
using static System.Console;
namespace CShp_IQueryable1
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var contexto = new AppDbContext())
            {
                var resultado = contexto.Pessoas.ToList();
                foreach (var p in resultado)
                {
                    Console.WriteLine($"{p.Nome} - {p.Sobrenome} - {p.Nascimento}");
                }
                ReadLine();
            }
        }
    }
}

No código:

 var resultado = contexto.Pessoas.ToList();

resultado é do tipo List<Pessoa> obtem todos os dados do banco de dados na tabela Pessoas.

Aqui ToList() é quem força o acesso ao banco de dados visto que contexto.Pessoas não faz nada.

Na verdade contexto.Pessoas é um DbSet<Pessoa> o qual por sua vez é um IQueryable<Pessoa>.

A inteface IQueryable<T> se parece com qualquer coleção (IEnumerable) por fora, mas age de maneira muito diferente.

Ao usar IQueryable, uma consulta é criada para a fonte de dados, neste caso o SQL Server sendo que a consulta é executada apenas quando o IQueryable for enumerado, ou seja, quando ToList ou foreach for chamado.

Isso permite que você crie consultas usando Where, OrderBy ou Select sem nunca ter que acessar o banco de dados. Se tivéssemos que acessar o banco de dados várias vezes, você pode imaginar como isso ia ficar lento.

Vamos criar uma consulta mais avançada:

    using (var contexto = new AppDbContext())
    {
                // Não acessa o banco de dados ainda...
                IQueryable<Pessoa> resultado = contexto.Pessoas.Include(g=> g.Genero)
                                                  .Where(p => p.Genero.GeneroId == 2)
                                                  .OrderBy(p => p.Nome);                
                // Agora sim, acessa o banco de dados
                List<Pessoa> mulheres = resultado.ToList();
                // Exibe o resultado
                foreach(var m in mulheres)
                {
                    Console.WriteLine($"{m.Nome} - {m.Sobrenome}");
                }
                ReadLine();
     }

Aqui montamos a consulta usando IQueryable mas não acessamos o banco de dados. Isso somente será feito quando aplicarmos o ToList();  e percorremos o resultado exibindo no console.

Essa abordagem permite criar consultas baseadas em uma condição:

            using (var contexto = new AppDbContext())
            {
                bool resultado = false;
                //Não acessa o banco de dados ainda...
                IQueryable<Pessoa> consulta = contexto.Pessoas.Include(g=> g.Genero)
                                                                   .Where(p => p.Genero.GeneroId == 2);
                if (resultado)
                {
                    consulta = consulta.OrderBy(p => p.Sobrenome);
                }                
                // Agora acessa o banco de dados
                List<Pessoa> mulheres = consulta.ToList();
                // Exibe o resultado
                foreach (var m in mulheres)
                {
                    Console.WriteLine($"{m.Nome} - {m.Sobrenome}");
                }
                ReadLine();
            }

Agora podemos criar consultas diferentes estabelecendo condições para atender nossas necessidades onde estamos reutilizando partes das  nossas consultas.

Podemos também parametrizar a consulta:

           using (var contexto = new AppDbContext())
            {
                // Não parametrizada
                var res1 = contexto.Pessoas.Where(p => p.Genero.GeneroId == 2);
                int feminino = 2;
                // Consulta parametrizada não usa o plano anterior
                var res2 = contexto.Pessoas.Where(p => p.Genero.GeneroId == feminino).ToList();
                
                int masculino = 1;
                // Consulta parametrizada usa o plano anterior
                var res3 = contexto.Pessoas.Where(p => p.Genero.GeneroId == masculino).ToList();
            }

Vamos agora criar um método de extensão para IQueryable que trabalha com datas com o nome MaisVelhoQue onde vamos comparar duas datas e retornar os valores que satisfaçam o critério definido.

Para isso criamos uma pasta Extensions no projeto e nesta pasta definimos a classe Extensions abaixo :

using CShp_IQueryable1.Model;
using Microsoft.EntityFrameworkCore;
using System;
using System.Linq;
namespace CShp_IQueryable1.Extensions
{
    public static class Extensions
    {
        public static IQueryable<Pessoa> MaisVelhoQue(this IQueryable<Pessoa> q, int idade)
        {
            return q
              .Where(p => (EF.Functions.DateDiffYear(p.Nascimento, DateTime.Today)) > idade);
        }
    }
}

Aqui estamos criando o método de extensão Extensions que atua em IQueryable e usa o recurso EF.DBFunctions do EF Core que fornece métodos CLR que são traduzidos para funções de banco de dados quando usados em consultas LINQ to Entities. Os métodos nesta classe são acessados via Funções.

Agora no método Main() podemos acessar essa consulta em nosso projeto:

    using (var contexto = new AppDbContext())
    {                
                    var pessoas = contexto.Pessoas.MaisVelhoQue(28).ToList();
                    foreach(var p in pessoas)
                    {
                       Console.WriteLine($"{p.Nome} - {p.Nascimento}");
                    }
                    Console.ReadLine();
    }

Aqui estamos usando o método de extensão MaisVelhoQue onde iremos retornar as pessoas com idade maior que 28 anos.

Pegue o projeto completo aqui:   CShp_IQueryable1.zip

"Muitas são, Senhor meu Deus, as maravilhas que tens operado para conosco, e os teus pensamentos não se podem contar diante de ti; se eu os quisera anunciar, e deles falar, são mais do que se podem contar."
Salmos 40:5

Referências:


José Carlos Macoratti