Friday, March 6, 2015

Having fun with C# dynamic and reflection: create a class that let us to avoid access modifier

Yeah, the title says it all.

Have you ever get tired of manually call reflection methods to get a value of an hidden field? Yes I do!

In my spare time I found a funny solution to work around this problem.
I found a dummy way to avoid access modifier without losing 'deafult' syntax: thanks to c# dynamic object!
public class MyClass
{
    private int value = 1 ; 
}
private static void Main(string[] args)
{
    MyClass myInstance = new MyClass();

    //cannot access private field 'value' here
    //myInstance.value = 2;

    dynamic myInstance2 = myInstance.Expose();

    //can do it!
    myInstance2.value = 2;
}

To achieve a result like that, I simply created a class inherited by DynamicObject called.. ObjectSpy... sounds good.

This class simply wraps an object, holds a reference to its instance and maps every field, properties, methods of its type. The dirty work is done by c# dynamic runtime and DynamicObject overridable methods.

When I type myInstance2.value, actually dynamic runtime "redirects" the call to its method TryGetMember or TrySetMember.

I just have to do the correct things in those methods: find the correct member, invoke it, return it and so forth...

N.B.
I think if you often use reflection to get or set hidden values, there is something wrong in your code and you should re-design your classes. This post is meant to show capabilities and potential of c# dynamic objects.

The following scripts are not a complete solution, these are only hints I wanted to share.
Anyway I reccomend you to give it a try ;)
class ObjectSpy : DynamicObject
{
    private readonly object _instance; 
    private readonly TypeHelper _typeHelper;
 
    //this class gets all the members of a type and provides a search method for every MemberType      
    internal sealed class TypeHelper
    {
        readonly MemberInfo[] members;

        internal TypeHelper(IReflect type)
        {
            members = type.GetMembers(
                         BindingFlags.Public |
                         BindingFlags.NonPublic |
                         BindingFlags.Static |
                         BindingFlags.Instance |
                         BindingFlags.GetProperty |
                         BindingFlags.SetProperty);
        }


        public FieldInfo GetFieldByName(string name)
        {
            return members.OfType<FieldInfo>().FirstOrDefault(f => f.Name == name);
        }

        public MemberInfo GetMemberByName(string name)
        {
            return members.FirstOrDefault(f => f.Name == name);
        }

        public PropertyInfo GetPropertyByName(string name)
        {
            return members.OfType<PropertyInfo>().FirstOrDefault(p => p.Name == name);
        }

        public MethodInfo GetMethodByNameAndArgs(string name, object[] args)
        {
            var argsLength = args == null ? 0 : args.Length;

            Func<ParameterInfo[],bool> match = parameters =>
            {
                if (argsLength != parameters.Length)
                    return false;

                for (int i = 0; i < argsLength; i++)
                {
                    var parameterType = parameters[i].ParameterType;
                    var input = (args[i] ?? new object()).GetType();
                    if (!input.IsAssignableFrom(parameterType))
                        return false;
                }
                return true;
            };

            return members.OfType<MethodInfo>().FirstOrDefault(f => f.Name == name &&
                                    match(f.GetParameters()));
        }
    }

    public ObjectSpy(object instance)
    {
        _instance = instance;
        _typeHelper = new TypeHelper(_instance.GetType());
    }

    private Exception NotSupportedException()
    {
        return new NotSupportedException();
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
        var member = _typeHelper.GetMemberByName(binder.Name);

        if (member != null)
        {
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    ((FieldInfo)member).SetValue(_instance, value);
                    return true;
                case MemberTypes.Property:
                    ((PropertyInfo)member).SetValue(_instance, value);
                    return true;
            }
        }

        throw NotSupportedException();
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
        var member = _typeHelper.GetMemberByName(binder.Name);

        if (member != null)
        {
            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    result = ((FieldInfo)member).GetValue(_instance);
                    return true;
                case MemberTypes.Property:
                    result = ((PropertyInfo)member).GetValue(_instance);
                    return true;
            }
        }

        throw NotSupportedException();

    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
    {
        var method = _typeHelper.GetMethodByNameAndArgs(binder.Name, args);

        if (method == null) throw NotSupportedException();

        result = method.Invoke(_instance, args);

        return true;
    }
 
    /*
       The following methods are 'delegating' instructions to the original object.
       I'm doing a kind of proxy.
    */
    
    public override bool TryGetIndex(GetIndexBinder binder, dynamic[] indexes, out object result)
    {
        if (indexes.Length > 1) throw NotSupportedException();

        result = ((dynamic)_instance)[indexes[0]];

        return true;
    }

    public override bool TrySetIndex(SetIndexBinder binder, dynamic[] indexes, dynamic value)
    {
        if (indexes.Length > 1) throw NotSupportedException();

        ((dynamic)_instance)[indexes[0]] = value;

        return true;
    }
}


static class ObjectSpyExtension
{
    // a simple extension method to simplify syntax
    public static ObjectSpy Expose(this object instance)
    {
        return new ObjectSpy(instance);
    }
}

No comments:

Post a Comment