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 database in fact. 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 normal one Map<>, loading all the entities into memory, or start 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 an implementation switchthat can be inserted 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 method GetExpression()must form an expression that receives an instance of FooEntity and returns a string representation for the property Enum.
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 the property name string, you can use the compiler syntax nameof(FooEntity.Enum)or even get property System.Reflection.PropertyInfoor getter data System.Reflection.MethodInfo. But for example, we have enough and explicitly set the name of the property.


To return a specific value, use the method Expression.Constant. 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 the input value we get through another expression, then Expression.Parameterwe do not need to declare the parameter through . 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 .


Also popular now: