2

这是这样的想法:

我们的应用程序有一个产品表,其中包含可翻译的名称。因为支持的语言数量可以扩展,所以每个产品都有一个 ProductTranslation 集合,其中包含一种文化(例如“en-US”)和每个可翻译属性。

域如下所示:

/// <summary>
///     Marks a class as translatable, i.e. there are properties that need to be different per language, such as a name or description. 
/// </summary>
/// <typeparam name="TTranslation"></typeparam>
public interface ITranslatable<TTranslation> where TTranslation: ITranslation
{
    /// <summary>
    ///     Gets or sets the translations
    /// </summary>
    TranslationCollection<TTranslation> Translations { get; set; }
}

/// <summary>
///    Marks this class as a translation of another class. 
/// </summary>
public interface ITranslation
{
    /// <summary>
    ///     Gets or sets the culture
    /// </summary>
    string Culture { get; set; }
}

public class Product : ITranslatable<ProductTranslation>
{
    private TranslationCollection<ProductTranslation> _translations;

    /// <summary>
    ///     Gets or sets the code.
    /// </summary>
    public virtual string Code { get; set; }

    /// <summary>
    ///     Gets or sets the price.
    /// </summary>
    public virtual decimal? Price { get; set; }

    public virtual TranslationCollection<ProductTranslation> Translations
    {
        get { return _translations ?? (_translations = new TranslationCollection<ProductTranslation>()); }
        set { _translations = value; }
    }
}

/// <summary>
///     Contains the translatable properties for <see cref="Product"/>
/// </summary>
public class ProductTranslation: ITranslation
{
    /// <summary>
    ///     Gets or sets the culture of this translation
    /// </summary>
    public string Culture { get; set; }

    /// <summary>
    ///     Gets or sets the name.
    /// </summary>
    public virtual string Name { get; set; }
}

您可能已经注意到,我使用自定义集合类进行翻译。(TranslationCollection 而不是默认的 ICollection)

该类扩展了 Collection,但添加了一个实用属性“Current”,该属性返回与当前 UI 文化匹配的翻译:

/// <summary>
///     Contains specific methods to work with translations
/// </summary>
/// <typeparam name="TTranslation"></typeparam>
public class TranslationCollection<TTranslation>: Collection<TTranslation> where TTranslation: ITranslation
{
    /// <summary>
    ///     Initializes an empty <see cref="TranslationCollection{TTranslation}"/>
    /// </summary>
    public TranslationCollection()
    {
    }

    /// <summary>
    ///     Initializes a new <see cref="TranslationCollection{TTranslation}"/> with the given <paramref name="list"/> as its contents
    /// </summary>
    /// <param name="list"></param>
    public TranslationCollection(IList<TTranslation> list) : base(list)
    {
    }

    /// <summary>
    ///     Returns the translation that has the same culture as the current UI culture.
    /// </summary>
    public TTranslation Current
    {
        get
        {
            return this.SingleOrDefault(t => t.Culture == CultureInfo.CurrentUICulture.Name);
        }
    }
}

如您所见,这里几乎没有发生什么,但我喜欢为此创建一个自定义集合类的想法,因为稍后当我们想要制作一些用于显示和表单的自定义 HTML 组件时,它可能会派上用场。

现在的问题:

当我们查询 products 表时,按其名称进行搜索将如下所示:

var products = dbContext.Products.Where(p => p.Translations.Where(t => t.Culture == CultureInfo.CurrentUICulture).Any(t => t.Name.ToLower().Contains("abc")))

但是,鉴于将来会有很多可翻译的表格(这是一个相当大的应用程序),如果我们可以这样写会很有趣:

var products = dbContext.Products.Where(p => p.Translations.Current.Name.ToLower().Contains("abc"))

当然,'Current' 属性是未映射的,当您运行此代码时,Entity Framework 将引发异常。但是,是否可以使用 ExpressionVisitor (或其他任何东西)自动将“当前”调用转换为其他东西

我做了第一次尝试,但有点卡住:

public class CurrentTranslationVisitor: ExpressionVisitor
{
    protected override Expression VisitMember(MemberExpression node)
    {
        if(node.Member.MemberType != MemberTypes.Property)
            return base.VisitMember(node);
        var propertyInfo = node.Member as PropertyInfo;
        if (propertyInfo == null)
            return base.VisitMember(node);
        if (!typeof (ITranslation).IsAssignableFrom(propertyInfo.PropertyType))
            return base.VisitMember(node);
        if (!string.Equals(propertyInfo.Name, "Current"))
            return base.VisitMember(node);

        // at this point we can be confident that the property is [someTranslatableObject].Translations.Current

    }
}

此时如何访问针对 Current 属性编写的代码?

例如,当表达式是

p => p.Translations.Current.Name.ToLower().Contains("abc")

我怎样才能访问

.Name.ToLower().Contains("abc")

建议和帮助将不胜感激!

4

1 回答 1

2

因此,首先我们将使用以下辅助方法来组合表达式。它允许组合表达式,而该组合在外部不可见。此Compose方法将采用 LambadExpression 和另一个输入与第一个输出相同的类型。如果这些是函数,我们只需调用一个并将其结果作为另一个的输入传递。由于这些是表达式,因此比这要复杂一些。我们需要使用表达式访问者将一个参数的所有实例替换为另一个参数的主体。

这个辅助函数需要的访问者:

public class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

方法本身:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = new ReplaceVisitor(first.Parameters.First(), param)
        .Visit(first.Body);
    var newSecond = new ReplaceVisitor(second.Parameters.First(), newFirst)
        .Visit(second.Body);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

请注意,这是取自我以前的答案

现在我们可以使用此Compose方法创建一个方法,该方法将 a 接收LambdaExpression到 aTranslationCollection并返回映射到表示当前文化LambdaExpression的单个对象的同一对象的 a 。ITranslation至此,我们已经完成了大部分工作:

public static Expression<Func<T, TTranslation>> SelectCurrent<T, TTranslation>
    (Expression<Func<T, TranslationCollection<TTranslation>>> expression)
    where TTranslation : ITranslation
{
    return expression.Compose(collection => 
        collection.FirstOrDefault(t => t.Culture == CultureInfo.CurrentUICulture.Name));
}

现在举个例子。我们可以获取可查询的产品,用于SelectCurrent获取当前翻译,然后Compose将该翻译映射到我们要应用的实际过滤器:

public static void Foo()
{
    IQueryable<Product> products = null;
    var query = products.Where(
        SelectCurrent((Product p) => p.Translations)
        .Compose(translation => translation.Name.ToLower().Contains("abc")));
}
于 2013-10-28T16:26:46.110 回答