It sounds like a joke, but there really is a class called
ExpandoObject. When I first heard of it, I thought it was a farce, but it is actually pretty useful. [Let me preface this by saying I wouldn't necessarily use this class in production code because it has the potential to be a maintenance nightmare, but it’s a good place to learn about dynamic objects.] The
ExpandoObject is a
DyanamicObject and a
DynamicObject is essentially an object that accepts and brokers member calls (methods and properties) made under the dynamic context (sort of like a proxy).
ExpandoObject is a type of
DynamicObject with a very specific implementation; it defines and overrides specific operations that can be performed on an object (like an actual method call or property set). It then creates properties at runtime that can be executed. The full documentation of the
ExpandoObject can be found here.
So what does it mean to create properties at runtime? With
ExpandoObject, you can do things like this:
dynamic ex = new System.Dynamic.ExpandoObject();
ex.Name = "test"; Console.WriteLine(ex.Name);
In this code example, I've created an instance of
ExpandoObject and assigned it to dynamic variable
ex. Variables marked as dynamic are not compile-time checked, but are runtime resolved.
ex.Name is the first property I create. I am going to make it of type string by assigning it a
string value. This compiles fine even though I've never defined the property “
Name;” I am going to let
ExpandoObject create it for me.
Under most circumstances,
ex.Name would fail at runtime because there is no runtime implementation. In other words, if you were to do run the following code, you would get an error:
dynamic v = new Object();
v.Name = "test";
An exception is thrown because “
Name,” even though it compiled, does not exist at runtime.
You can also define methods on
ex.PrintToConsole = (Action)(() => Console.WriteLine("some function"));
ex.PrintWithParam = (Action<string>)((string s) => Console.WriteLine(s));
ex.MethodWithReturn = (Func<int, int>)((int i) => i * i);
In the code example above, I defined the method
PrintToConsole to be a delegate of type action (does not take any parameter and returns
void); you can also create delegates of type
Func<>…this is a very slick way of defining dynamic methods.
So how does this all work? How does the
ExpandoObject know to intercept method calls and define properties? Well, as I mentioned above, the
ExpandoObject is a
DynamicObject (it’s actually an
IDynamicMetaObjectProvider). There are many overridable methods in
DynamicObject but for our purposes, the three most important are:
TryInvokeMember(…). When implemented in a subclass, the DLR will call the appropriate method during runtime. So, for instance, when a property is set on an instance of
ExpandoObject the DLR calls
TrySetMember (with some parameters) on
ExpandoObject does stuff and returns either
true if the call was successful (which it always is),
false otherwise. If
false is returned, you will get the error above (object does not contain a definition for blah blah blah). I wrote an absurdly simple implementation of my own
ExpandoObject for demonstration purposes.
Not much to it, really. When a setter is called,
TrySetMember will be executed. The binder parameter knows the name of the property that was set….so if you have
ex.MyProp = “text”, the
binder.Name property will be set to “
MyProp.’ In my case, I use this as a key into a hashtable… the value is the second parameter. Now when a
get member is executed,
TryGetMember will be executed with similar parameters: the name of the property that was executed and the result, which you must set before exiting the method. The result is literally the result of the operation. So if I have a property called
MyProp with a value “
text,” when I go to print that to the screen I should see “
text,’ because that’s what I stored in the dictionary when the property was set.
TryInvokeMember is a little more interesting. This is called when a method is executed on a dynamic object (like in my first code example when I defined the delegates). So if I do something like
TryInvokeMember will be executed with all the appropriate parameters set. Since
Func<> are all delegate types, I cast the value from the dictionary to a
Delegate then dynamically invoke it with the arguments provided.
MySimpleExpandoObject in code.