8
\$\begingroup\$

I've recently answered a question here, where the goal is to mimic some functions from System.Reflection in order to avoid it's direct usage as much as possible, because it works slow.

Currently available features are :

  1. Cloning properties from one instance to another.
  2. Setting properties of instances.
  3. Getting property values from instances.
  4. Set instance properties to default values, where the values are determined at the creation of the TypeAccessor<T> class.
  5. Creating new instances of the type argument T, using default constructor, again this is specified at the instantiation of the TypeAccessor<T> class

The OP's code is really good but I've decided to add a compile time type-safety to avoid misspelling a variable names.

It works with a GetterCache and a SetterCache which are separated for faster lookup.

It uses both a private method which is not compile-time typesafe and is used only internally and a public one which is compile-time typesafe and this is the method that you can call outside of the class, so the user won't be able to mess up his naming. But inside the class, we can't know what the type argument T is, so we still need to use some methods which work with string as parameters instead of Expression.

public class TypeAccessor<T>
{
    private readonly Func<T, T> m_applyDefaultValues;
    private readonly Func<T> m_constructType;

    public ReadOnlyCollection<string> CloneableProperties { get; }

    public ReadOnlyDictionary<string, Func<T, object>> GetterCache { get; }

    public ReadOnlyDictionary<string, Action<T, object>> SetterCache { get; }


    public TypeAccessor(T defaultValue, bool includeNonPublic = false)
    {
        PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
                                                            (includeNonPublic
                                                                ? BindingFlags.NonPublic
                                                                : BindingFlags.Default) |
                                                            BindingFlags.Public);

        GetterCache = new ReadOnlyDictionary<string, Func<T, object>>(properties.Select(propertyInfo => new
            {
                PropertyName = propertyInfo.Name,
                PropertyGetAccessor = propertyInfo.GetGetAccessor<T>(includeNonPublic)
            }).Where(a => a.PropertyGetAccessor != null)
            .ToDictionary(a => a.PropertyName, a => a.PropertyGetAccessor));

        SetterCache = new ReadOnlyDictionary<string, Action<T, object>>(properties.Select(propertyInfo => new
            {
                PropertyName = propertyInfo.Name,
                PropertySetAccessor = propertyInfo.GetSetAccessor<T>(includeNonPublic)
            }).Where(a => a.PropertySetAccessor != null)
            .ToDictionary(a => a.PropertyName, a => a.PropertySetAccessor));

        CloneableProperties = Array.AsReadOnly(GetterCache.Keys.Intersect(SetterCache.Keys).ToArray());


        if (typeof(T).IsValueType)
        {
            m_applyDefaultValues = instance => defaultValue;
            m_constructType = () => defaultValue;
        }
        else if (defaultValue != null)
        {
            var defaultConstructor = GetDefaultConstructor();
            var propertyValues = GetProperties(defaultValue, CloneableProperties).ToArray();

            m_applyDefaultValues = instance =>
            {
                SetProperties(instance, propertyValues);
                return instance;
            };
            m_constructType = () => m_applyDefaultValues(defaultConstructor());
        }
        else
        {
            m_applyDefaultValues = instance => default(T);
            m_constructType = () => default(T);
        }

    }

    public void CloneProperties(T source, T target)
    {
        SetProperties(target, GetProperties(source, CloneableProperties));
    }

    public void SetToDefault(ref T instance)
    {
        instance = m_applyDefaultValues(instance);
    }

    public T New()
    {
        return m_constructType();
    }

    private Dictionary<string, object> GetProperties(T instance, IEnumerable<string> properties)
        => properties?.ToDictionary(propertyName => propertyName,
               propertyName => GetProperty(instance, propertyName)) ?? new Dictionary<string, object>();

    public Dictionary<string, object> GetProperties(T instance,
        IEnumerable<Expression<Func<T, object>>> properties)
        => properties?.ToDictionary(property => GetMemberInfo(property).Name,
               property => GetProperty(instance, property)) ?? new Dictionary<string, object>();

    public Dictionary<string, object> GetProperties(T instance)
        => GetterCache.Keys.ToDictionary(key => key, key => GetProperty(instance, key));

    private object GetProperty(T instance, string propertyName)
        => GetterCache[propertyName].Invoke(instance);

    public TValue GetProperty<TValue>(T instance, Expression<Func<T, TValue>> property)
        => (TValue) GetterCache[GetMemberInfo(property).Name](instance);

    private void SetProperty(T instance, string propertyName, object value)
    {
        Action<T, object> setter;

        if (SetterCache.TryGetValue(propertyName, out setter))
        {
            setter(instance, value);
        }
        else
        {
            throw new KeyNotFoundException(
                $"a property setter with the name does not {propertyName} exist on {typeof(T).FullName}");
        }
    }

    public void SetProperty<TValue>(T instance, Expression<Func<T, TValue>> property, TValue value)
        => SetterCache[GetMemberInfo(property).Name](instance, value);


    private void SetProperties<TValue>(T instance, IEnumerable<KeyValuePair<string, TValue>> properties)
    {
        if (properties != null)
        {
            foreach (var property in properties)
            {
                SetProperty(instance, property.Key, property.Value);
            }
        }
    }

    public void SetProperties<TValue>(T instance,
        IEnumerable<KeyValuePair<Expression<Func<T, TValue>>, TValue>> propertiesInfo)
    {
        foreach (var propertyInfo in propertiesInfo)
        {
            SetterCache[GetMemberInfo(propertyInfo.Key).Name](instance, propertyInfo.Value);
        }
    }

    private MemberInfo GetMemberInfo(Expression expression)
    {
        LambdaExpression lambda = (LambdaExpression) expression;
        MemberExpression memberExpr = null;
        switch (lambda.Body.NodeType)
        {
            case ExpressionType.Convert:
                memberExpr =
                    ((UnaryExpression) lambda.Body).Operand as MemberExpression;
                break;
            case ExpressionType.MemberAccess:
                memberExpr = lambda.Body as MemberExpression;
                break;
        }
        return memberExpr.Member;
    }

    private static Func<T> GetDefaultConstructor()
    {
        var type = typeof(T);

        if (type == typeof(string))
        {
            return
                Expression.Lambda<Func<T>>(Expression.TypeAs(Expression.Constant(null), typeof(string))).Compile();
        }
        if (type.HasDefaultConstructor())
        {
            return Expression.Lambda<Func<T>>(Expression.New(type)).Compile();
        }
        return () => (T) FormatterServices.GetUninitializedObject(type);
    }
}

These are the extension classes:

public static class TypeAccessor
{
    /// <summary>
    /// Creates a new instance of the <see cref="TypeAccessor{_}"/> class using the specified <see cref="T"/>.
    /// </summary>
    public static TypeAccessor<T> Create<T>(T instance, bool includeNonPublic = false)
    {
        return new TypeAccessor<T>(instance, includeNonPublic);
    }
}

public static class TypeExtensions
{
    public static bool HasDefaultConstructor(this Type type)
    {
        return (type.IsValueType || (type.GetConstructor(Type.EmptyTypes) != null));
    }
}

public static class PropertyInfoExtensions
{
    /// <summary>
    /// Generates an <see cref="Expression{Func{_,_}}"/> that represents the current <see cref="PropertyInfo"/>'s getter.
    /// </summary>
    public static Expression<Func<TSource, TProperty>> GetGetAccessor<TSource, TProperty>(
        this PropertyInfo propertyInfo, bool includeNonPublic = false)
    {
        var getMethod = propertyInfo.GetGetMethod(includeNonPublic);

        if (getMethod != null && propertyInfo.GetIndexParameters().Length == 0)
        {
            var instance = Expression.Parameter(typeof(TSource), "instance");
            var value = Expression.Call(instance, getMethod);

            return Expression.Lambda<Func<TSource, TProperty>>(
                propertyInfo.PropertyType.IsValueType
                    ? Expression.Convert(value, typeof(TProperty))
                    : Expression.TypeAs(value, typeof(TProperty)),
                instance
            );
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// Generates a <see cref="Func{_,_}"/> delegate to the current <see cref="PropertyInfo"/>'s getter.
    /// </summary>
    /// <param name="includeNonPublic">Indicates whether a non-public get accessor should be returned.</param>
    public static Func<TSource, object> GetGetAccessor<TSource>(this PropertyInfo propertyInfo,
        bool includeNonPublic = false)
    {
        return propertyInfo.GetGetAccessor<TSource, object>(includeNonPublic)?.Compile();
    }

    /// <summary>
    /// Generates an <see cref="Expression{Action{_,_}};"/> that represents the current <see cref="PropertyInfo"/>'s setter.
    /// </summary>
    /// <param name="includeNonPublic">Indicates whether a non-public set accessor should be returned.</param>
    public static Expression<Action<TSource, TProperty>> GetSetAccessor<TSource, TProperty>(
        this PropertyInfo propertyInfo, bool includeNonPublic = false)
    {
        var setMethod = propertyInfo.GetSetMethod(includeNonPublic);

        if (setMethod != null && propertyInfo.GetIndexParameters().Length == 0)
        {
            var instance = Expression.Parameter(typeof(TSource), "instance");
            var value = Expression.Parameter(typeof(TProperty), "value");

            return Expression.Lambda<Action<TSource, TProperty>>(
                Expression.Call(
                    instance,
                    setMethod,
                    propertyInfo.PropertyType.IsValueType
                        ? Expression.Convert(value, propertyInfo.PropertyType)
                        : Expression.TypeAs(value, propertyInfo.PropertyType)
                ),
                instance,
                value
            );
        }
        else
        {
            return null;
        }
    }

    /// <summary>
    /// Generates an <see cref="Action{_,_}"/> delegate to the current <see cref="PropertyInfo"/>'s setter.
    /// </summary>
    /// <param name="includeNonPublic">Indicates whether a non-public set accessor should be returned.</param>
    public static Action<TSource, object> GetSetAccessor<TSource>(this PropertyInfo propertyInfo,
        bool includeNonPublic = false)
    {
        return propertyInfo.GetSetAccessor<TSource, object>(includeNonPublic)?.Compile();
    }
}

Here's a usage example:

public class Point2D
{
    public double X { get; set; }
    public double Y { get; set; }
}

var pointA = new Point2D {X = 9000.01, Y = 0.0};
var accessor = TypeAccessor.Create(pointA);
var pointB = new Point2D();

//obtains properties by name with compile time safety
Dictionary<string, object> a = accessor.GetProperties(pointA, new List<Expression<Func<Point2D, object>>>
{
    d => d.X,
    d => d.Y
});

accessor.CloneProperties(pointA, pointB); // pointB.X should now be 9000.01
accessor.SetProperty(pointA, p => p.X, 0.0); // sets pointA.X to 0.0
Console.WriteLine(accessor.GetProperty(pointA, p => p.X)); // prints pointA.X, should be 0.0
accessor.SetToDefault(ref pointA); // sets pointA's properties to default the accessor's default values
Console.WriteLine(accessor.GetProperty(pointA, p => p.X)); // prints pointA.X, should be 9000.01
\$\endgroup\$
0

2 Answers 2

4
\$\begingroup\$

Expressions & Factories

var getMethod = propertyInfo.GetGetMethod(includeNonPublic);

if (getMethod != null && propertyInfo.GetIndexParameters().Length == 0)
{
    var instance = Expression.Parameter(typeof(TSource), "instance");
    var value = Expression.Call(instance, getMethod);

    return Expression.Lambda<Func<TSource, TProperty>>(
        propertyInfo.PropertyType.IsValueType
            ? Expression.Convert(value, typeof(TProperty))
            : Expression.TypeAs(value, typeof(TProperty)),
        instance
    );
}

You call here the delegate via an expression (the setter does the same). This is like you've just called the delegate.

Use can use the Property expression to call the property directly:

public static Expression<Func<T, TProperty>> CreateGetterExpression<T, TProperty>(
            this PropertyInfo propertyInfo,
            bool nonPublic = false)
{
    var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
    if (!hasGetter || propertyInfo.GetIndexParameters().Any())
    {
        return null;
    }

    var obj = Expression.Parameter(typeof(T), "obj");
    var property = Expression.Property(obj, propertyInfo);
    return Expression.Lambda<Func<T, TProperty>>(property, obj);
}

Less nesting is usually better so if there is no getter or it's an indexer you can return right away.


The other method for creating setteers can be made pretty in a similar manner:

public static Expression<Action<T, TProperty>> CreateSetterExpression<T, TProperty>(
        this PropertyInfo propertyInfo,
        bool nonPublic = false)
{
    var hasSetter = propertyInfo.GetSetMethod() != null;
    if (!hasSetter || propertyInfo.GetIndexParameters().Any())
    {
        return null;
    }

    var obj = Expression.Parameter(typeof(T), "obj");
    var value = Expression.Parameter(typeof(TProperty), "value");
    var property = Expression.Property(obj, propertyInfo);
    return Expression.Lambda<Action<T, TProperty>>(
        Expression.Assign(property, value), obj, value
    );
}

We still need expressions for indexed properties. Here's an example for a property with a single index. I think more then two are not necessary.

public static Expression<Func<T, TIndex1, TProperty>> CreateGetterExpression<T, TIndex1, TProperty>(
        this PropertyInfo propertyInfo,
        bool nonPublic = false)
{
    var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
    if (!hasGetter || propertyInfo.GetIndexParameters().Length != 1)
    {
        return null;
    }

    var obj = Expression.Parameter(typeof(T), "obj");
    var index1 = Expression.Parameter(typeof(TIndex1), "i");
    var property = Expression.Property(obj, propertyInfo, index1);
    return Expression.Lambda<Func<T, TIndex1, TProperty>>(property, obj, index1);
}

The class that contains the above methods is PropertyInfoExtensions. I think it's not a good name. Even though they are extensions you should try to find a name that better describes the functionality the class contains. Here I find the name ExpressionFactory good because this class is a factory. It creates expressions. Consequently need its methods also new names like CreateSomething. GetSomething implies you already have it somewhere and just retrieve it which is not the case here.


Constructors

private static Func<T> GetDefaultConstructor()
{
    var type = typeof(T);

    if (type == typeof(string))
    {
        return Expression.Lambda<Func<T>>(
            Expression.TypeAs(Expression.Constant(null), 
            typeof(string))
        ).Compile();
    }

    if (type.HasDefaultConstructor())
    {
        return Expression.Lambda<Func<T>>(Expression.New(type)).Compile();
    }

    return () => (T)FormatterServices.GetUninitializedObject(type);
}

This is tricky. The string does not have a default constructor. This means that the New() method should throw an InvalidOperationException for types without a default constructor. An empty instance of a string is of no use. Strings are immutable so you cannot set anything useful and therefore it should be forbidden to create one.

In such cases the tester-doer pattern is recommendable. To implement it you'd need to add a property like HasDefaultConstructor or CanCreateNew. This would be the tester. New() is the doer.

There is one more issue in this method. The usage of this is invalid:

FormatterServices.GetUninitializedObject(..)

FormatterServices.GetUninitializedObject Method

Because the new instance of the object is initialized to zero and no constructors are run, the object might not represent a state that is regarded as valid by that object. The current method should only be used for deserialization when the user intends to immediately populate all fields. It does not create an uninitialized string, since creating an empty instance of an immutable type serves no purpose.

Returning this from the New() is of no use. No default constructor -> exception. All other workarounds do only harm.


Creating TypeAccessor

PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Instance |
                                                    (includeNonPublic
                                                        ? BindingFlags.NonPublic
                                                        : BindingFlags.Default) |
                                                    BindingFlags.Public);

You need to admit that this is quite ugly. But what do we have helper variables for?

var nonPublicFlag = includeNonPublic ? BindingFlags.NonPublic : BindingFlags.Default;
var properties = typeof(T).GetProperties(BindingFlags.Instance| BindingFlags.Public | nonPublicFlag);

Looks better, doesn't it? ;-)


GetterCache = new ReadOnlyDictionary<string, Func<T, object>>(properties.Select(propertyInfo => new
{
    ...
})

Another unecessarily long line. A dictionary implements the IReadOnlyDictionary interface. If we change the type of the GetterCache property to

public IReadOnlyDictionary<string, Func<T, object>> GetterCache { get; }

we no longer need the new ReadOnlyDictionary because an implicit cast will work:

GetterCache = properties.Select(propertyInfo => new
{
    ..
})
.Where(a => a.GetterDelegate != null)
.ToDictionary(a => a.PropertyName, a => a.GetterDelegate);

Getting and setting properties

private object GetProperty(T instance, string propertyName)
    => GetterCache[propertyName].Invoke(instance);

public TValue GetProperty<TValue>(T instance, Expression<Func<T, TValue>> property)
    => (TValue) GetterCache[GetMemberInfo(property).Name](instance);

private void SetProperty(T instance, string propertyName, object value)
{
    Action<T, object> setter;

    if (SetterCache.TryGetValue(propertyName, out setter))
    {
        setter(instance, value);
    }
    else
    {
        throw new KeyNotFoundException(
            $"a property setter with the name does not {propertyName} exist on {typeof(T).FullName}");
    }
}

public void SetProperty<TValue>(T instance, Expression<Func<T, TValue>> property, TValue value)
    => SetterCache[GetMemberInfo(property).Name](instance, value);

These methods are implemented in a very inconsitant way. Only one checks if the property exists in the dictionary, the other ones will crash. There's also a lot of repetition. The entire logic should be in only one getter/setter and the other overloads should call the main method. Lastly I think their names should be changed to GetValue/SetValue because this is what they do.


   public void CloneProperties(T source, T target)
   {
        SetProperties(target, GetProperties(source, CloneableProperties));
   }

    public void SetToDefault(ref T instance)
    {
        instance = _applyDefaultValues(instance);
    }

There's no need for these methods. You can always call

accessor.SetValue("prop", default(int));

or write an extension for either one.

\$\endgroup\$
8
  • 1
    \$\begingroup\$ Since I wrote these I can shed some light here. The reason the property isn't accessed directly is because I'm bypassing it to call T.get_Property() and T.set_Property(). The string doesn't have a default constructor so an expression is used to return (string)null because that is what string x would default to, the second if handles reference types with a default constructor, the third handles everything else. Sources to follow... \$\endgroup\$ Dec 30, 2016 at 8:05
  • 1
    \$\begingroup\$ @Kittoes0124 oh, right, then it applies to your question as well. You don't mind if I write the review here? ;-) but still, I find an exception is more appropriate for calling non-existing default-constructors rather then returning unusable things. \$\endgroup\$
    – t3chb0t
    Dec 30, 2016 at 8:11
  • \$\begingroup\$ I don't mind any of this at all. The intention here is that exceptions are only thrown in truly exceptional cases for this class despite what may or may not make sense. For example, using a TypeAccessor.Create(9000.01) to construct the value 9000.01 is ludicrous when you could just use the value. I also would find it odd if this was used on strings at all and let alone with null as the default. While it might be weird to me who am I to claim that it is invalid? Especially when all of the values that are returned are exactly what you expect them to be based on normal use. \$\endgroup\$ Dec 30, 2016 at 8:18
  • 1
    \$\begingroup\$ All that said, the last case should definitely throw instead of using the brute force workaround. The potentially invalid objects issue is unacceptable. \$\endgroup\$ Dec 30, 2016 at 8:22
  • 1
    \$\begingroup\$ @Kittoes0124 benchmarks would be shed light on the mystery of performance of each implementation but who's going to write them? :-] \$\endgroup\$
    – t3chb0t
    Dec 30, 2016 at 9:25
2
\$\begingroup\$

Putting everything inside the TypeAccessor class was just too much. I decided it needs a slimmig diet. There were so many changes required to turn it into a testable, maintainable and extendable solution that it's hard to name them all. The most important ones are:

  • splitted the TypeAccessor in two
  • botch caches are now lazy, this means, the expressions are only compiled if a property is read/written
  • removed unnecessary APIs
  • moved some APIs to extensions and factories
  • copying properties from one object to another or setting them to default can be implemented with extensions or other specialized types that can utilize the reader and writer as they need

Now there is a PropertyReader<T> that provides only three most basic APIs which are getting a value from a property, from a 1D indexer and from a 2D indexer.

public class PropertyReader<T>
{
    private readonly Dictionary<string, object> _cache = new Dictionary<string, object>();

    public TResult GetValue<TResult>(T obj, string propertyName)
    {
        return GetOrCreateGetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateGetterExpression<T, TResult>(propertyName).Compile()
        )(obj);
    }

    public TResult GetValue<TIndex1, TResult>(T obj, string propertyName, TIndex1 index1)
    {
        propertyName = typeof(TIndex1).FullName;
        return GetOrCreateGetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateGetterExpression<T, TIndex1, TResult>(propertyName).Compile()
        )(obj, index1);
    }

    public TResult GetValue<TIndex1, TIndex2, TResult>(T obj, string propertyName, TIndex1 index1, TIndex2 index2)
    {
        propertyName = typeof(TIndex1).FullName + ", " + typeof(TIndex2).FullName;
        return GetOrCreateGetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateGetterExpression<T, TIndex1, TIndex2, TResult>(propertyName).Compile()
        )(obj, index1, index2);
    }

    private TFunc GetOrCreateGetterDelegate<TFunc>(string propertyName, Func<TFunc> getterDelegateFactory)
    {
        var cacheItem = default(object);
        if (_cache.TryGetValue(propertyName, out cacheItem))
        {
            return (TFunc)cacheItem;
        }

        var getterDelegate = getterDelegateFactory();
        cacheItem = getterDelegate;
        _cache.Add(propertyName, cacheItem);

        return (TFunc)cacheItem;
    }
}

and its counterpart the PropertySetter<T>

public class PropertyWriter<T>
{
    private readonly Dictionary<string, object> _cache = new Dictionary<string, object>();

    public PropertyWriter<T> SetValue<TValue>(T obj, string propertyName, TValue value)
    {
        if (obj == null) { throw new ArgumentNullException(paramName: nameof(obj)); }
        if (propertyName == null) { throw new ArgumentNullException(paramName: nameof(propertyName)); }

        GetOrCreateSetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateSetterExpression<T, TValue>(propertyName).Compile()
        )(obj, value);
        return this;
    }

    public PropertyWriter<T> SetValue<TValue, TIndex1>(T obj, string propertyName, TValue value, TIndex1 index1)
    {
        if (obj == null) { throw new ArgumentNullException(paramName: nameof(obj)); }

        propertyName = typeof(TIndex1).FullName;

        GetOrCreateSetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateSetterExpression<T, TValue, TIndex1>(propertyName).Compile()
        )(obj, value, index1);
        return this;
    }

    public PropertyWriter<T> SetValue<TValue, TIndex1, TIndex2>(T obj, string propertyName, TValue value, TIndex1 index1, TIndex2 index2)
    {
        if (obj == null) { throw new ArgumentNullException(paramName: nameof(obj)); }

        propertyName = typeof(TIndex1).FullName + ", " + typeof(TIndex2).FullName;

        GetOrCreateSetterDelegate(
            propertyName,
            () => ExpressionFactory.CreateSetterExpression<T, TValue, TIndex1, TIndex2>(propertyName).Compile()
        )(obj, value, index1, index2);
        return this;
    }

    private TAction GetOrCreateSetterDelegate<TAction>(string propertyName, Func<TAction> setterDelegateFactory)
    {
        var cacheItem = default(object);
        if (_cache.TryGetValue(propertyName, out cacheItem))
        {
            return (TAction)cacheItem;
        }

        var getterDelegate = setterDelegateFactory();
        cacheItem = getterDelegate;
        _cache.Add(propertyName, cacheItem);

        return (TAction)cacheItem;
    }
}

That's all. The other convenience methods using expressions for property names are now provided by the PropertyReaderExtensions

public static class PropertyReaderExtensions
{
    public static TValue GetValue<T, TValue>(this PropertyReader<T> reader, T obj, Expression<Func<T, TValue>> property)
    {
        var propertyName = property.GetMemberName();
        return reader.GetValue<TValue>(obj, propertyName);
    }

    public static Dictionary<string, object> GetValues<T>(this PropertyReader<T> reader, T obj, params string[] propertyNames)
    {
        if (obj == null) { throw new ArgumentNullException(paramName: nameof(obj)); }
        if (propertyNames == null) { throw new ArgumentNullException(paramName: nameof(propertyNames)); }

        return propertyNames.ToDictionary(
            propertyName => propertyName,
            propertyName => reader.GetValue<object>(obj, propertyName)
        );
    }

    public static Dictionary<string, object> GetValues<T>(this PropertyReader<T> reader, T obj, ExpressionList<T> properties)
    {
        return properties.Select(p => p.GetMemberName()).ToDictionary(
            propertyName => propertyName,
            propertyName => reader.GetValue<object>(obj, propertyName));
    }
}

and agian, its counterpart the PropertyWriterExtensions:

public static class PropertyWriterExtensions
{
    public static PropertyWriter<T> SetValue<T, TValue>(this PropertyWriter<T> writer, T obj, Expression<Func<T, TValue>> property, TValue value)
    {
        var propertyName = property.GetMemberName();
        return writer.SetValue(obj, propertyName, value);
    }

    public static PropertyWriter<T> SetValues<T>(this PropertyWriter<T> writer, T obj, ExpressionDictionary<T> properties)
    {
        foreach (var property in properties)
        {
            writer.SetValue(obj, property.Key, property.Value);
        }
        return writer;
    }
}

There are also these two helper collections that encapsulate the lengthy element definition:

public class ExpressionList<T> : List<Expression<Func<T, object>>> { }

public class ExpressionDictionary<T> : Dictionary<Expression<Func<T, object>>, object> { }

and and extension to extract the member name:

public static class ExpressionExtensions
{
    public static string GetMemberName(this Expression expression)
    {
        var lambda = expression as LambdaExpression;
        if (lambda == null) { throw new ArgumentException("Expression must be a lambda expression."); }
        var memberExpression =
            (lambda.Body as MemberExpression) ??
            (lambda.Body as UnaryExpression)?.Operand as MemberExpression;

        if (memberExpression == null) { throw new ArgumentException("Expression must be a body expression."); }

        return memberExpression.Member.Name;
    }
}

The ExpressionFactory assumed the role of taking care of all expressions.

public static class ExpressionFactory
{
    private static BindingFlags GetBindingFlags(bool nonPublic)
    {
        var nonPublicFlag = nonPublic ? BindingFlags.NonPublic : BindingFlags.Default;
        return BindingFlags.Instance | nonPublicFlag | BindingFlags.Public;
    }

    public static Expression<Func<T, TProperty>> CreateGetterExpression<T, TProperty>(
        this PropertyInfo propertyInfo,
        bool nonPublic = false)
    {
        var hasGetter = propertyInfo.GetGetMethod(nonPublic) != null;
        if (!hasGetter || propertyInfo.GetIndexParameters().Any())
        {
            return null;
        }

        var obj = Expression.Parameter(typeof(T), "obj");
        var property = Expression.Property(obj, propertyInfo);
        return Expression.Lambda<Func<T, TProperty>>(property, obj);
    }

    public static Expression<Func<T, TProperty>> CreateGetterExpression<T, TProperty>(
        string propertyName,
        bool nonPublic = false
    )
    {
        return
            typeof(T)
            .GetProperty(propertyName, GetBindingFlags(nonPublic))
            .CreateGetterExpression<T, TProperty>();
    }

    public static Expression<Func<T, TIndex1, TProperty>> CreateGetterExpression<T, TIndex1, TProperty>(
            string propertyName,
            bool nonPublic = false
    )
    {
        var propertyInfo = FindProperty<T, TIndex1>(nonPublic);

        var hasGetter = propertyInfo?.GetGetMethod(nonPublic) != null;
        if (!hasGetter || propertyInfo.GetIndexParameters().Length != 1)
        {
            return null;
        }

        var obj = Expression.Parameter(typeof(T), "obj");
        var index1 = Expression.Parameter(typeof(TIndex1), "i");
        var property = Expression.Property(obj, propertyInfo, index1);
        return Expression.Lambda<Func<T, TIndex1, TProperty>>(property, obj, index1);
    }

    public static Expression<Func<T, TIndex1, TIndex2, TProperty>> CreateGetterExpression<T, TIndex1, TIndex2, TProperty>(
        string propertyName,
        bool nonPublic = false
    )
    {
        var propertyInfo = FindProperty<T, TIndex1, TIndex2>(nonPublic);

        var hasGetter = propertyInfo?.GetGetMethod(nonPublic) != null;
        if (!hasGetter || propertyInfo.GetIndexParameters().Length != 2)
        {
            return null;
        }

        var obj = Expression.Parameter(typeof(T), "obj");
        var index1 = Expression.Parameter(typeof(TIndex1), "i");
        var index2 = Expression.Parameter(typeof(TIndex2), "j");
        var property = Expression.Property(obj, propertyInfo, index1, index2);
        return Expression.Lambda<Func<T, TIndex1, TIndex2, TProperty>>(property, obj, index1, index2);
    }

    public static Expression<Action<T, TValue>> CreateSetterExpression<T, TValue>(
        string propertyName,
        bool nonPublic = false
    )
    {
        var propertyInfo = typeof(T).GetProperty(propertyName, GetBindingFlags(nonPublic));

        var obj = Expression.Parameter(typeof(T), "obj");
        var value = Expression.Parameter(typeof(TValue), "value");
        var property = Expression.Property(obj, propertyInfo);
        return Expression.Lambda<Action<T, TValue>>(Expression.Assign(property, value), obj, value);
    }

    public static Expression<Action<T, TValue, TIndex1>> CreateSetterExpression<T, TValue, TIndex1>(
        string propertyName,
        bool nonPublic = false
    )
    {
        var propertyInfo = FindProperty<T, TIndex1>(nonPublic);

        var obj = Expression.Parameter(typeof(T), "obj");
        var index1 = Expression.Parameter(typeof(TIndex1), "i");
        var value = Expression.Parameter(typeof(TValue), "value");
        var property = Expression.Property(obj, propertyInfo, index1);
        return Expression.Lambda<Action<T, TValue, TIndex1>>(Expression.Assign(property, value), obj, value, index1);
    }


    public static Expression<Action<T, TValue, TIndex1, TIndex2>> CreateSetterExpression<T, TValue, TIndex1, TIndex2>(
        string propertyName,
        bool nonPublic = false
    )
    {
        var propertyInfo = FindProperty<T, TIndex1, TIndex2>(nonPublic);

        var obj = Expression.Parameter(typeof(T), "obj");
        var index1 = Expression.Parameter(typeof(TIndex1), "i");
        var index2 = Expression.Parameter(typeof(TIndex2), "j");
        var value = Expression.Parameter(typeof(TValue), "value");
        var property = Expression.Property(obj, propertyInfo, index1, index2);
        return Expression.Lambda<Action<T, TValue, TIndex1, TIndex2>>(Expression.Assign(property, value), obj, value, index1, index2);
    }

    private static PropertyInfo FindProperty<T, TIndex1>(bool nonPublic = false)
    {
        var propertyInfo =
            typeof(T)
            .GetProperties(GetBindingFlags(nonPublic))
            .SingleOrDefault(p =>
                p.GetIndexParameters()
                .Select(pi => pi.ParameterType)
                .SequenceEqual(new[] { typeof(TIndex1) })
            );
        return propertyInfo;
    }

    private static PropertyInfo FindProperty<T, TIndex1, TIndex2>(bool nonPublic = false)
    {
        var propertyInfo =
            typeof(T)
            .GetProperties(GetBindingFlags(nonPublic))
            .SingleOrDefault(p =>
                p.GetIndexParameters()
                .Select(pi => pi.ParameterType)
                .SequenceEqual(new[] { typeof(TIndex1), typeof(TIndex2) })
            );
        return propertyInfo;
    }

    public static Expression<Func<T>> CreateDefaultConstructorExpression<T>()
    {
        return
            typeof(T).HasDefaultConstructor()
            ? Expression.Lambda<Func<T>>(Expression.New(typeof(T)))
            : null;
    }
}

Tests

To test it I wrote just six tests and I think this is actually enough.

The first test is for the PropertyReader

[TestClass]
public class PropertyReaderTest
{
    [TestMethod]
    public void GetValue_Property()
    {
        var reader = new PropertyReader<Foo>();
        var foo = new Foo
        {
            Bar = "baz"
        };
        Assert.AreEqual("baz", reader.GetValue<string>(foo, nameof(Foo.Bar)));
    }

    [TestMethod]
    public void GetValue_Indexer1D()
    {
        var reader = new PropertyReader<Foo>();
        var foo = new Foo
        {
            Bar = "baz"
        };
        Assert.AreEqual("a", reader.GetValue<int, string>(foo, null, 1));
    }

    [TestMethod]
    public void GetValue_Indexer2D()
    {
        var reader = new PropertyReader<Foo>();
        var foo = new Foo
        {
            Bar = "baz"
        };
        Assert.AreEqual("a8", reader.GetValue<int, int, string>(foo, null, 1, 8));
    }

    private class Foo
    {
        public string this[int i] => Bar[i].ToString();
        public string this[int i, int j] => Bar[i].ToString() + j;
        public string Bar { get; set; }
    }
}

and the second one is for the PropertyWriter

[TestClass]
public class PropertyWriterTest
{
    [TestMethod]
    public void SetValue_Property()
    {
        var reader = new PropertyWriter<Foo>();
        var foo = new Foo
        {
            Bar = "baz"
        };
        reader.SetValue(foo, nameof(Foo.Bar), "qux");

        Assert.AreEqual("qux", foo.Bar);
    }

    [TestMethod]
    public void SetValue_Indexer1D()
    {
        var reader = new PropertyWriter<Foo>();
        var foo = new Foo();

        reader.SetValue(foo, null, 2, 1);
        Assert.AreEqual(2, foo.Ints[1]);
    }

    [TestMethod]
    public void SetValue_Indexer2D()
    {
        var reader = new PropertyWriter<Foo>();
        var foo = new Foo();
        reader.SetValue(foo, null, 2, 1, 2);
        Assert.AreEqual(4, foo.Ints[1]);
    }

    private class Foo
    {
        public int[] Ints { get; } = new int[2];

        public int this[int i]
        {
            get { return Ints[i]; }
            set { Ints[i] = value; }
        }

        public int this[int i, int j]
        {
            get { return Ints[i] + j; }
            set { Ints[i] = value + j; }
        }

        public string Bar { get; set; }
    }
}
\$\endgroup\$
4
  • \$\begingroup\$ We no longer have a class named TypeAccessor but rather 2 smaller ones right ? (I haven't looked over the whole thing) \$\endgroup\$
    – Denis
    Dec 31, 2016 at 13:51
  • \$\begingroup\$ @denis exactly, the one can only read, the other can only write, each one has only a couple of basic APIs. It should be possible to implement everything else with extensions or other classes that can use the these two components. The nonPublic option is missing... here and there, didn't have time to fix it yet ;-] \$\endgroup\$
    – t3chb0t
    Dec 31, 2016 at 13:55
  • \$\begingroup\$ @Kittoes0124 this what I did to the TypeAccessor. \$\endgroup\$
    – t3chb0t
    Dec 31, 2016 at 14:05
  • \$\begingroup\$ I can't say I particularly like the class being cleaved in two here but I found most of your other alterations quite sound and tried to realize them in my own way. The OP has been updated with a Community Wiki answer and I'll be reviewing your implementation some more, bench-marking various details, and updating the CW based on the results. Unless someone beats me to it... \$\endgroup\$ Jan 6, 2017 at 2:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.