Ahmad Akra
Ahmad Akra

Reputation: 535

C#: How to set a property that has a value type using reflection without boxing or unboxing?

I am in a situation where I need to hydrate a large number of DTOs of various classes using reflection, but I'd like to avoid boxing and unboxing which hurts performance. Any idea how?

Example to illustrate:

    public class Person {
      public int Age { get; set; }
    }
    var person = new Person();
    var ageProp = typeof(Person).GetProperty("Age");
    ageProp.SetValue(person , 13); // Causes boxing!!

EDIT

Here is a better example:

public void CreateAndHydrateEntity(Type entityType, List<(string PropName, int PropIndex) properties, SqlDataReader reader) {
    var entity = Activator.CreateInstance(entityType);
    foreach(var (propName, index) in properties) {
        var prop = entityType.GetProperty(propName);
        prop.SetValue(entity, reader[index]); // Causes boxing!!
    }
}

Upvotes: 2

Views: 693

Answers (1)

Stefan Balan
Stefan Balan

Reputation: 622

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;
        }
    }
}

Upvotes: 0

Related Questions