Search code examples
c#entity-frameworkexpression-trees

Extending an access expression to check for value


I'm currently trying to wrestle with expression trees to make a bit of magic happen, but I keep hitting error after error.

I've got some properties like this on some of my domain objects (Entity Framework)

Expression<Func<DomainObject, LinkedDomainObject>> IncludeExpr
{
   get {
      return o => o.SomeLinkedObject;
   }
}

and another expression that checks that linked object for equality on some property (e.g. ID).

I did have an expression that also checked that linked object for being null, and that way I could compose a NotNull and Matched ID expression by inverting the null check expression and combining it via AndAlso with the ID check expression.

I want to take the o => o.SomeLinkedObject expression and linkedObject => linkedObject.ID == idVar expressions and mash them together to effectively get:

o => o.LinkedObject != null && o.LinkedObject.Id == idVar

But I can't for the life of me work out how I'd get an expression tree together based on those two separate expressions.


Solution

  • We can take a moment to create a helper method that can make solving this problem very straightforward. If we create a method that lets us compose expressions as easily as we can compose delegates, this becomes very easy. Our Compose method will accept an expression, and another that takes the output of the first and transforms it into something else, creating a new expression that can transform something of the type of the input of the first into the output of the second:

    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 = first.Body.Replace(first.Parameters[0], param);
        var newSecond = second.Body.Replace(second.Parameters[0], newFirst);
    
        return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
    }
    

    This is dependent on the following method to replace all instances of one expression with another:

    public static Expression Replace(this Expression expression,
        Expression searchEx, Expression replaceEx)
    {
        return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
    }
    
    internal 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);
        }
    }
    

    Now we can create an IsNotNull transformation very easily:

    public static Expression<Func<TSource, bool>> IsNotNull<TSource, TKey>(
        this Expression<Func<TSource, TKey>> expression)
    {
        return expression.Compose(key => key != null);
    }
    

    As for And-ing two expressions together, the easiest option if using a LINQ query provider is to just call Where on each expression separately, if that's an option. If not, you can use a PrediacteBuilder to And or Or two expressions together:

    public static class PredicateBuilder
    {
        public static Expression<Func<T, bool>> True<T>() { return f => true; }
        public static Expression<Func<T, bool>> False<T>() { return f => false; }
    
        public static Expression<Func<T, bool>> Or<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
        }
    
        public static Expression<Func<T, bool>> And<T>(
            this Expression<Func<T, bool>> expr1,
            Expression<Func<T, bool>> expr2)
        {
            var secondBody = expr2.Body.Replace(
                expr2.Parameters[0], expr1.Parameters[0]);
            return Expression.Lambda<Func<T, bool>>
                  (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
        }
    }