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.