6

I'm trying to project from my Order model to my OrderDTO model. Order has an enum. The problem is that projection doesn't work if I try to to get the Description attribute from the Enum. Here it's my code:

  • OrderStatus.cs:

    public enum OrderStatus {
        [Description("Paid")]
        Paid,
    
        [Description("Processing")]
        InProcess,
    
        [Description("Delivered")]
        Sent
    }
    
  • Order.cs:

    public class Order {
        public int Id { get; set; }
        public List<OrderLine> OrderLines { get; set; }
        public OrderStatus Status { get; set; }
    }
    
  • OrderDTO.cs:

    public class OrderDTO {
        public int Id { get; set; }
        public List<OrderLineDTO> OrderLines { get; set; }
        public string Status { get; set; }  
    }
    

With this following configuration in my AutoMapper.cs:

cfg.CreateMap<Order, OrderDTO>().ForMember(
    dest => dest.Status,
    opt => opt.MapFrom(src => src.Status.ToString())
);

Projection works, but I get an OrderDTO object like this:

 - Id: 1
 - OrderLines: List<OrderLines>
 - Sent //I want "Delivered"!

I don't want Status property to be "Sent", I want it to be as its associated Description attribute, in this case, "Delivered".

I have tried two solutions and none of them have worked:

  1. Using ResolveUsing AutoMapper function as explained here, but, as it's stated here:

ResolveUsing is not supported for projections, see the wiki on LINQ projections for supported operations.

  1. Using a static method to return the Description attribute in String by Reflection.

    cfg.CreateMap<Order, OrderDTO>().ForMember(
        dest => dest.Status,
        opt => opt.MapFrom(src => EnumHelper<OrderStatus>.GetEnumDescription(src.Status.ToString()))
    );
    

But this gives me the following error:

LINQ to Entities does not recognize the method 'System.String GetEnumDescription(System.String)' method, and this method cannot be translated into a store expression.

Then, how can I achieve this?

Sergio
  • 317
  • 1
  • 7
  • 18
  • Just thinking outside of the box, can't you just rename your enum `Sent` to `Delivered` like the description?. Also, having a distinction between `Sent` and `Delivered` would be nice because they are two different states. – jmesolomon Jun 05 '18 at 01:32
  • @jmesolomon That doesn't matter. It's just an example I thought – Sergio Jun 05 '18 at 01:41
  • @Sergio see this answer, this will help you https://stackoverflow.com/a/50434045/3901530 – Amit Jun 05 '18 at 02:21
  • There is no easy way to do that with ProjectTo, but you can look into [NeinLinq](https://github.com/axelheer/nein-linq). – Lucian Bargaoanu Jun 05 '18 at 05:28

2 Answers2

2

You can add an extension method like this one (borrowed the logic from this post):

public static class ExtensionMethods
{
    static public string GetDescription(this OrderStatus This)
    {
        var type = typeof(OrderStatus);
        var memInfo = type.GetMember(This.ToString());
        var attributes = memInfo[0].GetCustomAttributes(typeof(DescriptionAttribute), false);
        return ((DescriptionAttribute)attributes[0]).Description;
    }
}

Then access it in your map:

cfg => 
{
    cfg.CreateMap<Order, OrderDTO>()
    .ForMember
    (
        dest => dest.Status,
        opt => opt.MapFrom
        (
            src => src.Status.GetDescription()
        )
    );
}

This results in what you are asking for:

Console.WriteLine(dto.Status);  //"Delivered", not "sent"

See a working example on DotNetFiddle

Edit1: Don’t think you can add a local look up function like that to LINQ to entities. It would only work in LINQ to objects. The solution you should pursue perhaps is a domain table in the database that allows you to join to it and return the column that you want so that you don’t have to do anything with AutoMapper.

Sergio
  • 317
  • 1
  • 7
  • 18
John Wu
  • 50,556
  • 8
  • 44
  • 80
  • I have a similar problem but I need to access the parameters of the custom attribute – Nick Gallimore Jun 05 '18 at 05:39
  • This example shows you how to implement and access a parameter named Description. You ought you’ve able to adapt it to your own attribute with its own parameters. – John Wu Jun 05 '18 at 06:37
  • Thanks didn’t realize – Nick Gallimore Jun 05 '18 at 11:05
  • Thank you for your answer @JohnWu. I've implemented your extension method, apply it to my AutoMapper.cs, but I keep receiveing this error: `LINQ to Entities does not recognize the method 'System.String GetOrderStatusDescription(...Enums.OrderStatus)' method, and this method cannot be translated into a store expression`. I have uploaded an image with my code [here](https://i.imgur.com/7oe31V6.png) so that you can check it out. Thanks – Sergio Jun 05 '18 at 17:08
  • Don’t think you can add a local look up function like that to LINQ to entities. It would only work in LINQ to objects. The solution you should pursue perhaps is a domain table in the database that allows you to join to it and return the column that you want so that you don’t have to do anything with AutoMapper. – John Wu Jun 05 '18 at 18:25
  • Alright @JohnWu. Thank you for your time. I'm going to accept and edit your answer with your last comment. – Sergio Jun 05 '18 at 19:14
2

You can achieve an expression based enum description mapping by building up an expression that evaluates to a string containing a condition statement (such as switch/if/case depending on how the provider implements it) with the enum descriptions as the results.

Because the enum descriptions can be extracted ahead of time we can obtain them and use them as a constant for the result of the condition expression.

Note: I've used the above extension method GetDescription() but you can use whatever flavour of attribute extraction you need.

  public static Expression<Func<TEntity, string>> CreateEnumDescriptionExpression<TEntity, TEnum>(
    Expression<Func<TEntity, TEnum>> propertyExpression)
     where TEntity : class
     where TEnum : struct
  {
     // Get all of the possible enum values for the given enum type
     var enumValues = Enum.GetValues(typeof(TEnum)).Cast<Enum>();

     // Build up a condition expression based on each enum value
     Expression resultExpression = Expression.Constant(string.Empty);
     foreach (var enumValue in enumValues)
     {
        resultExpression = Expression.Condition(
           Expression.Equal(propertyExpression.Body, Expression.Constant(enumValue)),
           // GetDescription() can be replaced with whatever extension 
           // to get you the needed enum attribute.
           Expression.Constant(enumValue.GetDescription()),
           resultExpression);
     }

     return Expression.Lambda<Func<TEntity, string>>(
        resultExpression, propertyExpression.Parameters);
  }

Then your Automapper mapping becomes:

  cfg.CreateMap<Order, OrderDTO>().ForMember(
     dest => dest.Status, opts => opts.MapFrom(
             CreateEnumDescriptionExpression<Order, OrderStatus>(src => src.Status)));

When this is evaluated at runtime using Entity Framework with SQL server provider, the resulting SQL will be something like:

SELECT 
   -- various fields such as Id
   CASE WHEN (2 = [Extent1].[Status]) THEN N'Delivered' 
        WHEN (1 = [Extent1].[Status]) THEN N'Processing' 
        WHEN (0 = [Extent1].[Status]) THEN N'Paid' ELSE N'' END AS [C1]
FROM [Orders] as [Extent1]

This should also work for other Entity Framework DB providers.

mips
  • 2,137
  • 2
  • 19
  • 21
  • If you want, you can more closely integrate it with AM. See [this](https://github.com/AutoMapper/AutoMapper/blob/master/src/UnitTests/Projection/BindersAndResultConverters.cs). – Lucian Bargaoanu Jan 13 '20 at 06:13
  • Although a map from `Enum` to `string` would probably fit better. – Lucian Bargaoanu Jan 13 '20 at 06:33
  • @LucianBargaoanu Thanks for the insight into AutoMapper cfg.Advanced.QueryableBinders - I'll take a look. Would be nice not to specify the expression converter for each property. – mips Jan 14 '20 at 05:32
  • @LucianBargaoanu Not sure your meaning of "Enum to string would probably fit better"? – mips Jan 14 '20 at 05:33
  • A map would apply everywhere, for every `Enum` converted to string, so you don't need to specify it for each property. – Lucian Bargaoanu Jan 14 '20 at 06:16
  • Excellent solution! I was about to build my own custom expression if I don't found this one. – Eric Fan Sep 27 '22 at 07:50