【问题标题】:Using ExpressionVisitor to modify expression for automatic translations使用 ExpressionVisitor 修改表达式以进行自动翻译
【发布时间】:2015-04-03 15:28:25
【问题描述】:

我正在尝试在我的实体框架模型中添加对多语言分类字符串的支持。这就是我所拥有的:

实体:

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

    public string Name { get; set; }

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

ITranslatableEntity 接口:

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

然后是包含翻译的实体:

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; }
}

包含本地化字符串的属性名称在主实体和翻译实体(本例中为Name)中始终相同。

使用这样的模型我可以做这样的事情:

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();

问题是我不喜欢为每个包含任何可翻译实体的查询编写这种代码,所以我正在考虑使用QueryInterceptorExpressionVisitor 这会产生一些魔力并允许我替换像这样查询:

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

我想可以创建一个 ExpressionVisitor,它会:

  • 仅为实现ITranslatableEntity&lt;&gt; 接口的导航属性更改Select 块内的表达式
  • 如果当前语言不是默认语言,则将表达式x.ServiceState.Name改为

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

但我对表达式访问者和树木不太熟悉,所以我在这里有点迷路。有人能让我走上正轨吗?

【问题讨论】:

标签: c# linq lambda expression-trees visitor-pattern


【解决方案1】:

好的,看来我想出了一个可行的解决方案。

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);
    }
}

我还添加了TranslatableAttribute,这是任何即将翻译的属性所必需的。

该代码肯定缺少一些检查,但它已经在我的环境中运行。我也没有检查被替换的表达式是否在 Select 块内,但看起来 TranslatableAttribute 没有必要。

我使用了ParameterRebinder 和这个答案ExpressionVisitor soft delete 中的一些其他代码

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-10-13
    • 1970-01-01
    • 2018-06-30
    • 1970-01-01
    • 1970-01-01
    • 2020-11-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多