Building C# objects dynamically with ExpandoObject
Create objects that can be enhanced with properties, methods, and events.
Problem
You want to be able to build up an object to work with on the fly at runtime.
Solution
Use ExpandoObject
to create an object that you can add properties, methods, and events to and be able to data bind to in a user interface.
We can use ExpandoObject
to create an initial object to hold the Name
and current Country
of a person.
dynamic expando = new ExpandoObject();
expando.Name = "Brian";
expando.Country = "USA";
Once we have added properties directly, we can also add properties to our object in a more dynamic fashion using the AddProperty
method we have provided for you. One example of why you might do this is to add properties to your object from another source of data. We will add the Language
property.
// Add properties dynamically to expando
AddProperty(expando, "Language", "English");
The AddProperty
method takes advantage of the support that ExpandoObject
has for IDictionary<string, object>
and allows us to add properties using values we determine at runtime.
public static void AddProperty(ExpandoObject expando, string propertyName, object propertyValue)
{
// ExpandoObject supports IDictionary so we can extend it like this
var expandoDict = expando as IDictionary<string, object>;
if (expandoDict.ContainsKey(propertyName))
expandoDict[propertyName] = propertyValue;
else
expandoDict.Add(propertyName, propertyValue);
}
We can also add methods to the ExpandoObject
by using the Func<>
generic type which represents a method call. In our example, we will add a validation method for our object:
// Add method to expando
expando.IsValid = (Func<bool>)(() =>
{
// Check that they supplied a name
if(string.IsNullOrWhiteSpace(expando.Name))
return false;
return true;
});
if(!expando.IsValid())
{
// Don't allow continuation...
}
Now we can also define and add events to the ExpandoObject
using the Action<>
generic type. We will add two events, LanguageChanged
and CountryChanged
. LanguageChanged
will be added after defining the eventHandler
variable to hold the Action<object,EventArgs>
and CountryChanged
we will add directly as an inline anonymous method. CountryChanged
looks at the Country
that changed and invokes the LanguageChanged
event with the proper Language
for the Country
. (Note that LanguageChanged is also an anonymous method but sometimes it can make for cleaner code to have a variable for these).
// You can also add event handlers to expando objects
var eventHandler =
new Action<object, EventArgs>((sender, eventArgs) =>
{
dynamic exp = sender as ExpandoObject;
var langArgs = eventArgs as LanguageChangedEventArgs;
Console.WriteLine($"Setting Language to : {langArgs?.Language}");
exp.Language = langArgs?.Language;
});
// Add a LanguageChanged event and predefined event handler
AddEvent(expando, "LanguageChanged", eventHandler);
// Add a CountryChanged event and an inline event handler
AddEvent(expando, "CountryChanged",
new Action<object, EventArgs>((sender, eventArgs) =>
{
dynamic exp = sender as ExpandoObject;
var ctryArgs = eventArgs as CountryChangedEventArgs;
string newLanguage = string.Empty;
switch (ctryArgs?.Country)
{
case "France":
newLanguage = "French";
break;
case "China":
newLanguage = "Mandarin";
break;
case "Spain":
newLanguage = "Spanish";
break;
}
Console.WriteLine($"Country changed to {ctryArgs?.Country}, " +
$"changing Language to {newLanguage}");
exp?.LanguageChanged(sender,
new LanguageChangedEventArgs() { Language = newLanguage });
}));
The AddEvent
method we have provided for you to encapsulate the details of adding the event to the ExpandoObject
. This again takes advantage of ExpandoObject
’s support of IDictionary<string,object>
:
public static void AddEvent(ExpandoObject expando, string eventName, Action<object, EventArgs> handler)
{
var expandoDict = expando as IDictionary<string, object>;
if (expandoDict.ContainsKey(eventName))
expandoDict[eventName] = handler;
else
expandoDict.Add(eventName, handler);
}
Finally, ExpandoObject
supports INotifyPropertyChanged
which is the foundation of data binding to properties in .NET. We hook up the event handler and when the Country
property is changed, we fire the CountryChanged
event.
((INotifyPropertyChanged)expando).PropertyChanged +=
new PropertyChangedEventHandler((sender, ea) =>
{
dynamic exp = sender as dynamic;
var pcea = ea as PropertyChangedEventArgs;
if(pcea?.PropertyName == "Country")
exp.CountryChanged(exp, new CountryChangedEventArgs() { Country = exp.Country });
});
Now that our object is done being constructed, we can invoke it like this to simulate our friend travelling around the world:
Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, {expando.Language}");
Console.WriteLine();
Console.WriteLine("Changing Country to France...");
expando.Country = "France";
Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, {expando.Language}");
Console.WriteLine();
Console.WriteLine("Changing Country to China...");
expando.Country = "China";
Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, {expando.Language}");
Console.WriteLine();
Console.WriteLine("Changing Country to Spain...");
expando.Country = "Spain";
Console.WriteLine($"expando contains: {expando.Name}, {expando.Country}, {expando.Language}");
Console.WriteLine();
The output of this example is shown here:
expando contains: Brian, USA, English
Changing Country to France...
Country changed to France, changing Language to French
Setting Language to: French
expando contains: Brian, France, French
Changing Country to China...
Country changed to China, changing Language to Mandarin
Setting Language to: Mandarin
expando contains: Brian, China, Mandarin
Changing Country to Spain...
Country changed to Spain, changing Language to Spanish
Setting Language to: Spanish
expando contains: Brian, Spain, Spanish
Discussion
ExpandoObject
allows you to write code that is more readable than typical reflection code with GetProperty(“Field”)
syntax. It can be useful when dealing with XML or JSON for quickly setting up a type to program against instead of always having to create data transfer objects. The ability for ExpandoObject
to support data binding through INotifyPropertyChanged
is a huge win for anyone using WPF, MVC, or any other binding framework in .NET as it allows you to use these “objects on the fly” as well as other statically typed classes.
Since ExpandoObject
can take delegates as members, this allows us to attach methods and events to these dynamic types while the code looks like you are addressing a static type.
public static void AddEvent(ExpandoObject expando, string eventName, Action<object, EventArgs> handler)
{
var expandoDict = expando as IDictionary<string, object>;
if (expandoDict.ContainsKey(eventName))
expandoDict[eventName] = handler;
else
expandoDict.Add(eventName, handler);
}
For both AddEvent
and AddProperty
, you might be asking why didn’t we use extension methods for AddProperty
and AddEvent
? They both could hang off of ExpandoObject
and make the syntax even cleaner, right? Unfortunately, that is not possible as extension methods work by the compiler doing a search on all classes that might be a match for the extended class. This means that the DLR
would have to know all of this information at runtime as well (since ExpandoObject
is handled by the DLR
) and currently all of that information is not encoded into the call site for the class and methods.
The event argument classes for the LanguageChanged
and CountryChanged
events are listed here:
public class LanguageChangedEventArgs : EventArgs
{
public string Language { get; set; }
}
public class CountryChangedEventArgs : EventArgs
{
public string Country { get; set; }
}
See Also
See the “ExpandoObject
class”,”Func<>
delegate”, “Action<>
delegate”, and the “INotifyPropertyChanged
interface” in the MSDN documentation.