Como sabemos, a Microsoft incluiu nas linguagens .NET a capacidade de se escrever queries para interrogar coleções, de forma bem semelhante ao que fazemos quando desejamos trabalhar com alguma base de dados. Esse recurso ganhou o nome de LINQ, e várias funcionalidades e mudanças essas linguagens sofreram para conseguir acomodá-lo.
Para manipular um conjunto de informações, temos que ser capazes de executar duas tarefas: escrever/executar a query e iterar pelo resultado, caso ele exista. Parte disso já é suportado pelo .NET, através da interface IEnumerable<T>, que fornece um enumerador (representado pela interface IEnumerator<T>) para iterar por alguma coleção. A parte faltante (escrita) foi implementada também como uma interface e adicionada ao .NET Framework. Para isso, passamos a ter a interface IQueryable e IQueryable<T>. Com esses interfaces, somos capazes de expressar uma query através de uma AST (Abstract Syntax Tree), e mais tarde, traduzí-la para alguma fonte de dados específica.
Hoje temos os providers: LINQ To SQL, LINQ To XML e LINQ To Entities (Entity Framework). Cada um desses providers traduzem a query para uma fonte de dados específica, tais como: SQL Server (T-SQL), XML (XPath), ou até mesmo, Oracle (PL-SQL). A interface IQueryable<T> herda diretamente de IEnumerable<T>, para assim agregar a funcionalidade de iteração do resultado. A principal propriedade que ela expõe é a Expression, que retorna a instância da classe Expression, utilizada como raiz da expression tree. A imagem abaixo ilustra a hierarquia entre elas:
Existem duas classes dentro do assembly System.Core.dll, debaixo do namespace System.Linq, e que se chamam: Enumerable e Queryable. São duas classes estáticas que, via extension methods, agregam funcionalidades aos tipos IEnumerable<T> e IQueryable<T>, respectivamente. Grande parte dessas funcionalidades (métodos), refletem operadores para manipulação da query, tais como: Where, Sum, Max, Min, etc. A principal diferença entre essas duas classes é que enquanto a classe Enumerable recebe delegates como parâmetro, a classe Queryable recebe a instância da classe Expression (que envolve um delegate), que será utilizada para montar a query, e mais tarde, ser traduzida e executada pelo provider. Abaixo temos a assinatura do método Where para as duas classes:
namespace System.Data.Linq
{
public static class Enumerable
{
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source, Func<TSource, bool> predicate)
{
//…
}
}
public static class Queryable
{
public static IQueryable<TSource> Where<TSource>(
this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate)
{
//…
}
}
}
Ambas interfaces postergam a execução da query até o momento que você precisa realmente iterar pelo resultado. Da mesma forma, como IQueryable<T> herda de IEnumerable<T>, você pode definí-la como retorno de uma função, e mais tarde, iterar pelo resultado. Só que utilizar uma ou outra, tem um comportamento bastante diferente, que pode prejudicar a performance. Para exemplificar, vamos considerar um método que retorna as categorias da base de dados, e depois disso, vamos aplicar um filtro para retornar somente aquelas categorias cujo Id seja maior que 2. Com isso, escrevemos o seguinte código:
class Program
{
private static DBTestesEntities ctx = new DBTestesEntities();
static void Main(string[] args)
{
var query = RecuperarCategorias().Where(c => c.Id > 2);
foreach (var item in query)
Console.WriteLine(item.Descricao);
}
static IEnumerable<Dados> RecuperarCategorias()
{
return from c in ctx.Categorias select c;
}
}
Repare que depois de executar o método RecurarCategorias, invocamos o método de estensão Where, e neste caso, fornecido pela classe Enumerable, já que o retorno da função é do tipo IEnumerable<T>. Ao compilar a aplicação, o compilador faz o parse da query LINQ e a transforma em dados, fazendo com que a query seja representada por objetos do tipo Expression. Abaixo temos o código decompilado, e podemos perceber que a expressão lambda que passamos para o método Where, está sendo utilizada para filtrar o resultado.
internal class Program
{
private static DBTestesEntities ctx = new DBTestesEntities();
private static void Main(string[] args)
{
IEnumerable<Categoria> query = RecuperarCategorias().Where<Categoria>(delegate (Categoria c) {
return c.Id > 2;
});
foreach (Categoria item in query)
Console.WriteLine(item.Descricao);
}
private static IEnumerable<Categoria> RecuperarCategorias()
{
ParameterExpression CS$0$0001;
return ctx.Categorias.Select<Categoria, Categoria>(
Expression.Lambda<Func<Categoria, Categoria>>(CS$0$0001 = Expression.Parameter(typeof(Categoria), “c”),
new ParameterExpression[] { CS$0$0001 }));
}
}
O grande detalhe aqui é que as estensões para a interface IEnumerable<T>, recebem delegates como parâmetro, o que quer dizer que o filtro será aplicado do lado cliente, ou seja, ao invocar o método RecuperarCategorias, todas elas (categorias) serão retornadas, carregadas em memória, e depois aplicado o filtro, “descartando” aqueles elementos que não se enquadram no critério. Abaixo temos a query que chegou até o SQL Server:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Descricao] AS [Descricao]
FROM [dbo].[Categorias] AS [Extent1]
Agora, se o retorno da função RecuperarCategorias for do tipo IQueryable<Categoria>, o comportamento é totalmente diferente. O simples fato de alterar o tipo de retorno, é o suficiente para a compilação ser substancialmente diferente. No código abaixo temos esse código para ilustrar. Note que depois de invocar o método RecuperarCategorias, já temos o método Where na sequência. A diferença é que o método Where aqui é oriundo da classe Queryable, que ao invés de receber um delegate, recebe uma Expression, obrigando o compilador a reescrever o nosso código, transformando o filtro “c => c.Id > 2” em uma instância da classe Expression, expressando aquele mesmo critério.
internal class Program
{
private static DBTestesEntities ctx = new DBTestesEntities();
private static void Main(string[] args)
{
ParameterExpression CS$0$0000;
IQueryable<Categoria> query = RecuperarCategorias().Where<Categoria>(
Expression.Lambda<Func<Categoria, bool>>(
Expression.GreaterThan(Expression.Property(CS$0$0000 = Expression.Parameter(typeof(Categoria), “c”),
(MethodInfo) methodof(Categoria.get_Id)), Expression.Constant(2, typeof(int))),
new ParameterExpression[] { CS$0$0000 }));
foreach (Categoria item in query)
Console.WriteLine(item.Descricao);
}
private static IQueryable<Categoria> RecuperarCategorias()
{
ParameterExpression CS$0$0001;
return ctx.Categorias.Select<Categoria, Categoria>(
Expression.Lambda<Func<Categoria, Categoria>>(CS$0$0001 = Expression.Parameter(typeof(Categoria), “c”),
new ParameterExpression[] { CS$0$0001 }));
}
}
Apesar da ligeira mudança em tempo de compilação, isso permite o processamento remoto da query. Utilizar IQueryable<T> neste cenário, irá possibilitar o build-up de queries, ou seja, a medida que você vai adicionando critérios, operadores, etc., ele irá compondo a query principal e, finalmente, quando precisar iterar pelo resultado, apenas uma única query será disparada no respectivo banco de dados, já fazendo todo o filtro por lá, retornando menos dados. Isso pode ser comprovado visualizando a query que foi encaminhada para o banco de dados depois da alteração acima:
SELECT
[Extent1].[Id] AS [Id],
[Extent1].[Descricao] AS [Descricao]
FROM [dbo].[Categorias] AS [Extent1]
WHERE [Extent1].[Id] > 2
Excelente post Israel.
Abraços.
Excelente post.
Continue contribuindo para a comunidade!
Abraço