Executando verificação de segurança...
1

Usando C# Expression Trees para criar uma consulta personalizada no banco de dados com EF Core

Usar o EF Core como ORM nos livra de ficar escrevendo e dando manutenção em querys SQL escritas diretamente no código fonte e da preocupação de traduzir os resultados das querys para as entidades dos nossos projetos. Porém, algumas vezes temos problemas em traduzir uma LINQ query que fazemos em um DBSet/Repository para a query SQL esperada, resultando em um erro de tradução como:

System.InvalidOperationException: The LINQ expression '...' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync

E realmente, existem operações que são impossíveis de serem traduzidas pelo EF Core, mas o motivo do erro pode ser apenas uma má estruturação da arvore de expressões.

Expression Trees

Uma Expression Trees é a representação de uma série de comandos ligados numa estrutura parecida com a de uma árvore. Ela precisa ser construída de baixo para cima e seus nodos são representados pela classe Expression no C#. Para clarificar, este é um exemplo de como podemos construir uma simples expressão lambda usando uma Expression Tree:

Lambda:

Func<int, bool> MenorQueCinco = n => n < 5;

Construindo Expression:

using System.Linq.Expressions;

var parametro = Expression.Parameter(typeof(int));

Expression<Func<int, bool>> MenorQueCincoExpression = 
    Expression.Lambda<Func<int, bool>>(
        Expression.LessThan(
            parametro,
            Expression.Constant(5)
        ), parametro
    );

var MenorQueCinco = MenorQueCincoExpression.Compile();

Por fim, essa seria a representação visual da arvore:

image

Saber disso é importante pois, quando executamos algo do tipo:

var list = await _context.Entity.Where(x => x.Property == "value").ToListAsync();

O EF Core utiliza a estrutura da Expression Tree que representa o filtro "x => x.Property == "value"" para trabalhar como um compilador e gerar o código SQL adequado conforme vai passando nodo por nodo da arvore.

Exemplificando com um caso real de uso

O problema

Eu estava trabalhando com este tipo de query:

var result = await _context.Entity.Where(x => list.Contains(x.Property)).ToListAsync();

Que o EF Core traduz para:

SELECT * FROM ENTITY WHERE PROPERTY IN ( ... )

Porém, o client do Oracle tem um limite de 1000 registros que podem ser filtrados no IN, que quando for ultrapassado resulta nesse tipo de erro:

OracleRelationalCommand.ExecuteReaderAsync() :  Oracle.ManagedDataAccess.Client.OracleException (0x80004005): ORA-01795: o número máximo de expressões em uma lista é de 1000

Uma possível solução é fazer o split da lista em listas de no máximo 1000 itens. Criando uma função para fazer esse split, ficaria mais ou menos assim:

var splitedList = SplitList(filter, 1000);
var result = new List<Entity>();
foreach (var list in splitedList)
    result.AddRange(await _context.Entity.Where(x => list.Contains(x.Property)).ToListAsync());

Dessa forma resolvemos o problema inicial. No entanto, caso seja necessário executar esse tipo query em outras partes do código para diferentes tabelas e filtrando por diferentes colunas, teremos que repetir essa mesma lógica com essas 5 linhas de código sempre. Portanto, a criação de um método genérico para esse tipo de consulta poderia melhorar o código.

Uma tentativa inicial criando um Extension Method ficaria dessa forma:

public async static Task<List<T>> SelectInChunksAsync<T, U>(this IQueryable<T> repository, List<U> listFilter, Func<T, U> selector)
{
    var splitedList = SplitList(listFilter, 1000);
    var result = new List<T>();
    foreach (var list in splitedList)
        result.AddRange(await repository.Where(x => list.Contains(selector(x))).ToListAsync());
    return result;
}

Note que o parâmetro selector é um lambda que retorna o valor da propriedade que eu quero filtrar. Executando esse método numa lista que já está na memória funciona normalmente. Porém, quando executamos ele num DbSet temos aquele erro de tradução:

System.InvalidOperationException: The LINQ expression '...' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to 'AsEnumerable', 'AsAsyncEnumerable', 'ToList', or 'ToListAsync

Isso acontece porque o lambda selector não consegue ser traduzido dessa forma, sendo inserido diretamente na Expression que é passada dentro do Where()

Logo, uma possível solução é construir a Expression que vai dentro do Where() do zero, usando a classe Expression, e construindo os parâmetros e operações na ordem correta, de forma legível para o EF Core:

public async static Task<List<T>> SelectInChunksAsync<T, U>(this IQueryable<T> repository, List<U> listFilter, Expression<Func<T, U>> selector)
{
    var splitedList = SplitList(listFilter, 1000);
    var result = new List<T>();
    foreach(var list in splitedList) 
        result.AddRange(await repository.SelectInAsync(list, selector));
    return result;
}

private async static Task<List<T>> SelectInAsync<T, U>(this IQueryable<T> repository, List<U> listFilter, Expression<Func<T, U>> selector)
{
    // Expressao constante pra lista de filtro
    var listaConstante = Expression.Constant(listFilter, listFilter.GetType());

    // parametro da Entidade na query
    var parameter = Expression.Parameter(typeof(T));

    // Expressao para o propriedade indicada no selector
    var memberExpression = (MemberExpression)selector.Body;
    var accessor = Expression.MakeMemberAccess(parameter, memberExpression.Member);

    // chamando o contains
    var callContains = Expression.Call(typeof(Enumerable), "Contains", new Type[] { accessor.Type }, listaConstante, accessor);
    var predicate = Expression.Lambda<Func<T, bool>>(callContains, parameter);

    return await repository.Where(predicate).ToListAsync();
}

Note que que agora estamos instanciando o parâmetros selector como Expression<Func<T, U>>, o que nos permite manipulá-lo como uma Expression Tree. Além disso, adicionamos o método SelectInAsync, que ficou responsável por construir a Expression Tree e executar e query no banco. Note também que trabalhamos com diversos tipos de Expressions, Constant, Parameter, Member, MethodCall (Contains), cada uma com a sua função na cadeia de comandos.

Debugando o código e analisando as variáveis, o valor do filtro final predicate ficará dessa forma:

{Param_0 => value(System.Collections.Generic.List`1[Type]).Contains(Param_0.Property)}

Portanto, agora temos uma forma genérica de fazer uma busca em partes, fazendo esse request:

var result = await _context.DbSet.SelectInChunksAsync(filterList, x => x.ColumnToFilter);

Representando isso no SQL, é como se tivéssemos diferentes listas de valores para filtrar no mesmo select, dessa forma:

SELECT * FROM TABLE WHERE COLUMN IN ( ... )
UNION ALL 
SELECT * FROM TABLE WHERE COLUMN IN ( ... )
UNION ALL 
SELECT * FROM TABLE WHERE COLUMN IN ( ... )
...

Eu acredito que ter esse conhecimento sobre Expression Trees é uma carta na manga muito legal. Deixo aqui as referências que eu usei para escrever esse conteúdo:

Carregando publicação patrocinada...