Monday, March 31, 2008

Dynamic lookup

A while back Charlie Calvert and Mads Torgersen wrote about dynamic lookup being part of the plans for C# 4.0. A code block specified with the "dynamic" key word will allow dynamic lookup with syntax like follows:

static void Main(string[] args)
{
    dynamic
    {
        object myDynamicObject = GetDynamicObject();
        myDynamicObject.SomeMethod();         // call a method   
        myDynamicObject.someString = "value"; // Set a field
        myDynamicObject[0] = 25;              // Access an indexer
    }
}

While this is a welcome feature I want it now! Of course I could resort to reflection but it gets tiresome writing all the code needed just to get a simple field or property so I made a little library that lets me write the previous example as follows:

static void Main(string[] args)
{
    object myDynamicObject = GetDynamicObject();
    myDynamicObject.Member("SomeMethod").Call();   // call a method   
    myDynamicObject.Member("someString").Set("value").Call(); // Set a field
    myDynamicObject.Member("Item")[0].Set(25).Call();  // Access an indexer
}

 

This also allows me to access private, protected or internal members of objects and on top of that it can compile functions for invoking the member on several instances of the same type.

I provide the code here but I'm sure there are several bugs in it so please don't use it in your applications as is. Actually please don't use it for anything like that without asking permission first...

#region Using Directives
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Globalization;
using System.Reflection.Emit;
#endregion

namespace Minimal.Reflection
{
    /// <summary>
    /// Gives dynamic access to object members.
    /// </summary>
    /// <typeparam name="TValue">The type of the return value, in the case of void
    /// object should be used.</typeparam>
    /// <typeparam name="TInstance">The kind of object that the call is performed on.</typeparam>
    public class MemberCall<TInstance, TValue>
    {
        #region Fields
        private object[] parameters;
        private Type[] signature;
        private TInstance instance;
        private string memberName;
        private bool setValueToMember;
        private MemberInfo member;
        private TValue setValue;
        private static readonly BindingFlags memberBindingFlags = 
            BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic;
        #endregion

        #region Constructor
        /// <summary>
        /// Creates a new instance.
        /// </summary>
        /// <param name="instance">The instance to invoke the member on.</param>
        /// <param name="name">The name of the member to invoke.</param>
        public MemberCall(TInstance instance, string name)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("name");

            this.instance = instance;
            this.memberName = name;
            this.signature = Type.EmptyTypes;
        } 
        #endregion

        #region Properties
        /// <summary>
        /// Sets the index parameter.
        /// </summary>
        /// <param name="index">The index parameter to use.</param>
        /// <returns></returns>
        public MemberCall<TInstance, TValue> this[object index]
        {
            get
            {
                this.parameters = new object[] { index };
                return this;
            }
        }
        #endregion

        #region Methods
        /// <summary>
        /// Specifies a value that's to be set to the member (field or property).
        /// </summary>
        /// <param name="value">The value to set to the member when invoked.</param>
        /// <returns>This MemberCall instance.</returns>
        public MemberCall<TInstance, T> Set<T>(T value) where T : TValue
        {
            if (typeof(T).Equals(typeof(TValue)))
            {
                this.setValue = value;
                this.setValueToMember = true;
                return (MemberCall<TInstance, T>)(object)this;
            }
            else
            {
                var result = new MemberCall<TInstance, T>(this.instance, this.memberName);
                result.parameters = this.parameters;
                result.signature = this.signature;
                result.setValue = value;
                result.setValueToMember = true;

                return result;
            }
        }

        /// <summary>
        /// The parameters of the call (method or property).
        /// </summary>
        /// <typeparam name="TValue">The type of the parameter.</typeparam>
        /// <param name="parameter">The value of the parameter.</param>
        /// <returns>This instance.</returns>
        public MemberCall<TInstance, TValue> Parameters<T1>(T1 parameter)
        {
            signature = new Type[] { typeof(T1) };
            parameters = new object[] { parameter };
            return this;
        }

        /// <summary>
        /// The parameters of the call (method or property).
        /// </summary>
        /// <typeparam name="T1">The type of the first parameter.</typeparam>
        /// <typeparam name="T2">The type of the second parameter.</typeparam>
        /// <param name="parameter1">The value of the first parameter.</param>
        /// <param name="parameter2">The value of the second parameter.</param>
        /// <returns>This instance.</returns>
        public MemberCall<TInstance, TValue> Parameters<T1, T2>(T1 parameter1, T2 parameter2)
        {
            this.signature = new Type[] { typeof(T1), typeof(T2) };
            this.parameters = new object[] { parameter1, parameter2 };
            return this;
        }

        /// <summary>
        /// The parameters of the call (method or property).
        /// </summary>
        /// <typeparam name="T1">The type of the first parameter.</typeparam>
        /// <typeparam name="T2">The type of the second parameter.</typeparam>
        /// <typeparam name="T3">The type of the third parameter.</typeparam>
        /// <param name="parameter1">The value of the first parameter.</param>
        /// <param name="parameter2">The value of the second parameter.</param>
        /// <param name="parameter3">The value of the third parameter.</param>
        /// <returns>This instance.</returns>
        public MemberCall<TInstance, TValue> Parameters<T1, T2, T3>(T1 parameter1, T2 parameter2, T3 parameter3)
        {
            this.signature = new Type[] { typeof(T1), typeof(T2), typeof(T3) };
            this.parameters = new object[] { parameter1, parameter2, parameter3 };
            return this;
        }

        /// <summary>
        /// The parameters of the call (method or property).
        /// </summary>
        /// <typeparam name="T1">The type of the first parameter.</typeparam>
        /// <typeparam name="T2">The type of the second parameter.</typeparam>
        /// <typeparam name="T3">The type of the third parameter.</typeparam>
        /// <typeparam name="T4">The type of the fourth parameter.</typeparam>
        /// <param name="parameter1">The value of the first parameter.</param>
        /// <param name="parameter2">The value of the second parameter.</param>
        /// <param name="parameter3">The value of the third parameter.</param>
        /// <param name="parameter3">The value of the fourth parameter.</param>
        /// <returns>This instance.</returns>
        public MemberCall<TInstance, TValue> Parameters<T1, T2, T3, T4>(T1 parameter1, T2 parameter2, T3 parameter3, T4 parameter4)
        {
            parameters = new object[] { parameter1, parameter2, parameter3, parameter4 };
            return this;
        }

        /// <summary>
        /// The parameters of the call (method or property).
        /// </summary>
        /// <typeparam name="T1">The type of the first parameter.</typeparam>
        /// <typeparam name="T2">The type of the second parameter.</typeparam>
        /// <typeparam name="T3">The type of the third parameter.</typeparam>
        /// <typeparam name="T4">The type of the fourth parameter.</typeparam>
        /// <param name="parameter1">The value of the first parameter.</param>
        /// <param name="parameter2">The value of the second parameter.</param>
        /// <param name="parameter3">The value of the third parameter.</param>
        /// <param name="parameter3">The value of the fourth parameter.</param>
        /// <param name="additionalParameters">In case that there are more than four parameters in the call,
        /// if any of these parameters are null (Nothing in VB) the signature of the member must be
        /// specified with a call to MemberCall{TValue}.Signature.</param>
        /// <returns>This instance.</returns>
        public MemberCall<TInstance, TValue> Parameters<T1, T2, T3, T4>(T1 parameter1, T2 parameter2, T3 parameter3, T4 parameter4, params object[] additionalParameters)
        {
            var l = new List<object>() { parameter1, parameter2, parameter3, parameter4 };
            l.AddRange(additionalParameters);
            parameters = l.ToArray();


            if (signature.Length != additionalParameters.Length + 4)
            {
                signature = new Type[additionalParameters.Length + 4];
            }

            signature[0] = typeof(T1);
            signature[1] = typeof(T2);
            signature[2] = typeof(T3);
            signature[3] = typeof(T4);

            for (int i = 4; i < parameters.Length; i++)
            {
                if (parameters[i] != null && signature[i] == null)
                {
                    signature[i] = parameters[i].GetType();
                }
            }

            return this;
        }

        /// <summary>
        /// Specifies the signature of the member, this is only needed if there are more than four 
        /// parameters in the call and one
        /// of the parameters after the fourth parameter is null (Nothing in VB).
        /// </summary>
        /// <param name="types">The <see cref="Type" />s (in the correct order) that makes up the signature of 
        /// the member that will be invoked, only needed if there are more than four parameters in the call and one
        /// of the parameters after the fourth parameter is null (Nothing in VB).</param>
        /// <returns>This MemberCall instance.</returns>
        public MemberCall<TInstance, TValue> Signature(params Type[] types)
        {
            signature = types;
            return this;
        }

        /// <summary>
        /// Invokes the member and returns the value that the call
        /// returns. Invoking methods that returns "void" (Sub in VB)
        /// return null.
        /// </summary>
        /// <returns>The result of the invokation.</returns>
        /// <exception cref="InvalidOperationException">The specified member was not found.</exception>
        public TValue Call()
        {
            var member = GetMember();

            switch (member.MemberType)
            {
                case MemberTypes.Field:
                    return InvokeField((FieldInfo)member);
                case MemberTypes.Method:
                    return InvokeMethod((MethodInfo)member);
                case MemberTypes.Property:
                    return InvokeProperty((PropertyInfo)member);
                default:
                    throw new NotSupportedException();
            }
        }

        private MemberInfo GetMember()
        {
            if (this.member == null)
            {
                var members = instance.GetType().GetMember(memberName, memberBindingFlags);

                if (members.Length == 1)
                {
                    this.member = members[0];
                }
                else if (members.Length > 0)
                {
                    this.member = this.ResolveMember(members);
                }
                else
                {
                    this.ThrowMemberNotFound();
                }
            }

            return this.member;
        }

        private void ThrowMemberNotFound()
        {
            throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
                "The member {0} was not found on the type {1}.", this.memberName, this.instance.GetType()));
        }

        private MemberInfo ResolveMember(MemberInfo[] members)
        {
            if (members[0].MemberType == MemberTypes.Property)
            {
                return this.ResolveProperty(members);
            }
            else
            {
                return this.ResolveMethod(members);
            }
        }

        private MemberInfo ResolveProperty(MemberInfo[] properties)
        {
            var result =
                from i in properties
                where ((PropertyInfo)i).GetIndexParameters().Length == this.parameters.Length
                select i;

            var e = result.GetEnumerator();
            if (e.MoveNext() && !e.MoveNext())
            {
                // Return the only member that matched the parameter count.
                return e.Current;
            }

            this.ValidateSignature();
            var property = this.instance.GetType().GetProperty(this.memberName, memberBindingFlags, null, typeof(TValue), this.signature, null);

            if (property == null)
            {
                this.ThrowMemberNotFound();
            }

            return property;
        }

        private MemberInfo ResolveMethod(MemberInfo[] methods)
        {
            var result =
                from i in methods
                where ((MethodInfo)i).GetParameters().Length == this.parameters.Length
                select i;

            var e = result.GetEnumerator();
            if (e.MoveNext() && !e.MoveNext())
            {
                // Return the only member that matched the parameter count.
                return e.Current;
            }

            this.ValidateSignature();

            var method = this.instance.GetType().GetMethod(this.memberName, memberBindingFlags, null, this.signature, null);

            if (method == null)
            {
                this.ThrowMemberNotFound();
            }

            return method;
        }

        private void ValidateSignature()
        {
            foreach (var t in this.signature)
            {
                if (t == null)
                {
                    throw new NotSupportedException("The signature of the member must be specified.");
                }
            }
        }

        /// <summary>
        /// Compiles a function that can be used to set the
        /// value of the specified field or property to instances
        /// of the type TInstance.
        /// </summary>
        /// <returns>The compiled function.</returns>
        public Func<TInstance, TValue> CompileGetter()
        {
            MemberInfo member = this.GetMember();

            if (member.MemberType == MemberTypes.Field)
            {
                return this.CompileFieldGetter((FieldInfo)member);
            }

            if (member.MemberType == MemberTypes.Property)
            {
                return this.CompilePropertyGetter((PropertyInfo)member);
            }

            throw new InvalidOperationException("A getter can only be compiled for fields and non indexed properties.");
        }

        private Func<TInstance, TValue> CompilePropertyGetter(PropertyInfo property)
        {
            Type[] argumentTypes = new Type[] { typeof(TInstance) };
            DynamicMethod method = new DynamicMethod(string.Concat("GetProperty_", property.Name), typeof(TValue), argumentTypes, true);
            var getMethod = property.GetGetMethod(true);
            var gen = method.GetILGenerator(256);

            gen.Emit(OpCodes.Ldarg_0);
            gen.EmitCall(OpCodes.Callvirt, getMethod, null);

            if (!getMethod.ReturnType.Equals(typeof(TValue)) && !getMethod.ReturnType.IsClass)
            {
                gen.Emit(OpCodes.Box, getMethod.ReturnType);
            }

            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate<Func<TInstance, TValue>>();
        }

        private Func<TInstance, TValue> CompileFieldGetter(FieldInfo field)
        {
            Type[] argumentTypes = new Type[] { typeof(TInstance) };
            DynamicMethod method = new DynamicMethod(string.Concat("GetField_", field.Name), typeof(TValue), argumentTypes, true);
            var gen = method.GetILGenerator(256);

            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldfld, field);

            if (!field.FieldType.Equals(typeof(TValue)) && !field.FieldType.IsClass)
            {
                gen.Emit(OpCodes.Box, field.FieldType);
            }

            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate<Func<TInstance, TValue>>();
        }

        /// <summary>
        /// Compiles a method that can be called throught the 
        /// returned delegate to set the value of the member on
        /// the object specified in method.
        /// </summary>
        /// <returns>A delegate to the compiled method.</returns>
        public Action<TInstance, TValue> CompileSetter()
        {
            MemberInfo member = this.GetMember();

            if (member.MemberType == MemberTypes.Field)
            {
                return this.CompileFieldSetter((FieldInfo)member);
            }

            if (member.MemberType == MemberTypes.Property)
            {
                return this.CompilePropertySetter((PropertyInfo)member);
            }

            throw new InvalidOperationException("A setter can only be compiled for fields and non indexed properties.");
        }

        private Action<TInstance, TValue> CompilePropertySetter(PropertyInfo propertyInfo)
        {
            Type[] argumentTypes = new Type[] { typeof(TInstance), typeof(TValue) };
            DynamicMethod method = new DynamicMethod(string.Concat("SetProperty_", this.GetMember().Name), null, argumentTypes, true);
            MethodInfo setMethod = propertyInfo.GetSetMethod(true);

            var gen = method.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);
            gen.Emit(OpCodes.Ldarg_1);
            gen.EmitCall(OpCodes.Callvirt, setMethod, null);
            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate<Action<TInstance, TValue>>();
        }

        private Action<TInstance, TValue> CompileFieldSetter(FieldInfo field)
        {
            Type[] argumentTypes = new Type[] { typeof(TInstance), typeof(TValue) };
            DynamicMethod method = new DynamicMethod(string.Concat("SetField_", this.GetMember().Name), null, argumentTypes, true);

            var gen = method.GetILGenerator();
            gen.Emit(OpCodes.Ldarg_0);

            if (!typeof(TInstance).Equals(this.instance.GetType()))
            {
                if (typeof(TInstance).IsClass && !this.instance.GetType().IsClass)
                {
                    gen.Emit(OpCodes.Box, this.instance.GetType());
                }
                gen.Emit(OpCodes.Castclass, this.instance.GetType());
            }

            gen.Emit(OpCodes.Ldarg_1);

            if (!typeof(TValue).Equals(field.FieldType))
            {
                if (typeof(TValue).IsClass && !field.FieldType.IsClass)
                {
                    gen.Emit(OpCodes.Unbox_Any, field.FieldType);
                }
                else
                {
                    gen.Emit(OpCodes.Castclass, field.FieldType);
                }
            }

            gen.Emit(OpCodes.Stfld, field);
            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate<Action<TInstance, TValue>>();
        }

        /// <summary>
        /// Compiles a static method that takes the instance on wich the
        /// method will be invoked as the first parameter and then the
        /// parameters of the instance method as the following parameters.
        /// </summary>
        /// <typeparam name="TDelegate">The type of delegate to create.</typeparam>
        /// <returns>The compiled method as a delegate.</returns>
        public TDelegate CompileMethod<TDelegate>() where TDelegate : class
        {
            MethodInfo info = (MethodInfo)this.GetMember();

            List<Type> argumentTypes = new List<Type>();
            argumentTypes.Add(typeof(TInstance));

            foreach (var p in info.GetParameters())
            {
                argumentTypes.Add(p.ParameterType);
            }

            DynamicMethod method = new DynamicMethod(string.Concat("Call_", info.Name), info.ReturnType, argumentTypes.ToArray());
            var gen = method.GetILGenerator();

            for (int i = 0; i < argumentTypes.Count; i++)
            {
                gen.Emit(OpCodes.Ldarg, i); // parameters.
            }

            gen.EmitCall(OpCodes.Callvirt, info, null);

            gen.Emit(OpCodes.Ret);

            return method.CreateDelegate<TDelegate>();
        }

        private TValue InvokeField(FieldInfo field)
        {
            if (setValueToMember)
            {
                field.SetValue(instance, setValue);
                field.IsOfType(typeof(string));
                return setValue;
            }
            else
            {
                return (TValue)field.GetValue(instance);
            }
        }

        private TValue InvokeProperty(PropertyInfo property)
        {
            if (setValueToMember)
            {
                property.SetValue(instance, setValue, parameters);
                return setValue;
            }
            else
            {
                return (TValue)property.GetValue(instance, parameters);
            }
        }

        private TValue InvokeMethod(MethodInfo method)
        {
            return (TValue)method.Invoke(instance, parameters);
        }
        #endregion
    }

    public static class ReflectionExtensions
    {
        /// <summary>
        /// Calls the member with the specified name on the object.
        /// </summary>
        /// <typeparam name="TValue">The return type of the call.</typeparam>
        /// <typeparam name="TInstance">The type of the instance the member is called on.</typeparam>
        /// <param name="instance">The instance to perform the call on.</param>
        /// <param name="name">The name of the instance member to call.</param>
        /// <returns>The return value of the call - or - null (Nothing in VB) if
        /// the call is to a void (Sub in VB) method.</returns>
        public static MemberCall<TInstance, TValue> Member<TInstance, TValue>(this TInstance instance, string name)
        {
            return new MemberCall<TInstance, TValue>(instance, name);
        }

        /// <summary>
        /// Calls the member with the specified name on the object.
        /// </summary>
        /// <typeparam name="TValue">The type of the field or property or the return
        /// value of the method specified by the member name.</typeparam>
        /// <typeparam name="TInstance">The type the member will be called on.</typeparam>
        /// <param name="instance">The instance to perform the call on.</param>
        /// <param name="name">The name of the instance member to call.</param>
        /// <param name="ignored">A value of the type of the members return value, used only
        /// to get the type for the generic TValue type parameter.</param>
        /// <returns>The return value of the call - or - null (Nothing in VB) if
        /// the call is to a void (Sub in VB) method.</returns>
        public static MemberCall<TInstance, TValue> Member<TInstance, TValue>(this TInstance instance, string name, TValue ignored)
        {
            return new MemberCall<TInstance, TValue>(instance, name);
        }

        /// <summary>
        /// Calls the member with the specified name on the object.
        /// </summary>
        /// <typeparam name="TInstance">The type the member will be called on.</typeparam>
        /// <param name="instance">The instance to perform the call on.</param>
        /// <param name="name">The name of the instance member to call.</param>
        /// <returns>The return value of the call - or - null (Nothing in VB) if
        /// the call is to a void (Sub in VB) method.</returns>
        public static MemberCall<TInstance, object> Member<TInstance>(this TInstance instance, string name)
        {
            return new MemberCall<TInstance, object>(instance, name);
        }

        /// <summary>
        /// Completes the dynamic method and creates a delegate that can be
        /// used to call it.
        /// </summary>
        /// <typeparam name="T">The type of delegate to create.</typeparam>
        /// <param name="method">The method that the delegate is created for.</param>
        /// <returns>The created delegate.</returns>
        public static T CreateDelegate<T>(this DynamicMethod method) where T : class
        {
            return (T)(object)method.CreateDelegate(typeof(T));
        }

        /// <summary>
        /// Gets whether the object is of the specified type. An object is
        /// of the specified type if it is an instance of that direct type or
        /// if it is an instance of a class that is in the inheritance hierarchy
        /// of the specified type or if it implements an interface type
        /// that the type represents.
        /// </summary>
        /// <param name="instance">The instance to thest.</param>
        /// <param name="type">The type to check if this object is an instance of.</param>
        /// <returns>True or false.</returns>
        public static bool IsOfType(this object instance, Type type)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");
            if (type == null)
                throw new ArgumentNullException("type");

            return type.IsAssignableFrom(instance.GetType());
        }

        /// <summary>
        /// Gets whether the object is of the specified type. An object is
        /// of the specified type if it is an instance of that direct type or
        /// if it is an instance of a class that is in the inheritance hierarchy
        /// of the specified type or if it implements an interface type
        /// that the type represents.
        /// </summary>
        /// <param name="instance">The instance to thest.</param>
        /// <typeparam name="T">The type to check if this object is an instance of.</typeparam>
        /// <returns>True or false.</returns>
        public static bool IsOfType<T>(this object instance)
        {
            return instance.IsOfType(typeof(T));
        }
    }
}

No comments:

Post a Comment