Instrumentation d'un arbre d'expression - Comment obtenir le résultat calculé de chaque sous-arbre?

c# expression-trees instrumentation logging

Question

Je travaille dans Expression Trees, un moteur de règles.

Lorsque vous appelez ToString () sur un arbre d'expression, vous obtenez un joli texte de diagnostic:

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

J'ai écrit ce morceau de code pour tenter d'envelopper l'expression avec une fonction de journalisation:

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

Cela devrait me permettre d'insérer la journalisation à différents endroits de l'arborescence d'expression et d'obtenir des résultats intermédiaires ToString () lors de l'exécution de l'arborescence d'expression.

Ce que je n'ai pas encore compris, c'est comment obtenir le résultat calculé de chaque sous-expression et l'inclure dans la sortie du journal. Idéalement, j'aimerais que les résultats ressemblent à ceci, à des fins de diagnostic et d'audit:

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

Je suppose que j'ai besoin de parcourir l'arborescence à l'aide d'ExpressionVisitor et d'ajouter du code à chaque nœud, ou d'arborer et de compiler et d'exécuter chaque sous-arborescence individuellement, mais je n'ai pas encore tout à fait compris comment faire fonctionner cela.

Aucune suggestion?

Réponse acceptée

Bien que le post d’amon soit correct en théorie, il n’existe pas d’interprète (à ma connaissance) pour C # ExpressionTrees. Cependant, il existe un compilateur, et il y a un bon visiteur abstrait qui fonctionnerait bien à cette fin.

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

Je ne suis pas tout à fait sûr que ce code fonctionnera bien si vous avez un lambda imbriqué, mais si vous n'avez pas une telle chose, cela devrait le faire.


Code WithLog incorporé. Le code génère les éléments suivants:

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

Réponse populaire

Malheureusement, je n'ai pas d'expérience avec C # et les arbres d'expression, mais je connais un peu les interprètes.

Je suppose que votre arbre d'expression est une sorte d'AST où chaque nœud d'arbre est une classe dans une hiérarchie commune. Je suppose également que vous évaluez cet AST en appliquant le modèle d'interprétation, via une expr.Interpret(context) ou un visiteur ExpressionInterpreter .

Lorsque vous utilisez une méthode Interpret()

Vous voudrez introduire un nouveau type d'expression LoggedExpression avec la sémantique suivante:

  • il contient une expression
  • une fois évalué, il évalue le nœud enfant et affiche le nœud et le résultat sous forme stringifiée:
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;
  }
}

Si votre langage est plus complexe que de simples expressions booléennes, vous voudrez vous connecter avant d'évaluer, afin de pouvoir facilement déboguer les blocages, etc.

Vous devrez ensuite transformer votre arborescence d'expression en une arborescence d'expression consignée. Cela peut facilement être fait par une méthode AsLoggedExpression() , qui copie chaque nœud mais l’enveloppe dans une expression de journal:

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

Certains nœuds peuvent se renvoyer inchangés au lieu d’ajouter une journalisation, par exemple des constantes ou des expressions de journalisation elles-mêmes (l’ajout de la journalisation à une arborescence serait donc une opération idempotente).

Lors de l'utilisation d'un visiteur

Chaque méthode visit() du visiteur prend en charge l'évaluation d'un nœud d'arbre d'expression. En fonction de votre visiteur principal ExpressionInterpreter , nous pouvons dériver un LoggingExpressionInterpreter qui, pour chaque type de nœud, enregistre l'expression et l'évalue:

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

Ici, nous ne pouvons pas utiliser la composition au lieu de l'héritage car cela romprait la journalisation récursive. Il est fondamental que lors de l'évaluation d'un nœud, l'interpréteur de journalisation soit également utilisé pour tous les nœuds enfants. Si nous AcceptVisitor() héritage, les méthodes Visit() et AcceptVisitor() auraient besoin d'un argument explicite pour le visiteur, qui devrait être appliqué aux nœuds enfants.

Je préférerais fortement la méthode basée sur les visiteurs, car elle n'a pas à modifier l'arborescence des expressions et aboutit à un code moins total (je suppose).



Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow