That Guy
That Guy

Reputation: 379

Weird issue with setting value via reflection

I've got this helper method SetProperty that sets the property of an object via reflection. Below, is 2 scenarios where I use that method. The first method CreateInstance works just fine, but the second method Insert doesn't work.

In the 2nd method, the property set on the object is lost as soon as the method SetProperty returns. I've debugged it via visual studio. The object has the property set uptill the last closing curly braces. Then as control is returned to the caller Insert, the Property value is lost.

Method that sets properties

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void SetProperty(object destination, string propertyName, object value)
{
    var type = destination.GetType();
    var property = type.GetProperty(propertyName);
    var convertedVal = Convert(value, property.PropertyType);

    property.SetValue(destination, convertedVal);
}

The SetProperty method works fine in this method

public static T CreateInstance<T>(SqlDataReader row, IEnumerable<CLASS> columns)
{
    var type = typeof(T);
    var obj = Activator.CreateInstance(type, true);

    foreach (var column in columns)
    {
        SetProperty(obj, column.BackingPropertyName, column.Name);
    }

    return (T)obj;
}

The SetProperty method doesn't work in this method

public T Insert<T>(T obj, string table = null)
{
    // CODE CHUNK
    using (var conn = new SqlConnection(this.ConnectionString))
    {
        conn.Open();

        using (var cmd = new SqlCommand(query.ToString(), conn))
        {
            // CODE CHUNK

            var autoGeneratedValue = cmd.ExecuteScalar();

            if (temp.AutoGeneratedColumn != null)
            {
                ReflectionHelper.SetProperty(
                    obj,
                    temp.AutoGeneratedColumn.BackingPropertyName,
                    autoGeneratedValue
                );
            }
        }
    }

    return obj;
}

EDIT - Add Console app to enable duplication

To replicate create new console app, then paste this code in Program.cs (Or equivalent)

using System;
using System.Runtime.CompilerServices;

namespace ConsoleApplication1
{
    public struct Person
    {
        public int ID { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Gender { get; set; }
        public int Age { get; set; }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var p = new Person
            {
                ID = 93
            };

            var res = SetProperty<Person>(ref p, "Age", 34);
            Console.WriteLine(p.Age);
            Console.WriteLine(res.Age);
            Console.Read();
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static T SetProperty<T>(ref T destination, string propertyName, object value)
        {
            var type = destination.GetType();
            var property = type.GetProperty(propertyName);
            var convertedVal = Convert(value, property.PropertyType);

            property.SetValue(destination, convertedVal);

            return (T)destination;
        }

        private static object Convert(object source, Type destinationType)
        {
            if (destinationType == null)
            {
                throw new ArgumentNullException("destinationType");
            }

            if (destinationType.IsGenericType &&
                destinationType.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
            {
                if (source == null)
                {
                    return null;
                }
                destinationType = Nullable.GetUnderlyingType(destinationType);
            }

            return System.Convert.ChangeType(source, destinationType);
        }
    }
}

Upvotes: 1

Views: 128

Answers (1)

Matthew Watson
Matthew Watson

Reputation: 109567

From your repro, I can see what the issue is.

This is being caused by your Person struct being boxed.

Consider this line of code in the SetProperty<T>() method:

property.SetValue(destination, convertedVal);

The first parameter of SetValue() is of type object. What that means is that when the struct destination is passed to it, it is boxed and the property is set on the boxed copy - leaving the original one unchanged. (Note that boxing only affects value types such as structs - if Person was a class, it wouldn't be boxed and it would work as expected.)

To fix this, you have to "manually" box the Person struct by assigning it to an object, then set the property on that object and finally assign it back to the original struct ref, like so:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static T SetProperty<T>(ref T destination, string propertyName, object value)
{
    var type = destination.GetType();
    var property = type.GetProperty(propertyName);
    var convertedVal = Convert(value, property.PropertyType);

    object boxed = destination;

    property.SetValue(boxed, convertedVal);

    destination = (T) boxed;

    return (T)boxed;
}

Upvotes: 2

Related Questions