Building C# objects dynamically

Using ExpandoObject to create objects that you can add properties, methods, and events to.

Buy “C# 6.0 Cookbook” in early release.
Editor’s note: This is an excerpt from “C# 6.0 Cookbook,” by Jay Hilyard and Stephen Teilhet. It offers more than 150 code recipes to common and not-so-common problems that C# programmers face every day. In it, you’ll find recipes on asynchronous methods, dynamic objects, enhanced error handling, the Rosyln compiler, and more.

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.

tags: , , , , , ,