Search code examples
c#loggingexpression-treesinstrumentation

Instrumenting an expression tree -- How to get the computed result of each subtree?


I'm doing some work in Expression Trees, a rules engine of sorts.

When you call ToString() on an Expression Tree, you get a lovely bit of diagnostic text:

 ((Param_0.Customer.LastName == "Doe") 
     AndAlso ((Param_0.Customer.FirstName == "John") 
     Or (Param_0.Customer.FirstName == "Jane")))

I wrote this bit of code, in an attempt to wrap the Expression with some logging capability:

public Expression WithLog(Expression exp)
{
    return Expression.Block(Expression.Call(
        typeof (Debug).GetMethod("Print",
            new Type [] { typeof(string) }),
            new [] { Expression.Call(Expression.Constant(exp),
            exp.GetType().GetMethod("ToString")) } ), exp);
}

This should allow me to insert logging at various places within the expression tree and get intermediate ToString() results when the expression tree executes.

What I haven't quite figured out is how to get the computed result of each sub-expression and include it in the log output. Ideally, I would like to see output that looks something like this, for diagnostic and auditing purposes:

Executing Rule: (Param_0.Customer.LastName == "Doe") --> true
Executing Rule: (Param_0.Customer.FirstName == "John") --> true
Executing Rule: (Param_0.Customer.FirstName == "Jane") --> false
Executing Rule: (Param_0.Customer.FirstName == "John") Or (Param_0.Customer.FirstName == "Jane")) --> true
Executing Rule: (Param_0.Customer.LastName == "Doe") AndAlso ((Param_0.Customer.FirstName == "John") Or (Param_0.Customer.FirstName == "Jane")) --> true

I suspect that I either need to walk the tree using ExpressionVisitor and add some code to each node, or walk the tree and compile and execute each subtree individually, but I haven't quite figured out how to make this work yet.

Any suggestions?


Solution

  • While amon's post is correct in theory, there isn't an interpreter (that I know of) for C# ExpressionTrees. However, there is a compiler, and there is a nice abstract visitor which would work well for this purpose.

    public class Program
    {
        static void Main(string[] args)
        {
    
            Expression<Func<int, bool>> x = (i => i > 3 && i % 4 == 0);
            var visitor = new GetSubExpressionVisitor();
            var visited = (Expression<Func<int, bool>>)visitor.Visit(x);
            var func = visited.Compile();
            var result = func(4);
        }
    }
    
    public class GetSubExpressionVisitor : ExpressionVisitor
    {
        private readonly List<ParameterExpression> _parameters = new List<ParameterExpression>();
    
        protected override Expression VisitLambda<T>(Expression<T> node)
        {
            _parameters.AddRange(node.Parameters);
            return base.VisitLambda(node);
        }
    
        protected override Expression VisitBinary(BinaryExpression node)
        {
            switch (node.NodeType)
            {
                case ExpressionType.Modulo:
                case ExpressionType.Equal:
                case ExpressionType.GreaterThanOrEqual:
                case ExpressionType.LessThanOrEqual:
                case ExpressionType.NotEqual:
                case ExpressionType.GreaterThan:
                case ExpressionType.LessThan:
                case ExpressionType.And:
                case ExpressionType.AndAlso:
                case ExpressionType.Or:
                case ExpressionType.OrElse:
                    return WithLog(node);
            }
            return base.VisitBinary(node);
        }
    
        public Expression WithLog(BinaryExpression exp)
        {
            return Expression.Block(
                Expression.Call(
                    typeof(Debug).GetMethod("Print", new Type[] { typeof(string) }),
                    new[] 
                    { 
                        Expression.Call(
                            typeof(string).GetMethod("Format", new [] { typeof(string), typeof(object), typeof(object)}),
                            Expression.Constant("Executing Rule: {0} --> {1}"),
                            Expression.Call(Expression.Constant(exp), exp.GetType().GetMethod("ToString")),
                            Expression.Convert(
                                exp,
                                typeof(object)
                            )
                        )
                    }
                ),
                base.VisitBinary(exp)
            );
        }
    }
    

    I'm not entirely sure how well this code will work if you have a nested lambda, but if you don't have such a thing, this should do it.


    Incorporated WithLog code. The code outputs the following:

    Executing Rule: ((i > 3) AndAlso ((i % 4) == 0)) --> True
    Executing Rule: (i > 3) --> True
    Executing Rule: ((i % 4) == 0) --> True
    Executing Rule: (i % 4) --> 0