Search code examples
c#.netormexpression-trees

Getting an expression tree from inside a method's body


Context:

I'm making an Expression parser, which would take my LINQ queries, and turn them into specific byte arrays. Think ORM for a custom data storage, euhm, thing. I'll use SQL in my examples for familiarity.

class ExpressionParser<T>
{
    public string ParseWhere(Expression<Func<T, bool>> predicate)
    {
        // Takes an expression, follows the expression tree, building an SQL query.
    }
}

Example:

Take an example class FooData with a few dummy properties:

class FooData
{
    public int Status { get; set; }
    public bool Active { get; set; }
}
var parser = new ExpressionParser<FooData>();
var query = parser.ParseWhere(foo => foo.Active && (foo.Status == 3 || foo.Status == 4));
// Builds "WHERE active AND (status = 3 OR status = 4)"

This works great, my parser runs through the expression tree, builds a WHERE statement, and returns it.

Problem:

Now I see that, for example, Active && (Status == 3 || Status == 4) is a special case that will be used all over the whole project. So naturally I extract it to a computed property:

class FooData
{
    public int Status { get; set; }
    public bool Active { get; set; }
    public bool IsSpecialThing => Active && (Status == 3 || Status == 4);
}
var query = parser.ParseWhere(foo => foo.IsSpecialThing);

Should the expression be evaluated, the result would be the same. However, this doesn't work any more. Instead of a full expression tree that I can make a query from, all I get is a tree with one PropertyExpression that tells me nothing.

I tried changing it to a method, adding a [MethodImpl(MethodImplOptions.AggressiveInlining)] attribute, nothing seems to make Expression look inside my method / property.

Question:

Is it possible to make an Expression look deeper - into a property getter / method body? If not - is there an alternative to Expression that would?

If it's not possible at all, what should one do in this case? It would really suck to copy-paste long parts of queries tens (hundreds?) of times in a project.


Solution

  • The problem here is that this:

    public bool IsSpecialThing => Active && (Status == 3 || Status == 4);
    

    Is equivalent to this:

    public bool IsSpecialThing { get { return Active && (Status == 3 || Status == 4); } }
    

    Note that they're both compiled methods. You can see this because the type is Func<FooData,bool>, rather than Expression<Func<FooData,bool>>. Short answer: No, you can't inspect it*

    If you replace your class definition with this:

    public class FooData
    {
        public int Status { get; set; }
        public bool Active { get; set; }
    
        public static Expression<Func<FooData, bool>> IsSpecialThing = (foo) => foo.Active && (foo.Status == 3 || foo.Status == 4); 
    }
    

    You can then use it as follows:

    var parser = new ExpressionParser<FooData>();
    var query = parser.ParseWhere(FooData.IsSpecialThing);
    

    Note that this raises more difficulties. I'm assuming you'd want to write something like:

    ParseWhere(f => f.IsSpecialThing() && f.SomethingElse)
    

    The problem here is that IsSpecialThing is it's own lambda function, with it's own parameters. So it would be equivalent of writing:

    ParseWhere(f => (ff => IsSpecialThing(ff)) && f.SomethingElse)
    

    To combat this, you'd need to write a few helper methods which let you AND and OR LambdaExpressions properly:

    public class ParameterRewriter<TArg, TReturn> : ExpressionVisitor
    {
        Dictionary<ParameterExpression, ParameterExpression> _mapping;
        public Expression<Func<TArg, TReturn>> Rewrite(Expression<Func<TArg, TReturn>> expr, Dictionary<ParameterExpression, ParameterExpression> mapping)
        {
            _mapping = mapping;
            return (Expression<Func<TArg, TReturn>>)Visit(expr);
        }
    
        protected override Expression VisitParameter(ParameterExpression p)
        {
            if (_mapping.ContainsKey(p))
                return _mapping[p];
            return p;
        }
    }
    

    The above will take a mapping between parameters, and replace them in the given expression tree.

    Leveraging it:

    public static class ExpressionExtensions
    {
        public static Expression<Func<T, bool>> OrElse<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        {
            var rewrittenRight = RewriteExpression(left, right);
    
            return Expression.Lambda<Func<T, bool>>(Expression.OrElse(left.Body, rewrittenRight.Body), left.Parameters);
        }
    
        public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        {
            var rewrittenRight = RewriteExpression(left, right);
    
            return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(left.Body, rewrittenRight.Body), left.Parameters);
        }
    
        private static Expression<Func<T, bool>> RewriteExpression<T>(Expression<Func<T, bool>> left, Expression<Func<T, bool>> right)
        {
            var mapping = new Dictionary<ParameterExpression, ParameterExpression>();
            for (var i = 0; i < left.Parameters.Count; i++)
                mapping[right.Parameters[i]] = left.Parameters[i];
    
            var pr = new ParameterRewriter<T, bool>();
            var rewrittenRight = pr.Rewrite(right, mapping);
            return rewrittenRight;
        }
    }
    

    What the above essentially does is, if you write this:

    Expression<Func<FooData, bool>> a = f => f.Active;
    Expression<Func<FooData, bool>> b = g => g.Status == 5;
    Expression<Func<FooData, bool>> c = a.AndAlso(b);
    

    Will return you f => f.Active && f.Status == 5 (note how the parameter g was replaced with f.

    Putting it all together:

    var parser = new ExpressionParser<FooData>();
    var result = parser.ParseWhere(FooData.IsSpecialThing.AndAlso(f => f.Status == 6));
    


    *Note it is technically possible to parse the generated IL, but you'll be in for a hell of a time.