📜 ⬆️ ⬇️

Get the enum data in the Automapper projection

Bit of education


I really love Automapper, especially its QueryableExtensions and the ProjectTo <> method . In short, this method allows you to make a projection of types directly in the SQL query. This made it possible to get dto from the actual database. Those. You do not need to get all the entity from the database, load them into memory, use Automapper.Map<> , which led to a large consumption and memory traffic.


Projection type


To obtain a projection in linq, it was necessary to write something like this:


  from user in dbContext.Users where user.IsActive select new { Name = user.Name, Status = user.IsConnected ? "Connected" : "Disconnected" } 

Using QueryableExtensions, this code can be replaced with the following (of course, provided that the User-> UserInfo conversion rules are already described)


 dbContext.Users.Where(x => x.IsActive).ProjectTo<UserInfo>(); 

Enum and problems with it


There is one drawback to the projection that needs to be considered. This is a limitation on the operations performed. Not everything can be translated into a SQL query . In particular, it is impossible to obtain information on the type of transfer. For example, there is the following Enum


  public enum FooEnum { [Display(Name = "Любой")] Any, [Display(Name = "Открытый")] Open, [Display(Name = "Закрытый")] Closed } 

There is an entity in which a property of type FooEnum is declared. In dto, you do not need to get Enum itself, but the value of the Name property of the DisplayAttribute attribute. To realize this through a projection does not work out, since retrieving an attribute value requires a Reflection, which SQL simply "knows nothing" about.


As a result, you have to either use the usual Map<> , loading all the entities into memory, or get an additional table with Enum values ​​and foreign keys on it.


The solution is - Expressions


But "there is a hole on the old woman." After all, all Enum values ​​are known in advance. In SQL, there is a switch implementation that you can insert when creating a projection. It remains to understand how to do it. HashTag: "Trees-expressions-our-all."


Automapper, when projecting types, can convert expression to an expression that converts to the corresponding SQL query after the Entity Framework.


At first glance, the syntax for creating expression trees at runtime is extremely inconvenient. But after a few small tasks solved, everything becomes obvious. To solve the Enum problem, you need to create an embedded tree of conditional expressions that return values, depending on the source data. About


 IF enum=Any THEN RETURN "Любой" ELSE IF enum=Open THEN RETURN "Открытый" ELSE enum=Closed THEN RETURN "Закрытый" ELSE RETURN "" 

Let's define the method signature.


  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() { } 

The GetExpression() method should form an expression that gets an instance of FooEntity and returns a string representation for the Enum property.
First we define the input parameter and get the property value itself.


 ParameterExpression value = Expression.Parameter(typeof(FooEntity), "x"); var propertyExpression = Expression.Property(value, "Enum"); 

Instead of using the property name string, you can use the nameof(FooEntity.Enum) compiler nameof(FooEntity.Enum) or even get information about the System.Reflection.PropertyInfo property or the System.Reflection.MethodInfo getter. But for example, we have enough and explicitly set the name of the property.


To return a specific value, use the Expression.Constant method. We form the default


  Expression resultExpression = Expression.Constant(string.Empty); 

After that, consistently "wrap" the result in the condition.


  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(); } } 

All that is left is to issue a result.


  return Expression.Lambda<Func<TEntity, string>>(resultExpression, value); 

A little more reflection


Copying all Enum values ​​is extremely inconvenient. Let's fix it


  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); } 

Improve the retrieval of the property value.


The disadvantage of the code above is the hard binding of the type of entity being used. If a similar problem needs to be solved in relation to another class, it is necessary to think of a way to get the value of an enumerated type property. So let expression do it for us. As a parameter of the method, we will pass an expression that gets the value of the property, and the code itself - we simply generate a set of results for the possible properties. Templates to help us


  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); } 

A few explanations. Since we get the input value through another expression, we do not need to declare the parameter through the Expression.Parameter . We take this parameter from the property of the input expression, and use the body of the expression to get the value of the property.
Then use the new method as follows:


  CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression<FooEntity, FooEnum>(x => x.Enum))); 



All successful development of trees of expression.


I highly recommend reading articles by Maxim Arshinov . Especially about expression trees in enterprise development .



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