Understanding Lambda Expressions - Part 4
From Tutorials
Expression Trees
Sometimes you might need to write lambda expressions based on something the user types in dynamically in the GUI that can not be determined until the user is using the program.
Expression Trees are like reflection for lambda expressions.
In this example, assume the UI lets the user choose the field and the value that they want to filter a list down to.
[TestMethod] public void ExpressionTrees_DynamicCreation() { // Here are the inputs from the user, they want // all the users with an age greater than 39. String UserField = "Age"; String UserOperator = ">"; String UserValue = "39"; String UserFunction = "Where"; // Here is the datasource we are working on. // NOTE: This is the same person class from part 3. Dictionary<String, Person> people = new Dictionary<string, Person>(); people.Add("Danny", new Person() { FirstName = "Danny", LastName = "Duo", Age = 42 }); people.Add("Foo", new Person() { FirstName = "Foo", LastName = "Fighter", Age = 47 }); people.Add("Jimmy", new Person() { FirstName = "Jimmy", LastName = "Crack", Age = 35 }); people.Add("John", new Person() { FirstName = "John", LastName = "McGrue", Age = 39 }); // Lambda expressions have the parameters inputs on the left and the body // on the right, Here we are creating left half. But we are doing it dynamically. // And it's not actually hooked into a full Lambda Expression yet. We'll just // keep this part around until it comes time to make the full Lambda Expression. // // Our end goal here is to dynamically create the complete statement of: // people.Values.Where<Person>(mahPerson=>mahPerson.Age > 39); // // Here we are creating the left 'mahPerson' (which we explain in code, it is a person). ParameterExpression peMahPerson = Expression.Parameter(typeof(Person), "mahPerson"); // Next we'll create the body for the first expression which is Age > 39 // There is a left side (Age) and right side (39) to expressions, seperated by an operator (>). // We'll create the left (Age) side, which happens to be a property of the person class. Expression left = Expression.Property(peMahPerson, UserField); // Our "39" string that was entered by the user needs to be an Int32 because Age is // an Int32, so we're using Convert to cast it to the same type that Age is. object objUserValue = Convert.ChangeType(UserValue, left.Type); // The right side of the expression as far as our dynamic code is concerned is a constant // We also specify that the value is an int32 with left.Type (The type of the left parameter (Age) is Int32). Expression right = Expression.Constant(objUserValue, left.Type); // And finally to pull the left and right into one expression we use the operator // If you happen to know SQL you can probably begin to visualize how the SQL language could be interpreted // to do queries against your custom data objects. // // We'll pretend our UI has a bunch of graphical image buttons, and we do a switch statement to determine // what the user wants. Expression body1; switch (UserOperator) { case ">": default: body1 = Expression.GreaterThan(left, right); break; } // We'll need to get a queryable representation of our data // This will be used in the MethodCallExpression below. // This way we don't have to hardcode "typeof(Person)" there, thus we are making the type dynamic. IQueryable<Person> queryableData = people.Values.AsQueryable<Person>(); // We'll built up the predicate we have so far into a lambda expression Expression lambda1 = Expression.Lambda(body1, new ParameterExpression[] { peMahPerson }); // people.Values.Where<TSource> // The where clause wants one type to make it strongly typed, which is named TSource // in the intellisense of the Where clause. // queryableData.ElementType provides that type, and since the Where clause only // needs to know about 1 type, we end up with an array of 1 type. Type[] FunctionTypes = new Type[] { queryableData.ElementType }; // Now here we could just hardcode the rest of the expression and use the where clause // directly. // people.Where(...) // but we have chosen to give the user the ability to choose this as well, so here is // how we create the call to the where. // Whenever you want an expression to call a method, it's Expression.Call // // The Expression.Call factory uses params[] to get it's options and it can be confusing, especially the last 2 params in this // example below. We are creating the full expression of: // people.Values.Where<Person>(mahPerson=>mahPerson.Age > 39); // Here is what each of the parameters below represent // // typeof(Queryable) // UserFunction - Where // FunctionTypes - <Person> // queryableData.Expression - people.Values // lambda1 - mahPerson=>mahPerson.Age > 39 MethodCallExpression functionTheyWanted1 = Expression.Call( typeof(Queryable), UserFunction, FunctionTypes, queryableData.Expression, lambda1); // Before continuing we'll just verify that this first expression we have made is correct IQueryable<Person> results = queryableData.Provider.CreateQuery<Person>(functionTheyWanted1); List<Person> peopleResults1 = results.ToList<Person>(); Assert.AreEqual(2, peopleResults1.Count); Assert.AreEqual(42, peopleResults1[0].Age); Assert.AreEqual(47, peopleResults1[1].Age); // Note that our original queryableData is still the same. // Also assume that the app lets them select as many operations, filters, etc as they // want and these values below represent the second operation the user wants to do // In this case the user picks to order by the length of the last name (descending), so the UI does not provide // an option for the operator or the value. String UserField2 = "LastName"; String UserFieldProperty2 = "Length"; String UserFunction2 = "OrderByDescending"; // Next we'll create up the orderby clause. which piggybacks of the results of the first function // We'll create up a parameter that presents a property call to Person.LastName.Length // Notice we use the Expressions.Property() factory method to create this. Expression lastName = Expression.Property(peMahPerson, UserField2); Expression lengthOfString = Expression.Property(lastName, UserFieldProperty2); // knowing that lastname is a string we could also do (if we wanted to hardcode some things) // lengthOfString = Expression.Property(lastName, typeof(string).GetProperty(UserFieldProperty2)); // we know that the OrderBy operator specified the Source and the Key as the required types, // so we'll make up a type array of those 2 types. // people.Values.OrderBy<TSource, TKey> // The OrderBy clause wants two types to make it strongly typed, which are named TSource // and TKey in the intellisense of the Where clause. // // queryableData.ElementType provides the source type // our key we are ordering by is the length of the last name, which is an int, but we can just put in lengthOfString.Type // here to give it the right types. FunctionTypes = new Type[] { queryableData.ElementType, lengthOfString.Type }; // We'll built up the keyselector we have so far into a lambda expression Expression lambda2 = Expression.Lambda(lengthOfString, new ParameterExpression[] { peMahPerson }); // Now we create our 2nd function, which represents this // people.Values.Where<Person>(mahPerson=>mahPerson.Age > 39).OrderByDescending<Person, Int32>(mahPerson=>mahPerson.LastName.Length); // note here that we are using functionTheyWanted1 to build our statement. // // again each of the parameters make up part of the statement above // typeof(Queryable) // userfunction2 - OrderBy // FunctionTypes - <Person, Int32> // functionTheyWanted1 - people.Values.Where<Person>(mahPerson=>mahPerson.Age > 39) // lambda2 - (mahPerson=>mahPerson.LastName.Length) MethodCallExpression functionTheyWanted2 = Expression.Call( typeof(Queryable), UserFunction2, FunctionTypes, functionTheyWanted1, lambda2); List<Person> peopleResults2 = queryableData.Provider.CreateQuery<Person>(functionTheyWanted2).ToList(); Assert.AreEqual(2, peopleResults2.Count); // Note: these 2 records are now in a different order than they were in peopleResults1 due to the orderbydecending Assert.AreEqual(47, peopleResults2[0].Age); Assert.AreEqual(42, peopleResults2[1].Age); // NOTE: once you have many left/right/operator combinations and you want to put them together into one statement you can // put them together using Expression.OrElse() and Expression.AndAlso(); }
Expression Tree Visitor Basics
In the above example we showed how to create Lambda expressions dynamically. In this example we'll show how to write code that will examine an already created Lambda expression and identify each part.
[TestMethod] public void ExpressionTrees_Visitors_Basics() { // Create an expression tree. Expression<Func<int, bool>> exprTree = num => num < 5; // There are many ExpressionTypes (50+), the one we created above is ExpressionType.Lambda // The lambda expression only takes 1 parameter (num) as we can see above. // An Expression Tree can have a number of parameters, in that case you might use a for each loop // to look at all of them. // In our case we know we only have one, so we are using .Parameters[0] to get at it. // param is an ExpressionType.Parameter ParameterExpression param = (ParameterExpression)exprTree.Parameters[0]; // The operation here is an ExpressionType.LessThan, which falls into a greater category of Binary Expressions // When BinaryExpressions are evaluated they return true or false. // All binary expressions have a left and right side to the expression, so we dig into that below. BinaryExpression operation = (BinaryExpression)exprTree.Body; // The Left ends up being an ExpressionType.Parameter as well, it's our call to whatever the variable "num" is set to. ParameterExpression left = (ParameterExpression)operation.Left; // right is an ExpressionType.Constant, nothing about it needs to be resolved any further. ConstantExpression right = (ConstantExpression)operation.Right; // As you can see we've only covered 4 ExpressionTypes, to build a class that would be able to parse all the // ExpressionTypes and turn it into a string (or a sql statement for example) can take a bunch of code. // Above we are casting the types directly to BinaryExpression, ParameterExpression, etc because we know that // is what they are, but in a truely dynamic parser we wouldn't know ahead of time. // Instead you would want to check the exp.NodeType for each part of the tree and depending on the NodeType // proceed from there. String basics = param.Name + "=>" + left.Name + " " + operation.NodeType + " " + right.Value; Assert.AreEqual("num=>num LessThan 5", basics); }
Here we have gone through the basics of "de-compiling" a Lambda Expression.
