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