Obtendo dados de enumeração em uma perspectiva do Automapper

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 .

Source: https://habr.com/ru/post/pt439484/


All Articles