Metaprogramming is everywhere in the DLR. All the LINQ Expression examples you saw in Chapter 2 and all the DLR Hosting API examples you saw in Chapter 6 are metaprograms. Because metaprogramming plays such a pervasive role in the DLR, we're going to dive deeper into the subject and show you some advanced and marvelous uses of metaprogramming.
We will begin with an overview of metaprogramming. The overview will discuss what metaprogramming is and where it is used in the DLR, then we'll look at one type of metaprogramming that adds and removes methods or properties to or from a class or an instance of a class. We will illustrate the metaprogramming technique in Ruby and Python, then write some infrastructure code that enables us to use the same kind of metaprogramming technique in C# through the DLR. The infrastructure code will consist of two classes called ClassMetaObject
and ExpandoClass
.
We will then take a detour and build a custom LINQ query provider. The purpose of this exercise is to illustrate a typical use of DLR Expression as a metaprogramming technique. The exciting thing about the custom LINQ query provider is that we are going to gradually evolve it into a code-generation framework that utilizes the ClassMetaObject
and ExpandoClass
classes. At the end, we will arrive at a code-generation framework that is in spirit similar to frameworks such as the popular Ruby on Rails.
Metaprograms are programs that generate or manipulate other programs, and metaprogramming is, of course, the writing of metaprograms. Based on these definitions, you can see why the LINQ Expression examples in Chapter 2 are metaprograms. They are metaprograms because, in those examples, we construct LINQ expression trees and use those trees to generate executable IL code. The code we write to construct and compile LINQ expression trees is a metaprogram. The programs manipulated by the metaprograms consist of code represented by the LINQ expression tree. Similarly, the DLR Hosting API examples in Chapter 6 are metaprograms because those examples take Python code or Ruby code as input and manipulate the code by running it and interacting with it. The DLR Hosting API examples are metaprograms and the Python code and Ruby code are the programs manipulated by the metaprograms.
It's common to see the concept of code as data in metaprograms. In the case of the LINQ Expression examples in Chapter 2, the LINQ expression trees are data that represent code elements, such as if conditions and while loops. Once code is in the form of data (i.e., expression trees), we can write metaprograms that manipulate the code as data. In the case of the DLR Hosting API examples, the Python and Ruby code is data. Once that code is in the form of data, we can write metaprograms that manipulate the code via the DLR Hosting API.
Metaprograms can generate or manipulate programs at compile time or at runtime. An example of a metaprogram that manipulates a program at compile time is code generation. If you are a C++ programmer and you use C++ macros in your code, you are doing compile-time metaprogramming. That's because, when you compile the C++ code, the macros are processed by the preprocessor to generate code that then gets compiled together with the rest of your code by the C++ compiler. The preprocessor is the metaprogram in this case and the C++ macros in your code are the programs being manipulated by the metaprogram.
Another example of compile-time metaprogramming is a compiler. A compiler is a metaprogram that manipulates the code it compiles. All of the metaprogramming we do with the DLR happens at runtime. For example, when we use the DLR Hosting API to run Python or Ruby code, we do that at runtime. When we use an expression visitor to modify an expression tree or when we compile a lambda expression into an invocable delegate, we do that at runtime. However, that's not to say that we can't do compile-time metaprogramming with the DLR. If we use an expression visitor to modify an expression tree and use that modified tree to generate some C# code at compile time, that would be compile-time metaprogramming.
One common type of metaprogramming is the adding and removing of methods or properties to and from a class at runtime. In static languages like C#, this is in general not supported because classes are compiled at compile time and can't be modified when a program runs.
There are ways to create the illusion that a compiled C# class is modified at runtime. For example, Spring.NET AOP (Aspect Oriented Programming), a library for doing aspect-oriented programming on the .NET platform, generates proxy classes of compiled C# classes at runtime. The proxy classes add aspect-related behavior to the original C# classes and create the illusion that the original C# classes are modified to behave differently. The truth is the original C# classes are not still kept intact. They are just proxied.
Note: For the sake of thoroughness, I'll point out that there is, in fact, a library that can really modify a compiled Java class as the class is being loaded by a class loader. However, that library is an exceptional case that does not invalidate my main line of discussion in this chapter.
Even though you might be able to modify a compiled Java or C# class at runtime if you are determined, doing so is generally not supported. On the other hand, many dynamic languages naturally support the addition and removal of methods and properties to and from a class at runtime. Moreover, we can also add or remove methods and properties to and from a particular instance of a class without affecting the other instances of the same class. The next part of this chapter will show you how to modify a class and its instances by adding methods or properties at runtime in Ruby, in Python, and, last but not least, in C# through the DLR.
Now we are going to write some metaprograms that manipulate programs by adding methods or properties to a class or to an instance of a class. We will do the same exercise three times for three different languages—first Ruby, then Python, and finally C# through DLR. Even though C# does not in general support the kind of metaprogramming we discuss in this section, you will see that with the DLR, C# as well as VB.NET developers can benefit the metaprogramming techniques that Ruby and Python developers enjoy. The metaprogramming technique we discuss in this section is the foundation that enables many marvelous applications. As you'll see later in this chapter, we will take what we build in this section to add methods and properties to a C# class or its instances and use it to facilitate runtime code generation.
Now we'll see how to define a class in Ruby and then dynamically modify the class. The examples will show you how to add methods to a class so that all instances of the class support those methods. You can also add methods only to a specific instance. In that case, only that specific instance will support those methods and all other instances of the same class will not.
You can try out this section's Ruby code by typing it in an interactive Ruby console. Alternatively, since I have put all the code for this section in the metaExamples.rb file in the MetaExamples project of this chapter's code download, you can simply run the MetaExamples project in debug mode. The MetaExamples project is a .NET console application. The entry point Main
method of the console application calls the RunRubyMetaExamples
method shown in Listing 8-1 to run the Ruby code in metaExamples.rb. The RunRubyMetaExamples
method uses the DLR Hosting API to run the Ruby code. We discussed the DLR Hosting API in detail in Chapter 6, so I won't duplicate that discussion here and explain the code in Listing 8-1.
Listing 8-1. C# Method That Runs the Ruby Code in metaExamples.rb
private static void RunRubyMetaExamples()
{
ScriptEngine engine = IronRuby.Ruby.CreateEngine();
engine.ExecuteFile(@"RubymetaExamples.rb");
}
To begin, let's define a Ruby class called Customer
as in Listing 8-2.
Listing 8-2. Define a Ruby Class Called Customer
class Customer
def initialize(name, age)
@name = name
@age = age
end
def to_s()
@name
end
end
The Customer
class defines two methods: initialize
and to_s
. The initialize
method will be called by the Ruby runtime when an instance of the Customer
class is created. The initialize
method takes two input parameters and stores them in two class member variables, @name
and @age
. In Ruby, the naming convention requires the name of a class member variable begin with @
. The to_s
method of the Customer
class will be called when a Customer
instance is converted to a string object, for example, when we print a Customer
instance to the console.
With the Customer
class defined, let's create a couple of instances of it and see how they work. Listing 8-3 shows this part of the example's code.
Listing 8-3. Create Instances of the Customer
Class
bob = Customer.new("Bob", 26)
mary = Customer.new("Mary", 30)
puts bob
puts mary
In Listing 8-3, we create two instances of Customer
. In Ruby, a class is an object. The name of a class is a constant that points to the class object. To create an instance of a Ruby class, you call the new
method on the name of the class, as the code Customer.new(“Bob”, 26)
in Listing 8-3 shows. Because the name of the class is a reference to the class object, what you are effectively doing is calling the new
method on the class object, which causes the initialize
method of that class to be called. After two instances of Customer
are created, we print those two instances to the screen by calling the puts
method. The method puts
is Ruby's built-in method for printing objects to the screen. Internally, puts
calls the to_s
method of the Customer
class to print out the names of Bob and Mary, our two Customer
instances.
To demonstrate the metaprogramming capability Ruby provides, let's suppose we want to modify the Customer
class so that we can set Bob and Mary as each other's spouse. To achieve this, we simply use line 2 to add a spouse attribute accessor to the Customer
class. The code in line 2 will add two accessor methods, spouse
and spouse=
, to the Customer
class. A class member variable in a Ruby class is by default private and not accessible to the world outside of the class. In order to make the @spouse
class member variable accessible to the world outside of the Customer
class, we need the code in line 2 that adds the spouse
and spouse=
accessor methods to the Customer
class. With those accessor methods, we can set Mary as Bob's spouse, as line 12 shows. When line 12 assigns the variable mary
to the spouse
attribute of bob
, the spouse=
method of bob
will be invoked. The spouse=
method in Listing 8-4 is implemented in such a way that if Mary is Bob's spouse, then Bob is also set as Mary's spouse. Notice that the code in Listing 8-4 is executed after Listings 8-3 and 8-2. That means even after instances of the Customer
class are created in Listing 8-3, we can still modify the class and the instances will just pick up the new spouse
and spouse=
accessor methods we added. Because the spouse
and spouse=
accessor methods are added to the Customer
class, all the instances of the Customer
class will support those two accessor methods.
Listing 8-4. Modify the Customer
Class in Ruby
1) class Customer
2) attr_accessor :spouse
3)
4) def spouse=(spouse)
5) if @spouse != spouse
6) @spouse = spouse
7) spouse.spouse = self
8) end
9) end
10) end
11)
12) bob.spouse = mary
13)
14) puts "Bob's spouse is " + bob.spouse.to_s
15) puts "Mary's spouse is " + mary.spouse.to_s
Listing 8-4 shows how to add methods to a class so that all instances of the class will support those methods. Listing 8-5 shows how to add a method to a particular Customer
instance, not to the Customer
class. The code in adds a calculate_late_fee
method to customer Bob and a different calculate_late_fee
method to customer Mary. Bob's late fee is 200 while Mary's is 100. Because the calculate_late_fee
method is associated with a particular instance, we can have one implementation of the method for Bob that returns 200 and another for Mary that returns 100.
Listing 8-5. Add Methods at the Instance Level
def bob.calculate_late_fee()
200
end
def mary.calculate_late_fee()
100
end
puts "Bob's late fee is " + bob.calculate_late_fee.to_s
puts "Mary's late fee is " + mary.calculate_late_fee.to_s
We just saw how to add methods to a class as well as to an instance of a class in Ruby. Now we'll look at the same example in the Python language. You can run this section's Python code listings in sequence by typing them in a Python interactive console. Alternatively, because I have put all the Python code for this section in the metaExamples.py file in the MetaExamples project of this chapter's code download, you can run the MetaExamples project's entry point Main
method. This calls the RunPythonMetaExamples
method in MetaExamples' Program.cs file to execute the Python code in metaExamples.py.
As in the previous example, our first step is to define the Customer
class in Python, as Listing 8-6 shows. The code in Listing 8-6 defines the Customer
class as a subclass of the object
class. The Customer
class contains two methods: __init__
and __str__
. The method __init__
is the constructor that will be called when new instances of the Customer
class are created. The method __str__
will be called when we convert a Customer
instance to a string representation. Methods of a class must take an explicit self
argument that represents the instance on which the methods are invoked. That's why both __init__
and __str__
have self
as an input parameter. The input parameter does not have to be named self
. This is just a naming convention that most Python programmers follow. You can think of the self
parameter as sort of Python's equivalent of the this
variable in C#. The body of the __init__
method assigns the name and age of a customer to the name and age attributes of the self
parameter. Attributes in a Python class like the ones in our example are like class member variables in a C# class. But unlike class member variables in C#, attributes in a Python class don't need to be explicitly declared. That's why you don't see the name and age attributes declared anywhere in Listing 8-6, yet the __init__
method can assign the name and age of a customer to the name and age attributes of the self
parameter.
Listing 8-6. Define the Customer Class in Python
class Customer(object):
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return self.name
If we now create instances of the Python Customer
class, as Listing 8-7 shows, we can print the string representations of those instances and expect to see the names Bob and Mary show up on the screen.
Listing 8-7. Create Instances of the Customer
Class in Python
bob = Customer("Bob", 26)
mary = Customer("Mary", 30)
print bob
print mary
Now here's the part where we add the method for setting a customer's spouse to the Customer
class. Listing 8-8 shows how to do that in Python. To add a method to an already defined class in Python, we first define a Python function by itself. In Listing 8-8, we define the set_spouse
function alone in lines 1 to 3. The body of the function ensures that if Bob is Mary's spouse, then Mary is also Bob's spouse. After the set_spouse
function is defined, we add it to the Customer
class as the set_spouse
method of the Customer
class in line 5. Because the set_spouse
method is added to the Customer
class, all instances of the Customer
class will support that method. In line 7, we test our modification to the Customer
class by calling the set_spouse
method on the variable bob
. When we do this, the set_spouse
function is called with the self
parameter set to the variable bob
. As a matter fact, we can replace line 7 in Listing 8-8 with this equivalent code set_spouse(bob, mary)
and everything will work the same. Lines 9 and 10 print out Bob's spouse and Mary's spouse to show that the testing we do in line 7 works correctly.
Listing 8-8. Modify the Customer
Class in Python
1) def set_spouse(self, spouse):
2) self.spouse = spouse
3) spouse.spouse = self
4)
5) Customer.set_spouse = set_spouse
6)
7) bob.set_spouse(mary)
8)
9) print "Bob's spouse is " + str(bob.spouse)
10) print "Mary's spouse is " + str(mary.spouse)
Next we will add methods to individual Customer
instances. To do so in Python, we make use of a standard Python module called types
. Before we can use that module, we need to do a few things so that the IronPython runtime will be able to locate the module. First, the types
module is not included as part of the DLR source code. In order to run the code in Listing 8-9, you need to download IronPython from http://ironpython.codeplex.com
and install it. I downloaded IronPython 2.6.1 for .NET 4.0 and installed it in C:Program Files (x86)IronPython 2.6 for .NET 4.0. If you install it in a different folder, you'll need to modify the path in line 2 of Listing 8-9 accordingly. The installation of IronPython places the standard Python types
module in C:Program Files (x86)IronPython 2.6 for .NET 4.0Lib. That's the path we need to add to Python's system path so that the IronPython runtime knows to look there for modules we want to import. So in line 2 of Listing 8-9, we add the path to Python's system path. In line 3 we import the types
module. Without installing IronPython and without the code in line 2, importing the types
module in line 3 would fail.
After the types
module is successfully imported, the rest of the code is pretty similar to the Ruby example we saw earlier. We first define a function called bob_late_fee
in lines 5 and 6. Then we call the MethodType
function of the types
module to associate the bob_late_fee
function with the variable bob
. Similarly for the variable mary
, we define a function called mary_late_fee
and use the MethodType
function of the types
module to associate the mary_late_fee
function with the variable mary
. If you run the code in Listing 8-9, you should see on the screen that Bob's late fee is 200 and Mary's late fee is 100. This shows that in Python, as in Ruby, we can add a method to a particular instance without affecting other instances of the same class.
Listing 8-9. Add Methods to Instances of the Customer
Class in Python
1) import sys
2) sys.path.append(r'C:Program Files (x86)IronPython 2.6 for .NET 4.0Lib')
3) import types
4)
5) def bob_late_fee(self):
6) return 200
7)
8) bob.calculate_late_fee = types.MethodType(bob_late_fee, bob)
9)
10) def mary_late_fee(self):
11) return 100
12)
13) mary.calculate_late_fee = types.MethodType(mary_late_fee, mary)
14)
15) print "Bob's late fee is " + str(bob.calculate_late_fee())
16) print "Mary's late fee is " + str(mary.calculate_late_fee())
So far you've seen how to add methods to a class as well as to an instance of a class in both Ruby and Python. Many other dynamic languages, such as Groovy, also provide the means for modifying a class's or an object's behavior at runtime. With the advent of the DLR, the good news is we don't have to code in a dynamic language like Ruby or Python to benefit from the metaprogramming capabilities those languages provide. With a little bit of work, we can define a class in C# and be able to add methods to the class and also to instances of the class. Let's see how that is done.
In this section, we will define two main classes: ClassMetaObject
and ExpandoClass
. The purpose of ClassMetaObject
is to hold the methods we add to individual objects, and the purpose of ExpandoClass
is to hold the methods we add to a class. As an example of how you can use these methods, we will define a Customer
class that derives from ClassMetaObject
. When we create an instance of the Customer
class and add methods to that instance, those methods will be stored in an instance of ClassMetaObject
. When we add methods to the Customer
class, those methods will be stored in an instance of ExpandoClass
.
The Customer
class code is shown in Listing 8-10. As you can see, the C# Customer
class mimics the Ruby and Python Customer
classes we saw in the previous sections. The C# Customer
class defines a constructor that takes the name and age of a customer. The ToString
method is overridden to return the name of a customer. One important thing to note about the C# Customer
class is that it derives from ClassMetaObject
so that instances of the Customer
class can be associated with new properties and methods. Furthermore, the Customer class contains a private static member variable _class
that points to an instance of ExpandoClass
. This is so that new properties and methods can be added to the Customer
class. I made the member variable _class
a static variable because I want all instances of the Customer
class to share one single ExpandoClass
instance that holds all the properties and methods we add to the Customer
class. We will look at how ClassMetaObject
and ExpandoClass
are implemented in a minute. First, however, I'd like to repeat the Ruby and Python examples we saw in the previous sections and show how the same example works in C# using the Customer
class.
Listing 8-10. Define the Customer
Class in C#
public class Customer : ClassMetaObject
{
private static ExpandoClass _class = new ExpandoClass();
public static dynamic CLASS
{
get { return _class; }
}
private string name;
private int age;
public Customer(string name, int age)
{
this.name = name;
this.age = age;
}
public override string ToString()
{
return this.name;
}
protected override ExpandoClass Class
{
get { return _class; }
}
}
Like the Ruby and Python examples in the previous sections, the code in Listing 8-11 creates two instances of the Customer
class. After the two Customer
instances are created, we want to define a method for setting a customer's spouse and a method for retrieving a customer's spouse. The method we define for setting a customer's spouse is the SetSpouse
delegate in Listing 8-11. The delegate takes two input parameters that represent the two parties in a marital relationship. The method body of the SetSpouse
delegate enforces the rule that if Bob is Mary's spouse, then Mary must also be Bob's spouse. Notice that the two input parameters of SetSpouse
are of the type dynamic
. Within the method body of the SetSpouse
delegate, the code accesses the Spouse
property of the input parameter self
. The Spouse
property is not defined originally in the C# Customer
class. But that's okay because we add the Spouse
property to the Customer
class in line 16. In line 17, we add the SetSpouse
delegate as the SetSpouse
method to the Customer
class. As you can see from lines 16 and 17, to add a property or method to the Customer
class, we add it to the customerClass
variable, which is obtained from the static CLASS
property of the Customer
class. Recall from the code in Listing 8-10 that the static CLASS
property of the Customer
class returns the single ExpandoClass
instance that's shared among all Customer
instances and that's meant to store all new properties and methods added to the Customer
class.
After adding the Spouse
property and the SetSpouse
method to the Customer
class, we call the SetSpouse
method on the variable bob
in line 18, just as we did in the previous Ruby and Python examples. When we print out Bob's spouse and Mary's spouse in lines 20 and 21, we can verify that things do work as expected and that we have dutifully officiated at the matrimony. And just as what happens to newlyweds who simply go on a honeymoon and forget to pay their wedding expenses on time, Bob and Mary incurred late fees on their credit card accounts. To reflect that irresponsibility on the part of Bob and Mary, the code in lines 23 and 24 assigns one anonymous delegate as the CalculateLateFee
method to the variable bob
and another anonymous delegate as the CalculateLateFee
method to the variable mary
. When you print out Bob's and Mary's late fees, sure enough you will see that Bob has a late fee of 200 and Mary has a late fee of 100 dollars.
Listing 8-11. An Example of Adding Methods at the Class and Instance Levels in C#
1) private static void RunMetaLibExample()
2) {
3) dynamic customerClass = Customer.CLASS;
4) dynamic bob = new Customer("Bob", 26);
5) dynamic mary = new Customer("Mary", 30);
6)
7) Action<dynamic, dynamic> SetSpouse = (self, spouse) =>
8) {
9) if (self.Spouse != spouse)
10) {
11) self.Spouse = spouse;
12) spouse.Spouse = self;
13) }
14) };
15)
16) customerClass.Spouse = null;
17) customerClass.SetSpouse = SetSpouse;
18) bob.SetSpouse(bob, mary);
19)
20) Console.WriteLine("Bob's spouse is {0}.", bob.Spouse);
21) Console.WriteLine("Mary's spouse is {0}.", mary.Spouse);
22)
23) bob.CalculateLateFee = (Func<int>) (() => { return 200; });
24) mary.CalculateLateFee = (Func<int>)(() => { return 100; });
25)
26) Console.WriteLine("Bob's late fee is {0}.", bob.CalculateLateFee());
27) Console.WriteLine("Mary's late fee is {0}.", mary.CalculateLateFee());
28) }
Let's see how ClassMetaObject
and ExpandoClass
are implemented. Listing 8-12 shows the ClassMetaObject
code. ClassMetaObject
derives from the System.Dynamic.DynamicObject
class that the DLR provides. We discussed DynamicObject
in Chapter 5.
Basically, the class DynamicObject
defines some methods that you can override in a derived class to define the late-binding behavior of the derived class's instances. Here in ClassMetaObject
we override the TryGetMember
and TrySetMember
methods inherited from DynamicObject
. The TryGetMember
method of ClassMetaObject
will be called when we try to access a property or call a method on an instance of ClassMetaObject
or a class that derives from ClassMetaObject
.
For example, in Listing 8-11, when the code bob.Spouse
is executed, because the Spouse
property is not defined in the C# Customer
class, the TryGetMember
method that the Customer
class inherits from ClassMetaObject
will be called to perform the late binding of the Spouse
property. The logic of the TryGetMember
method implemented in ClassMetaObject
first checks if the requested property or method is available at the instance level by looking up the property or method name in the items
dictionary. The items
dictionary is a private member variable in ClassMetaObject
that holds dynamic properties and methods at the instance level. If the requested property or method is not found at the instance level, the TryGetMember
method in ClassMetaObject
proceeds to perform the class-level lookup by calling the TryGetMember
method on the Class
property.
As Listing 8-12 shows, the Class
property is a reference to an ExpandoClass
instance. The job of the Class
property is to hold the dynamic properties and methods at the class level. Because every subclass of ClassMetaObject
will have its own class-level dynamic properties and methods, I make the Class
property an abstract property. Every subclass of ClassMetaObject
should implement the abstract Class
property by returning its own ExpandoClass
instance in the Class
property's get
method. That's what the Customer
class does, and you can see that if you take a look at how the Class
property is implemented in the Customer
class in Listing 8-10.
The TrySetMember
in ClassMetaObject
is implemented a little differently from the TryGetMember
. The code in the TrySetMember
sets a property or method at the instance level by putting an entry in the items
dictionary. Unlike the TryGetMember
method, the code does not bother with setting properties and methods at the class level. This is because the TrySetMember
method is called when code like self.Spouse = spouse
in Listing 8-11 is executed. As you can see, when such code is executed, we want to set the Spouse
property of the instance referenced by self
. In other words, we want to set the Spouse
property at the instance level. If the client code wants to set a property at the class level, then instead of doing something like self.Spouse = spouse
, the client code should set the property by using code like the customerClass.Spouse = null
in Listing 8-11. This code will cause the TrySetMember
method of ExpandoClass
, which you will see in a minute, to be called.
In summary, an important point to keep in mind when using the ClassMetaObject
and ExpandoClass
classes is that when setting a dynamic property or method at the instance level, you need to set it to an instance of ClassMetaObject
. If you want to set a dynamic property or method at the class level, you need to set it to an instance of ExpandoClass
. However, you can retrieve an instance-level or class-level dynamic property or method by getting it from a ClassMetaObject
instance.
Listing 8-12. The ClassMetaObject
Class
public abstract class ClassMetaObject : DynamicObject
{
protected abstract ExpandoClass Class
{
get;
}
private Dictionary<string, object> items = new Dictionary<string, object>();
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
if (items.TryGetValue(binder.Name, out result))
return true;
else
return Class.TryGetMember(binder, out result);
}
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
items[binder.Name] = value;
return true;
}
}
The ExpandoClass
code is similar to that of ClassMetaObject
, only simpler. Listing 8-13 shows how the ExpandoClass
class is implemented. Like ClassMetaObject
, ExpandoClass
also derives from DynamicObject
and overrides the TryGetMember
and TrySetMember
methods. ExpandoClass
defines a private member variable called items
to hold class-level dynamic properties and methods.
Listing 8-13. The ExpandoClass
Class
public class ExpandoClass : DynamicObject
{
Dictionary<string, object> items = new Dictionary<string, object>();
public override bool TryGetMember(
GetMemberBinder binder, out object result)
{
return items.TryGetValue(binder.Name, out result);
}
public override bool TrySetMember(
SetMemberBinder binder, object value)
{
items[binder.Name] = value;
return true;
}
}
To illustrate how ExpandoClass
works in concert with ClassMetaObject
, I'll trace how the SetSpouse
method is added to the Customer
class and then later invoked on a Customer
instance. Recall the following code snippet in Listing 8-11:
customerClass.SetSpouse = SetSpouse;
bob.SetSpouse(bob, mary);
In the code snippet, when we assign the SetSpouse
delegate to the SetSpouse
member of customerClass
, because customerClass
is an instance of ExpandoClass
, the TrySetMember
method of ExpandoClass
will be invoked and the SetSpouse
method will be added to the C# Customer
class at the class level. When we call the SetSpouse
method on bob
, because bob
is an instance of ClassMetaObject
, the TryGetMember
method of ClassMetaObject
will be invoked. The TryGetMember
method of ClassMetaObject
will not find a method by the name SetSpouse
because the SetSpouse
method was added to bob
at the instance level. So the TryGetMember
method of ClassMetaObject
will proceed to call the TryGetMember
method of ExpandoClass
and the SetSpouse
method we added to the C# Customer
class will be retrieved and eventually called.
So far in this chapter, I have shown how we can add methods to a class and to a particular instance of a class dynamically, completely in C# without any use of a dynamic language such as Ruby or Python. Now we are going to take a detour and look at another kind of metaprogramming technique that is made possible by the DLR. The metaprogramming technique I'll show you is based on DLR Expression, and I will demonstrate it by implementing a custom LINQ query provider. The exciting thing about doing this is that we are going to gradually evolve the custom LINQ query provider into a code-generation framework that utilizes the ClassMetaObject
and ExpandoClass
classes we built in the previous section. At the end, we'll arrive at a code-generation framework that is in spirit similar to frameworks such as the popular Ruby on Rails.
In this section we'll build a LINQ query provider. However, our true goal is to understand not the LINQ query provider itself, but rather the DLR Expression metaprogramming the LINQ query provider is based on. As you'll see, DLR Expression allows us to represent code as data. The data is in the form of expression trees that we can easily manipulate using the Visitor design pattern we looked at in Chapter 2. We saw many examples of DLR Expression in Chapter 2. The code here will be similar, except that this time our DLR Expression example is framed in the context of LINQ query providers.
LINQ is a component of the .NET Framework that allows writing code like the following to query a data source:
IEnumerable<Customer> selectedCustomers =
from c in customers
where c.FirstName.Equals("Bob") select c;
In the code snippet, the variable customers
is the data source from which we want to select customers whose first name is Bob. The variable customers
might represent data in database, information in XML files, or a collection of objects in memory. As far as the query is concerned, it doesn't matter whether the underlying data store is a database or an XML file. As long as there is a LINQ query provider that knows how to take our LINQ query and fetch the right data from the underlying data store, our LINQ query will run just fine.
From this little explanation of LINQ, you can see that the two major players in LINQ are queries and query providers. Queries are decoupled from the actual data store. The only link between queries and the actual store is a query provider. A query provider knows how to take queries and execute them against a particular data store. In this section, the custom query provider we will implement is one that executes queries against a collection of in-memory objects.
The implementation of the custom query provider consists of three main classes: Query<T>
, QueryProvider<T>
, and QueryExpressionVisitor<T>
. The Query<T>
class represents the queries that will be processed by our custom query provider as DLR expression trees. The QueryProvider<T>
class implements the logic of our custom query provider. QueryProvider<T>
is the class that contains the logic for executing instances of the Query<T>
class. When a QueryProvider<T>
instance executes instances of the Query<T>
class, it uses instances of the QueryExpressionVisitor<T>
class to manipulate the DLR expression trees of the Query<T>
instances. We will now go over the code of the three classes, Query<T>
, QueryProvider<T>
, and QueryExpressionVisitor<T>
. You will see as an example a typical use of DLR Expression as a metaprogramming technique.
Listing 8-14 shows the Query<T>
class. To be a class that represents LINQ queries, Query<T>
must implement the IQueryable<T>
interface. Of the property accessors and interface methods we must implement in order to implement the IQueryable<T>
interface, the two most important ones are the Expression
property get
accessor and the Provider
property get
accessor. As mentioned previously, the Query<T>
class represents queries as DLR expression trees. The Expression
property is the evidence of that. The property holds a DLR expression that can have child expressions. Those child expressions can have child expressions, and so on. All together, the expressions form an expression tree that represents a query. As to the Provider
property, it's there in Query<T>
to decouple queries from the actual data store. It is the link between queries and the actual store.
Listing 8-14. The Query<T>
Class
public class Query<T> : IQueryable<T>
{
private IQueryProvider provider;
private Expression expression;
public Query(IQueryProvider provider)
{
this.provider = provider;
this.expression = Expression.Constant(this);
}
public Query(IQueryProvider provider, Expression expression)
{
if (!typeof(IQueryable<T>).IsAssignableFrom(expression.Type))
throw new ArgumentException("expression");
this.provider = provider;
this.expression = expression;
}
Expression IQueryable.Expression
{
get { return expression; }
}
Type IQueryable.ElementType
{
get { return typeof(T); }
}
IQueryProvider IQueryable.Provider
{
get { return provider; }
}
public IEnumerator<T> GetEnumerator()
{
return ((IEnumerable<T>)provider.Execute(expression)).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)provider.Execute(expression)).GetEnumerator();
}
}
Listing 8-15 shows the QueryProvider<T>
class. To be a class that represents LINQ query providers, QueryProvider<T>
must implement the IQueryProvider
interface. The constructor of QueryProvider<T>
takes a list of objects as input and assigns that list to the class member variable records
. This variable represents the data store our custom query provider works against. I chose to use a list of objects as the data store for the sake of simplicity without losing any generality. Had I chosen a database as the data store, we would need to go through the extra work of setting up a database.
The IQueryProvider
interface defines four methods and we implement all of them in QueryProvider<T>
. The four methods are the two CreateQuery
methods and the two Execute
methods. Our implementation of the generic version of the CreateQuery
method simply creates an instance of Query<T>
and returns it. The non-generic version of the CreateQuery
method is implemented to throw a NotImplementedException
because the method is not needed in our example. The generic version of the Execute
method delegates its work to the non-generic version of the Execute
method, which is where the interesting things happen. The non-generic version of the Execute
method takes an expression tree that represents a LINQ query as input and executes that query against the data store records
. The expression tree that represents a LINQ query can be very complex and can contain expressions that represent where clauses, order-by clauses, group-by clauses, and so on. A practical implementation of the Execute
method would need to be able to handle most of those different clauses. For the purpose of our example, it's enough to just handle the where clauses. In fact, all I want the query provider to be able to handle is the following query:
Query<Customer> customers = new Query<Customer>(provider);
from c in customers where c.FirstName.Equals("Bob") select c;
The variable provider
in the query is an instance of QueryProvider<T>
. The query uses the Customer
class that we haven't introduced yet. But, basically, the query invokes our custom query provider to fetch customers whose first name is Bob. The query is constructed using the keywords from
, in
, where
, and select
that the C# language provides. Those keywords are just syntactic sugar over a set of underlying methods defined in the System.Linq.Queryable
class. When the C# compiler sees those keywords, it translates them into calls to the underlying methods in System.Linq.Queryable
. If we don't use the syntactic sugar, we can equivalently express our query with the following code:
Query<Customer> customers = new Query<Customer>(provider);
Queryable.Where<Customer>(customers, c => c.FirstName.Equals(firstName));
The return value of Queryable
's Where<T>
method is IQueryable<T>
. The first input parameter of the Where<T>
method is also of type IQueryable<T>
. What the Where<T>
does internally is very simple. It constructs a MethodCallExpression
instance that represents a call to the Where<T>
method. The MethodCallExpression
instance has two input arguments that are made child expressions of the MethodCallExpression
instance. The two input arguments are expressions that represent the first and second input parameters of the Where<T>
method. After constructing the MethodCallExpression
instance, the Where<T>
method creates a new instance of Query<T>
. Then it sets the new Query<T>
instance's Expression
property to the MethodCallExpression
instance. Once you understand how our query is represented as a MethodCallExpression
instance, it should be easy to understand why the non-generic version of the Execute
method is implemented the way it is in Listing 8-15.
In the Execute
method, we first check if the input expression that represents the query to execute is a MethodCallExpression
. If so, we further check if the MethodCallExpression
represents a call to the Where<T>
method of the Queryable
class. If that's not the case, then the query is outside the scope of what we want our custom query provider to support and therefore we throw a NotSupportedException
. If the input expression parameter of the Execute
method does represent a call to the Where<T>
method of the Queryable
class, we use an expression visitor to visit the MethodCallExpression
and its descendant expressions. The expression visitor is an instance of QueryExpressionVisitor<T>
, whose code is shown Listing 8-16. The expression visitor's job is to retrieve the lambda function that makes up the where clause of our query. In our query, that lambda function is c => c.FirstName.Equals(firstName)
and it is stored as a descendant expression under the MethodCallExpression
. Once the visitor retrieves the expression representing the lambda function, in line 39 of Listing 8-15 we get that expression from the visitor's Predicate
property and use the expression to find matching objects in the class member variable records.
Because the LINQ component of the .NET Framework comes with a LINQ query provider called LINQ to Objects for executing LINQ queries against IEnumerable<T>
collections such as the class member variable records
, line 39 simply uses the LINQ to Objects query provider to find the matching objects. This might strike you as a little absurd because we could have used the LINQ to Objects query provider directly without building a custom query provider that uses the LINQ to Objects provider internally. That's true, except that if we had used the LINQ to Objects query provider directly, I wouldn't be able to use our custom query provider as a typical example of how DLR Expression is used as a metaprogramming technique. Besides, you can think of this use of the LINQ to Objects provider as simplification of a more practical case where we would have more complex logic for querying the data store. Next, let's take a look at how to implement the QueryExpressionVisitor<T>
class to retrieve the expression that represents the lambda function in a where clause.
Listing 8-15. The QueryProvider<T>
Class
1) public class QueryProvider<T> : IQueryProvider
2) {
3) private IList<T> records;
4)
5) public QueryProvider(IList<T> records)
6) {
7) this. records = records;
8) }
9)
10) public IQueryable<T> CreateQuery<T>(Expression expression)
11) {
12) if (expression == null)
13) return new Query<T>(this);
14) else
15) return new Query<T>(this, expression);
16) }
17)
18) public IQueryable CreateQuery(Expression expression)
19) {
20) throw new NotImplementedException();
21) }
22)
23) public TResult Execute<TResult>(Expression expression)
24) {
25) return (TResult)this.Execute(expression);
26) }
27)
28) public object Execute(Expression expression)
29) {
30) if (!(expression is MethodCallExpression))
31) throw new NotSupportedException("The expression needs to be a
MethodCallExpression");
32)
33) MethodCallExpression methodCallExpression = (MethodCallExpression) expression;
34) if (methodCallExpression.Method.DeclaringType == typeof(Queryable)
35) && methodCallExpression.Method.Name == "Where")
36) {
37) QueryExpressionVisitor<T> visitor = new QueryExpressionVisitor<T>();
38) visitor.Visit(methodCallExpression);
39) return records.Where<T>(visitor.Predicate);
40) }
41) else
42) throw new NotSupportedException(
43) "The expression needs to be a call to the Where method of
Queryable");
44) }
45) }
To qualify as a DLR expression visitor class, a class must derive from System.Linq.Expressions.ExpressionVisitor
. The class ExpressionVisitor
defines methods for different expression classes. A subclass of ExpressionVisitor
will inherit those methods and override the ones that it wants to provide custom logic for.
In the case of our example, QueryExpressionVisitor<T>
overrides the VisitMethodCall
method it inherits from ExpressionVisitor
. The overridden VisitMethodCall
method will be called for every MethodCallExpression
node in the expression tree being traversed. Because the lambda expression we want QueryExpressionVisitor<T>
to retrieve is a child expression of a MethodCallExpression
, we override the VisitMethodCall
method in QueryExpressionVisitor<T>
. In the overridden VisitMethodCall
method, we check whether the method call expression node being visited represents a method call to the Where<T>
method of the Queryable
class. If so, we proceed to get the second argument of the method call expression because that second argument is the expression that represents the lambda function in a where clause.
One little thing to be mindful of is that the lambda expression we want to retrieve might be wrapped by unary expressions whose node type is ExpressionType.Quote
. The reason for quoting a lambda expression is so that when the quoted expression is compiled, it will compile into an expression instead of a lambda function, as would be the case without the quoting. Because of the possible quoting of the lambda expression, the code in Listing 8-16 uses a method called GetPastQuotes
to get past the unary expressions to the lambda expression we are interested in. When we get a hold of the lambda expression, we assign it to the Predicate
field of QueryExpressionVisitor<T>
so that we can use it in the Execute
method of QueryProvider<T>
.
Listing 8-16. The QueryExpressionVisitor<T>
Class
internal class QueryExpressionVisitor<T> : ExpressionVisitor
{
public Func<T, bool> Predicate;
internal QueryExpressionVisitor()
{ }
protected override Expression VisitMethodCall(MethodCallExpression m)
{
if (m.Method.DeclaringType == typeof(Queryable) && m.Method.Name == "Where")
{
//The second argument of the method call expression is a lambda expression that
serves
//as the predicate for the 'Where' clause.
LambdaExpression lambda = (LambdaExpression)GetPastQuotes(m.Arguments[1]);
Predicate = (Func<T, bool>) lambda.Compile();
}
return base.VisitMethodCall(m);
}
private Expression GetPastQuotes(Expression expression)
{
while (expression.NodeType == ExpressionType.Quote)
expression = ((UnaryExpression) expression).Operand;
return expression;
}
}
This section uses the implementation of a custom query provider to demonstrate the use of DLR Expression as a metaprogramming technique. We saw that queries written in code end up being represented by DLR expressions. This is the concept of code as data in action. The code in this case is the query code from c in customers where c.FirstName.Equals(“Bob”) select c;
and the data is the MethodCallExpression
and its descendant expressions that represent the query code. We also saw how the DLR expressions are interpreted and executed by a query provider. This is the concept of data as code in action. The data in this case is the DLR expressions and that data is used as code by our custom query provider. The code we write to interpret and execute DLR expressions is a metaprogram. The program that the metaprogram acts on is the code represented by the DLR expressions.
Now that we have gone through the implementation of a custom query provider, I am going to show you three ways of using that query provider, as well as the pros and cons of each approach. At the end of this part of the chapter, you will arrive at a primitive code-generation prototype that is in spirit similar to frameworks like Ruby on Rails.
Before we start to look at the three different ways of using our custom query provider, there is some preparation work to do. First, let's define a Customer
class like the one in Listing 8-17. The Customer
class is straightforward. We will use it as the type of the objects we query.
Listing 8-17. The Customer
Class for Trying Out Our Custom Query Provider
public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override string ToString()
{
return FirstName + " " + LastName;
}
}
Next we need some instances of the Customer
class to serve as the data source of our LINQ queries. For that, let's create the DataStore
class shown in Listing 8-18. The code in Listing 8-18 is pretty simple. It creates a list of Customer
instances and uses that list to create an instance of QueryProvider<Customer>
whenever the static GetQueryProvider
method is called.
Listing 8-18.The DataStore
Class That Contains the Data We Will Query Against
public class DataStore
{
private static IList<Customer> customers = new List<Customer>(new Customer[] {
new Customer {FirstName="Bob", LastName="Smith"},
new Customer {FirstName="John", LastName="Smith"},
new Customer {FirstName="Bill", LastName="Jones"},
new Customer {FirstName="Mary", LastName="Jones"},
new Customer {FirstName="Bob", LastName="Jones"}});
public static IQueryProvider GetCustomerQueryProvider()
{
return new QueryProvider<Customer>(customers);
}
}
We have now completed the preparation work and we are ready to see the three ways to use our custom query provider. The first approach does not involve any late binding and therefore will be familiar to those who have worked with LINQ queries before. Let's take a look.
Because this approach to using our custom query provider does not involve any late binding, I'll refer to it as the static data access approach. In the architecture of a software system, it is not uncommon to have a layer that handles data access. The responsibility of the data access layer is to (a) handle the interactions with data stores such as a database, and (b) decouple the rest of the software system from the specifics of the data stores. For the purpose of our example, let's imagine that we are building the data access layer of a software system. The data access layer will interact with our custom query provider. We want the data access layer to abstract away the fact that we are using a LINQ query provider so that if we ever need to swap out the LINQ query provider and replace it with, say, an object-relational mapping component like NHibernate, the rest of the software system can stay the same.
To achieve the data-access abstraction we want, we can define the interface ICustomerDao
shown in Listing 8-19. The interface defines the signature of the FindByFirstName
and FindByLastName
methods and does not dictate what data store we should use in implementing the interface. We might have a concrete implementation of the ICustomerDao
interface that uses a database as the backing data store. We might have another concrete implementation of the ICustomerDao
interface that uses in-memory objects as the backing data store. Because the rest of our software system works with the data-store-agnostic ICustomerDao
interface, we can swap out one concrete implementation and swap in another and the rest of our software system won't be affected.
Listing 8-19. The ICustomerDao
Interface
public interface ICustomerDao
{
IEnumerable<Customer> FindByFirstName(string firstName);
IEnumerable<Customer> FindByLastName(string lastName);
}
Listing 8-20 shows the CustomerDao
class that implements the ICustomerDao
interface by using a LINQ query provider internally. As you can see, the implementation of the FindByFirstName
method uses the from
, where
, in
, and select
C# language syntactic sugar to construct and return an instance of IQueryable<Customer>
as an instance of IEnumerable<Customer>
. This is okay because IQueryable<T>
derives from the IEnumerable<T>
interface. The implementation of the FindByLastName
method is similar to that of the FindByFirstName
method.
Listing 8-20. The CustomerDao
Class
public class CustomerDao : ICustomerDao
{
private IQueryProvider provider;
public CustomerDao(IQueryProvider provider)
{
this.provider = provider;
}
public IEnumerable<Customer> FindByFirstName(string firstName)
{
return from c in provider.CreateQuery<Customer>(null)
where c.FirstName.Equals(firstName)
select c;
}
public IEnumerable<Customer> FindByLastName(string lastName)
{
return from c in provider.CreateQuery<Customer>(null)
where c.LastName.Equals(lastName)
select c;
}
}
The code in the CustomerDao
class does not run by itself. Listing 8-21 shows an example that creates an instance of CustomerDao
and calls the FindByFirstName
and FindByLastName
methods on the instance. The example is pretty self-explanatory. One thing to note is that when FindByFirstName
or FindByLastName
returns an IQueryable<Customer>
type cast as an IEnumerable<Customer>
instance, the LINQ query represented by the IEnumerable<Customer>
instance is not executed yet. The LINQ query is executed when the code in Listing 8-21 starts iterating through the IEnumerable<Customer>
instance. This is because when that happens, the GetEnumerator
method of the Query
class will be called. If you look at the code in the GetEnumerator
method of the Query
class, you will see that there the LINQ query is executed by a query provider.
Listing 8-21. An Example of Using the CustomerDao
Class
private static void RunCustomerDaoExample()
{
CustomerDao customerDao = new CustomerDao(DataStore.GetCustomerQueryProvider());
IEnumerable<Customer> customers = customerDao.FindByFirstName("Bob");
foreach (var item in customers)
Console.WriteLine(item);
customers = customerDao.FindByLastName("Jones");
foreach (var item in customers)
Console.WriteLine(item);
}
Even though the ICustomerDao
interface and the CustomerDao
class provide a nice abstraction of the underlying data access details to the rest of our software system, one downside of this approach is the amount of boilerplate code we need to write. For each property, such as FirstName
in the Customer
class, we need to define a method like FindByFirstName
method in ICustomerDao
and implement that method in CustomerDao
. It would be nice if we could freely define properties like FirstName
and LastName
in Customer
and the rest of the data access code, such as the FindByFirstName
and FindByLastName
methods, would just be there automatically. Well, that's what our next approach is going to do.
Let's look at the second approach for using our custom query provider in the data access layer. In this approach, we will leverage the metaprogramming facilities made possible by DLR Expression and dynamic objects so that methods like FindByFirstName
and FindByLastName
don't need to be manually coded. Because the approach we are going to look at uses the late-binding capability of the DLR, I'll refer to it as the dynamic data access approach. Listing 8-22 shows the data access layer class, DynamicDao<T>
, that has the logic for responding to invocations of the FindByFirstName
and FindByLastName
methods, without us needing to write those methods manually. The idea of the dynamic data access approach is that we don't define and implement methods like FindbyFirstName
and FindByLastName
in DynamicDao<T>
. Instead we make DynamicDao<T>
a subclass of DynamicObject
. So when the methods FindbyFirstName
and FindByLastName
are invoked on an instance of DynamicDao<T>
, the TryInvokeMember
method of DynamicDao<T>
will be invoked to handle the late binding of those method invocations. In the body of the TryInvokeMember
method, we implement the late-binding logic in such a way that if the method invoked is FindByFirstName
or FindByLastName
, we return an IQueryable<Customer>
instance. The IQueryable<Customer>
instance when executed will return only customers whose first name (or last name) matches the queried first name (or last name).
In Listing 8-22, the code in the TryInvokeMember
method first gets the invoked method name from the Name
property of the binder
parameter. If the invoked method is FindByFirstName
, the Name
property of the binder
parameter will be the string “FindByFirstName”. So we strip out “FindBy” and obtain the property name “FirstName”. If the invoked method is FindByFirstName
, we want the TryInvokeMember
method to return as the result an IQueryable<Customer>
instance equivalent to the IQueryable<Customer>
instance returned by the FindByFirstName
method of the CustomerDao
class we saw in the previous section. The bulk of the code in the TryInvokeMember
method is to construct a DLR expression that represents the predicate lambda function to use in the where clause of the query object we aim to construct. The predicate lambda function expressed in C# code will look something like this:
(T x) => x.[propertyName].Equals(arg);
In this C# code, if the TryInvokeMember
function is invoked to find customers whose first name is “Bob”, then T
will be the type Customer, [propertyName]
will be FirstName and arg
will be “Bob”. The predicate lambda function in C# maps very nicely to the predicate expression we try to construct in Listing 8-22. The x.[propertyName]
part in the C# lambda function above is a property member access and it maps to the Expression.MakeMemberAccess
method call in line 18. In the body of the C# lambda function, the call to the Equals
method maps to the Expression.Call
method call in line 17. The whole C# lambda function maps to the Expression.Lambda
method call in line 16. The C# lambda function has one input parameter x
, which maps to the parameter variable created in line 14.
Once the predicate expression is constructed, we pass it as the input parameter to the Where<T>
method call in line 26 so that the query we return as the late-binding result will have a where clause with the desired predicate for matching customers.
Listing 8-22. The DynamicDao<T>
Class
1) public class DynamicDao<T> : DynamicObject
2) {
3) private IQueryProvider provider;
4)
5) public DynamicDao(IQueryProvider provider)
6) {
7) this.provider = provider;
8) }
9)
10) public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args,
11) out object result)
12) {
13) String propertyName = binder.Name.Substring(6); //6 is the length of 'FindBy'
14) ParameterExpression parameter = Expression.Parameter(typeof(T));
15) PropertyInfo propertyInfo = typeof(T).GetProperty(propertyName);
16) Expression<Func<T, bool>> predicate = Expression.Lambda<Func<T, bool>>(
17) Expression.Call(
18) Expression.MakeMemberAccess(
19) parameter,
20) propertyInfo),
21) propertyInfo.PropertyType.GetMethod("Equals", new Type[]
{typeof(object)}),
22) Expression.Constant(args[0])),
23) parameter);
24)
25) Query<T> query = new Query<T>(provider);
26) result = query.Where<T>(predicate);
27) return true;
28) }
29) }
Listing 8-23 shows an example that creates an instance of DynamicDao<Customer>
and calls the FindByFirstName
and FindByLastName
methods on the instance. You can run the code and verify that everything works as expected.
Listing 8-23. An Example of Using the DynamicDao<T>
Class
private static void RunDynamicDaoExample()
{
dynamic customerDao = new DynamicDao<Customer>(DataStore.GetCustomerQueryProvider());
IEnumerable<Customer> customers = customerDao.FindByFirstName("Bob");
foreach (var item in customers)
Console.WriteLine(item);
customers = customerDao.FindByLastName("Jones");
foreach (var item in customers)
Console.WriteLine(item);
}
The dynamic data access approach shown in this section frees us from having to define and implement methods like FindByFirstName
and FindByLastName
for each property in the Customer
class. However, the code in the TryInvokeMember
method of DynamicDao<T>
looks pretty ad hoc to me. The code works for our simple example, but in practical cases, we could have different kinds of methods than the “FindBy” methods we want to bind late. If we handle those practical cases the way we do in DynamicDao<T>
, we will have to parse those different kinds of method names and use those names to guide the program's execution path. We would need to refactor the code in DynamicDao<T>
substantially so that we don't put all the late-binding logic in the TryInvokeMember
method. Essentially, the problem with DynamicDao<T>
as it is implemented is the lack of a well-structured mechanism for routing a method invocation to the right late-binding logic. One way to amend the issue is to refactor the code in DynamicDao<T>
. Another way is to leverage the ClassMetaObject
and ExpandoClass
classes we built earlier in this chapter. The next section shows how to do that.
We will now show the third approach for using our custom query provider in the data access layer. In this approach, we will leverage the metaprogramming features of the ClassMetaObject
and ExpandoClass
classes we built earlier so that we have a well-structured mechanism for routing a method invocation to the right late-binding logic. The idea of this approach is to generate methods like FindByFirstName
and FindByLastName
at runtime and add those methods to a data access layer class. With this approach, we no longer need to parse method names and use those names to pick the right late-binding logic, as we did in the DynamicDao<T>
class. Because the approach demonstrated in this section is based on the concept of code generation, I will refer to it as the generated data access approach.
Listing 8-24 shows the class GeneratedDao<T>
to which we will add the FindByFirstName
and FindByLastName
methods. The code in Listing 8-24 might look complicated at first, but it's actually quite simple once you understand the code structure. First, the class GeneratedDao<T>
derives from ClassMetaObject
. That means we can add new methods to GeneratedDao<T>
at the class level or instance level. New methods added to GeneratedDao<T>
at the class level are added to the static _class
variable. Our goal is to add a “FindBy” method for each property in type T
to GeneratedDao<T>
at the class level. To achieve that, in Listing 8-24 we define the AddMethods
method that loops through all the properties of T
and calls the AddMethodForProperty
method for each property. AddMethodForProperty
calls the CreateNewMethodExpression
method to get an expression that represents the new method to be added for a property. Using FindByFirstName
as an example, the expression returned by CreateNewMethodExpression
when compiled will be equivalent to the following C# code:
Func<String, IEnumerable<Customer>> FindByFirstName =
(firstName) =>
{
IQueryable<Customer> query = provider.CreateQuery<Customer>(null);
return query.Where(c => c.FirstName.Equals(firstName));
};
The equivalent C# code is the same as the FindByFirstName
method we saw in CustomerDao
. Here we are just implementing the same method in terms of DLR expressions. The CreateNewMethodExpression
method internally calls the GetWhereMethodInfo
method to get a System.Reflection.MethodInfo
instance for the Where
method of the Queryable
class. CreateNewMethodExpression
calls the CreatePredicateExpression
to get the expression that represents the predicate (the c => c.FirstName.Equals(firstName)
in the above code snippet) in the query's where clause. Once AddMethodForProperty
gets the lambda expression returned by the CreateNewMethodExpression
method, it constructs an expression that adds the lambda expression as a new method to _class
by calling the TrySetMember
method on _class
.
Listing 8-24. The GeneratedDao<T>
Class
public class GeneratedDao<T> : ClassMetaObject
{
private static ExpandoClass _class = new ExpandoClass();
protected override ExpandoClass Class
{
get { return _class; }
}
private IQueryProvider provider;
private MethodInfo whereMethod = null;
public GeneratedDao(IQueryProvider provider)
{
this.provider = provider;
AddMethods();
}
private void AddMethods()
{
PropertyInfo[] properties = typeof(T).GetProperties();
foreach (PropertyInfo propertyInfo in properties)
AddMethodForProperty(propertyInfo);
}
private void AddMethodForProperty(PropertyInfo propertyInfo)
{
LambdaExpression newMethod = CreateNewMethodExpression(propertyInfo);
SetMemberBinder binder = new SimpleSetMemberBinder("FindBy" + propertyInfo.Name, false);
Expression addMethodExpression = Expression.Call(
Expression.Constant(_class),
_class.GetType().GetMethod("TrySetMember"),
Expression.Constant(binder), newMethod
);
Func<bool> func = Expression.Lambda<Func<bool>>(addMethodExpression).Compile();
func();
}
private Expression<Func<T, bool>> CreatePredicateExpression(
PropertyInfo propertyInfo, ParameterExpression argExpression)
{
//predicate = (T x) =>
//{
// x.propertyName.Equals(arg);
//}
ParameterExpression parameter = Expression.Parameter(typeof(T));
return Expression.Lambda<Func<T, bool>>(
Expression.Call(
Expression.MakeMemberAccess(
parameter,
propertyInfo),
propertyInfo.PropertyType.GetMethod("Equals", new Type[] { typeof(object) }),
argExpression),
parameter);
}
private MethodInfo GetWhereMethodInfo()
{
if (whereMethod != null)
return whereMethod;
MethodInfo[] allMethods = typeof(Queryable).GetMethods(
BindingFlags.Public | BindingFlags.Static);
foreach (var method in allMethods)
{
if (method.Name.Equals("Where"))
{
ParameterInfo[] parameters = method.GetParameters();
Type[] genericTypes = parameters[1].ParameterType.GetGenericArguments();
if (genericTypes[0].GetGenericArguments().Length == 2)
whereMethod = method;
}
}
whereMethod = whereMethod.MakeGenericMethod(new Type[] { typeof(T) });
return whereMethod;
}
private LambdaExpression CreateNewMethodExpression(PropertyInfo propertyInfo)
{
ParameterExpression argExpression = Expression.Parameter(propertyInfo.PropertyType);
Expression<Func<T, bool>> predicate = CreatePredicateExpression(propertyInfo,
argExpression);
//provider.CreateQuery<Customer>(null);
Expression queryExpression = Expression.Call(
Expression.Constant(provider), "CreateQuery",
new Type[] { typeof(T) }, Expression.Constant(null, typeof(Expression)));
//query.Where(c => c.FirstName.Equals(firstName));
Expression body = Expression.Call(null,
GetWhereMethodInfo(), queryExpression,
predicate);
return Expression.Lambda(body, argExpression);
}
}
To try out the GeneratedDao<T>
class, you can run the code in Listing 8-25. This code creates an instance of GeneratedDao<Customer>
and calls the FindByFirstName
and FindByLastName
methods on the instance.
Listing 8-25. An Example of Using the GeneratedDao<T>
Class
private static void RunGeneratedDaoExample()
{
dynamic customerDao = new GeneratedDao<Customer>(DataStore.GetCustomerQueryProvider());
IEnumerable<Customer> customers = customerDao.FindByFirstName("Bob");
foreach (var item in customers)
Console.WriteLine(item);
customers = customerDao.FindByLastName("Jones");
foreach (var item in customers)
Console.WriteLine(item);
}
This chapter gives an overview of metaprogramming and then shows some exciting ways you can use metaprogramming in your .NET applications. Thanks to the DLR, your applications don't need to use dynamic languages in order to benefit from the metaprogramming techniques traditionally available only in dynamic languages. In particular, this chapter implements two classes, ClassMetaObject
and ExpandoClass
, that serve as the foundation of other marvelous applications of metaprogramming. As an example of the wonderful things you can do with ClassMetaObject
and ExpandoClass
, we use those classes in building a code-generation framework that is in spirit similar to frameworks such as the popular Ruby on Rails. Because the code-generation framework is just an example, it omits a lot of details and shows only the concept. Though I can't promise, I have plans in my mind to continue the development of the code-generation example shown in this chapter, and to experiment with model-driven development and domain-specific language development with it. You are welcome to head over the dpier project web site at http://code.google.com/p/dpier/
and check out the progress.
52.14.82.217