Немного ликбеза
Я очень люблю Automapper, особенно его QueryableExtensions и метод ProjectTo<>. Если вкратце, то данный метод позволяет делать проекцию типов прямо в SQL-запросе. Это позволяло получать dto фактически из базы данных. Т.е. не нужно получать все entity из базы, грузить их в память, применять Automapper.Map<>, что приводило к большому расходу и трафику памяти.
Проекция типа
Для получения проекции в linq требовалось написать что-то подобное:
from user in dbContext.Users where user.IsActive select new { Name = user.Name, Status = user.IsConnected ? "Connected" : "Disconnected" }
Используя QueryableExtensions, этот код можно заменить на следующий (конечно, при условии, что правила преобразования User -> UserInfo уже описано)
dbContext.Users.Where(x => x.IsActive).ProjectTo<UserInfo>();
Enum и проблемы с ним
У проекции есть один недостаток, который необходимо учитывать. Это ограничение на выполняемые операции. Не все можно транслировать в SQL-запрос. В частности, нельзя получить информацию по типу-перечислению. Например, есть следующий Enum
public enum FooEnum { [Display(Name = "Любой")] Any, [Display(Name = "Открытый")] Open, [Display(Name = "Закрытый")] Closed }
Есть entity, в котором объявлено свойство типа FooEnum. В dto необходимо получить не сам Enum, а значение свойства Name атрибута DisplayAttribute. Реализовать это через проекцию не получиться, т.к. получение значения атрибута требует Reflection, о чем SQL просто "ничего не знает".
В итоге приходится либо использовать обычный Map<>, загружая все сущности в память, либо заводить дополнительную таблицу со значениями Enum и внешними ключами на нее.
Решение есть — Expressions
Но "и на старуху найдется проруха". Ведь все значения Enum заранее известны. В SQL есть реализация switch, которую можно вставить при формировании проекции. Остается понять, как это сделать. ХэшТэг: "Деревья-выражений-наше-все".
Automapper при проекции типов может преобразовать expression в выражение, которое после Entity Framework конвертирует в соответствующий SQL-запрос.
На первый взгляд, синтаксис создания деревьев выражений в runtime крайне неудобен. Но после нескольких небольших решенных задач все становится очевидно. Для решения проблемы с Enum необходимо создать вложенное дерево условных выражений, возвращающих значения, в зависимости от исходных данных. Примерно такое
IF enum=Any THEN RETURN "Любой" ELSE IF enum=Open THEN RETURN "Открытый" ELSE enum=Closed THEN RETURN "Закрытый" ELSE RETURN ""
Определимся с сигнатурой метода.
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() { }
Метод GetExpression() должен сформировать выражение, получающее экземпляр FooEntity и возвращающее строковое представление для свойства Enum.
Для начала определим входной параметр и получим само значение свойства
ParameterExpression value = Expression.Parameter(typeof(FooEntity), "x"); var propertyExpression = Expression.Property(value, "Enum");
Вместо строки имени свойства можно использовать синтаксис компилятора nameof(FooEntity.Enum) или даже получить данные о свойстве System.Reflection.PropertyInfo или геттера System.Reflection.MethodInfo. Но для примера нам хватит и явного задания имени свойства.
Чтобы вернуть конкретное значение, используем метод Expression.Constant. Формируем значение по умолчанию
Expression resultExpression = Expression.Constant(string.Empty);
После этого, последовательно "оборачиваем" результат в условие.
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(); } }
Все, что осталось, это оформить результат
return Expression.Lambda<Func<TEntity, string>>(resultExpression, value);
Еще немного рефлексии
Копипастить все значения Enum крайне неудобно. Давайте это исправим
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); }
Усовершенствуем получение значения свойства
Недостаток кода выше — жесткая привязка типа используемой сущности. Если подобную задачу необходимо решить применительно к другому классу, необходимо придумать способ получения значения свойства типа-перечисление. Так пусть за нас это делает expression. В качестве параметра метода будем передавать выражение, получающее значение свойства, а сам код — просто формируем набор результатов по возможным этого свойства. Шаблоны нам в помощь
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); }
Несколько пояснений. Т.к. входное значение мы получаем через другое выражение, то объявлять параметр через Expression.Parameter нам не нужно. Этот параметр мы берем из свойства входного выражения, а тело выражения используем для получения значения свойства.
Тогда использовать новый метод можно так:
CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression<FooEntity, FooEnum>(x => x.Enum)));
Всем удачного освоения деревьев выражений.
Крайне рекомендую почитать статьи Максима Аршинова. Особенно про Деревья выражений в enterprise-разработке.