Over the weekend I decided to start work on my own version of FastMember. I began with my take on an implementation of TypeAccessor
so that I could work with the properties of any type, create an instance of the type, and one or two other little tricks. I've reached a point where I'm out of ideas on how to improve the design of the class further so now I'm looking for feedback.
Usage:
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(); // pointB.X should be 0.0
accessor.CloneProperties(pointA, pointB) // pointB.X should now be 9000.01
accessor[pointA, "X"] = 0.0; // sets pointA.X to 0.0
Console.WriteLine(accessor[pointA, "X"]) // prints pointA.X, should be 0.0
accessor.Default(ref pointA); // sets pointA's properties to default the accessor's default values
Console.WriteLine(accessor[pointA, "X"]); // prints pointA.X, should be 9000.01
Notes:
One of the design goals was to ensure that everything always works as you might expect it to based on the current context. If the Type
you send to TypeAccessor
has a property that is read-only then a method like CloneProperties
will simply ignore that property because it's not cloneable. GetProperties
returns an actual instance of Dictionary<string, object>
because it makes no sense to let the method yield when:
- Some nasty behaviors become possible because reference types could be nulled during any yield
- Two other options exist for accessing properties one at a time
As far as I know, every single "expensive" operation that can be lifted into the constructor has been (which was very important to me for performance). I can't say I'm pleased with the amount of code there but it really doesn't belong anywhere else. I suppose it could be split up into two or three private methods but I'm not sure that would be any better...
Dependencies:
There are three method calls in this class that are not part of .NET: GetGetAccessor
, GetSetAccessor
, and HasDefaultConstructor
. Links to their implementation is provided below for reference.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.Serialization;
public interface ITypeAccessor<T>
{
object this[T instance, string propertyName] { get; set; }
IEnumerable<string> CloneableProperties { get; }
IDictionary<string, Func<T, object>> GetterCache { get; }
IDictionary<string, Action<T, object>> SetterCache { get; }
void CloneProperties(T source, T target);
void Default(ref T instance);
object GetProperty(T instance, string propertyName);
IDictionary<string, object> GetProperties(T instance, IEnumerable<string> properties);
T New();
void SetProperty(T instance, string propertyName, object value);
void SetProperties(T instance, IEnumerable<KeyValuePair<string, object>> properties);
}
/// <summary>
/// Provides cached access to a <see cref="T"/>'s property getters and setters.
/// </summary>
public class TypeAccessor<T> : ITypeAccessor<T>
{
private readonly Func<T, T> m_applyDefaultValues;
private readonly ReadOnlyCollection<string> m_cloneableProperties;
private readonly Func<T> m_constructType;
private readonly ReadOnlyDictionary<string, Func<T, object>> m_getterCache;
private readonly ReadOnlyDictionary<string, Action<T, object>> m_setterCache;
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> to get or set the value of a property by name.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to get or set a property on.</param>
/// <param name="propertyName">The name of the property to affect on the instance of <see cref="T"/>.</param>
/// <returns></returns>
public object this[T instance, string propertyName] {
get {
return GetProperty(instance, propertyName);
}
set {
SetProperty(instance, propertyName, value);
}
}
/// <summary>
/// Returns a read-only cache of property names that belong to <see cref="T"/> and have both a getter and a setter.
/// </summary>
public IEnumerable<string> CloneableProperties {
get {
return m_cloneableProperties;
}
}
/// <summary>
/// Returns a read-only cache of property getters that belong to <see cref="T"/>.
/// </summary>
public IDictionary<string, Func<T, object>> GetterCache {
get {
return m_getterCache;
}
}
/// <summary>
/// Returns a read-only cache of property setters that belong to <see cref="T"/>.
/// </summary>
public IDictionary<string, Action<T, object>> SetterCache {
get {
return m_setterCache;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="TypeAccessor{_}"/> class.
/// </summary>
/// <param name="defaultValue">The instance of <see cref="T"/> that will be used as this <see cref="TypeAccessor"/>'s default value.</param>
/// <param name="includeNonPublic">Indicates whether non-public properties should be included.</param>
public TypeAccessor(T defaultValue, bool includeNonPublic = false) {
var properties = typeof(T).GetProperties(BindingFlags.Instance | (includeNonPublic ? BindingFlags.NonPublic : BindingFlags.Default) | BindingFlags.Public);
m_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(
k => k.PropertyName,
v => v.PropertyGetAccessor
));
m_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(
k => k.PropertyName,
v => v.PropertySetAccessor
));
m_cloneableProperties = Array.AsReadOnly(GetterCache.Keys.Intersect(SetterCache.Keys).ToArray());
if (typeof(T).IsValueType) {
m_applyDefaultValues = (T instance) => { return defaultValue; };
m_constructType = () => { return defaultValue; };
}
else if (defaultValue != null) {
var defaultConstructor = GetDefaultConstructor();
var propertyValues = GetProperties(defaultValue, CloneableProperties).ToArray();
m_applyDefaultValues = (T instance) => { SetProperties(instance, propertyValues); return instance; };
m_constructType = () => { return m_applyDefaultValues(defaultConstructor()); };
}
else {
m_applyDefaultValues = (T instance) => { return default(T); };
m_constructType = () => { return default(T); };
}
}
/// <summary>
/// Retrieves all property values from a source <see cref="T"/> and applies them to a target <see cref="T"/>.
/// </summary>
/// <param name="source">The source instance of <see cref="T"/> to retrieve property values from.</param>
/// <param name="target">The target instance of <see cref="T"/> to apply property values to.</param>
public void CloneProperties(T source, T target) {
SetProperties(target, GetProperties(source, CloneableProperties));
}
/// <summary>
/// Sets all propertes on the specified instance of <see cref="T"/> to this <see cref="TypeAccessor{_}"/>'s default values.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to get a property from.</param>
public void Default(ref T instance) {
instance = m_applyDefaultValues(instance);
}
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> and specified <see cref="IEnumerable{_}"/> to get all property values.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to get properties from.</param>
public IDictionary<string, object> GetProperties(T instance) {
return GetterCache.Keys.ToDictionary(
propertyName => propertyName,
propertyName => GetProperty(instance, propertyName)
);
}
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> and specified <see cref="IEnumerable{_}"/> to get property values by name.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to get properties from.</param>
/// <param name="properties">The list of property names that values will be retrieved from the instance of <see cref="T"/>.</param>
public IDictionary<string, object> GetProperties(T instance, IEnumerable<string> properties) {
if (properties != null) {
return properties.ToDictionary(
propertyName => propertyName,
propertyName => GetProperty(instance, propertyName)
);
}
else {
return new Dictionary<string, object>();
}
}
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> to get the value of a property by name.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to get a property from.</param>
/// <param name="propertyName">The name of the property to retrieve from the instance of <see cref="T"/>.</param>
public object GetProperty(T instance, string propertyName) {
Func<T, object> getter;
if (GetterCache.TryGetValue(propertyName, out getter)) {
return getter(instance);
}
else {
throw new KeyNotFoundException(message: string.Format("a property getter with the name does not {0} exist on {1}", propertyName, typeof(T).FullName));
}
}
/// <summary>
/// Creates a new <see cref="T"/> using this <see cref="TypeAccessor{_}"/>'s default type constructor.
/// </summary>
public T New() {
return m_constructType();
}
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> and specified <see cref="IEnumerable{KeyValuePair{_,_}}"/> to set property values by name.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to set property values for.</param>
/// <param name="properties">The dictionary of property names and values that will be applied to the instance of <see cref="T"/>.</param>
public void SetProperties(T instance, IEnumerable<KeyValuePair<string, object>> properties) {
if (properties != null) {
foreach (var property in properties) {
SetProperty(instance, property.Key, property.Value);
}
}
}
/// <summary>
/// Uses the <see cref="TypeAccessor{_}"/> to set the value of a property by name.
/// </summary>
/// <param name="instance">The instance of <see cref="T"/> to set a property value for.</param>
/// <param name="propertyName">The name of the property to alter on the instance of <see cref="T"/>.</param>
/// <param name="value">The value that will be applied to the property.</param>
public 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(message: string.Format("a property setter with the name does not {0} exist on {1}", propertyName, typeof(T).FullName));
}
}
/// <summary>
/// Generates a <see cref="Func{_}"/> delegate to the default constructor of <see cref="T"/>.
/// </summary>
public 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();
}
else if (type.HasDefaultConstructor()) {
return Expression.Lambda<Func<T>>(Expression.New(type)).Compile();
}
else {
return () => (T)FormatterServices.GetUninitializedObject(type);
}
}
}
/// <summary>
/// Provides cached access to a <see cref="T"/>'s property getters and setters.
/// </summary>
public static class TypeAccessor
{
/// <summary>
/// Creates a new instance of the <see cref="ITypeAccessor{_}"/> interface using the specified <see cref="T"/>.
/// </summary>
/// <param name="defaultValue">The instance of <see cref="T"/> that will be used as this <see cref="ITypeAccessor{_}"/>'s default value.</param>
/// <param name="includeNonPublic">Indicates whether non-public properties should be included.</param>
public static ITypeAccessor<T> Create<T>(T defaultValue, bool includeNonPublic = false) {
return new TypeAccessor<T>(defaultValue, includeNonPublic);
}
}
Edit: Per request, I've added an interface at top to act as a sort of outline for all the class's desired functionality.
TypeAccessor
class performs all necessary reflection in one shot and buildsdelegates
for property manipulation and type construction. \$\endgroup\$point
. \$\endgroup\$TypeAccessor
is used as a template for construction. All cloneable properties become the default values for anyT
constructed by theTypeAccessor
. \$\endgroup\$