.NET 4 adds some nice tools to the toolbox. Chief among them is support for dynamic languages and dynamic features in strongly typed languages. In this post we’ll examine how to use reflection to work with unknown data types then we’ll see how to use dynamics to accomplish the same task. Next, we’ll see an example of interacting with IronRuby from within a C# application. Finally, we’ll take a brief look at two of the specialized classes in the System.Dynamic namespace.
Introducing Dynamic Programming
.NET has historically been a statically typed environment. By virtue of being statically typed we get many benefits such as type safety and compile-time member checking. There are times though that we don’t know the type of a variable when we’re writing the code. Consider this code:
module Bank type BankAccount() = class let mutable _balance = 0m member this.CurrentBalance with get() = _balance member this.Deposit(amount) = let lastBalance = _balance _balance <- _balance + amount ( lastBalance, _balance) member this.Withdrawal(amount) = let lastBalance = _balance let tmpBalance = _balance - amount if tmpBalance < 0m then raise(new System.Exception("Balance cannot go below zero")) _balance <- tmpBalance ( lastBalance, _balance) member this.Close() = let lastBalance = _balance _balance <- 0m ( lastBalance, _balance) end
The code above defines a simple type in F#. Don’t worry if you’re not familiar with it – all that’s important to understand here is that the BankAccount type has a parameterless constructor, a read-only CurrentBalance property, and three methods (Deposit, Withdrawal, and Close) that each return a two item tuple.
What if we want to work with this type from a C# assembly? In many cases it will be possible to add a reference to the F# project but what if that isn’t possible? What if we’re getting a reference to the type from an IoC container, a factory method, or a COM interop component that returns object? In those cases we may not have enough information to cast the instance to a known type.
Reflecting on the Past
In the past when these situations would arise we had to resort to reflection to access an object’s members. Aside from being costly, using reflection requires a lot of extra code and often involves additional casting.
var account = BankAccountFactory.GetBankAccount(); var accountType = account.GetType(); var depositMethod = accountType.GetMethod("Deposit"); var currentBalanceProperty = accountType.GetProperty("CurrentBalance"); var withdrawalMethod = accountType.GetMethod("Withdrawal"); var closeMethod = accountType.GetMethod("Close"); Console.WriteLine(Resources.CurrentBalanceFormat, ((Tuple<decimal, decimal>)depositMethod.Invoke(account, new object[] { 1000m })).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, ((decimal)currentBalanceProperty.GetValue(account, null))); Console.WriteLine(Resources.CurrentBalanceFormat, ((Tuple<decimal, decimal>)withdrawalMethod.Invoke(account, new object[] { 250m })).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, ((Tuple<decimal, decimal>)withdrawalMethod.Invoke(account, new object[] { 100m })).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, ((Tuple<decimal, decimal>)depositMethod.Invoke(account, new object[] { 35m })).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, ((Tuple<decimal, decimal>)closeMethod.Invoke(account, new object[] { })).Item2);
Look at those pipes! There’s a ton of code and most of it is just plumbing. Before we can do anything useful with the BankAccount instance we need to get a MemberInfo (specifically PropertyInfo or MethodInfo) instance for each member we want to use. Once we have the MemberInfo instances we need to call the Invoke or GetValue method passing the source instance, and other required arguments before casting the result of the call to the expected type! Isn’t there a better way?
Dynamic Language Runtime to the Rescue!
Prior to .NET 4 we were stuck with reflection but .NET 4 introduces the Dynamic Language Runtime (DLR) to help reduce this complexity. The DLR is exposed to C# through the dynamic type. Rather than declaring variables as type object we can define them as type dynamic.
Note: Please, please, pleeeease don’t confuse var with dynamic. in C# var uses type inference to determine the actual type of the variable at compile-time whereas dynamic defers type resolution to the DLR at run-time. Variables defined with var are still strongly typed.
The dynamic type is just like any other type in that it can be used for defining variables, fields, method return values, method arguments, etc… Variables defined as dynamic can be used like we’re working with directly with an instance of a known type. Let’s take the reflection example and revise it to use dynamics instead.
dynamic account = BankAccountFactory.GetBankAccount(); Console.WriteLine(Resources.CurrentBalanceFormat, account.Deposit(1000m).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, account.CurrentBalance); Console.WriteLine(Resources.CurrentBalanceFormat, account.Withdrawal(250m).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, account.Withdrawal(100m).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, account.Deposit(35m).Item2); Console.WriteLine(Resources.CurrentBalanceFormat, account.Close().Item2);
As you can see, the code is much more concise. We don’t have any of the complexity required by remoting and dynamics typically performs better than reflection. This convenience does come at a price though.
When we use dynamic types we lose all of the compile-time support that comes with a strongly typed environment. That means we lose type safety, type checking, and even IntelliSense. If something is wrong we won’t find out about it until run-time so it is especially important to have good tests around any code using dynamics.
Using Dynamic Languages
We’ve seen how to use the dynamic type so let’s take a look at using the DLR with an actual dynamic language (F# is functional, not dynamic). In this example we’ll define a class using an IronRuby script hosted within a C# application. We’ll then create and use an instance of that type.
var engine = Ruby.CreateEngine(); dynamic account = engine.Execute( @"class BankAccount attr_reader :CurrentBalance def initialize() @CurrentBalance = 0.0 end def Deposit(amount) @CurrentBalance = @CurrentBalance + amount @CurrentBalance end def Withdrawal(amount) @CurrentBalance = @CurrentBalance - amount @CurrentBalance end def Close() @CurrentBalance = 0.0 @CurrentBalance end end return BankAccount.new" );
The IronRuby code snippet passed to engine.Execute defines a class very similar to the F# class we used earlier. The ScriptEngine‘s Execute method evaluates the script and returns a dynamic that will be bound to the IronRuby type. Once we have that reference we can use the DLR to manipulate the instance as follows:
Console.WriteLine(Resources.CurrentBalanceFormat, account.Deposit(1000m)); Console.WriteLine(Resources.CurrentBalanceFormat, account.CurrentBalance); Console.WriteLine(Resources.CurrentBalanceFormat, account.Withdrawal(250m)); Console.WriteLine(Resources.CurrentBalanceFormat, account.Withdrawal(100m)); Console.WriteLine(Resources.CurrentBalanceFormat, account.Deposit(35m)); Console.WriteLine(Resources.CurrentBalanceFormat, account.Close());
With the exception that the IronRuby class doesn’t return a Tuple the C# code is identical to that used to work with the F# class. In both cases the DLR handles resolving properties, methods, and data types despite the fact that the underlying class is not only entirely different but is also completely unrelated. This illustrates how dynamics can also simplify working with similar classes that don’t share a common interface or base class.
The Microsoft Scripting classes don’t restrict us to using inline scripts. We can also use the ScriptEngine‘s ExecuteFile method to invoke external scripts. Unlike with the Execute method which returns dynamic ExecuteFile returns an instance of ScriptScope that can be used to dive back into the engine and provide more control for using the loaded script(s).
var scope = engine.ExecuteFile("IronRubySample.ir"); dynamic globals = engine.Runtime.Globals; dynamic account = globals.BankAccount.@new();
Special Dynamic Types
In addition to declaring any unknown types as dynamic the .NET Framework now provides classes that allow dynamic behavior from the traditional .NET languages. Each of these types are located in the System.Dynamic namespace and instances must be defined as type dynamic to avoid static typing and take advantage of their dynamic capabilities.
Of the classes in the System.Dynamic namespace we’ll only be looking at ExpandoObject and DynamicObject here.
ExpandoObject
ExpandoObject is a dynamic type that allows members to be added or removed at run-time. The behavior of ExpandoObject is similar to that of objects in traditional dynamic languages.
public sealed class ExpandoObject : IDynamicMetaObjectProvider, IDictionary<string, Object>, ICollection<KeyValuePair<string, Object>>, IEnumerable>, IEnumerable, INotifyPropertyChanged
For the following examples assume we have an ExpandoObject defined as:
dynamic car = new ExpandoObject(); car.Make = "Toyota"; car.Model = "Prius"; car.Year = 2005; car.IsRunning = false;
In their simplest form ExpandoObjects will only use properties:
Console.WriteLine("{0} {1} {2}", car.Year, car.Make, car.Model);
…but we can also add methods:
car.TurnOn = (Action)(() => { Console.WriteLine("Starting {0}", car.Model); car.IsRunning = true; }); car.TurnOff = (Action)(() => { Console.WriteLine("Stopping {0}", car.Model); car.IsRunning = false; }); Console.WriteLine("Is Running? {0}", car.IsRunning); car.TurnOn(); Console.WriteLine("Is Running? {0}", car.IsRunning); car.TurnOff(); Console.WriteLine("Is Running? {0}", car.IsRunning);
…and events:
var OnStarted = (Action<dynamic, EventArgs>)((dynamic c, EventArgs ea) => { if (c.Started != null) { c.Started(c, new EventArgs()); } }); var OnStopped = (Action<dynamic, EventArgs>)((dynamic c, EventArgs ea) => { if (c.Stopped != null) { c.Stopped(c, new EventArgs()); } }); car.Started = null; car.Started += (Action<dynamic, EventArgs>)((dynamic c, EventArgs ea) => Console.WriteLine("{0} Started", c.Model)); car.Stopped = null; car.Stopped += (Action<dynamic, EventArgs>)((dynamic c, EventArgs ea) => Console.WriteLine("{0} Stopped", c.Model)); car.TurnOn = (Action)(() => { car.IsRunning = true; OnStarted(car, EventArgs.Empty); }); car.TurnOff = (Action)(() => { car.IsRunning = false; OnStopped(car, EventArgs.Empty); }); Console.WriteLine("Is Running? {0}", car.IsRunning); car.TurnOn(); Console.WriteLine("Is Running? {0}", car.IsRunning); car.TurnOff(); Console.WriteLine("Is Running? {0}", car.IsRunning);
In addition to the standard IDynamicMetaObjectProvider interface ExpandoObject also implements several interfaces for accessing members as though they were a dictionary. The DLR will handle adding members through its binding mechanism but we need to use the dictionary syntax to remove them.
var carDict = (IDictionary<string, object>)car; Console.WriteLine("{0} {1} {2}", carDict["Year"], carDict["Make"], carDict["Model"]);
DynamicObject
While ExpandoObject allows us to dynamically add and remove members at run-time, DynamicObject allows us to control that behavior.
public class DynamicObject : IDynamicMetaObjectProvider
Since DynamicObject is an abstract class doesn’t expose a public constructor we must create a derived class to take advantage of its features. A side effect of this we can also define members directly on the class and the DLR will handle resolving them correctly.
public class DynamicCar : System.Dynamic.DynamicObject { public DynamicCar() { Extensions = new System.Dynamic.ExpandoObject(); } private ExpandoObject Extensions { get; set; } public string Make { get; set; } public string Model { get; set; } public int Year { get; set; } public override bool TryGetMember(GetMemberBinder binder, out object result) { string name = binder.Name.ToLower(); Console.WriteLine("Getting: {0}", name); return (Extensions as IDictionary<string, object>).TryGetValue(name, out result); } public override bool TrySetMember(SetMemberBinder binder, object value) { var name = binder.Name.ToLower(); Console.WriteLine("Setting: {0} -> {1}", name, value); (Extensions as IDictionary<string, object>)[name] = value; return true; } public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result) { Console.WriteLine("Invoking: {0}", binder.Name); return base.TryInvokeMember(binder, args, out result); } }
Once the type is defined we create and use instances just like any other dynamic type:
dynamic car = new DynamicCar() { Make = "Toyota", Model = "Prius", Year = 2005 }; car.IsRunning = false; car.TurnOn = (Action)(() => car.IsRunning = true); car.TurnOff = (Action)(() => car.IsRunning = false); Console.WriteLine("Make: {0}", car.Make); Console.WriteLine("Model: {0}", car.Model); Console.WriteLine("Year: {0}", car.Year); Console.WriteLine("IsRunning: {0}", car.IsRunning); car.TurnOn(); car.TurnOff();
Notice how we are able to take advantage of object initializer syntax because the members we’re setting are defined on the class itself rather than being dynamic. We can still access those members normally later on despite the variable being defined as dynamic.
The output shows how we’ve changed the behavior of the dynamic members while the static members are unaffected. In this example actions affecting the dynamic members display a message.
Can’t Everything be Dynamic?
It’s true that there’s nothing preventing us from declaring everything as dynamic but it’s usually not a good idea in statically typed languages like C#. In addition to losing all compile-time support that comes from statically typed languages, code that uses dynamic typing generally performs worse than code using static typing. Generally speaking, only use dynamics when you have a good reason.
“Since DynamicObject is an abstract class we must create a derived class to take advantage of its features.”
Minor typo — DynamicObject is NOT an abstract class
Good catch. One would think I’d have noticed that given that I included the definition in the post. I’ve updated the post accordingly.
Thanks for the feedback.