Issues

Resilient Dynamic Properties in Razor Views Using SafeExpandoObject

Properties on a model class that are dynamic, case-insensitive, and fault-tolerant.

Background

Formulate Pro comes with a feature called Designed Emails. As part of this feature, I wanted to make it as easy as possible to create a Razor view that would be able to access data submitted with a form. The idea is that when a user submits a contact form, you can send them a nice looking email that contains some of the field values they entered.

Since content editors might fiddle with these Formulate fields, I still wanted the emails to send even if some of the field alias or names got changed. On top of that, I didn't want the programmer of the Razor views to have to worry about the exact case (i.e., uppercase or lowercase letters) of the field as it was entered in the CMS (in the Formulate form designer).

This is why I decided to implement resilient dynamic properties.

Definition

In short, resilient dynamic properties allow you to access the properties of your model with the following goals:

  • Dynamic This way, you don't need to create a class to quickly access properties on your model. This also allows you to skip a compile step.
  • Fault-Tolerant If you attempt to access a property that doesn't exist, null is returned rather than throwing an exception.
  • Case-Insensitive You can access the property using uppercase or lowercase letters.

If this reminds you of something, you aren't alone. This is similar to how properties work in JavaScript. If you have an object in JavaScript, you can access properties on it dynamically, and those properties are fault-tolerant (i.e., undefined will be returned on any property that hasn't been set). JavaScript is still case-sensitive, but it is otherwise very similar to resilient dynamic properties.

Quick Example

In Formulate Pro, you can access the value of the first name field from a form in the following way within a Razor view:

Hello @Model.Values.FirstName,

Similarly, all of these will return the same result:

@Model.Values.firstname
@Model.Values.firstName
@Model.Values.FIRSTNAME

If you aren't a fan of typing all that, you can shorten it a bit by assigning the values object to a variable:

@{
    var values = Model.Values;
}
Hello @values.FirstName @values.LastName,

Supposing a content editor removes the "Last Name" field from the form and changes the "First Name" field to instead be called the "Full Name" field, none of the above code samples would throw an error. Instead, a null value would be returned. This will allow your emails to continue to work until the problem is corrected.

Dynamic Properties in C#

C# has a concept called dynamic properties, and another related concept called expando objects. You can use dynamic properties to access (and set) values on an expando object. Here's an example of a function body that makes use of both of these concepts:

var instance = new ExpandoObject() as dynamic;
instance.SomeProperty = "Some Value";
return instance.SomeProperty;

That works fine in C#. However, the following would not compile in C#:

var instance = new ExpandoObject();
instance.SomeProperty = "Some Value";
return instance.SomeProperty;

While an expando object facilitates dynamically adding and accessing properties, you will be unable to do so elegantly without assigning that expando object to an instance of type dynamic. The inverse is also true. That is, the following code will compile, but it will throw an exception at runtime:

var instance = new object() as dynamic;
instance.SomeProperty = "Some Value";
return instance.SomeProperty;

The dynamic type obscures the underling type, so the compiler doesn't know the property you are attempting to set and get does not exist.

These two features alone will not get you resilient dynamic properties. For that, you need to go a step further.

Pulling Back the Curtain Behind ExpandoObject

If you look at the definition of the ExpandoObject class, you'll see that it implements the IDynamicMetaObjectProvider interface. This is the interface you need to implement if you wish to provide similar dynamic properties. In other words, ExpandoObject isn't all that special; so long as you create a class that implements the same IDynamicMetaObjectProvider interface, you will be able to create an object that allows for dynamic getting and setting of values based on whatever arbitrary logic that you want.

While you could probably implement the IDynamicMetaObjectProvider interface from scratch, it seemed a bit daunting to me personally, so I opted to find an easier way.

Use MetaObject or DynamicObject for the Heavy Lifting

The easiest way I could find to implement the IDynamicMetaObjectProvider interface was to rely on a NuGet package called MetaObject. When I built Formulate Pro, I made use of this NuGet package when implementing the SafeExpandoObject class, which is my custom version of ExpandoObject that allows for resilient dynamic properties. Here's the first step of what that looks like:

using System.Dynamic;
using System.Linq.Expressions;

public class SafeExpandoObject : IDynamicMetaObjectProvider
{
    public DynamicMetaObject GetMetaObject(Expression e) => new MetaObject(e, this);
}

This is only a first step (while the interface is technically implemented, there are some extra steps to implement), but this shows how easy this NuGet package makes implementing your own ExpandoObject; virtually no custom code is necessary, as it handles all those complexities for you. Here is Formulate Pro's implementation of SafeExpandoObject, minus the code comments and a few unnecessary functions (for brevity):

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Dynamic;
using System.Linq.Expressions;

public class SafeExpandoObject : IDynamicMetaObjectProvider
{
    private Dictionary<string, object> Values { get; set; }
    private object FallbackValue { get; set; }

    public SafeExpandoObject(object fallbackValue = null)
    {
        Values = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
        FallbackValue = fallbackValue;
    }

    public bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = Values.ContainsKey(binder.Name) ? Values[binder.Name] : FallbackValue;
        return true;
    }

    public bool TrySetMember(SetMemberBinder binder, object value)
    {
        Values[binder.Name] = value;
        return true;
    }

    public DynamicMetaObject GetMetaObject(Expression e) => new MetaObject(e, this);
}

If you like, you can also subclass DynamicObject rather than directly implementing the IDynamicMetaObjectProvider interface. Both approaches are very similar, but implementing the interface rather than subclassing allows your class to derive from another class, should you need to do that. Here's what it would look like if you subclass DynamicObject:

using System;
using System.Collections.Generic;
using System.Dynamic;

public class SafeExpandoObject : DynamicObject
{
    private Dictionary<string, object> Values { get; set; }
    private object FallbackValue { get; set; }

    public SafeExpandoObject(object fallbackValue = null)
    {
        Values = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
        FallbackValue = fallbackValue;
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        result = Values.ContainsKey(binder.Name) ? Values[binder.Name] : FallbackValue;
        return true;
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        Values[binder.Name] = value;
        return true;
    }
}

You can find a similar implementation in the documentation for .Net: https://docs.microsoft.com/en-us/dotnet/api/system.dynamic.dynamicobject?view=netcore-3.1#examples

How to Use SafeExpandoObject

Now that we've created SafeExpandoObject, the following is all we need to use it in a console app:

using System;

class Program
{
    static void Main(string[] args)
    {
        var person = new SafeExpandoObject() as dynamic;
        person.Name = Console.ReadLine();
        Console.WriteLine($"My name is {person.naME}.");
        System.Threading.Thread.Sleep(10000);
    }
}

Notice that I set it with person.Name, but I read it with person.naME. This demonstrates that it is not case-sensitive. You can also go a step further:

using System;

class Program
{
    static void Main(string[] args)
    {
        var person = new SafeExpandoObject() as dynamic;
        var name = Console.ReadLine();
        if (!string.IsNullOrWhiteSpace(name))
        {
            person.Name = name;
        }
        Console.WriteLine($"My name is {person.naME ?? "unknown"}.");
        System.Threading.Thread.Sleep(10000);
    }
}

If you run the app and press ENTER without entering a name, the person.Name property is never set. In that case, the code just outputs the name as "unknown" (i.e., rather than throwing an exception because the property doesn't exist). Here's what it looks like if you enter a name:

Here's what it looks like if you do not enter a name:

The Internals of SafeExpandoObject

How does all this work? By implementing the IDynamicMetaObjectProvider interface and returning a MetaObject from GetMetaObject, you provide the essential building blocks for dynamic property access (sublcassing DynamicObject also provides these building blocks).

The TrySetMember and TryGetMember provide a way to set property values and get property values. In the case of SafeExpandoObject, accessing a property value has been written in a way that facilitates case-insensitive and fault-tolerant access.

The case-insensitive implementation is managed using a dictionary with a string comparer that ignores case:

Values = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);

That ensures values can be accessed regardless of the case of the property name.

The fault-tolerance is managed by checking if the dictionary contains a key and, if not, specifying a fallback value:

result = Values.ContainsKey(binder.Name) ? Values[binder.Name] : FallbackValue;

In this case, the fallback value was specified via the constructor (and when not specified, a null is used as the fallback value). If you wanted the default value to be "-1" (e.g., in the case of accessing a property that is an integer that should not be nullable), you could do that by specifying "-1" as the fallback value via the constructor:

var person = new SafeExpandoObject(-1) as dynamic;

How to Use SafeExpandoObject

You can use SafeExpandoObject (or your own similar implementation) whenever you need to customize what ExpandoObject does. Here are some ideas for how this might be used:

  • Decorator Pattern Copy an object of an arbitrary type, then add additional properties to the copy. This is similar to the decorator pattern: https://en.wikipedia.org/wiki/Decorator_pattern
  • Dynamic Objects Construct objects from arbitrary data, and allow easy access to that data. This is what Formulate Pro does for the designed email functionality.
  • Indirection Wrap an object so you can run some code whenever a property is accessed. For example, in case you want to log that the property was accessed, or if you want to set a breakpoint for any property access, or if you want to run profiling to see how long property accesses take.
  • Translations Provide a simple way to get translations (e.g., norwegian.Hi might return "Hei").
  • Conciseness Remove a few characters when accessing a dictionary (e.g., instance.Item rather than instance["Item"]).
  • Access Private Properties If you have a class that has private properties you need to access, you could use reflection to do so, and you could create a custom expando object to make the syntax straightforward.
  • Automatic Abbreviations If you are working with a class that has overly verbose property names, you could create a wrapper class that allows you to access properties via abbreviations for those properties (e.g., wrapper.GUID rather than instance.GloballyUniqueIDentifier).

I'm sure there are plenty of other use cases that I'm not thinking of. Leave a comment below if you think of another interesting way to use SafeExpandoObject.

comments powered by Disqus