Um pouco de programa educacional
Eu realmente gosto do Automapper, especialmente seus QueryableExtensions e o método ProjectTo <> . Em resumo, esse método permite a projeção de tipos diretamente na consulta SQL. Permitiu receber dto realmente de um banco de dados. I.e. não é necessário obter todas as entidades do banco de dados, carregá-las na memória, use o Automapper.Map<>
, o que levou a um grande consumo e tráfego de memória.
Tipo de projeção
Para obter uma projeção no linq, você precisava escrever algo como isto:
from user in dbContext.Users where user.IsActive select new { Name = user.Name, Status = user.IsConnected ? "Connected" : "Disconnected" }
Usando QueryableExtensions, esse código pode ser substituído pelo seguinte (é claro, desde que as regras de conversão User -> UserInfo já estejam descritas)
dbContext.Users.Where(x => x.IsActive).ProjectTo<UserInfo>();
Enum e problemas com ele
A projeção tem uma desvantagem que precisa ser considerada. Esta é uma restrição nas operações executadas. Nem tudo pode ser traduzido em uma consulta SQL . Em particular, não é possível obter informações por tipo de enumeração. Por exemplo, existe o seguinte Enum
public enum FooEnum { [Display(Name = "")] Any, [Display(Name = "")] Open, [Display(Name = "")] Closed }
Existe uma entidade na qual uma propriedade do tipo FooEnum é declarada. No dto, você não precisa obter o próprio Enum, mas o valor da propriedade Name do atributo DisplayAttribute. Perceber isso através da projeção não funciona, porque obter o valor do atributo requer Reflexão, sobre a qual o SQL simplesmente “não sabe nada”.
Como resultado, você precisa usar o Map<>
habitual Map<>
, carregar todas as entidades na memória ou iniciar uma tabela adicional com valores de Enum e chaves estrangeiras.
Existe uma solução - Expressões
Mas "haverá um golpe na velha". Afinal, todos os valores de Enum são conhecidos antecipadamente. O SQL possui uma implementação de switch
que você pode inserir ao formar uma projeção. Resta entender como fazer isso. HashTag: "Árvores de expressão - nosso tudo".
O Automapper, ao projetar tipos, pode converter expressão em uma expressão que, após o Entity Framework, converte na consulta SQL correspondente.
À primeira vista, a sintaxe para criar árvores de expressão em tempo de execução é extremamente inconveniente. Mas depois de alguns pequenos problemas resolvidos, tudo se torna óbvio. Para resolver o problema com o Enum, você precisa criar uma árvore aninhada de expressões condicionais que retornam valores, dependendo dos dados de origem. Algo assim
IF enum=Any THEN RETURN "" ELSE IF enum=Open THEN RETURN "" ELSE enum=Closed THEN RETURN "" ELSE RETURN ""
Decida a assinatura do método.
public class FooEntity { public int Id { get; set; } public FooEnum Enum { get; set; } } public class FooDto { public int Id { get; set; } public string Name { get; set; } } // Automapper CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression())); private Expression<Func<FooEntity, string>> GetExpression() { }
O método GetExpression()
deve gerar uma expressão que recebe uma instância de FooEntity e retorna uma representação de seqüência de caracteres para a propriedade Enum
.
Primeiro, defina o parâmetro de entrada e obtenha o próprio valor da propriedade
ParameterExpression value = Expression.Parameter(typeof(FooEntity), "x"); var propertyExpression = Expression.Property(value, "Enum");
Em vez da string do nome da propriedade, você pode usar o nameof(FooEntity.Enum)
compilador nameof(FooEntity.Enum)
ou até obter dados sobre a propriedade System.Reflection.PropertyInfo
ou o getter System.Reflection.MethodInfo
. Mas, por uma questão de exemplo, basta definirmos explicitamente o nome da propriedade.
Para retornar um valor específico, usamos o método Expression.Constant
. Formamos o valor padrão
Expression resultExpression = Expression.Constant(string.Empty);
Depois disso, "envolvemos" sucessivamente o resultado em uma condição.
resultExpression = Expression.Condition( Expression.Equal(propertyExpression, Expression.Constant(FooEnum.Any)), Expression.Constant(EnumHelper.GetShortName(FooEnum.Any)), resultExpression); resultExpression = Expression.Condition( Expression.Equal(propertyExpression, Expression.Constant(FooEnum.Open)), Expression.Constant(EnumHelper.GetShortName(FooEnum.Open)), resultExpression); resultExpression = Expression.Condition( Expression.Equal(propertyExpression, Expression.Constant(FooEnum.Closed)), Expression.Constant(EnumHelper.GetShortName(FooEnum.Closed)), resultExpression);
public static class EnumHelper { public static string GetShortName(this Enum enumeration) { return (enumeration .GetType() .GetMember(enumeration.ToString())? .FirstOrDefault()? .GetCustomAttributes(typeof(DisplayAttribute), false)? .FirstOrDefault() as DisplayAttribute)? .ShortName ?? enumeration.ToString(); } }
Tudo o que resta é elaborar o resultado
return Expression.Lambda<Func<TEntity, string>>(resultExpression, value);
Um pouco mais de reflexão
Copiar todos os valores do Enum é extremamente inconveniente. Vamos consertar
var enumValues = Enum.GetValues(typeof(FooEnum)).Cast<Enum>(); Expression resultExpression = Expression.Constant(string.Empty); foreach (var enumValue in enumValues) { resultExpression = Expression.Condition( Expression.Equal(propertyExpression, Expression.Constant(enumValue)), Expression.Constant(EnumHelper.GetShortName(enumValue)), resultExpression); }
Vamos melhorar obtendo o valor da propriedade
A desvantagem do código acima é a forte ligação do tipo de entidade usada. Se um problema semelhante precisar ser resolvido em relação a outra classe, você precisará criar uma maneira de obter o valor de uma propriedade da enumeração de tipo. Então deixe a expressão fazer isso por nós. Como parâmetro do método, passaremos uma expressão que recebe o valor da propriedade e o próprio código - simplesmente formamos um conjunto de resultados para essa propriedade. Modelos para nos ajudar
public static Expression<Func<TEntity, string>> CreateEnumShortNameExpression<TEntity, TEnum>(Expression<Func<TEntity, TEnum>> propertyExpression) where TEntity : class where TEnum : struct { var enumValues = Enum.GetValues(typeof(TEnum)).Cast<Enum>(); Expression resultExpression = Expression.Constant(string.Empty); foreach (var enumValue in enumValues) { resultExpression = Expression.Condition( Expression.Equal(propertyExpression.Body, Expression.Constant(enumValue)), Expression.Constant(EnumHelper.GetShortName(enumValue)), resultExpression); } return Expression.Lambda<Func<TEntity, string>>(resultExpression, propertyExpression.Parameters); }
Alguns esclarecimentos. Porque obtemos o valor de entrada por meio de outra expressão e não precisamos declarar o parâmetro por meio de Expression.Parameter
. Pegamos esse parâmetro na propriedade da expressão de entrada e usamos o corpo da expressão para obter o valor da propriedade.
Então você pode usar o novo método como este:
CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression<FooEntity, FooEnum>(x => x.Enum)));
Todo o desenvolvimento bem-sucedido de árvores de expressão.
Eu recomendo a leitura dos artigos de Maxim Arshinov . Especialmente sobre árvores de expressão no desenvolvimento empresarial .