Point one, I know "Assignator" isn't a real word. I couldn't find a better fit so right now that word is added to my Visual Studio's dictionary!
As a part of my (future) MVVM framework, I needed a class to assign values to a property of an object. As it's a quite generic framework, the class itself needed to be generic. I had the choice to use reflection (*booo*) or to give it a try and Expression
compilation at runtime (*cheers*)! Guess what, I went with expression compilation.
public interface IPropertyAssignator<T> where T : class
{
void Assign(T target, string propertyName, object value);
}
/// <summary>
/// Dynamically assigns values to properties of an object
/// </summary>
/// <typeparam name="T">Assignation's target type</typeparam>
/// <remarks>This class is not thread safe.</remarks>
public class DynamicPropertyAssignator<T> : IPropertyAssignator<T> where T : class
{
private readonly bool _lazyPopulate;
private readonly Dictionary<string, Action<T, object>> _expressionMap;
/// <summary>
/// Creates an instance of DynamicPropertyAssignator.
/// </summary>
/// <param name="lazyPopulate">If true, assignation expressions will be generated only when needed.</param>
public DynamicPropertyAssignator(bool lazyPopulate = true)
{
_lazyPopulate = lazyPopulate;
_expressionMap = new Dictionary<string, Action<T, object>>();
if (!lazyPopulate)
{
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties)
{
CreateAndAddAssignExpression(property.Name, property.PropertyType);
}
}
}
/// <summary>
/// Assigns <paramref name="value"/> to <paramref name="propertyName"/> of <paramref name="target"/>
/// </summary>
/// <param name="target">Target to be modified</param>
/// /// <param name="propertyName">Property of the target to modify</param>
/// <param name="value">Value to assign the target</param>
/// <exception cref="ArgumentException">If the assignation arguments are of incoherent types.</exception>
public void Assign(T target, string propertyName, object value)
{
if (target == null) throw new ArgumentNullException(nameof(target));
if (propertyName == null) throw new ArgumentNullException(nameof(propertyName));
if (value == null) throw new ArgumentNullException(nameof(value));
Action<T, object> assignExpression = null;
if(_lazyPopulate && !_expressionMap.TryGetValue(propertyName, out assignExpression))
{
if (PropertiesMatch(value.GetType(), propertyName))
{
assignExpression = CreateAndAddAssignExpression(propertyName, value.GetType());
}
else
{
//Add null to show we checked that this property doesn't exist in T, so we skip the checks the next time.
_expressionMap.Add(propertyName, null);
}
}
try
{
ApplyExpression(assignExpression, target, value);
}
catch (InvalidCastException ice)
{
throw new ArgumentException($"{typeof(T)}.{propertyName} isn't assignable to {value.GetType()}", ice);
}
}
/// <summary>
/// Call of the <paramref name="expression"/> with given parameters.
/// </summary>
/// <param name="expression">Expression to use</param>
/// <param name="target">Target of modification</param>
/// <param name="value">Value to apply</param>
protected virtual void ApplyExpression(Action<T, object> expression, T target, object value)
{
expression?.Invoke(target, value);
}
/// <summary>
/// Dynamically creates an Action that will be used assign a value to the target's property and adds it to the map.
/// </summary>
/// <param name="propertyName">Name of the property to assign</param>
/// <param name="propertyType">Type of the property to assign</param>
/// <returns>An Assign expression compiled to an Action</returns>
private Action<T, object> CreateAndAddAssignExpression(string propertyName, Type propertyType)
{
var targetParameter = Expression.Parameter(typeof(T), "target");
var targetProperty = Expression.Property(targetParameter, propertyName);
var valueParameter = Expression.Parameter(typeof(object),"value");
//This is necessary because we hold a Action<T,object>.
var convertedValue = Expression.Convert(valueParameter, propertyType);
var assignation = Expression.Assign(targetProperty, convertedValue);
//Final result looks like : (target, value) => target.Property = (propertyType)value
var action = Expression.Lambda<Action<T, object>>(assignation, targetParameter, valueParameter).Compile();
_expressionMap.Add(propertyName, action);
return action;
}
/// <summary>
/// Asserts that the property exists on <typeparamref name="T"/> and that the type fits
/// </summary>
/// <param name="valueType">Type of the value that would be assigned</param>
/// <param name="propertyName">Property name of <typeparamref name="T"/> to check</param>
/// <returns></returns>
private static bool PropertiesMatch(Type valueType, string propertyName)
{
var property = typeof(T).GetProperty(propertyName);
return (property != null && property.PropertyType.IsAssignableFrom(valueType));
}
}
There are some goals to my review (Obviously, anything else is welcomed). I'd want to know if :
- my code and comments are self-explanatory enough that I don't need to explain my code.
- the expression generation is good
- the caching method that I use for the expression seems good.
I ran benchmarks of this class versus an "assignator" that uses reflection and mine quickly becomes much faster, which is good. But for sure, if something could be made faster, that'd be great.