Utilisation d'ExpressionVisitor pour modifier l'expression des traductions automatiques

c# expression-trees lambda linq visitor

Question

J'essaie d'ajouter un support pour les chaînes de classification multilingues dans mon modèle Entity Framework. C'est ce que j'ai

L'entité:

public partial class ServiceState : ITranslatableEntity<ServiceStateTranslation>
{
    public int Id { get; set; }

    public string Name { get; set; }

    public virtual ICollection<ServiceStateTranslation> Translations { get; set; }
}

Interface ITranslatableEntity:

public interface ITranslatableEntity<T>
{
    ICollection<T> Translations { get; set; }
}

Ensuite, l'entité contenant les traductions:

public partial class ServiceStateTranslation
{
    public int Id { get; set; }

    [Index("IX_ClassificationTranslation", 1, IsUnique = true)]
    public int MainEntityId { get; set; }

    public ServiceState MainEntity { get; set; }

    [Index("IX_ClassificationTranslation", 2, IsUnique = true)]
    public string LanguageCode { get; set; }

    public string Name { get; set; }
}

Les noms des propriétés contenant des chaînes localisées sont toujours les mêmes dans l'entité principale et dans l'entité de traduction ( Name dans ce cas).

En utilisant un tel modèle, je peux faire quelque chose comme ceci:

var result = query.Select(x => new
        {
            Name = x.Name,
            StateName =
                currentLanguageCode == DEFAULTLANGUAGECODE
                    ? x.ServiceState.Name
                    : x.ServiceState.Translations.Where(i => i.LanguageCode == currentLanguageCode)
                        .Select(i => i.Name)
                        .FirstOrDefault() ?? x.ServiceState.Name
        }).ToList();

Le problème est que je n'aime pas écrire ce genre de code pour chaque requête contenant une entité traduisible. Je songe donc à utiliser QueryInterceptor et un ExpressionVisitor qui feraient un peu de magie et me permettraient de remplacer la requête par quelque chose comme:

var result = query.Select(x => new
        {
            Name = x.Name,
            StateName = x.ServiceState.Name
        }).ToLocalizedList(currentLanguageCode, DEFAULTLANGUAGECODE);

Je suppose qu'il est possible de créer un ExpressionVisitor qui:

  • Ne changez que les expressions à l'intérieur des blocs de Select pour les propriétés de navigation mettant en œuvre l'interface ITranslatableEntity<>
  • Si la langue actuelle n'est pas la langue par défaut, modifiez l'expression x.ServiceState.Name en

    x.ServiceState.Translations.Where(i => i.LanguageCode == currentLanguageCode)
                    .Select(i => i.Name)
                    .FirstOrDefault() ?? x.ServiceState.Name
    

Mais je ne connais pas très bien les visiteurs d'expression et les arbres, alors je suis un peu perdu ici. Quelqu'un pourrait-il me mettre sur la bonne voie?

Réponse acceptée

OK, on ​​dirait que j'ai trouvé une solution qui marche.

public class ClassificationTranslationVisitor : ExpressionVisitor
{
    private string langCode = "en";
    private string defaultLangCode = "en";
    private string memberName = null;
    private Expression originalNode = null;

    public ClassificationTranslationVisitor(string langCode, string defaultLanguageCode)
    {
        this.langCode = langCode;
        this.defaultLangCode = defaultLanguageCode;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (langCode == defaultLangCode)
        {
            return base.VisitParameter(node);
        }

        if (!node.Type.GetCustomAttributes(typeof(TranslatableAttribute), false).Any() && originalNode == null)
        {
            return base.VisitParameter(node);
        }

        if (IsGenericInterface(node.Type, typeof(ITranslatableEntity<>)))
        {
            return AddTranslation(node);
        }

        return base.VisitParameter(node);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node == null || node.Member == null || node.Member.DeclaringType == null)
        {
            return base.VisitMember(node);
        }

        if (langCode == defaultLangCode)
        {
            return base.VisitMember(node);
        }

        if (!node.Member.GetCustomAttributes(typeof(TranslatableAttribute), false).Any() && originalNode == null)
        {
            return base.VisitMember(node); 
        }

        if (IsGenericInterface(node.Member.DeclaringType, typeof(ITranslatableEntity<>)))
        {
            memberName = node.Member.Name;
            originalNode = node;
            return Visit(node.Expression);
        }

        if (IsGenericInterface(node.Type, typeof(ITranslatableEntity<>)))
        {
            return AddTranslation(node);
        }

        return base.VisitMember(node);
    }

    private Expression AddTranslation(Expression node)
    {
        var expression = Expression.Property(node, "Translations");
        var resultWhere = CreateWhereExpression(expression);
        var resultSelect = CreateSelectExpression(resultWhere);
        var resultIsNull = Expression.Equal(resultSelect, Expression.Constant(null));
        var testResult = Expression.Condition(resultIsNull, originalNode, resultSelect);
        memberName = null;
        originalNode = null;
        return testResult;
    }

    private Expression CreateWhereExpression(Expression ex)
    {
        var type = ex.Type.GetGenericArguments().First();
        var test = CreateExpression(t => t.LanguageCode == langCode, type);
        if (test == null)
            return null;
        return Expression.Call(typeof(Enumerable), "Where", new[] { type }, ex, test);
    }

    private Expression CreateSelectExpression(Expression ex)
    {
        var type = ex.Type.GetGenericArguments().First();

        ParameterExpression itemParam = Expression.Parameter(type, "lang");
        Expression selector = Expression.Property(itemParam, memberName);
        var columnLambda = Expression.Lambda(selector, itemParam);

        var result = Expression.Call(typeof(Enumerable), "Select", new[] { type, typeof(string) }, ex, columnLambda);
        var stringResult = Expression.Call(typeof(Enumerable), "FirstOrDefault", new[] { typeof(string) }, result);
        return stringResult;
    }

    /// <summary>
    /// Adapt a QueryConditional to the member we're currently visiting.
    /// </summary>
    /// <param name="condition">The condition to adapt</param>
    /// <param name="type">The type of the current member (=Navigation property)</param>
    /// <returns>The adapted QueryConditional</returns>
    private LambdaExpression CreateExpression(Expression<Func<ITranslation, bool>> condition, Type type)
    {
        var lambda = (LambdaExpression)condition;
        var conditionType = condition.GetType().GetGenericArguments().First().GetGenericArguments().First();
        // Only continue when the condition is applicable to the Type of the member
        if (conditionType == null)
            return null;
        if (!conditionType.IsAssignableFrom(type))
            return null;

        var newParams = new[] { Expression.Parameter(type, "bo") };
        var paramMap = lambda.Parameters.Select((original, i) => new { original, replacement = newParams[i] }).ToDictionary(p => p.original, p => p.replacement);
        var fixedBody = ParameterRebinder.ReplaceParameters(paramMap, lambda.Body);
        lambda = Expression.Lambda(fixedBody, newParams);

        return lambda;
    }

    private bool IsGenericInterface(Type type, Type interfaceType)
    {
        return type.GetInterfaces().Any(x =>
            x.IsGenericType &&
            x.GetGenericTypeDefinition() == interfaceType);
    }
}

public class ParameterRebinder : ExpressionVisitor
{
    private readonly Dictionary<ParameterExpression, ParameterExpression> map;

    public ParameterRebinder(Dictionary<ParameterExpression, ParameterExpression> map)
    {
        this.map = map ?? new Dictionary<ParameterExpression, ParameterExpression>();
    }

    public static Expression ReplaceParameters(Dictionary<ParameterExpression, ParameterExpression> map, Expression exp)
    {
        return new ParameterRebinder(map).Visit(exp);
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        ParameterExpression replacement;

        if (map.TryGetValue(node, out replacement))
            node = replacement;

        return base.VisitParameter(node);
    }
}

J'ai également ajouté TranslatableAttribute qui est requis pour toutes les propriétés sur le point d'être traduites.

Certaines vérifications manquent dans le code, mais cela fonctionne déjà sur mon environnement. Je ne vérifie pas non plus si l'expression remplacée se trouve dans un bloc Select , mais il semble que ce ne soit pas nécessaire avec TranslatableAttribute .

J'ai utilisé ParameterRebinder et un autre code de cette réponse. ExpressionVisitor soft delete



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow