Un peu de programme éducatif
J'aime vraiment Automapper, en particulier ses QueryableExtensions et la méthode ProjectTo <> . En bref, cette méthode permet la projection de types directement dans la requête SQL. Il permettait de recevoir dto réellement d'une base de données. C'est-à-dire pas besoin de récupérer toutes les entités de la base de données, de les charger en mémoire, d'utiliser Automapper.Map<>
, ce qui a entraîné une consommation et un trafic mémoire importants.
Type de projection
Pour obtenir une projection en linq, vous devez écrire quelque chose comme ceci:
from user in dbContext.Users where user.IsActive select new { Name = user.Name, Status = user.IsConnected ? "Connected" : "Disconnected" }
En utilisant QueryableExtensions, ce code peut être remplacé par ce qui suit (bien sûr, à condition que les règles de conversion User -> UserInfo soient déjà décrites)
dbContext.Users.Where(x => x.IsActive).ProjectTo<UserInfo>();
Enum et problèmes avec
La projection a un inconvénient qui doit être pris en compte. Il s'agit d'une restriction sur les opérations effectuées. Tout ne peut pas être traduit en une requête SQL . En particulier, il n'est pas possible d'obtenir des informations par type d'énumération. Par exemple, il y a l'énumération suivante
public enum FooEnum { [Display(Name = "")] Any, [Display(Name = "")] Open, [Display(Name = "")] Closed }
Il existe une entité dans laquelle une propriété de type FooEnum est déclarée. Dans dto, vous devez obtenir non Enum lui-même, mais la valeur de la propriété Name de l'attribut DisplayAttribute. Réaliser cela à travers la projection ne fonctionne pas, car obtenir la valeur de l'attribut nécessite Reflection, dont SQL «ne sait rien».
En conséquence, vous devez soit utiliser la Map<>
habituelle Map<>
, charger toutes les entités en mémoire, soit démarrer une table supplémentaire avec des valeurs Enum et des clés étrangères.
Il y a une solution - Expressions
Mais "il y aura un slammer sur la vieille femme". Après tout, toutes les valeurs d'Enum sont connues à l'avance. SQL a une implémentation de switch
que vous pouvez insérer lors de la formation d'une projection. Reste à comprendre comment procéder. HashTag: "Expression Trees-Our-All".
Automapper, lors de la projection de types, peut convertir une expression en une expression qui, après Entity Framework, se convertit en requête SQL correspondante.
À première vue, la syntaxe pour créer des arborescences d'expressions lors de l'exécution est extrêmement gênante. Mais après quelques petits problèmes résolus, tout devient évident. Pour résoudre le problème avec Enum, vous devez créer un arbre imbriqué d'expressions conditionnelles qui renvoient des valeurs, en fonction des données source. Quelque chose comme ça
IF enum=Any THEN RETURN "" ELSE IF enum=Open THEN RETURN "" ELSE enum=Closed THEN RETURN "" ELSE RETURN ""
Décidez de la signature de la méthode.
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() { }
La méthode GetExpression()
doit générer une expression qui reçoit une instance de FooEntity et renvoie une représentation sous forme de chaîne pour la propriété Enum
.
Tout d'abord, définissez le paramètre d'entrée et obtenez la valeur de la propriété elle-même
ParameterExpression value = Expression.Parameter(typeof(FooEntity), "x"); var propertyExpression = Expression.Property(value, "Enum");
Au lieu de la chaîne de nom de propriété, vous pouvez utiliser la nameof(FooEntity.Enum)
compilateur nameof(FooEntity.Enum)
ou même obtenir des données sur la propriété System.Reflection.PropertyInfo
ou le getter System.Reflection.MethodInfo
. Mais par exemple, il nous suffit de définir explicitement le nom de la propriété.
Pour renvoyer une valeur spécifique, nous utilisons la méthode Expression.Constant
. Nous formons la valeur par défaut
Expression resultExpression = Expression.Constant(string.Empty);
Après cela, nous "enveloppons" successivement le résultat dans une 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(); } }
Il ne reste plus qu'à établir le résultat
return Expression.Lambda<Func<TEntity, string>>(resultExpression, value);
Un peu plus de réflexion
Copier toutes les valeurs d'Enum est extrêmement gênant. Fixons-le
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); }
Améliorons l'obtention de la valeur de la propriété
L'inconvénient du code ci-dessus est la liaison étroite du type d'entité utilisé. Si un problème similaire doit être résolu par rapport à une autre classe, vous devez trouver un moyen d'obtenir la valeur d'une propriété de type énumération. Alors laissez l'expression le faire pour nous. En tant que paramètre de la méthode, nous passerons une expression qui reçoit la valeur de la propriété, et le code lui-même - nous formons simplement un ensemble de résultats pour cette propriété possible. Modèles pour nous aider
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); }
Quelques précisions. Parce que nous obtenons la valeur d'entrée via une autre expression, puis nous n'avons pas besoin de déclarer le paramètre via Expression.Parameter
. Nous prenons ce paramètre de la propriété de l'expression d'entrée et utilisons le corps de l'expression pour obtenir la valeur de la propriété.
Ensuite, vous pouvez utiliser la nouvelle méthode comme ceci:
CreateMap<FooEntity, FooDto>() .ForMember(x => x.Enum, options => options.MapFrom(GetExpression<FooEntity, FooEnum>(x => x.Enum)));
Tout développement réussi d'arbres d'expression.
Je recommande fortement de lire les articles de Maxim Arshinov . En particulier sur les arbres d'expression dans le développement des entreprises .