Skip to main content
2 of 5
deleted 21 characters in body
Jamal
  • 35.2k
  • 13
  • 134
  • 238

Accessing properties by name with Compile-time typesafety

I've recently answered a question here.

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

It uses both a private method which is not compile-time typesafe and a public one which is compile-time typesafe, 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 strings as parameters instead of Expressions.

Denis
  • 8.6k
  • 5
  • 33
  • 76