Here's what I did for my needs, hope it helps someone.
tldr: I create "mappings" that I keep in a dictionary for each property (used as an expression, no property names as strings => easy to refactor). Using the mapping looks like
class ExampleClass
{
public int TestProperty1 { get; set; }
}
var m = new Mapping<ExampleClass, int>(dest => dest.TestProperty1);
var destObj = new ExampleClass();
m.SetValue(destObj, 1);
And the Mapping
class:
- the expression parameter accepts a property or a field that must belong to the respective (destination) type
- for properties, the default setter is saved as a delegate
- for fields a setter is created from IL and saved as a delegate
SetValue
simply calls this delegate with the destination object and value
- I left the version that uses
FieldInfo
/PropertyInfo
default SetValue
method which takes objects as parameters so it causes boxing, in case anyone wants to do some benchmarks (hint: the difference is HUGE!)
Code below:
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
public class Mapping<TDst, TValue>
{
private readonly MemberInfo dstMember;
private readonly Action<TDst, TValue> dstSetter = null!;
private static MemberInfo DestinationMember<T, TMember>(Expression<Func<T, TMember>> expression)
{
if (expression.Body is not MemberExpression memberExpression)
throw new ArgumentException($"Expression '{expression.Name}' does not refer to a field or property.");
var type = typeof(T);
if (memberExpression.Member.ReflectedType == null ||
(type != memberExpression.Member.ReflectedType &&
!type.IsSubclassOf(memberExpression.Member.ReflectedType)))
throw new ArgumentException(
$"Expresion '{memberExpression.Member.Name}' refers to a property that is not from type {type}.");
return memberExpression.Member;
}
public Mapping(Expression<Func<TDst, TValue>> dstExp)
{
dstMember = DestinationMember(dstExp);
if (dstMember is PropertyInfo { CanWrite: false }) throw new ArgumentException("Destination is read-only");
switch (dstMember)
{
case FieldInfo fi:
var dynamicMethod = new DynamicMethod(string.Empty, typeof(void),
new[] { typeof(TDst), typeof(TValue) }, true);
var ilGenerator = dynamicMethod.GetILGenerator();
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Stfld, fi);
ilGenerator.Emit(OpCodes.Ret);
dstSetter = (Action<TDst, TValue>)dynamicMethod.CreateDelegate(typeof(Action<TDst, TValue>));
break;
case PropertyInfo pi:
var setter = pi.GetSetMethod(true)!;
dstSetter = (Action<TDst, TValue>)Delegate.CreateDelegate(typeof(Action<TDst, TValue>), setter);
break;
}
}
public void SetValue(TDst destination, TValue value) => dstSetter(destination, value);
public void SetValueWithBoxing(TDst destination, TValue value)
{
switch (dstMember)
{
case FieldInfo fi:
fi.SetValue(destination, value);
break;
case PropertyInfo pi:
pi.SetValue(destination, value);
break;
}
}
}