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 :
- Cloning properties from one instance to another.
- Setting properties of instances.
- Getting property values from instances.
- Set instance properties to default values, where the values are determined at the creation of the
TypeAccessor<T>
class. - Creating new instances of the type argument
T
, using default constructor, again this is specified at the instantiation of theTypeAccessor<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