📜 ⬆️ ⬇️

Calculate dynamic expressions in C # 4.0 with dynamic

Hello.
Yesterday I published on Habré a translation of an article about one of the new features of the fourth version of C # 4.0 - the dynamic keyword. There was a heated discussion in the comments, the main reasons for which were two things: the performance of the speakers and the area of ​​their application. In this article I will not touch upon the first question, but I will try to give an example of how a new opportunity allows you to solve a very real problem in a couple of hours with minimal effort.


Prehistory


For a better understanding of the task, I will briefly describe to you my first commercial project, which I and my colleagues were engaged in two years ago. Our task was to implement in C # 2.0 a kind of object classifier, which answered the question of whether a certain document (let it be something like a tax return) meets a certain criterion. The criterion was a complex predicate, which was formed by officers of the state service responsible for processing these documents and helped to highlight in the stream of documents those to which the service should have paid closer attention. Well, why should they not pay attention, I will not tell you, however, it is not difficult to guess :) So, the expression was stuffed by the officer through a special editor on the Ajax, after which it could be used in two ways: used in runtime to check the object in memory or generate SQL A query that allowed you to select objects that meet the criteria from the database. The first regime was subsequently removed from the system, but I still remember how difficult and at the same time interesting it was to implement it. Then everything about everything took us several months. Thanks to the new features of the C # 4.0 language, I was able to recreate a similar (although, of course, much more primitive) classifier of objects in just a couple of hours.

Implementation


So let's get started. As is known, any expression is in its essence a tree whose nodes are constants, variables, operators, and functions. Our classifier will be such a tree that will receive an object and give an answer, whether our object belongs to this class.
First, select the base class for the top of the expression tree:
public abstract class ExpressionNode
{
public abstract dynamic Evaluate();

public virtual void HandleObject( object obj)
{
}

public abstract IEnumerable <ExpressionNode> GetChildren();
}


* This source code was highlighted with Source Code Highlighter .

The Evaluate method calculates the value for the given vertex and the entire subtree, the vertex of which it is. Notice that the value returned by this method has a dynamic type. HandleObject allows you to process the object before the calculation in order to store the necessary data. GetChildren is used to navigate the tree.
Class for constants:
public class Constant : ExpressionNode
{
private readonly dynamic _value;

public Constant(dynamic value )
{
_value = value ;
}

public override dynamic Evaluate()
{
return _value;
}

public override IEnumerable <ExpressionNode> GetChildren()
{
yield break ;
}
}


* This source code was highlighted with Source Code Highlighter .

A constant is a very simple vertex of an expression that stores a value and returns it if necessary.
The class that provides access to the properties of the object being processed (variables of our expression):
public class ObjectProperty : ExpressionNode
{
private dynamic _value = null ;
private readonly string _name;
private bool _propertyNotFound;

public ObjectProperty( string name)
{
_name = name;
}

public override void HandleObject( object obj)
{
try
{
_value = obj.GetType().GetProperty(_name).GetValue(obj, new object [0]);
}
catch (Exception ex)
{
_propertyNotFound = true ;
_value = null ;
}
}

public override dynamic Evaluate()
{
if (_propertyNotFound)
{
throw new PropertyNotFoundException();
}
return _value;
}

public override IEnumerable <ExpressionNode> GetChildren()
{
yield break ;
}
}

* This source code was highlighted with Source Code Highlighter .

Very similar to the constant, but the value is calculated for each new object during its processing. Note that if the object has no property, the exception will be thrown only at the stage of calculation, and not at the stage of processing. This feature allows you to create complex expressions with lazy calculations.
To check whether a property exists on an object, the following function is used:
public class HasPropertyChecker : ExpressionNode
{
private readonly string _name;
private bool _result;

public HasPropertyChecker( string name)
{
_name = name;
}

public override dynamic Evaluate()
{
return _result;
}

public override IEnumerable <ExpressionNode> GetChildren()
{
yield break ;
}

public override void HandleObject( object obj)
{
_result = obj.GetType().GetProperty(_name) != null ;
}
}

* This source code was highlighted with Source Code Highlighter .

The remaining vertices of the expression are operators; they can be of three types: unary, binary, and aggregator.
Base class for unary:
public abstract class UnaryOperator : ExpressionNode
{
private readonly ExpressionNode _argument;
protected UnaryOperator(ExpressionNode arg)
{
_argument = arg;
}

protected abstract dynamic EvaluateUnary(dynamic arg);

public override dynamic Evaluate()
{
return EvaluateUnary(_argument.Evaluate());
}

public override IEnumerable <ExpressionNode> GetChildren()
{
yield return _argument;
}
}

* This source code was highlighted with Source Code Highlighter .

And the simplest example of such an operator is logical negation:
[Operator( "not" )]
public class Not : UnaryOperator
{
public Not( params ExpressionNode[] args) : this (args[0]) { Debug.Assert(args.Length == 1); }
public Not(ExpressionNode arg) : base (arg) { }

protected override dynamic EvaluateUnary(dynamic arg)
{
return !arg;
}
}


* This source code was highlighted with Source Code Highlighter .

Notice that the statement is marked with a special attribute that defines its name. It will come in handy later to form an XML-based tree.
The basic binary operator and its example, the addition operator:
public abstract class BinaryOperator : ExpressionNode
{
private readonly ExpressionNode _argument1;
private readonly ExpressionNode _argument2;

protected BinaryOperator(ExpressionNode arg1, ExpressionNode arg2)
{
_argument1 = arg1;
_argument2 = arg2;
}

protected abstract dynamic EvaluateBinary(dynamic arg1, dynamic arg2);

public override dynamic Evaluate()
{
return EvaluateBinary(_argument1.Evaluate(), _argument2.Evaluate());
}

public override IEnumerable <ExpressionNode> GetChildren()
{
yield return _argument1;
yield return _argument2;
}
}

[Operator( "add" )]
public class Add : BinaryOperator
{
public Add( params ExpressionNode[] args) : this (args[0], args[1]) { Debug.Assert(args.Length == 2); }
public Add(ExpressionNode arg1, ExpressionNode arg2) : base (arg1, arg2) { }

protected override dynamic EvaluateBinary(dynamic arg1, dynamic arg2)
{
return arg1 + arg2;
}
}

* This source code was highlighted with Source Code Highlighter .

And, finally, the basic aggregation operator and its example, the logical "AND":
public abstract class AgregateOperator : ExpressionNode
{
private readonly ExpressionNode[] _arguments;

protected AgregateOperator( params ExpressionNode[] args)
{
_arguments = args;
}

protected abstract dynamic EvaluateAgregate( IEnumerable <dynamic> args);

public override dynamic Evaluate()
{
return EvaluateAgregate(_arguments.ConvertAll(x => x.Evaluate()));
}

public override IEnumerable <ExpressionNode> GetChildren()
{
return _arguments;
}
}

[Operator( "and" )]
public class And : AgregateOperator
{
public And( params ExpressionNode[] args) : base (args) { }

protected override dynamic EvaluateAgregate( IEnumerable <dynamic> args)
{
foreach (dynamic arg in args)
{
if (!arg)
{
return arg;
}
}
return true ;
}
}

* This source code was highlighted with Source Code Highlighter .

The whole tree (in the form of a link to a vertex-root) is stored in a wrapper class:
public class BooleanExpressionTree
{
public BooleanExpressionTree(ExpressionNode root)
{
Root = root;
}

public ExpressionNode Root { get ; private set ; }

public bool EvaluateOnObject(dynamic obj)
{
lock ( this )
{
PrepareTree(obj, Root);
return ( bool )Root.Evaluate();
}
}

private void PrepareTree(dynamic obj, ExpressionNode node)
{
node.HandleObject(obj);
foreach (ExpressionNode child in node.GetChildren())
{
PrepareTree(obj, child);
}
}
}

* This source code was highlighted with Source Code Highlighter .

The expressions themselves are stored as XML and are generated in runtime. Who cares how this happens, you can look at the source - everything is trivial.
')

Test objects


So, for testing, I prepared several different classes (yes, they are strange, but, believe me, it’s more interesting with them than with tax returns):
public class Human
{
public object Legs { get { return new object (); } }
public object Hands { get { return new object (); } }
public int LegsCount { get { return 2; } }
public int HandsCount { get { return 2; } }
public string Name { get ; set ; }
}

public class OldMan : Human
{
public DateTime BirthDate { get { return new DateTime (1933, 1, 1); } }
}

public class Baby : Human
{
public DateTime BirthDate { get { return new DateTime (2009, 1, 1); } }
}

public class Animal
{
public int LegsCount { get { return 4; } }
public object Tail { get { return new object (); } }
public string Name { get ; set ; }
}

public class Dog : Animal
{
public DateTime BirthDate { get ; set ; }
}

* This source code was highlighted with Source Code Highlighter .

Create instances of test objects:
static IEnumerable < object > PrepareTestObjects()
{
List < object > objects = new List < object >();
objects.Add( new Human { Name = "Some Stranger" });
objects.Add( new OldMan { Name = "Ivan Petrov" });
objects.Add( new OldMan { Name = "John Smith" });
objects.Add( new Baby { Name = "Vasya Pupkin" });
objects.Add( new Baby { Name = "Bart Simpson" });
objects.Add( new Dog { Name = "Sharik" , BirthDate = new DateTime (2004, 11, 11) });
objects.Add( new Dog { Name = "Old Zhuchka" , BirthDate = new DateTime (1900, 11, 11) });
return objects;
}

* This source code was highlighted with Source Code Highlighter .


Expressions


Examples of expressions used in testing:
IsHuman:
< and >
< has-property name ="Hands" />
< has-property name ="Legs" />
< not >
< has-property name ="Tail" />
</ not >
< equals >
< property name ="HandsCount" />
< constant value ="2" type ="int" />
</ equals >
< equals >
< property name ="LegsCount" />
< constant value ="2" type ="int" />
</ equals >
</ and >
IsNotAmerican:
< and >
< has-property name ="Name" />
< or >
< like >
< property name ="Name" />
< constant value ="Ivan" type ="string" />
</ like >
< like >
< property name ="Name" />
< constant value ="Vasya" type ="string" />
</ like >
</ or >
</ and >
IsOld:
< and >
< has-property name ="BirthDate" />
< gt >
< sub >
< constant value ="2009-12-23" type ="datetime" />
< property name ="BirthDate" />
</ sub >
< constant value ="22000.00:00:00" type ="timespan" />
</ gt >
</ and >

* This source code was highlighted with Source Code Highlighter .

I think it is clear from the appearance of XML that they check these expressions (they are purposely primitive, like the objects themselves, so as not to bore readers with unnecessary complexity). Here is how these expressions classify the generated objects:
Test Is Human:
Some Stranger - True
Ivan Petrov - True
John Smith - True
Vasya Pupkin - True
Bart Simpson - True
Sharik - False
Old Zhuchka - False
Test Is Old:
Some Stranger - False
Ivan Petrov - True
John Smith - True
Vasya Pupkin - False
Bart Simpson - False
Sharik - False
Old Zhuchka - True
Test Is Not American:
Some Stranger - False
Ivan Petrov - True
John Smith - False
Vasya Pupkin - True
Bart Simpson - False
Sharik - False
Old Zhuchka - False


findings


The speakers introduced to C # completely new features that allow you to quickly, using minimal code, to implement a number of non-trivial tasks, such as the computation of dynamic expressions. This feature often has to be paid for with performance, but sometimes it should be ignored, in return for a compact, well-readable code that is easy to debug and maintain. Of course, in this case, you can do with standard means of reflection, apply code generation or even use another, possibly initially dynamic language (the same Python), but, IMHO, the use of speakers in this case is the simplest and most beautiful solution.
Download the code here (60 Kb).
Progg it

Source: https://habr.com/ru/post/79258/


All Articles