Question

This is the idea:

Our application has a table of Products, which have translatable names. Because the amount of supported languages can expand, every product has a collection of ProductTranslation, which contains a culture (for instance 'en-US') and every translatable property.

The domain looks like this:

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

As you may have noticed, I'm using a custom collection class for the translations. (TranslationCollection instead of the default ICollection)

This class extends Collection but adds a utility property 'Current' that returns the translation that matches the current UI culture:

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

As you can see, there's very little going on here, but I like the idea of having a custom collection class for this, as it may come in handy later when we want to make some custom HTML components for display and forms.

Now the question:

When we query the products table, a search by its name would look something like this:

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

However, seeing as there will be a lot of translatable tables in the future (it's a rather large application), it would be very interesting if we could write:

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

Of course, that 'Current' property is unmapped and Entity Framework will throw an exception when you run this code. However, would it be possible to automatically convert the 'Current' call to something else, using an ExpressionVisitor (or anything else)

I've made a first attempt, but am a bit stuck:

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

    }
}

How can I access the code that is written against the Current property at this point?

For instance, when the expression is

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

how can I gain access to

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

Suggestions and help would be much appreciated!

Was it helpful?

Solution

So, to start off with we'll use the following helper method to combine expressions. It allows expressions to be composed without that composition being visible externally. This Compose method will take a LambadExpression and another who's input is the same type as the output of the first. If these were functions we would just call one and pass it's result as the input of the other. Since these are expressions it's a tad more complex than that though. We'll need to use an expression visitor to replace all instances of the parameter of one with the body of another.

The visitor that this helper function needs:

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

The method itself:

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

Note that this is taken from this previous answer of mine

Now we can use this Compose method to create a method that will take in a LambdaExpression of something to a TranslationCollection and return a LambdaExpression of that same object mapped to a single ITranslation object representing the current culture. At this point most of the work has already been done for us:

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

Now for an example usage. We can take a products queryable, use SelectCurrent to get the current translation and then Compose to map that translation to the actual filter that we want to apply:

public static void Foo()
{
    IQueryable<Product> products = null;
    var query = products.Where(
        SelectCurrent((Product p) => p.Translations)
        .Compose(translation => translation.Name.ToLower().Contains("abc")));
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top