In this article, we are going to learn how to create a class dynamically in C#. Thanks to the introduction of the dynamic type in C# 4, working with dynamic classes has become easier, but we shouldn’t abuse it. Dynamic classes are very powerful, but they bring a substantial overhead as well.

To download the source code for this article, you can visit our GitHub repository.

Let’s start.

Why Do We Need Dynamic Classes?

Before looking at the code, let’s imagine a scenario where using a dynamic class is a reasonable choice. We need to aggregate data coming from several weather stations. The problem is that each station returns this data as JSON, each with a different schema. This means that the first weather station could return the temperature as “Temperature1”, the second one as “Temperature2”, and so on.

We might not want to create a data class for each weather station response, so what is it that we can do in this case? 

How to Create a Class Dynamically With ExpandoObject

ExpandoObject is part of the System.Dynamic namespace and allows us to add/remove properties to it at runtime. Creating an instance of the ExpandoObject is as simple as:

dynamic expando = new ExpandoObject();

Then, if we need to add a new property to this instance, it is sufficient to initialize it using the dot-notation:

expando.MyNewProperty = "dynamic property"

In our example, we have one of the weather stations that returns this JSON object:

{ 
    "Temperature1": 10.5, 
    "Humidity1": 50
}

How can we create an ExpandoObject from this JSON string? We have 2 options: Newtonsoft.Json or System.Text.Json. While the second one should guarantee better performances, it does not yet provide full support for dynamic objects. With System.Text.Json, even if we specify ExpandoObject to be the deserialization type, each property is wrapped in a JsonElement:

dynamic expando = JsonSerializer.Deserialize<ExpandoObject>(jsonWeather);

If we try to access expando.Temperature1 we get a JsonElement representing the value 10.5. On the other hand, with Newtonsoft.Json, properties are deserialized as expected: 

var expandoConverter = new ExpandoObjectConverter();
dynamic expando = JsonConvert.DeserializeObject<ExpandoObject>(
    json, expandoConverter);

expando.Temperature1 and expando.Humidity1 return 10.5 and 50, respectively.

We can also simulate a method call by assigning a lambda to a new property:

var toCsvFormatter = (dynamic thisObj) => 
    () => 
    {
        StringBuilder sb = new StringBuilder();

        foreach (var prop in (IDictionary<String, Object>)thisObj)
            if (prop.Key != "Format")
                sb.Append($"{prop.Value},");

        sb.Remove(sb.Length - 1, 1);

        return sb.ToString();
    };

expando.Format = toCsvFormatter(expando);

toCsvFormatter(expando) is a partial application of the toCsvFormatter lambda. This is convenient because we cannot access this, which is expando, directly from inside the lambda. The only way to access it is to pass it as an argument to the lambda itself. Instead of passing expando every time we need to call Format(), we do it at initialization time.

We call Format() the same way we call a method from a statically compiled class. Starting from the previous expando initialization:

var formattedExpando = expando.Format();
Assert.AreEqual("10.5,50", formattedExpando);

In the lambda body, we are looping over each property of expando. The ExpandoObject implements the IDictionary<string, object> interface, which allows us to enumerate properties as key-value pairs or to remove one with:

((IDictionary<string, Object>)expando).Remove("Temperature1");

Another interesting feature of the ExpandoObject is the possibility to attach an event handler when any dynamic property changes. Let’s create a class that aggregates data coming from weather stations:

public class WeatherAggregator
{
    public WeatherAggregator(dynamic weatherExpando1, dynamic weatherExpando2, dynamic weatherExpando3)
    {
        foreach (var weatherExp in new dynamic[] { weatherExpando1, weatherExpando2, weatherExpando3 })
        {
            ((INotifyPropertyChanged)weatherExp).PropertyChanged += new PropertyChangedEventHandler(
                (sender, e) => 
                {
                    ComputeAggregatedWeather();
                }
            );
        }

        ComputeAggregatedWeather();
    }

    private void ComputeAggregatedWeather()
    {
        // ...
    }
}

Each time we change a property (like temperature or humidity) of any expando, we should also recompute the aggregation operations. We can do it by leveraging the INotifyPropertyChanged interface implemented by ExpandoObject.

This works by adding an event handler of type PropertyChangedEventHandler to the PropertyChanged member of all the expando objects. In this event handler, we recompute the aggregated data by calling ComputeAggregatedWeather().

Advantages and Disadvantages of ExpandoObject

ExpandoObject is very simple to use and it is probably the best way to bypass static type checking. This is a huge advantage, that should usually make it our favorite way to work with dynamic classes. However, we might need a custom dynamic binding logic, which is different from the one implemented in the ExpandoObject.

Let’s find out how to customize the dynamic binding behavior.

How to Create a Class Dynamically With DynamicObject

DynamicObject is a class in the System.Dynamic namespace, but we have to inherit it because of the constructor protection level (protected). Let’s see how we can do that by solving the weather stations problem again. We are going to wrap the dynamic object created by the JSON deserialization with a DynamicWeatherData object:

dynamic weatherObj = new DynamicWeatherData(
    JsonSerializer.Deserialize<JsonObject>(json)
);

In this case, we have used System.Text.Json, but it is possible with the Json.NET library as well. With Json.NET, instead of deserializing into a JsonObject, we could have used a JObject, which supports DLR. We will not cover the details of the serialization of dynamic objects, but we have written an article on how to deserialize JSON into a dynamic object. This is how we can implement a basic version of DynamicWeatherData:

public class DynamicWeatherData : DynamicObject
{
    JsonObject _weatherData;

    public DynamicWeatherData(JsonObject weatherData)
    {
        _weatherData = weatherData;
    }

    public override bool TrySetMember(SetMemberBinder binder, object? value)
    {
        if (!_weatherData.ContainsKey(binder.Name))
            return false;

        _weatherData[binder.Name] = value == null ? null : JsonValue.Create(value);

        return true;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object? result)
    {
        if (!_weatherData.ContainsKey(binder.Name))
        {
            result = null;
            return false;
        }

        result = _weatherData[binder.Name]?.AsValue();
        return true;
    }
}

The first thing we see is that we completely lost the simplicity of ExpandoObject. Now we have to implement the class behavior, starting with where and how to store the members of the class itself. We have chosen JsonObject as data storage, but properties are dynamically accessed through TryGetMember() and TrySetMember():

weatherObj.Temperature1 = 10;
Assert.IsTrue(weatherObj.Temperature1.GetType().IsAssignableTo(typeof(JsonValue)));
Assert.AreEqual(10, (int)weatherObj.Temperature1);

When we assign 10 to weatherObj.Temperature1, TrySetMember() is called. In this case, binder.Name is equal to "Temperature1" and value is 10. If the property name already exists in _weatherData, then we store the new value as a JsonValue and return true.

When we try to get weatherObj.Temperature1 in the assertions, TryGetMember() is called. binder.Name is equal to "Temperature1", but we must initialize result before returning. If the property name already exists in _weatherData, then we return the stored value as a JsonValue and return true. The explicit cast to int is necessary because the result type is JsonValue.

Both TryGetMember() and TrySetMember() return a boolean value, which represents the success or failure of the method execution. In case of failure (false), the runtime binder usually throws an exception.

So far, there is no reason to wrap a JsonObject with a DynamicObject because we are just overcomplicating the design. After all, what have we gained over the solution with ExpandoObject? We should probably not adopt this approach when we can achieve the same result with ExpandoObject.

The beauty of DynamicObject is that we can customize many other aspects of our class. The first one we are going to see is TryInvokeMember():

public override bool TryInvokeMember(InvokeMemberBinder binder, object?[]? args, out object? result)
{
    if (binder.Name == "Format" && args?.Length == 0)
    {
        var (temperature, humidity) = GetProperties();

        result = $"{temperature},{humidity}";
        return true;
    }
    else
    {
        result = null;
        return false;
    }
}

private (double, int) GetProperties()
{
    var temperatureAttr = _weatherData
        .First(kv => kv.Key.StartsWith("Temperature"))
        .Value!.GetValue<double>();
    var humidityAttr = _weatherData
        .First(kv => kv.Key.StartsWith("Humidity"))
        .Value!.GetValue<int>();

    return (temperatureAttr, humidityAttr);
}

binder.Name contains the method name we are trying to call, args are the arguments provided to the method. The advantage we have here over the ExpandoObject solution is that we have direct access to the class methods. The only supported method in the example is Format(), which we can call with:

var formattedWeather = weatherObj.Format();
Assert.AreEqual("10.5,50", formattedWeather);

What if we wanted to cast our dynamic object to a data class, like WeatherData:

public class WeatherData 
{
    public WeatherData(double temperature, int humidity) 
    {
        this.Temperature = temperature;
        this.Humidity = humidity;
    }

    public double Temperature { get; init; }
    public int Humidity { get; init; }
}

With an ExpandoObject, we would need to create a method that cast the dynamic object to the data class involved. This is not a real problem, but we would lose the possibility to use explicit/implicit casting. Instead, to support the casting operator in DynamicWeatherData, we just need to override TryConvert()

public override bool TryConvert(ConvertBinder binder, out object? result)
{
    if (binder.Type.Equals(typeof(WeatherData)))
    {
        var (temperature, humidity) = GetProperties();

        result = new WeatherData(temperature, humidity);
        return true;
    }
    else 
    {
        result = null;
        return false;
    }
}

binder contains the target type, which can come from implicit or explicit casting. This means that this is perfectly valid:

WeatherData castObj = weatherObj;

Where the type of weatherObj is DynamicWeatherData.

We have seen just a few possible methods we can override to customize the dynamic behavior of our class. Was this the right way to solve the initial problem?

Advantages and Disadvantages of DynamicObject

ExpandoObject and DynamicObject have one thing in common: they both implement IDynamicMetaObjectProvider. This interface is responsible for DLR support. The difference is that DynamicObject is much more flexible than ExpandoObject, which is limited to what we have shown previously. In the examples we proposed, DynamicObject was unnecessary and the benefits we obtained didn’t pay off the implementation costs.

For these reasons, before opting for DynamicObject, we should try to solve the problem with ExpandoObject.

And what if DynamicObject isn’t good enough?

Create a Class Dynamically in C# With Roslyn

Roslyn, the .NET compiler, has some public APIs that we can use to compile source code at runtime. Finding a reasonable use case for them is probably more difficult than using them. It might be convenient if we wanted to execute a piece of code on a remote device.

We are not going to cover this topic extensively here because it is very complex. We have added a simple example in the GitHub repository (see CSharpRuntimeCompilerUnitTest.cs), but it is just the tip of the iceberg. If we don’t have a choice and we are left with this solution, our advice is to start from this article.

Conclusion

Creating a class dynamically in C# isn’t cheap at all in terms of performance. Moreover, we lose static type-checking and the risk of introducing a bug is higher. Typically, we should give priority to ExpandoObject because easier to use. We have also seen the approach based on DynamicObject, which is harder to maintain but more flexible than ExpandoObject.