[Из песочницы] Получаем данные enum в проекции Automapper
Я очень люблю 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();
У проекции есть один недостаток, который необходимо учитывать. Это ограничение на выполняемые операции. Не все можно транслировать в SQL-запрос. В частности, нельзя получить информацию по типу-перечислению. Например, есть следующий Enum
public enum FooEnum
{
[Display(Name = "Любой")]
Any,
[Display(Name = "Открытый")]
Open,
[Display(Name = "Закрытый")]
Closed
}
Есть entity, в котором объявлено свойство типа FooEnum. В dto необходимо получить не сам Enum, а значение свойства Name атрибута DisplayAttribute. Реализовать это через проекцию не получиться, т.к. получение значения атрибута требует Reflection, о чем SQL просто «ничего не знает».
В итоге приходится либо использовать обычный Map<>
, загружая все сущности в память, либо заводить дополнительную таблицу со значениями Enum и внешними ключами на нее.
Но «и на старуху найдется проруха». Ведь все значения 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()
.ForMember(x => x.Enum, options => options.MapFrom(GetExpression()));
private Expression> 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>(resultExpression, value);
Копипастить все значения Enum крайне неудобно. Давайте это исправим
var enumValues = Enum.GetValues(typeof(FooEnum)).Cast();
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> CreateEnumShortNameExpression(Expression> propertyExpression)
where TEntity : class
where TEnum : struct
{
var enumValues = Enum.GetValues(typeof(TEnum)).Cast();
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>(resultExpression, propertyExpression.Parameters);
}
Несколько пояснений. Т.к. входное значение мы получаем через другое выражение, то объявлять параметр через Expression.Parameter
нам не нужно. Этот параметр мы берем из свойства входного выражения, а тело выражения используем для получения значения свойства.
Тогда использовать новый метод можно так:
CreateMap()
.ForMember(x => x.Enum, options => options.MapFrom(GetExpression(x => x.Enum)));
Всем удачного освоения деревьев выражений.
Крайне рекомендую почитать статьи Максима Аршинова. Особенно про Деревья выражений в enterprise-разработке.