Making a DynamicObject implementation be case insensitive

To me this seems to be a very straight forward request but I cannot find the ‘easy’ way to do this in .Net even though there seems to be a clear indication that this is supported Out of The Box. Here’s an example of what I want to do:

//Simple class
public class Foo
{
    public string Hello {get;set;}
    public string World {get;set;}
    public int AddNumbers(int num1, int num2)
    {
        return num1 + num2;
    }
}

//Use the simple class but allow case insensitive
// access to it's properties and members
var foo = new Foo{ Hello = "hi", World = "earth" };

//make it dynamic
dynamic dFoo = foo;

//access the dynamic object's props and methods 
// in a case insensitive way:
Assert.AreEqual(foo.Hello, dFoo.hello);
Assert.AreEqual(foo.World, dFoo.world);
Assert.AreEqual(foo.AddNumbers(1,2), dFoo.addNumbers(1,2));

DynamicObject

In order to do the above, you can implement ‘DynamicObject’ and override a couple of methods:

public class Foo : DynamicObject
{
    public string Hello {get;set;}
    public string World {get;set;}
    public int AddNumbers(int num1, int num2)
    {
        return num1 + num2;
    }
    
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        //Here we would need to match lower cased binder.Name
        // to a real method that our object has
        var name = binder.Name.ToLowerInvariant();
        //TODO: Do the matching
        
        return base.TryInvokeMember(binder, args, out result);
    }
    
    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        //Here we would need to match lower cased binder.Name
        // to a real property that our object has
        var name = binder.Name.ToLowerInvariant();
        //TODO: Do the matching
        
        return base.TryGetMember(binder, out result);
    }
}

I don’t know about you, but I’d rather not have to write all the code to do the matching and executing. This seems like a pretty straight forward thing to want to do and after looking at the innards of some of this stuff, it turns out that the ‘GetMemberBinder’ object has a property conveniently called `IgnoreCase`… surely this functionality already exists!?

Well I unfortunately cannot find anything worthwhile on Google about ‘GetMemberBinder.IgnoreCase’, I also can’t really see where or how it gets used when looking at decompiled sources.

So I’ve reverted to handling this on my own but I would really really really like to know if there’s a built in and simple way to do this in .Net since it sure seems like it should exist.

CaseInsensitiveDynamicObject

This is a base class implementation to achieve what I want. It’s not error proof and I’m sure with some objects it won’t work 100% as expected but for simple objects this will work. Using this implementation is super easy:

public class Foo : CaseInsensitiveDynamicObject
{
    public string Hello {get;set;}
    public string World {get;set;}
    public int AddNumbers(int num1, int num2)
    {
        return num1 + num2;
    }
}

By doing that, my original requirement works! Here’s the code that does this … because I was lazy and didn’t want to create more classes than I needed I’m using Tuples so the code is fully of super generic fun ;)

/// <summary>
/// This will check enable dynamic access to properties and methods in a case insensitive manner
/// </summary>
/// <typeparam name="T"></typeparam>
/// <remarks>
/// This works by using reflection on the type - the reflection lookup is lazy so it will not execute unless a dynamic method needs to be accessed
/// </remarks>
public abstract class CaseInsensitiveDynamicObject<T> : DynamicObject
    where T: class
{
    /// <summary>
    /// Used for dynamic access for case insensitive property access
    /// </summary>`
    private static readonly Lazy<IDictionary<string, Func<T, object>>> CaseInsensitivePropertyAccess = new Lazy<IDictionary<string, Func<T, object>>>(() =>
    {
        var props = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public)
            .DistinctBy(x => x.Name);
        return props.Select(propInfo =>
        {
            var name = propInfo.Name.ToLowerInvariant();
            Func<T, object> getVal = propInfo.GetValue;
            return new KeyValuePair<string, Func<T, object>>(name, getVal);

        }).ToDictionary(x => x.Key, x => x.Value);
    });

    /// <summary>
    /// Used for dynamic access for case insensitive property access
    /// </summary>
    private static readonly Lazy<IDictionary<string, Tuple<ParameterInfo[], Func<T, object[], object>>>> CaseInsensitiveMethodAccess
        = new Lazy<IDictionary<string, Tuple<ParameterInfo[], Func<T, object[], object>>>>(() =>
        {
            var props = typeof(T).GetMethods(BindingFlags.Instance | BindingFlags.Public)
                .Where(x => x.IsSpecialName == false && x.IsVirtual == false)
                .DistinctBy(x => x.Name);
            return props.Select(methodInfo =>
            {
                var name = methodInfo.Name.ToLowerInvariant();
                Func<T, object[], object> getVal = methodInfo.Invoke;
                var val = new Tuple<ParameterInfo[], Func<T, object[], object>>(methodInfo.GetParameters(), getVal);
                return new KeyValuePair<string, Tuple<ParameterInfo[], Func<T, object[], object>>>(name, val);

            }).ToDictionary(x => x.Key, x => x.Value);
        });

    /// <summary>
    /// Used for dynamic access for case insensitive method access
    /// </summary>
    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        var name = binder.Name.ToLowerInvariant();
        if (CaseInsensitiveMethodAccess.Value.ContainsKey(name) == false)
            return base.TryInvokeMember(binder, args, out result);

        var val = CaseInsensitiveMethodAccess.Value[name];
        var parameters = val.Item1;
        var callback = val.Item2;
        var fullArgs = new List<object>(args);
        if (args.Length <= parameters.Length)
        {
            //need to fill them up if they're optional
            for (var i = args.Length; i < parameters.Length; i++)
            {
                if (parameters[i].IsOptional)
                {
                    fullArgs.Add(parameters[i].DefaultValue);
                }
            }
            if (fullArgs.Count == parameters.Length)
            {
                result = callback((T)(object)this, fullArgs.ToArray());
                return true;
            }
        }
        return base.TryInvokeMember(binder, args, out result);
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var name = binder.Name.ToLowerInvariant();
        if (CaseInsensitivePropertyAccess.Value.ContainsKey(name) == false)
            return base.TryGetMember(binder, out result);

        result = CaseInsensitivePropertyAccess.Value[name]((T)(object)this);
        return true;
    }
}

 

If anyone know how to make this work OOTB with .Net, perhaps using the `GetMemberBinder.IgnoreCase` I’d love to hear it!

Author

Shannon Thompson

I'm a Senior Software Engineer working full time at Microsoft. Previously, I was working at Umbraco HQ for about 10 years. I maintain several open source projects (many related to Umbraco) such as Articulate, Examine and Smidge, and I also have a commercial software offering called ExamineX. Welcome to my blog :)

comments powered by Disqus