Chapter 7. Expression Evaluation

7.1. Introduction

The Spring.Expressions namespace provides a powerful expression language for manipulating an object at runtime. The language supports setting and getting of property values, property assignment, method invocation, accessing the context of arrays, collections and indexers, logical and arithemtic operators, named variables, and retrieval of objects by name from Spring's IoC container.

The functionality provided in this namespace serves as the foundation for a variety of other features in Spring.NET such as enhanced property evaluation in the XML based configuration of the IoC container, a Data Validation framework, and a Data Binding framework for ASP.NET. You will likely find other cool uses for this library in your own work where run-time evaluation of criteria based on an object's state is requried. For those with a Java background, the Spring.Expressions namespace provides functionality similar to the Java based Object Graph Navigation Language, OGNL

This chapter covers the features of the expression language using an Inventor and Inventor's Society class as the target objects for expression evaluation. The class declarations and the data used to populate them are listed at the end of the chapter in section Section 7.4, “Classes used in the examples”. These classes are blatently taken from the NUnit tests for the Expressions namespace which you can refer to for additional example usage.

7.2. Evaluating Expressions

The central class in Spring.Expressions is ExpressionEvaluator whose methods are shown below.

public static object GetValue(object root, string expression);

public static object GetValue(object root, string expression, IDictionary variables)

public static void SetValue(object root, string expression, object newValue)

public static void SetValue(object root, string expression, IDictionary variables, object newValue)

The first argument is the 'root' object that the expression string (2nd argument) will be evaluated against. The third argument is used to to support variables in the expression and will be discussed later. Simple usage to get the value of an object property is shown below using the Inventor class. You can find the class listing in section Section 7.4, “Classes used in the examples”.

Inventor tesla = new Inventor("Nikola Tesla", new DateTime(1856, 7, 9), "Serbian");

tesla.PlaceOfBirth.City = "Smiljan";

string evaluatedName = (string) ExpressionEvaluator.GetValue(tesla, "Name"); 

string evaluatedCity = (string) ExpressionEvaluator.GetValue(tesla, "PlaceOfBirth.City"));

The value of 'evaluatedName' is 'Nikola Tesla' and that of 'evaluatedCity' is 'Smiljan'. A period is used to navigate the nested properties of the object. Similarly to set the property of an object, say we want to rewrite history and change Tesla's city of birth, we would simply add the following line

ExpressionEvaluator.SetValue(tesla, "PlaceOfBirth.City", "Novi Sad");

The other class in the namespace you may have occasion to use is Expression. It is useful if you intend to perform many evaluations of an expression against the same object and want to increase the performance of the evaluation. The motivation for this class is that the ExpressionEvaluator classes parses the expression on each invocation of GetValue or SetValue. The Expression class on the other hand will cache the parsed expression as well as reflection look-ups for increased performance. The methods of this class are listed below

public static IExpression Parse(string expression)

public override object Get(object context, IDictionary variables)

public override void Set(object context, IDictionary variables, object newValue)

The retrieval of the Name property in the previous example using the Expression class is shown below

IExpression exp = Expression.Parse("Name");

string evaluatedName = (string) exp.GetValue(tesla, null);

There are a few exception classes to be aware of when using the ExpressionEvaluator. These are InvalidPropertyException, when you refer to a property that doesn't exist, NullValueInNestedPathException, when a null value is encountered when traversing through the nested property list, and ArgumentException and NotSupportedException when you pass in values that are in error in some other manner.

The expression language is based on a grammar and uses ANTLR to construct the lexer and parser. Errors relating to bad syntax of the langage will be caught at this level of the language implementation. For those interested in the digging deeper into the implementation, the grammar file is named Expression.g and is located in the src directory of the namespace. As a side note, the release version of the ANTLR DLL included with Spring.NET was signed with the Spring.NET key. Upcoming releases of ANTLR will provide strongly signed assemblies.

7.3. Language Reference

7.3.1. Literal expressions

The types of literal expressions supported are strings, dates, numeric values (int, real, and hex), boolean and null. String are delimited by single quotes. To put a single quote itself in a string use the backslash character. The following listing shows simple usage of literals. Typically they would not be used in isolation like this, but as part of a more complex expression, for example using a literal on one side of a logical comparison operator.

string helloWorld = (string) ExpressionEvaluator.GetValue(null, "'Hello World'"); // evals to "Hello World"

string tonyPizza  = (string) ExpressionEvaluator.GetValue(null, "'Tony\\'s Pizza'"); // evals to "Tony's Pizza"

double avogadrosNumber = (double) ExpressionEvaluator.GetValue(null, "6.0221415E+23");

int maxValue = (int)  ExpressionEvaluator.GetValue(null, "0x7FFFFFFF");  // evals to 2147483647

DateTime birthday = (DateTime) ExpressionEvaluator.GetValue(null, "date('1974/08/24')");

DateTime exactBirthday = 
  (DateTime) ExpressionEvaluator.GetValue(null, " date('19740824T131030', 'yyyyMMddTHHmmss')");

bool trueValue = (bool) ExpressionEvaluator.GetValue(null, "true");

object nullValue = ExpressionEvaluator.GetValue(null, "null");

Note that the extra backslash character in Tony's Pizza is to satisfy C# escape syntax. Numbers support the use of the negative sign, exponential notation, and decimal points. By default real numbers are parsed using Double.Parse unless the format character "M" or "F" is supplied, in which case Decimal.Parse and Single.Parse would be used respectfully. As shown above, if two arguments are given to the date literal then DateTime.ParseExact will be used. Note that all parse methods of classes that are used internally reference the CultureInfo.InvariantCulture.

7.3.2. Properties, Arrays, Lists, Dictionaries, Indexers

As shown in the previous example in Section 7.2, “Evaluating Expressions”, navigating through properties is easy, just use a period to indicate a nested property value. The instances of Inventor class, pupin and tesla, were populated with data listed in section Section 7.4, “Classes used in the examples”. To navigate "down" and get Tesla's year of birth and Pupin's city of birth the following expressions are used

int year = (int) ExpressionEvaluator.GetValue(tesla, "DOB.Year"));  // 1856

string city = (string) ExpressionEvaluator.GetValue(pupin, "PlaCeOfBirTh.CiTy");  // "Idvor"

For the sharp-eyed, that isn't a typo in the property name for place of birth. The expression uses mixed cases to demonstrate that the evaluation is case insensitive.

The contents of arrays and lists are obtained using square bracket notation.

// Inventions Array
string invention = (string) ExpressionEvaluator.GetValue(tesla, "Inventions[3]"); // "Induction motor"

// Members List
string name = (string) ExpressionEvaluator.GetValue(ieee, "Members[0].Name"); // "Nikola Tesla"

// List and Array navigation
string invention = (string) ExpressionEvaluator.GetValue(ieee, "Members[0].Inventions[6]") // "Wireless communication"

The contents of dictionaries are obtained by specifying the literal key value between single quotes.

// Officer's Dictionary
Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers['president']";

string city = (string) ExpressionEvaluator.GetValue(ieee, "Officers['president'].PlaceOfBirth.City"); // "Idvor"

ExpressionEvaluator.SetValue(ieee, "Officers['advisors'][0].PlaceOfBirth.Country", "Croatia");

You may also specify non literal values in place of the quoted literal values by using another expression inside the square brackets such as variable names or static properties/methods on other types. These features are discussed on other sections.

Indexers are similarly referenced using square brackets. The following is a small example that shows the use of indexers. Multidimensional indexers are also supported.

public class Bar
{
    private int[] numbers = new int[] {1, 2, 3};

    public int this[int index]
    {
        get { return numbers[index];}
        set { numbers[index] = value; }
    }
}

Bar b = new Bar();

int val = (int) ExpressionEvaluator.GetValue(bar, "[1]") // evaluated to 2

ExpressionEvaluator.SetValue(bar, "[1]", 3);  // set value to 3

7.3.3. Methods

Methods are invoked in typical C# programming syntax. You may also invoke methods on literals.

//string literal 
char[] chars = (char[]) ExpressionEvaluator.GetValue(null, "'test'.ToCharArray(1, 2)"))  // 't','e'

//date literal 
int year = (int) ExpressionEvaluator.GetValue(null, "date('1974/08/24').AddYears(31).Year") // 2005

// object usage, calculate age of tesla navigating from the IEEE society.

ExpressionEvaluator.GetValue(ieee, "Members[0].GetAge(date('2005-01-01')") // 149 (eww..a big anniversary is coming up ;)

7.3.4. Operators

7.3.4.1. Relational operator

The relational operators; equal, not equal, less than, less than or equal, greater than, and greater than or equal are supported using standard operator notation. These operators take into account if the object implements the IComparable interface. Enumerations are also supported but you will need to register the enumeration type, as described in Section Section 7.3.8, “Type Registration”, in order to use an enumeration value in an expression if it is not contained in the mscorlib.

ExpressionEvaluator.GetValue(null, "2 == 2")  // true

ExpressionEvaluator.GetValue(null, "date('1974-08-24') != DateTime.Today"  // true

ExpressionEvaluator.GetValue(null, "2 < -5.0") // false

ExpressionEvaluator.GetValue(null, "DateTime.Today <= date('1974-08-24')") // false

ExpressionEvaluator.GetValue(null, "'Test' >= 'test'") // true

Enumerations can be evaluated as shown below

FooColor fColor = new FooColor();

ExpressionEvaluator.SetValue(fColor, "Color", KnownColor.Blue);

bool trueValue = (bool) ExpressionEvaluator.GetValue(fColor, "Color == KnownColor.Blue"); //true

Where FooColor is the following class.

public class FooColor
{
    private KnownColor knownColor;

    public KnownColor Color
    {
        get { return knownColor;}
        set { knownColor = value; }
    }
}

7.3.4.2. Logical operators

The logical operators that are supported are and, or, and not. Their use is demonstrated below

// AND
bool falseValue = (bool) ExpressionEvaluator.GetValue(null, "true and false"); //false

string expression = @"IsMember('Nikola Tesla') and IsMember('Mihajlo Pupin')";
bool trueValue = (bool) ExpressionEvaluator.GetValue(ieee, expression);  //true

// OR
bool trueValue = (bool) ExpressionEvaluator.GetValue(null, "true or false");  //true

string expression = @"IsMember('Nikola Tesla') or IsMember('Albert Einstien')";
bool trueValue = (bool) ExpressionEvaluator.GetValue(ieee, expression); // true

// NOT
bool falseValue = (bool) ExpressionEvaluator.GetValue(null, "!true");

// AND and NOT
string expression = @"IsMember('Nikola Tesla') and !IsMember('Mihajlo Pupin')";
bool falseValue = (bool) ExpressionEvaluator.GetValue(ieee, expression);

7.3.4.3. Mathmatical operators

The addition operator can be used on numbers, string, dates. Subtraction can be used on numbers and dates. Multiplication and division can be used only on numbers. Other mathmatical operators supported are modulus (%) and exponential power (^). Standard operator precedence is enforced. These operators are demonstrated below

// Addition
int two = (int)ExpressionEvaluator.GetValue(null, "1 + 1"); // 2

String testString = (String)ExpressionEvaluator.GetValue(null, "'test' + ' ' + 'string'"); //'test string'

DateTime dt = (DateTime)ExpressionEvaluator.GetValue(null, "date('1974-08-24') + 5"); // 8/29/1974
            
// Subtraction

int four = (int) ExpressionEvaluator.GetValue(null, "1 - -3"); //4

Decimal dec = (Decimal) ExpressionEvaluator.GetValue(null, "1000.00m - 1e4"); // 9000.00

TimeSpan ts = (TimeSpan) ExpressionEvaluator.GetValue(null, "date('2004-08-14') - date('1974-08-24')"); //10948.00:00:00

// Multiplication

int six = (int) ExpressionEvaluator.GetValue(null, "-2 * -3"); // 6

int twentyFour = (int) ExpressionEvaluator.GetValue(null, "2.0 * 3e0 * 4"); // 24

// Division

int minusTwo = (int) ExpressionEvaluator.GetValue(null, "6 / -3"); // -2

int one = (int) ExpressionEvaluator.GetValue(null, "8.0 / 4e0 / 2"); // 1

// Modulus

int three = (int) ExpressionEvaluator.GetValue(null, "7 % 4"); // 3

int one = (int) ExpressionEvaluator.GetValue(null, "8.0 % 5e0 % 2"); // 1

// Exponent

int sixteen = (int) ExpressionEvaluator.GetValue(null, "-2 ^ 4"); // 16

// Operator precedence

int minusFortyFive = (int) ExpressionEvaluator.GetValue(null, "1+2-3*8^2/2/2"); // -45

7.3.5. Assignment

Setting of a property is done by using the equals operator. This would typically be done within a call to GetValue since in the simple case SetValue offers the same functionality. Assignment in this manner is useful when combining multiple operators in an expression list, discussed in the next section. Some examples of assignment are shown below

Inventor inventor = new Inventor();
String aleks = (String) ExpressionEvaluator.GetValue(inventor, "Name = 'Aleksandar Seovic'");
DateTime dt = (DateTime) ExpressionEvaluator.GetValue(inventor, "DOB = date('1974-08-24')");

//Set the vice president of the society
Inventor tesla = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers['vp'] = Members[0]");

7.3.6. Expression lists

Multiple expressions can be executed at the same time by separating them with a semicolon and enclosing the entire expression within curly braces. The value returned is the value of the last expression in the list. Examples of this are shown below

//Perform property assignments and then return name property.

String pupin = (String) ExpressionEvaluator.GetValue(ieee.Members, 
  "{ [1].PlaceOfBirth.City = 'Beograd'; [1].PlaceOfBirth.Country = 'Serbia'; [1].Name}"));

// pupin = "Mihajlo Pupin"

7.3.7. Types

To represent a System.Type in an expression use the following syntax.

Type dateType = (Type) ExpressionEvaluator.GetValue(null, "type('System.DateTime')"

Type evalType = (Type) ExpressionEvaluator.GetValue(null, "type('Spring.Expressions.ExpressionEvaluator, Spring.Core')"

bool trueValue = (bool) ExpressionEvaluator.GetValue(tesla, "type('System.DateTime') == DOB.GetType()")

The implemention delegates to Spring's ObjectUtils.ResolveType for the actual type resolution.

7.3.8. Type Registration

To refer to a type within an expression that is not in the mscorlib you need to register it with the TypeRegistry. This will allow you to refer to a shorthand name of the type within your expressions. This is commonly used in expression that use the new operator or refer to a static properties of an object. Example usage is shown below.

TypeRegistry.RegisterType("Society", typeof(Society));

Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers[Society.President]");

7.3.9. Constructors

Constructors can be invoked using the new operator. For classes outside mscorlib you will need to register your types so they can be resolved. Examples of using constructors are shown below

// simple ctor
DateTime dt = (DateTime) ExpressionEvaluator.GetValue(null, "new DateTime(1974, 8, 24)");

// Register Inventor type then create new inventor instance within Add method inside an expression list.  
// Then return the new count of the Members collection.

TypeRegistry.RegisterType("Inventor", typeof(Inventor));
int three = (int) ExpressionEvaluator.GetValue(ieee.Members, "{ Add(new Inventor('Aleksandar Seovic', date('1974-08-24'), 'Serbian')); Count}"));
            

7.3.10. Variables

Variables can referenced in the expression using the syntax #variableName. The variables are passed in and out of the expression using the dictionary parameter in ExpressionEvaluator's GetValue or SetValue methods.

public static object GetValue(object root, string expression, IDictionary variables)

public static void SetValue(object root, string expression, IDictionary variables, object newValue)

The variable name is the key value of the dictionary. Example usage is shown below;

IDictionary vars = new Hashtable();
vars["newName"] = "Mike Tesla";
ExpressionEvaluator.GetValue(tesla, "Name = #newName", vars));

You can also use the dictionary as a place to store values of the object as they are evaluated inside the expression. For example to change Tesla's first name back again and keep the old value;

ExpressionEvaluator.GetValue(tesla, "{ #oldName = Name; Name = 'Nikola Tesla' }", vars);
String oldName = (String)vars["oldName"]; // Mike Tesla

Variable names can also be used inside indexers or maps instead of literal values. For example;

vars["prez"] = "president";
Inventor pupin = (Inventor) ExpressionEvaluator.GetValue(ieee, "Officers[#prez]", vars);

7.3.10.1. The 'this' variable

7.3.10.2. The 'root' variable

7.3.11. If then else...

You can use the ternary operator for performing if-then-else conditional logic inside the expression. A minimal example is;

String aTrueString  = (String) ExpressionEvaluator.GetValue(null, "false ? 'trueExp' : 'falseExp'") // trueExp

In this case, the boolean false results in returning the string value 'trueExp'. A less artificial example is shown below

ExpressionEvaluator.SetValue(ieee, "Name", "IEEE");
IDictionary vars = new Hashtable();
vars["queryName"] = "Nikola Tesla";

string expression = @"IsMember(#queryName) 
                      ? #queryName + ' is a member of the ' + Name + ' Society' 
                      : #queryName + ' is not a member of the ' + Name + ' Society'";

String queryResultString = (String) ExpressionEvaluator.GetValue(ieee, expression, vars));

// queryResultString = "Nikola Tesla is a member of the IEEE Society"

7.3.12. Spring Object References

Expressions can refer to objects that are declared in Spring's application context using the syntax @contextName:objectName. If no contextName is specified the default root context name (Spring.RootContext) is used. Using the application context defined in the MovieFinder example from Chapter 16, Quickstarts, the following expression returns the number of movies directed by Roberto Benigni.

public static void Main()
{
  . . .

// Retrieve context defined in the spring/context section of 
// the standard .NET configuration file.
IApplicationContext ctx = ContextRegistry.GetContext();

int numMovies = (int) ExpressionEvaluator.GetValue(null, 
                      "@MyMovieLister.MoviesDirectedBy('Roberto Benigni').Length");

  . . .
}

The variable numMovies is evaluated to 2 in this example.

7.3.13. Null Context

If you do not specify a root object, i.e. pass in null, then the expressions evaluated either have to be literal values, i.e. ExpressionEvaluator.GetValue(null, "2 + 3.14"), refer to classes that have static methods or properties, i.e. ExpressionEvaluator.GetValue(null, "DateTime.Today"), create new instances of objects, i.e. ExpressionEvaluator.GetValue(null, "new DateTime(2004, 8, 14)") or refer to other objects such as those in the variable dictionary or in the IoC container. The latter two usages will be discussed later.

7.4. Classes used in the examples

The following simple classes are used to demonstrate the functionality of the expression language.

public class Inventor
{
    public string Name;
    public string Nationality;
    public string[] Inventions;
    private DateTime dob;
    private Place pob;

    public Inventor() : this(null, DateTime.MinValue, null)
    {}

    public Inventor(string name, DateTime dateOfBirth, string nationality)
    {
        this.Name = name;
        this.dob = dateOfBirth;
        this.Nationality = nationality;
        this.pob = new Place();
    }

    public DateTime DOB
    {
        get { return dob; }
        set { dob = value; }
    }

    public Place PlaceOfBirth
    {
        get { return pob; }
    }

    public int GetAge(DateTime on)
    {
        // not very accurate, but it will do the job ;-)
        return on.Year - dob.Year;
    }
}

public class Place
{
    public string City;
    public string Country;
}

public class Society
{
    public string Name;
    public static string Advisors = "advisors";
    public static string President = "president";

    private IList members = new ArrayList();
    private IDictionary officers = new Hashtable();

    public IList Members
    {
        get { return members; }
    }

    public IDictionary Officers
    {
        get { return officers; }
    }

    public bool IsMember(string name)
    {
        bool found = false;
        foreach (Inventor inventor in members)
        {
            if (inventor.Name == name)
            {
                found = true;
                break;
            }
        }
        return found;
    }
}
The code listings in this chapter use instances of the data populated with the following information.
Inventor tesla = new Inventor("Nikola Tesla", new DateTime(1856, 7, 9), "Serbian");
tesla.Inventions = new string[]
    {
        "Telephone repeater", "Rotating magnetic field principle",
        "Polyphase alternating-current system", "Induction motor",
        "Alternating-current power transmission", "Tesla coil transformer",
        "Wireless communication", "Radio", "Fluorescent lights"
    };
tesla.PlaceOfBirth.City = "Smiljan";

Inventor pupin = new Inventor("Mihajlo Pupin", new DateTime(1854, 10, 9), "Serbian");
pupin.Inventions = new string[] {"Long distance telephony & telegraphy", "Secondary X-Ray radiation", "Sonar"};
pupin.PlaceOfBirth.City = "Idvor";
pupin.PlaceOfBirth.Country = "Serbia";

Society ieee = new Society();
ieee.Members.Add(tesla);
ieee.Members.Add(pupin);
ieee.Officers["president"] = pupin;
ieee.Officers["advisors"] = new Inventor[] {tesla, pupin};