Un poco de programa educativo
Realmente me gusta Automapper, especialmente sus QueryableExtensions y el m茅todo ProjectTo <> . En resumen, este m茅todo permite la proyecci贸n de tipos directamente en la consulta SQL. Permiti贸 recibir dto realmente de una base de datos. Es decir no es necesario obtener todas las entidades de la base de datos, cargarlas en la memoria, usar Automapper.Map<>
, lo que condujo a un gran consumo y tr谩fico de memoria.
Tipo de proyecci贸n
Para obtener una proyecci贸n en linq, necesitabas escribir algo como esto:
from user in dbContext.Users where user.IsActive select new { Name = user.Name, Status = user.IsConnected ? "Connected" : "Disconnected" }
Usando QueryableExtensions, este c贸digo se puede reemplazar con lo siguiente (por supuesto, siempre que las reglas de conversi贸n Usuario -> Informaci贸n de usuario ya est茅n descritas)
dbContext.Users.Where(x => x.IsActive).ProjectTo<UserInfo>();
Enum y problemas con ella
La proyecci贸n tiene un inconveniente que debe considerarse. Esta es una restricci贸n en las operaciones realizadas. No todo se puede traducir en una consulta SQL . En particular, no es posible obtener informaci贸n por tipo de enumeraci贸n. Por ejemplo, existe la siguiente enumeraci贸n
public enum FooEnum { [Display(Name = "")] Any, [Display(Name = "")] Open, [Display(Name = "")] Closed }
Hay una entidad en la que se declara una propiedad de tipo FooEnum. En dto, no necesita obtener Enum, sino el valor de la propiedad Name del atributo DisplayAttribute. Darse cuenta de esto a trav茅s de la proyecci贸n no funciona, porque obtener el valor del atributo requiere Reflection, del cual SQL simplemente "no sabe nada".
Como resultado, debe usar el Map<>
habitual Map<>
, cargar todas las entidades en la memoria o iniciar una tabla adicional con valores Enum y claves for谩neas.
Hay una soluci贸n: expresiones
Pero "habr谩 un golpe en la anciana". Despu茅s de todo, todos los valores de Enum se conocen de antemano. SQL tiene una implementaci贸n de switch
que puede insertar al formar una proyecci贸n. Queda por entender c贸mo hacer esto. HashTag: "Expression Trees-Our-All".
Automapper, al proyectar tipos, puede convertir una expresi贸n en una expresi贸n que, despu茅s de Entity Framework, se convierte en la consulta SQL correspondiente.
A primera vista, la sintaxis para crear 谩rboles de expresi贸n en tiempo de ejecuci贸n es extremadamente inconveniente. Pero despu茅s de algunos peque帽os problemas resueltos, todo se vuelve obvio. Para resolver el problema con Enum, debe crear un 谩rbol anidado de expresiones condicionales que devuelvan valores, seg煤n los datos de origen. Algo como esto
IF enum=Any THEN RETURN "" ELSE IF enum=Open THEN RETURN "" ELSE enum=Closed THEN RETURN "" ELSE RETURN ""
Decidir sobre la firma del m茅todo.
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() { }
El m茅todo GetExpression()
debe generar una expresi贸n que recibe una instancia de FooEntity y devuelve una representaci贸n de cadena para la propiedad Enum
.
Primero, defina el par谩metro de entrada y obtenga el valor de la propiedad
ParameterExpression value = Expression.Parameter(typeof(FooEntity), "x"); var propertyExpression = Expression.Property(value, "Enum");
En lugar de la cadena de nombre de propiedad, puede usar el nameof(FooEntity.Enum)
compilador nameof(FooEntity.Enum)
o incluso obtener datos sobre la propiedad System.Reflection.PropertyInfo
o el captador System.Reflection.MethodInfo
. Pero por el bien de los ejemplos, es suficiente para nosotros establecer expl铆citamente el nombre de la propiedad.
Para devolver un valor espec铆fico, utilizamos el m茅todo Expression.Constant
. Formamos el valor por defecto
Expression resultExpression = Expression.Constant(string.Empty);
Despu茅s de eso, sucesivamente "envolvemos" el resultado en una condici贸n.
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(); } }
Todo lo que queda es elaborar el resultado.
return Expression.Lambda<Func<TEntity, string>>(resultExpression, value);
Un poco mas de reflexion
Copiar todos los valores de Enum es extremadamente inconveniente. Vamos a arreglarlo
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); }
Mejoremos obteniendo el valor de la propiedad
La desventaja del c贸digo anterior es la estrecha vinculaci贸n del tipo de entidad utilizada. Si se necesita resolver un problema similar en relaci贸n con otra clase, debe encontrar una forma de obtener el valor de una propiedad de enumeraci贸n de tipos. As铆 que deja que la expresi贸n lo haga por nosotros. Como par谩metro del m茅todo, pasaremos una expresi贸n que recibe el valor de la propiedad y el c贸digo en s铆 mismo; simplemente formamos un conjunto de resultados para esta propiedad posible. Plantillas para ayudarnos
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); }
Algunas aclaraciones Porque obtenemos el valor de entrada a trav茅s de otra expresi贸n, luego no necesitamos declarar el par谩metro a trav茅s de Expression.Parameter
. Tomamos este par谩metro de la propiedad de la expresi贸n de entrada y usamos el cuerpo de la expresi贸n para obtener el valor de la propiedad.
Entonces puedes usar el nuevo m茅todo como este:
CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression<FooEntity, FooEnum>(x => x.Enum)));
Todo el desarrollo exitoso de 谩rboles de expresi贸n.
Recomiendo leer los art铆culos de Maxim Arshinov . Especialmente sobre Expression Trees en el desarrollo empresarial .