检测表达式树 - 如何获取每个子树的计算结果?

时间:2015-12-02 06:38:28

标签: c# logging expression-trees instrumentation

我在Expression Trees中做了一些工作,这是一种规则引擎。

当您在表达式树上调用ToString()时,您会得到一些可疑的诊断文本:

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

我编写了这段代码,试图用一些日志记录功能包装Expression:

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

这应该允许我在表达式树中的各个位置插入日志记录,并在表达式树执行时获得中间ToString()结果。

我还没想到的是如何获取每个子表达式的计算结果并将其包含在日志输出中。理想情况下,我希望看到类似的输出,用于诊断和审计目的:

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

我怀疑我需要使用ExpressionVisitor遍历树并向每个节点添加一些代码,或者遍历树并单独编译和执行每个子树,但我还没有弄清楚如何使这个工作爱好。

有什么建议吗?

2 个答案:

答案 0 :(得分:3)

虽然amon的帖子在理论上是正确的,但是没有C#ExpressionTrees的解释器(我知道)。但是,有一个编译器,并且有一个很好的抽象访问者可以很好地用于此目的。

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

如果你有一个嵌套的lambda,我不完全确定这段代码的效果如何,但如果你没有这样的东西,那就应该这样做。

合并WithLog代码。代码输出以下内容:

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

答案 1 :(得分:2)

不幸的是,我没有C#和表达式树的经验,但我对翻译有点了解。

我假设你的表达式树是一种AST,其中每个树节点都是一个公共层次结构中的类。我还假设您通过expr.Interpret(context)方法或ExpressionInterpreter访问者应用解释器模式来评估此AST。

使用Interpret()方法时

您将需要引入具有以下语义的新表达式LoggedExpression

  • 它包含一个表达式
  • 在评估时,它会评估子节点并打印出字符串化的子节点并生成结果:
class LoggedExpression : Expression {
  private Expression child;
  public LoggedExpression(Expression child) { ... }
  public string ToString() { return child.ToString(); }
  public bool Interpret() {
    bool result = child.Interpret();
    log("Executing rule: " + child + " --> " + result);
    return result;
  }
}

如果您的语言比简单的布尔表达式更复杂,那么您需要在评估之前进行记录,以便您可以轻松地调试挂起等。

然后,您必须将表达式树转换为已记录的表达式树。这可以通过方法AsLoggedExpression()轻松完成,该方法复制每个节点但将其包装在日志表达式中:

class Or : Expression {
  private Expression left;
  private Expression right;
  ...
  public Expression AsLoggedExpression() {
    return new LoggedExpression(new Or(left.AsLoggedExpression(), right.AsLoggedExpression()));
  }
}

某些节点可以自行返回而不是添加日志记录,例如常量或日志记录表达式本身(因此,将记录添加到树中将是幂等操作)。

使用访客时

访问者中的每个visit()方法负责评估表达式树节点。给定您的主ExpressionInterpreter访问者,我们可以派生LoggingExpressionInterpreter,为每个节点类型记录表达式并对其进行评估:

class LoggingExpressionInterpreter : ExpressionInterpreter {
  ...
  public bool Visit(Expression.Or ast) {
    bool result = base.Visit(ast);
    log("Executing rule: " + child + " --> " + result);
    return result;
  }
}

在这里,我们不能使用组合而不是继承,因为这会破坏递归日志记录。在评估任何节点时,记录解释器也用于所有子节点,这是至关重要的。如果我们取消继承,Visit()AcceptVisitor()方法将需要一个应该应用于子节点的访问者的显式参数。

我非常喜欢基于访问者的方法,因为它不必修改表达式树,导致总代码减少(我猜)。