Page 48 - MSDN Magazine, September 2019
P. 48
Figure 6 Visualization of Final Expression Tree Using Expression Trees I:
Mapping Code Constructs to External APIs
Expression trees were originally designed to enable mapping of Visual Basic or C# syntax into a different API. The classic use case is generating a SQL statement, like this:
SELECT * FROM Persons WHERE Persons.LastName LIKE N'D%'
This statement could be derived from code like the following snippet, which uses member access (. operator), method calls inside the expression, and the Queryable.Where method. Here’s the code in Visual Basic:
Dim personSource As IQueryable(Of Person) = ...
Dim qry = personSource.Where(Function(x) x.LastName.StartsWith("D"))
and here’s the code in C#:
IQueryable<Person> personSource = ...
var qry = personSource.Where(x => x.LastName.StartsWith("D");
How does this work? There are two overloads that could be used with the lambda expression—Enumerable.Where and Que- ryable.Where. However, overload resolution prefers the overload in which the lambda expression is an expression lambda—that is, Queryable.Where—over the overload that takes a delegate. The compiler then replaces the lambda syntax with calls to the appro- priate factory methods.
At runtime, the Queryable.Where method wraps the passed-in expression tree with a Call node whose Method property references Queryable.Where itself, and which takes two parameters—person- Source, and the expression tree from the lambda syntax (Figure 6). (The Quote node indicates that the inner expression tree is being passed to Queryable.Where as an expression tree and not as a delegate.)
A LINQ database provider (such as Entity Framework, LINQ2- SQL or NHibernate) can take such an expression tree and map the different parts into the SQL statement at the beginning of this section. Here’s how:
• ExpresssionType.Call to Queryable.Where is parsed as a SQL WHERE clause
• ExpressionType.MemberAccess of LastName on an instance of Person becomes reading the LastName field in the Persons table—Persons.LastName
• ExpressionType.Call to the StartsWith method with a Constant argument is translated into the SQL LIKE operator, against a pattern that matches the beginning of a constant string:
LIKE N'D%'
It’s thus possible to control external APIs using code constructs and conventions, with all the benefits of the compiler—type safety and correct syntax on the various parts of the expression tree, and IDE autocompletion. Some other examples:
Creating Web Requests Using the Simple.OData.Client library (bit.ly/2YyDrsx), you can create OData requests by passing in expression trees to the various methods. The library will output the correct request (Figure 7 shows the code for both Visual Basic and C#).
Reflection by Example Instead of using reflection to get hold of a MethodInfo, you can write a
method call within an expression, and pass that expression to a function that extracts the specific overload used in the call. Here’s the reflection code first in Visual Basic:
Dim writeLine as MethodInfo = GetType(Console).GetMethod( "WriteLine", { GetType(String) })
And the same code in C#:
MethodInfo writeLine = typeof(Console).GetMethod( "WriteLine", new [] { typeof(string) });
Now here’s how this function and its usage could look in Visual Basic:
Function GetMethod(expr As Expression(Of Action)) As MethodInfo Return CType(expr.Body, MethodCallExpression).Method
End Function
Dim mi As MethodInfo = GetMethod(Sub() Console.WriteLine(""))
and here’s how it could look in C#:
public static MethodInfo GetMethod(Expression<Action> expr) => (expr.Body as MethodCallExpression).Method;
MethodInfo mi = GetMethod(() => Console.WriteLine(""));
This approach also simplifies getting at extension methods and
constructing closed generic methods, as shown here in Visual Basic:
Dim wherePerson As MethodInfo = GetMethod(Sub() CType(Nothing, IQueryable(Of Person)).Where(Function(x) True)))
It also provides a compile-time guarantee that the method and overload exist, as shown here in C#:
// Won’t compile, because GetMethod expects Expression<Action>, not Expression<Func<..>>
MethodInfo getMethod = GetMethod(() => GetMethod(() => null));
Grid Column Configuration If you have some kind of grid UI, and you want to allow defining the columns declaratively, you could
Figure 7 Requests Using the Simple.OData.Client Library and Expression Trees
' Visual Basic
Dim client = New ODataClient("https://services.odata.org/v4/ TripPinServiceRW/")
Dim people = Await client.For(Of People)
.Filter(Function(x) x.Trips.Any(Function(y) y.Budget > 3000)) .Top(2)
.Select(Function(x) New With { x.FirstName, x.LastName}) .FindEntriesAsync
// C#
var client = new ODataClient("https://services.odata.org/v4/ TripPinServiceRW/");
var people = await client.For<People>()
.Filter(x => x.Trips.Any(y => y.Budget > 3000)) .Top(2)
.Select(x => new {x.FirstName, x.LastName}) .FindEntriesAsync();
Outgoing request:
> https://services.odata.org/v4/TripPinServiceRW/People?$top=2 &
$select=FirstName, LastName & $filter=Trips/any(d:d/Budget gt 3000)
40 msdn magazine
.NET Development