C# - Expression Trees


 Neste artigo vamos recordar o que é e como funcionam as Expression Trees no C#
 
As Expression Trees são uma parte poderosa e avançada da linguagem C# que permite representar e manipular expressões lambda em um formato de árvore.

As Expression Trees ou Árvores de Expressão, são recurso exclusivo do C#, que fornecem uma maneira de representar o código como uma estrutura de dados. Em alto nível, eles permitem que os desenvolvedores tratem o código como dados, permitindo a geração dinâmica e a manipulação do código durante o tempo de execução. Esse recurso é especialmente útil em cenários como a criação de consultas LINQ dinâmicas ou a criação de delegados compilados em tempo de execução.

Como funcionam as Expression Trees

Em vez de compilar uma expressão lambda diretamente em código executável, as Expression Trees representam a estrutura lógica da expressão em uma forma de árvore. Cada nó na árvore representa uma operação ou parte da expressão.

Essa representação permite que você manipule e analise as expressões em tempo de execução. Você pode criar, modificar e até mesmo combinar diferentes expressões para formar novas expressões.

Este recurso é amplamente utilizadas no contexto de LINQ (Language Integrated Query), onde são usadas para traduzir expressões lambda em consultas SQL ou outras formas de consulta específicas do provedor de dados.

Exemplo básico:  Vamos criar um exemplo onde utilizaremos Expression Trees para construir uma expressão que representa a soma de dois números.

Para isso vamos criar um projeto Console no .NET 8 chamado Cshp_ExpTrees usando o recurso Top Level Statement.

using System.Linq.Expressions;

Console.WriteLine("\nUsando Expression Trees");
Console.WriteLine(
"Para representar a soma de dois números #\n");
Console.WriteLine(
"a e b com valor 5 e 10\n");

ParameterExpression paramA = Expression.Parameter(typeof(int), "a");
ParameterExpression paramB = Expression.Parameter(
typeof(int), "b");

// Corpo da expressão: a + b
BinaryExpression body = Expression.Add(paramA, paramB);

// Criando a expressão lambda
Expression<Func<
int, int, int>> soma = Expression.Lambda<Func<int, int, int>>(body, paramA, paramB);

// Compilando e executando a expressão
Func<
int, int, int> functionSoma = soma.Compile();

int resultado = functionSoma(5, 10);

// Exibindo o resultado
Console.WriteLine(
"Resultado da soma: " + resultado);
 

  • Criamos dois parâmetros paramA e paramB que representam os operandos da soma.
  • Criamos uma expressão binária body que representa a operação de adição entre paramA e paramB.
  • Usamos Expression.Lambda para criar uma expressão lambda que aceita dois inteiros e retorna um inteiro.
  • Compilamos a expressão lambda em uma função utilizando Compile.
  • Finalmente, executamos a função com os valores 5 e 10 e exibimos o resultado.

O básico

As expressões lambdas no C# são uma forma conscisa de representar métodos anônimos. Exemplo:

 
Func<int, int, int> add = (a, b) => a + b;

Porém quando desejamo inspecionar ou manipular a própria expressão lambda usamos árvores de expressão , assim:

 
Expression<Func<int, int, int>> addExpression = (a, b) => a + b;

A principal diferença é  o invólucro Expression<> que informa ao compilador para tratar o lambda com um dados e não como código executável.

Usando esta lógica podemos criar árvore de expressões manualmente usando os métodos Factory da classe conforme o exemplo a seguir:

var paramA = Expression.Parameter(typeof(int), "a");
var
paramB = Expression.Parameter(typeof(int), "b");

var body = Expression.Add(paramA, paramB);

var addExpression = Expression.Lambda<Func<int, int, int>>(body, paramA, paramB);

var compiled = addExpression.Compile();

Console.WriteLine(compiled(1, 2));
 

Neste exemplo criamos uma árvore de expressão fizemoa a compilação par aum delegate e executamos.

Exemplos práticos

Vamos criar um exemplo prático usando Expression Trees e LINQ para construir uma consulta dinâmica que filtra uma lista de objetos com base em critérios específicos.

Neste exemplo, consideraremos uma classe Pessoa com propriedades como Nome, Idade e Cidade. Vamos criar uma consulta LINQ que filtra pessoas com base em critérios dinâmicos.

Em um projeto console - Cshp_ExpTrees - vamos criar uma classe Pessoa :

class Pessoa
{
  public string? Nome { get; set; }
 
public int Idade { get; set; }
 
public string? Cidade { get; set; }
}

A seguir na classe Program vamos definir o código abaixo:

// Lista de pessoas
using
Cshp_ExpTrees;
using
System.Linq.Expressions;

List<Pessoa> pessoas = new List<Pessoa>
{
   new Pessoa { Nome = "Alice", Idade = 25, Cidade = "São Paulo" },
  
new Pessoa { Nome = "Maria", Idade = 30, Cidade = "Rio de Janeiro" },
  
new Pessoa { Nome = "Carlos", Idade = 22, Cidade = "Campinas" },
  
new Pessoa { Nome = "Amanda", Idade = 22, Cidade = "Santos" },
  
new Pessoa { Nome = "Silvia", Idade = 22, Cidade = "Salvador" }
};

Console.WriteLine("Clientes\n");

foreach
(var pessoa in pessoas)
    Console.WriteLine(
$"{pessoa.Nome} \t {pessoa.Cidade}");

// Critérios de filtragem (dinâmicos)
var
criterioFiltro = BuildFilter<Pessoa>("Cidade", "São Paulo");

// Aplicando a consulta LINQ com Expression Trees
var
pessoasFiltradas = pessoas.AsQueryable().Where(criterioFiltro).ToList();

// Exibindo os resultados
Console.WriteLine(
"\nCliente(s) filtrado(s):");

foreach (var pessoa in pessoasFiltradas)
{
   Console.WriteLine(
$"Nome: {pessoa.Nome}, Idade: {pessoa.Idade}, Cidade: {pessoa.Cidade}");
}

Console.ReadLine();

static Expression<Func<T, bool>> BuildFilter<T>(string propertyName, object value)
{
  
var parameter = Expression.Parameter(typeof(T), "x");
  
var property = Expression.Property(parameter, propertyName);
  
var constant = Expression.Constant(value);
  
var equal = Expression.Equal(property, constant);
  
return Expression.Lambda<Func<T, bool>>(equal, parameter);
}

Entendendo o código:

  1. Criamos uma classe Pessoa que representa pessoas com propriedades como Nome, Idade e Cdade.
  2. Criamos uma lista de pessoas (pessoas) com alguns dados de exemplo.
  3. Utilizamos um método chamado BuildFilter para construir dinamicamente uma Expression Tree que representa o critério de filtro. Este método aceita o nome da propriedade e o valor desejado como parâmetros.
  4. Aplicamos a Expression Tree gerada na consulta LINQ usando Where.
  5. Exibimos os resultados filtrados.

Executando teremos o resultado abaixo:

Além disso podemos usar classe ExpressionVisitor que fornece uma maneira de percorrer e modificar uma árvore de expressão. Ele usa o padrão Visitor, onde um método separado é chamado para cada tipo de nó na árvore.

Aqui está um exemplo simples de ExpressionVisitor que substitui todas as instâncias de um parâmetro específico por uma expressão constante:

using System.Linq.Expressions;

Expression<Func<int, int>> expr = x => x * 100;
Console.WriteLine(
"Expressão original : x * 100 ");

var replacer = new ParameterReplacer(expr.Parameters[0], Expression.Constant(100));
var
novaExpressao = replacer.Visit(expr.Body) as BinaryExpression;

Console.WriteLine("\nExpressão com parâmetro substituido");
Console.WriteLine(novaExpressao);
// saida: (100 * 100)

Console.ReadLine();

public
class ParameterReplacer : ExpressionVisitor
{
  private readonly ParameterExpression _oldParameter;
 
private readonly Expression _replacement;
 
public ParameterReplacer(ParameterExpression oldParameter, Expression replacement)
  {
     _oldParameter = oldParameter;
     _replacement = replacement;
  }

   protected override Expression VisitParameter(ParameterExpression node)
   {
    
if (node == _oldParameter)
        
return _replacement;
    
    
return base.VisitParameter(node);
    }
}

Resultado:

As árvores de expressão são uma ferramenta poderosa no arsenal do desenvolvedor C#, permitindo a geração e manipulação dinâmica de código. Ao compreender e aproveitar seus recursos, você poderá criar aplicativos mais flexíveis e dinâmicos.

Pegue o código do projeto aqui:  Cshp_ExpTrees.zip

"(Disse Jesus) Quem crê no Filho de Deus, em si mesmo tem o testemunho; quem a Deus não crê mentiroso o fez, porquanto não creu no testemunho que Deus de seu Filho deu"
1 João 5:10

Referências:


José Carlos Macoratti