Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

General DynamicObject Proxy and Fast Reflection Proxy

0.00/5 (No votes)
15 Sep 2010 1  
Extending functionality by wrapping entity using DynamicObject. Improving performance of Reflection by using cache and expressions

Introduction

Some time ago, I found a very interesting article "Using DynamicObject to implement general proxy classes". It was not the first proxy solution based on DynamicObject and I started thinking how I could unify all solutions and improve performance. This article is a result of my investigations.

My solution solves some problems and adds some features to the original solution:

  • Faster access to properties than Reflection
  • Access to the fields the same way as to the properties and optional access to the non-public members
  • Possibility of using custom implemented proxy instead of Reflection based proxy

I extracted the basic dynamic proxy logic and separated a single dynamic proxy into a chain of dynamic and static proxies. The dynamic proxy inherits DynamicObject and wraps the IStaticProxy interface to allow access using dynamic syntax. The static proxy defines access to the underlying object and to its properties and methods. You can implement a custom proxy and pass it to the dynamic proxy. Default implementation of static proxy is ReflectionProxy.

Performance

Access via Reflection is very slow. To improve performance, I developed two ideas: property cache and expression based property access. Instead of Type.GetProperty() or Type.GetProperties().First(...), I use the much faster Dictionary as cache. Instead of using PropertyInfo.GetValue() or SetValue(), I compile getter or setter lambda expression and call it. It is much slower for single usage because of compilation time, but much faster for multiple usages.

Brief Description of DynamicObject

The DynamicObject class enables you to define which operations can be performed on dynamic objects and how to perform those operations. For example, you can define what happens when you try to get or set an object property or call a method.

This class can be useful if you want to create a more convenient interface for a module. For example, instead of ugly syntax like obj.SetProperty("Count", 1), you can provide the ability to use much simpler syntax, like obj.Count = 1. The property name will be resolved in runtime. To implement the dynamic behavior, you can inherit from the DynamicObject class and override necessary methods. For example, if you need operations for setting and getting properties, you can override the TrySetMember and TryGetMember methods.

We will use this for wrapping a static proxy. Accessing any property will be via static proxy. Depending on the static proxy implementation, you can access the property of the object or e.g. field in database or entry in INI file, etc.

Class Diagram of the Library

Click to enlarge image

Dimmed classes are code from the original article adapted to the new base functionality. I place this code here just because of compatibility issues, but the original code and changes can be found in the original article. I made some changes in names and structure of this code. DynamicProxy was split into NotifyingProxy and new DynamicProxy, where the first one received slim functionality of INotifyPropertyChanged interface and the second one received all basic functionality of wrapping static proxy and underlying object. Instead of using two ValidationProxy classes, I extracted major validation functionality into IPropertyValidator interface and created two corresponding implementations. This schema allows derived class DataErrorInfoProxy to work with both of them or with any custom implementation.

DynamicProxy

This is the base class for further implementations of wrappers. It provides a simple implementation to access the properties of the underlying object via IStaticProxy interface:

  public class DynamicProxy : DynamicObject
  {
    public DynamicProxy(IStaticProxy proxy)
    {
      if (proxy == null)
        throw new ArgumentNullException("proxy");
      Proxy = proxy;
    }

    public DynamicProxy(object proxiedObject)
    {
      if (proxiedObject == null)
        throw new ArgumentNullException("proxiedObject");
      Proxy = proxiedObject is IStaticProxy ? 
	(IStaticProxy)proxiedObject : new ReflectionProxy(proxiedObject);
    }

    protected IStaticProxy Proxy { get; private set; }

    protected virtual void SetMember(string propertyName, object value) 
      { Proxy.SetProperty(propertyName, value); }
    protected virtual object GetMember(string propertyName) 
      { return Proxy.GetProperty(propertyName); }

    public override bool TryConvert(ConvertBinder binder, out object result)
    {
      Debug.Assert(Proxy != null);
      if (binder.Type == typeof(IStaticProxy))
      {
        result = Proxy;
        return true;
      }
      if (Proxy.ProxiedObject != null && 
	binder.Type.IsAssignableFrom(Proxy.ProxiedObject.GetType()))
      {
        result = Proxy.ProxiedObject;
        return true;
      }
      return base.TryConvert(binder, out result);
    }

    public override bool TryGetMember(GetMemberBinder binder, out object result)
    {
      Debug.Assert(Proxy != null);
      try
      {
        result = GetMember(binder.Name);
        return true;
      }
      catch(Exception ex)
      {
        throw new InvalidOperationException("Cannot get member", ex);
      }
    }

    public override bool TrySetMember(SetMemberBinder binder, object value)
    {
      Debug.Assert(Proxy != null);
      try
      {
        SetMember(binder.Name, value);
        return true;
      }
      catch(Exception ex)
      {
        throw new InvalidOperationException("Cannot set member", ex);
      }
    }

    public override bool TryInvokeMember(InvokeMemberBinder binder, 
	object[] args, out object result)
    {
      Debug.Assert(Proxy != null);
      return Proxy.TryInvokeMethod(binder.Name, out result, args) || 
	base.TryInvokeMember(binder, args, out result);
    }
  }

Points of Interest

The method TryConvert is implemented to allow some special actions when the user implicitly tries to convert a DynamicProxy object to another type. If the object is converted to the type of the underlying object, it returns the underlying object. When converting to the interface IStaticProxy it returns the static proxy object.

IStaticProxy

This is the interface for further implementation of the access to the different types of entities.

  public interface IStaticProxy
  {
    object ProxiedObject { get; }
    Type GetPropertyType(string propertyName);
    void SetProperty(string propertyName, object value);
    object GetProperty(string propertyName);
    bool TrySetProperty(string propertyName, object value);
    bool TryGetProperty(string propertyName, out object result);
    object InvokeMethod(string methodName, params object[] args);
    bool TryInvokeMethod(string methodName, out object result, params object[] args);
  }

ReflectionProxy

It implements IStaticProxy and allows access to the members and methods of the underlying object using Reflection engine of the .NET.

  public class ReflectionProxy : IStaticProxy
  {
    protected readonly TypeInfoCache TypeInfoCache;
    protected readonly bool FullAccess;

    public ReflectionProxy(object proxiedObject, bool fullAccess = false, 
	Type forceType = null)
    {
      if (proxiedObject == null) throw new ArgumentNullException("proxiedObject");
      ProxiedObject = proxiedObject;
      if (forceType != null)
      {
        if (!ProxiedObject.GetType().IsSubclassOf(forceType) && 
		ProxiedObject.GetType() != forceType)
          throw new ArgumentException("Forced type should be super 
		class of the object type");
      }
      else
        forceType = ProxiedObject.GetType();
      TypeInfoCache = GlobalTypeInfoCache.GetTypeInfo(forceType);
      FullAccess = fullAccess;
    }
    [...]
    #region IStaticProxy implementation
    public object ProxiedObject { get; private set; }
    public Type GetPropertyType(string propertyName)
    {
      var pie = TypeInfoCache.GetPropertyInfoEx(propertyName, FullAccess);
      if (pie == null)
        throw new ArgumentException("Property " + propertyName + 
	" doesn't exist in type " + TypeInfoCache.Type);
      return pie.Type;
    }
    public virtual void SetProperty(string propertyName, object value)
    {
      var pie = TypeInfoCache.GetPropertyInfoEx(propertyName, FullAccess);
      if (pie == null)
        throw new ArgumentException("Property " + propertyName + 
		" doesn't exist in type " + TypeInfoCache.Type);
      var setter = pie.FastSetter;
      if (setter == null)
      {
        if (FullAccess) setter = pie.Setter;
        if (setter == null)
          throw new ArgumentException("Property " + propertyName + 
		" doesn't have write access in type " + TypeInfoCache.Type);
      }
      if (pie.Type != value.GetType())
        value = Convert.ChangeType(value, pie.Type);
      setter(ProxiedObject, value);
    }
    public virtual object GetProperty(string propertyName)
    {
      var pie = TypeInfoCache.GetPropertyInfoEx(propertyName, FullAccess);
      if (pie == null)
        throw new ArgumentException("Property " + propertyName + 
		" doesn't exist in type " + TypeInfoCache.Type);
      var getter = pie.FastGetter;
      if (getter == null)
      {
        if (FullAccess) getter = pie.Getter;
        if (getter == null)
          throw new ArgumentException("Property " + propertyName + 
		" doesn't have read access in type " + TypeInfoCache.Type);
      }
      return getter(ProxiedObject);
    }
    public virtual bool TrySetProperty(string propertyName, object value)
    {
      [...]
    }
    public virtual bool TryGetProperty(string propertyName, out object result)
    {
      [...]
    }
    public virtual object InvokeMethod(string methodName, params object[] args)
    {
      [...]
    }
    public virtual bool TryInvokeMethod(string methodName, 
	out object result, params object[] args)
    {
      [...]
    }
    #endregion
  }

Points of Interest

It treats fields as properties. If a property with a specified name is not found, it searches for the field with the same name.
Parameter FullAccess allows access to non-public members. It uses global type info cache to improve reflection performance.

TypeInfoCache

This class caches PropertyInfo and FieldInfo reflection information in the Dictionary.

  public class TypeInfoCache
  {
    [...]
    private readonly Dictionary<string, PropertyInfoEx> _propertyInfoMap = 
		new Dictionary<string, PropertyInfoEx>();
    public readonly Type Type;
    public TypeInfoCache(Type type) { Type = type; }
    private const BindingFlags DefaultLookup = 
	BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance;
    public PropertyInfoEx GetPropertyInfoEx
	(string propertyName, bool fullAccess = false)
    {
      PropertyInfoEx pie;
      if (!_propertyInfoMap.TryGetValue(propertyName, out pie))
      {
        PropertyInfo pi = GetPropertyInfo
		(propertyName, DefaultLookup | BindingFlags.NonPublic);
        if (pi != null)
          _propertyInfoMap.Add(propertyName, pie = new PropertyInfoEx(pi));
        else
        {
          // if property not found - search for field with the same name
          FieldInfo fi = GetFieldInfo
		(propertyName, DefaultLookup | BindingFlags.NonPublic);
          if (fi != null)
            _propertyInfoMap.Add(propertyName, pie = new PropertyInfoEx(fi));
        }
      }
      if (pie != null && (fullAccess || pie.IsPublic)) return pie;
      return null;
    }
    private PropertyInfo GetPropertyInfo(string propertyName, BindingFlags lookup)
    {
      try
      {
        return Type.GetProperty(propertyName, lookup);
      }
      catch (Exception)
      {
        foreach (var propertyInfo in Type.GetProperties(lookup))
          if (propertyInfo.Name == propertyName)
            return propertyInfo;
      }
      return null;
    }
    private FieldInfo GetFieldInfo(string fieldName, BindingFlags lookup)
    {
      [...]
    }
    [...]
  }

PropertyInfoEx

This inner class unifies access to the property and the field and also holds setters and getters for the member.

  public class PropertyInfoEx
  {
    internal PropertyInfoEx(PropertyInfo propertyInfo)
    {
      Debug.Assert(propertyInfo != null, "PropertyInfo should be specified");
      PropertyInfo = propertyInfo;
      IsPublic = propertyInfo.GetGetMethod() != null || 
			propertyInfo.GetSetMethod() != null;
    }
    internal PropertyInfoEx(FieldInfo fieldInfo)
    {
      Debug.Assert(fieldInfo != null, "FieldInfo should be specified");
      FieldInfo = fieldInfo;
      IsPublic = !fieldInfo.IsPrivate && fieldInfo.IsPublic;
    }
    public PropertyInfo PropertyInfo { get; private set; }
    public FieldInfo FieldInfo { get; private set; }
    #region FastGetter
    private bool _getterInaccessible;
    private Func<object, object> _getter;
    public Func<object, object> FastGetter
    {
      get
      {
        if (_getter == null && !_getterInaccessible)
        {
          _getter = PropertyInfo != null ? 
	    PropertyInfo.GetValueGetter<object>() : FieldInfo.GetValueGetter<object>();
          if (_getter == null) _getterInaccessible = true;
        }
        return _getter;
      }
    }
    #endregion
    #region FastSetter
    private bool _setterInaccessible;
    private Action<object, object> _setter;
    public Action<object, object> FastSetter
    {
      get
      {
        if (_setter == null && !_setterInaccessible)
        {
          _setter = PropertyInfo != null ? 
	    PropertyInfo.GetValueSetter<object>() : FieldInfo.GetValueSetter<object>();
          if (_setter == null) _setterInaccessible = true;
        }
        return _setter;
      }
    }
    #endregion
    #region Getter
    public Func<object, object> Getter
    {
      get
      {
        if (PropertyInfo == null || PropertyInfo.CanRead) return TheGetter;
        return null;
      }
    }
    private object TheGetter(object theObject) 
      { return PropertyInfo != null 
          ? PropertyInfo.GetValue(theObject, null) 
          : FieldInfo.GetValue(theObject); }
    #endregion
    #region Setter
    public Action<object, object> Setter
    {
      get
      {
        if (PropertyInfo != null)
        {
          if (PropertyInfo.CanWrite)
            return TheSetter;
        }
        else if (!FieldInfo.IsInitOnly)
          return TheSetter;
        return null;
      }
    }
    public void TheSetter(object theObject, object value)
    {
      if (PropertyInfo != null)
        PropertyInfo.SetValue(theObject, value, null);
      else // if(FieldInfo != null)
        FieldInfo.SetValue(theObject, value);
    }
    #endregion
    public bool IsPublic { get; private set; }
    public Type Type { get { return PropertyInfo != null ? 
	PropertyInfo.PropertyType : FieldInfo.FieldType; } }
  } 

Points of Interest

FastSetter and FastGetter allows fast access to public members by using dynamically compiled lambda expressions instead of access via reflection.

FieldInfoExtensions and PropertyInfoExtensions

These classes implement two extension functions each.

  • Func<object, T> GetValueGetter<T>()
  • Action<object, T> GetValueSetter<T>()

These functions dynamically build lambda expressions to get or set specified fields or properties.

  public static class PropertyInfoExtensions
  {
    public static Func<object, T> GetValueGetter<T>(this PropertyInfo propertyInfo)
    {
      if (!propertyInfo.CanRead || propertyInfo.GetGetMethod() == null) return null;
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked
		(instance, propertyInfo.DeclaringType);
      var property = Expression.Property(castedInstance, propertyInfo);
      var convert = Expression.Convert(property, typeof(T));
      var expression = Expression.Lambda(convert, instance);
      return (Func<object, T>)expression.Compile();
    }
    public static Action<object, T> GetValueSetter<T>(this PropertyInfo propertyInfo)
    {
      if (!propertyInfo.CanWrite || propertyInfo.GetSetMethod() == null) return null;
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked
		(instance, propertyInfo.DeclaringType);
      var argument = Expression.Parameter(typeof(T), "a");
      var setterCall = Expression.Call(
        castedInstance,
        propertyInfo.GetSetMethod(),
        Expression.Convert(argument, propertyInfo.PropertyType));
      return Expression.Lambda<Action<object, T>>
		(setterCall, instance, argument).Compile();
    }
  }
  public static class FieldInfoExtensions
  {
    public static Func<object, T> GetValueGetter<T>(this FieldInfo fieldInfo)
    {
      if (!fieldInfo.IsPublic) return null;
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked
			(instance, fieldInfo.DeclaringType);
      var field = Expression.Field(castedInstance, fieldInfo);
      var convert = Expression.Convert(field, typeof(T));
      var expression = Expression.Lambda(convert, instance);
      return (Func<object, T>)expression.Compile();
    }
    public static Action<object, T> GetValueSetter<T>(this FieldInfo fieldInfo)
    {
      if (!fieldInfo.IsPublic || fieldInfo.IsInitOnly) return null;
      var instance = Expression.Parameter(typeof(Object), "i");
      var castedInstance = Expression.ConvertChecked
			(instance, fieldInfo.DeclaringType);
      var argument = Expression.Parameter(typeof(T), "a");
      var setter = Expression.Assign(
        Expression.Field(castedInstance, fieldInfo),
        Expression.Convert(argument, fieldInfo.FieldType));
      return Expression.Lambda<Action<object, T>>
		(setter, instance, argument).Compile();
    }
  }

FullAccessProxy

This class gives access to the non-public members of the underlying object.

  internal class FullAccessProxy : DynamicProxy
  {
    public FullAccessProxy(object o, Type forceType = null) :
      base(new ReflectionProxy(o, true, forceType)) { }
  }

Points of Interest

This class originally was complicated enough and almost totally duplicated the functionality of the DynamicProxy and ReflectionProxy classes, except speed improving features. In addition, it was very similar to the DynamicProxy from the original article. Now it contains only a constructor that restricts proxy to ReflectionProxy with FullAccess rights.

Performance Measuring

The original code uses construction object.GetType().GetProperties().First(...) to get PropertyInfo. Usage of the Dictionary is 100 times faster and about 5-7 times faster than usage of object.GetType().GetProperty(...).
Usage of the compiled lambda expressions is about 10 times faster than Reflection. However, compilation of expression is about 300 times slower. Because of it, we compile expression once per property on the first access.
Anyway, we cannot reach closer to the performance of the direct property access. In my tests, static proxy access is about 100 times slower and dynamic proxy access is about 150-300 times slower than direct access.
On the other hand, if we measure property set with implemented INotifyPropertyChanged, it looks much better. In this case, dynamic access is just 2-4 times slower.

DynamicObject

The class DynamicObject has some nice features. One of them is the possibility to declare real properties in derived classes and access them the same way as virtual properties. Reflection checks the real properties first and only if there is none matching property found, it will call TryGetMember. However, it can cause unanticipated problem. Property in the Proxy can hide property in the underlying object and make it totally unavailable if the names are the same. Therefore, I try to initialize most parameters in the constructor.

Things to be Done

  • Implement reflection info caching for methods
  • Invoke methods via expressions

History

  • 1.0 - Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here