Otimização da Junção de Expressões em ORMs: Lidando com NewExpression

A necessidade de registrar descobertas técnicas durante o desenvolvimento é algo que muitos profissionais sentem, mas nem sempre praticam. Frequentemente, a complexidade dos proejtos e a urgência em resolver problemas levam ao adiamento da documentação, que acaba caindo no esquecimento. Este artigo visa abordar uma questão específica encontrrada no desenvolvimento de frameworks ORM em .NET: a junção de expressões do tipo NewExpression.

Descrição do Cenário

Em cenários de atualização parcial de dados, onde o frontend envia apenas um subconjunto de campos, é comum a necessidade de preencher automaticamente metadados como o usuário que realizou a alteração e a data/hora da modificação. Considere uma tabela de agendamentos.

Inicialmente, temos uma expressão que define os campos a serem enviados pelo frontend (Expressão A):


var expA = ExpressionHelper.CreateExpression<AppointmentDto>(a => new {
   a.Id,
   a.PatientName,
   a.PatientNamePy,
   a.IdentityCardNumber,
   a.Birthday,
   a.PatientAge,
   a.PatientSex,
   a.PatientPhone,
   a.Address
});
 

No momento de persistir os dados, precisamos adicionar campos como LastUpdateUserId e UpdateTime. Isso resulta em uma segunda expressão (Expressão B):


var expB = ExpressionHelper.CreateExpression<AppointmentDto>(a => new {
   a.Id,
   a.PatientName,
   a.PatientNamePy,
   a.IdentityCardNumber,
   a.Birthday,
   a.PatientAge,
   a.PatientSex,
   a.PatientPhone,
   a.Address,
   a.LastUpdateUserId,
   a.UpdateTime
});
 

O método ExpressionHelper.CreateExpression<T> é um auxiliar para encurtar a sintaxe, retornando a expressão lambda recebida. A utilização de var pode simplificar a declaração inicial, mas o autor adverte sobre a perda de legibilidade e manutenibilidade em projetos maiores sem documentação adequada, onde a inferência de tipo pode ocultar a intenção original.

Comparando as Expressões A e B, notamos uma grande quantidade de código duplicado. A proliferação de expressões desse tipo pode inflar consideravelmente o tamanho das classes. A busca é por uma forma de combinar expressões, de modo que possamos derivar a Expressão B a partir da Expressão A, adicionando os campos extras de forma concisa. Algo como var expB = expA.Add(a => a.LastUpdateUserId);.

Solução Proposta

Para atender a essa necessidade específica dentro de um ORM customizado, foi criada uma nova classe de expressão, NewObjectExpression, para gerenciar a junção de NewExpression.


using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

/// <summary>
/// Expressão para construção de novos objetos, permitindo a combinação de membros.
/// </summary>
public class NewObjectExpression : Expression, IArgumentProvider
{
   private IList<Expression> _arguments;

   internal NewObjectExpression(ConstructorInfo constructor, IList<Expression> arguments, List<MemberInfo> members)
   {
       Constructor = constructor ?? throw new ArgumentNullException(nameof(constructor));
       _arguments = arguments ?? new List<Expression>();
       Members = members ?? new List<MemberInfo>();

       UpdateExpressionString();
   }

   public override Type Type => Constructor.DeclaringType;
   public sealed override ExpressionType NodeType => ExpressionType.New;

   public ConstructorInfo Constructor { get; }

   public ReadOnlyCollection<Expression> Arguments => new ReadOnlyCollection<Expression>(_arguments);

   Expression IArgumentProvider.GetArgument(int index) => _arguments[index];
   int IArgumentProvider.ArgumentCount => _arguments.Count;

   /// <summary>
   /// Representação em string dos membros incluídos.
   /// </summary>
   public string ExpressionString { get; private set; } = "";

   public List<MemberInfo> Members { get; private set; }

   /// <summary>
   /// Atualiza os membros e a representação em string da expressão.
   /// </summary>
   /// <param name="newArguments">Novos argumentos.</param>
   /// <param name="newMembers">Novos membros.</param>
   /// <returns>A instância atualizada de NewObjectExpression.</returns>
   public NewObjectExpression Update(IEnumerable<Expression> newArguments, IEnumerable<MemberInfo> newMembers)
   {
       _arguments = newArguments?.ToList() ?? _arguments;
       Members = newMembers?.ToList() ?? Members;
       UpdateExpressionString();
       return this;
   }

   private void UpdateExpressionString()
   {
       ExpressionString = Members != null && Members.Any()
           ? string.Join(",", Members.Select(m => m.Name))
           : "";
   }
}
 

Os membros a serem tratados são armazenados na propriedade Members. Métodos auxiliares foram copiados da implementação original de NewExpression.

Em seguida, uma extensão para Expression<Func<T, object>> foi criada para adicionar (Add) e remover (Remove) membros. As funções And e Or para Expression<Func<T, bool>> são mencionadas, mas sua explicação foi adiada.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

public static class ExpressionExtensions
{
   /// <summary>
   /// Combina duas expressões do tipo NewExpression, adicionando novos membros.
   /// </summary>
   /// <typeparam name="T">O tipo do parâmetro da expressão.</typeparam>
   /// <param name="sourceExpr">A expressão original.</param>
   /// <param name="expandExpr">A expressão com os membros a serem adicionados.</param>
   /// <returns>Uma nova expressão combinada.</returns>
   public static Expression<Func<T, object>> Add<T>(this Expression<Func<T, object>> sourceExpr, Expression<Func<T, object>> expandExpr)
   {
       var parameter = Expression.Parameter(typeof(T), "p");
       var combinedMembers = new Dictionary<string, MemberInfo>();

       // Processa a expressão original
       ProcessExpressionBody(sourceExpr.Body, parameter, combinedMembers);

       // Processa a expressão de expansão
       ProcessExpressionBody(expandExpr.Body, parameter, combinedMembers, true);

       // Cria a nova expressão NewObjectExpression
       var constructor = typeof(object).GetConstructor(Array.Empty<Type>()); // Construtor genérico
       var newObjectExp = new NewObjectExpression(constructor, null, combinedMembers.Values.ToList());

       return Expression.Lambda<Func<T, object>>(newObjectExp, parameter);
   }

   /// <summary>
   /// Combina duas expressões do tipo NewExpression, removendo membros especificados.
   /// </summary>
   /// <typeparam name="T">O tipo do parâmetro da expressão.</typeparam>
   /// <param name="sourceExpr">A expressão original.</param>
   /// <param name="removeExpr">A expressão com os membros a serem removidos.</param>
   /// <returns>Uma nova expressão com os membros removidos.</returns>
   public static Expression<Func<T, object>> Remove<T>(this Expression<Func<T, object>> sourceExpr, Expression<Func<T, object>> removeExpr)
   {
       var parameter = Expression.Parameter(typeof(T), "p");
       var currentMembers = new Dictionary<string, MemberInfo>();
       var membersToRemove = new HashSet<string>();

       // Processa a expressão original
       ProcessExpressionBody(sourceExpr.Body, parameter, currentMembers);

       // Processa a expressão de remoção
       ProcessExpressionBody(removeExpr.Body, parameter, membersToRemove, true, true);

       // Filtra os membros
       var finalMembers = currentMembers.Where(kvp => !membersToRemove.Contains(kvp.Key)).Select(kvp => kvp.Value).ToList();

       if (!finalMembers.Any())
       {
           throw new InvalidOperationException("A remoção de membros resultou em uma expressão vazia.");
       }

       var constructor = typeof(object).GetConstructor(Array.Empty<Type>());
       var newObjectExp = new NewObjectExpression(constructor, null, finalMembers);

       return Expression.Lambda<Func<T, object>>(newObjectExp, parameter);
   }

   private static void ProcessExpressionBody<T>(Expression body, ParameterExpression parameter, Dictionary<string, MemberInfo> memberMap, bool addToList = false, bool isRemoval = false)
   {
       if (body is NewExpression newExpr)
       {
           foreach (var member in newExpr.Members ?? Enumerable.Empty<MemberInfo>())
           {
               if (addToList) memberMap[member.Name] = member;
               else if (!isRemoval) memberMap[member.Name] = member;
           }
       }
       else if (body is NewObjectExpression newObjectExp)
       {
           foreach (var member in newObjectExp.Members ?? Enumerable.Empty<MemberInfo>())
           {
                if (addToList) memberMap[member.Name] = member;
                else if (!isRemoval) memberMap[member.Name] = member;
           }
       }
       else if (body is UnaryExpression unaryExpr && unaryExpr.Operand is MemberExpression memberExpr)
       {
           if (addToList) memberMap[memberExpr.Member.Name] = memberExpr.Member;
           else if (!isRemoval) memberMap[memberExpr.Member.Name] = memberExpr.Member;
       }
       // Adicionar mais casos conforme necessário (ex: MemberExpression direto)
   }
    private static void ProcessExpressionBody<T>(Expression body, ParameterExpression parameter, HashSet<string> removalSet, bool addToList = false, bool isRemoval = false)
   {
       if (body is NewExpression newExpr)
       {
           foreach (var member in newExpr.Members ?? Enumerable.Empty<MemberInfo>())
           {
               if (isRemoval) removalSet.Add(member.Name);
           }
       }
       else if (body is NewObjectExpression newObjectExp)
       {
           foreach (var member in newObjectExp.Members ?? Enumerable.Empty<MemberInfo>())
           {
               if (isRemoval) removalSet.Add(member.Name);
           }
       }
       else if (body is UnaryExpression unaryExpr && unaryExpr.Operand is MemberExpression memberExpr)
       {
           if (isRemoval) removalSet.Add(memberExpr.Member.Name);
       }
   }

   /// <summary>
   /// Combina duas expressões booleanas usando o operador AND.
   /// </summary>
   public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
   {
       var invokedExpr = Expression.Invoke(right, left.Parameters.Cast<Expression>());
       return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body, invokedExpr), left.Parameters);
   }

   /// <summary>
   /// Combina duas expressões booleanas usando o operador OR.
   /// </summary>
   public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
   {
       var invokedExpr = Expression.Invoke(right, left.Parameters.Cast<Expression>());
       return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body, invokedExpr), left.Parameters);
   }
}
 

Com essas extensões, a atualização de dados pode ser realizada de forma mais elegante:


// Supondo que 'exp' seja a expressão original (Expressão A)
// Atualiza apenas o campo LastUpdateUserId
Dbc.Db.Update(dto, exp.Add(a => a.LastUpdateUserId));

// Atualiza múltiplos campos adicionais
Dbc.Db.Update(dto, exp.Add(a => new { a.LastUpdateUserId, a.UpdateTime }));
 

Internamente, o ORM precisa de um método para interpretar a NewObjectExpression e extrair os nomes das propriedades.


using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

public static class ExpressionHelper
{
   /// <summary>
   /// Extrai os nomes das propriedades de uma expressão lambda.
   /// </summary>
   /// <typeparam name="T">O tipo do parâmetro da expressão.</typeparam>
   /// <param name="expr">A expressão lambda a ser analisada.</param>
   /// <returns>Uma lista com os nomes das propriedades.</returns>
   /// <exception cref="NotSupportedException">Se o tipo do corpo da expressão não for suportado.</exception>
   public static List<string> GetPropertyNames<T>(Expression<Func<T, object>> expr)
   {
       var propertyNames = new List<string>();

       if (expr.Body is NewExpression newExp)
       {
           // Expressão do tipo t => new { t.Prop1, t.Prop2 }
           propertyNames.AddRange(newExp.Members.Select(member => member.Name));
       }
       else if (expr.Body is NewObjectExpression customNewExp)
       {
           // Expressão customizada NewObjectExpression
           propertyNames.AddRange(customNewExp.Members.Select(member => member.Name));
       }
       else if (expr.Body is UnaryExpression unaryExp && unaryExp.Operand is MemberExpression memberExp)
       {
           // Expressão do tipo t => t.Prop1
           propertyNames.Add(memberExp.Member.Name);
       }
       else
       {
           throw new NotSupportedException($"Tipo de corpo de expressão não suportado: {expr.Body.GetType().Name}");
       }

       return propertyNames;
   }

   /// <summary>
   /// Método auxiliar para simplificar a declaração de expressões lambda.
   /// </summary>
   public static Expression<Func<T, object>> CreateExpression<T>(Expression<Func<T, object>> expr)
   {
       return expr;
   }
}
 

Dessa forma, as extensões Add e Remove para Expression<Func<T, object>> foram implementadas, permitindo um código mais conciso no ORM.

Considerações Finais

Antes de chegar à solução com NewObjectExpression, outras abordagens foram exploradas. A tentativa de manipular diretamente NewExpression ou criar classes que herdam dele enfrentou restrições devido às validações internas do .NET e à natureza anônima das expressões. A criação de uma classe personalizada como NewObjectExpression oferece uma alternativa flexível, embora inicialmente restrita ao framework interno.

O autor convida a comunidade a compartilhar métodos alternativos ou aprimoramentos para esta solução.

Tags: C# .NET ORM Expression Trees linq

Publicado em 7-6 09:39