2
\$\begingroup\$

In the following contrived example, I have a collection of PropertyManagers that each contains selector and assigner delegates to read from and write to a property on the generic type. It feels clunky for lots of reasons, not least of which is that both delegates are basically there to serve the same purpose: To identify the property on the generic that I want to read from then write back into.

using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;

namespace Tests
{
    [TestFixture]
    public class ReadWritePropertySelectorTests
    {
        public void Round<T>(IEnumerable<T> collection,
            params PropertyManager<T>[] propertyManagers)
        {
            foreach (var item in collection)
            {
                foreach (var propertyManager in propertyManagers)
                {
                    var valueToRound = propertyManager.Selector(item);
                    propertyManager.Assigner(item, Math.Round(valueToRound, 1));
                }
            }
        }
        public class PropertyManager<T>
        {
            public Func<T, decimal> Selector { get; set; }
            public Action<T, decimal> Assigner { get; set; }
        }
        public class Combatant
        {
            public decimal Attack { get; set; }
            public decimal Defense { get; set; }
            public decimal Health { get; set; }
        }
        [Test]
        public void Test()
        {
            // Arrange
            var combatants = new[]
            {
                new Combatant { Attack = 33.12M, Defense = 12.771M, Health = 60.1181M }
            };

            // Act
            Round(combatants,
                new PropertyManager<Combatant>
                {
                    Selector = c => c.Attack, Assigner = (c, v) => c.Attack = v
                },
                new PropertyManager<Combatant>
                {
                    Selector = c => c.Defense, Assigner = (c, v) => c.Defense = v
                },
                new PropertyManager<Combatant>
                {
                    Selector = c => c.Health, Assigner = (c, v) => c.Health = v
                });

            // Assert
            var array = combatants.ToArray();
            Assert.AreEqual(33.1M, array[0].Attack);
            Assert.AreEqual(12.8M, array[0].Defense);
            Assert.AreEqual(60.1M, array[0].Health);
        }
    }
}

Is there any way to redo this to make the call to Round() more concise, preferably removing the need for a strongly typed PropertyManager? I think ultimately calling it as follows would be awesome, but I can't figure out the language constructs to use to get there:

Round(combatants, c => c.Attack, c => c.Defense, c => c.Health);
\$\endgroup\$
  • \$\begingroup\$ Why are you testing a function of your test class? \$\endgroup\$ – svick Feb 22 '14 at 19:02
  • \$\begingroup\$ Because it's a "contrived example". \$\endgroup\$ – mo. Feb 25 '14 at 16:14
3
\$\begingroup\$

Yup, it is possible, but you'll have to delve into the dark world of Expression Trees.

This function creates getter and setter functions using the out parameters. It's ugly, and you usually don't want to use two out parameters, but hopefully it'll get you moving in the right direction.

public static bool CreateGetterSetter<T, V>(Expression<Func<T, V>> getterExpression, out Func<T, V> getter, out Action<T, V> setter)
{
    if (getterExpression == null)
        throw new ArgumentNullException("getterExpression");

    var memberExpression = getterExpression.Body as MemberExpression;
    if (memberExpression == null || memberExpression.NodeType != ExpressionType.MemberAccess || memberExpression.Member == null)
        throw new ArgumentException("The expression must get a member (property or field).", "getterExpression");

    // The expression passed in is the getter, so just compile it.
    getter = getterExpression.Compile();

    // The setter function takes two parameters as input.
    var paramT = Expression.Parameter(typeof(T));
    var paramV = Expression.Parameter(typeof(V));

    // Create the setter function
    if (memberExpression.Member.MemberType == MemberTypes.Field)
    {
        // The getter retrieves a field. 
        var field = memberExpression.Member as FieldInfo;
        if (field == null)
            throw new ArgumentException("Could not get field info for member " + memberExpression.Member.ToString());

        // This expression represents the field on the parameter.
        var fieldExpr = Expression.Field(paramT, field);

        // This expression is what the field will be set to.
        Expression rightExpr;
        if (paramV.Type == field.FieldType)
            rightExpr = paramV;
        else
            rightExpr = Expression.Convert(paramV, field.FieldType);

        // This is the assignment expression (sets the field to the parameter value).
        var assign = Expression.Assign(fieldExpr, rightExpr);

        // Compile the expressions into a lambda.
        setter = Expression.Lambda<Action<T, V>>(assign, paramT, paramV).Compile();
    }
    else if (memberExpression.Member.MemberType == MemberTypes.Property)
    {
        // The getter retrieves a property.
        var property = memberExpression.Member as PropertyInfo;
        if (property == null)
            throw new ArgumentException("Could not get property info for member " + memberExpression.Member.ToString());

        // This expression represents what the property will be set to.
        Expression paramSet;
        if (paramV.Type == property.PropertyType)
            paramSet = paramV;
        else
            paramSet = Expression.Convert(paramV, property.PropertyType);

        // Handle read-only properties (have no setter).
        if (!property.CanWrite)
        {
            setter = (t, v) => Debug.Write("Cannot set read-only property: " + property.ToString());
            return false;
        }

        // This expression represents calling the property setter.
        var call = Expression.Call(paramT, property.SetMethod, paramSet);

        // Compile the expressions into a lambda.
        setter = Expression.Lambda<Action<T, V>>(call, paramT, paramV).Compile();
    }
    else
    {
        // The member was not a field or a property.
        setter = (t, v) => Debug.Write("Setter invoked for invalid expression");
        return false;
    }
    return true;
}

Usage:

public class A
{
    public string Property { get; set;}
}

// Define the variables to hold the out parameters.
Func<A, string> getter;
Action<A, string> setter;

// Call function to use expression trees to create the getter and setter.
CreateGetterSetter<A, string>(a => a.Property, out getter, out setter);

// Now you can use "getter" to get A.Property...
var inst = new A { Property = "value" };
var value = getter(inst); // value = "value"

// ... and "setter" to set A.Property...
setter(inst, "new"); // inst.Property = "new"
value = getter(inst); // value = "new";

And to implement Round this way...

public static void Round<T>(IEnumerable<T> collection, params Expression<Func<T, decimal>>[] getterExpressions)
{
    // Create all the lambdas up-front. This is expensive, so you should cache this somewhere rather than 
    // re-compiling all the expressions every time.
    var gettersSetters = new Tuple<Func<T, decimal>, Action<T, decimal>>[getterExpressions.Length]; 
    for(int i=0;i<getterExpressions.Length;i++)
    {
        Func<T, decimal> getter;
        Action<T, decimal> setter;
        CreateGetterSetter<T, decimal>(getterExpressions[i], out getter, out setter);
        gettersSetters[i] = Tuple.Create(getter, setter);
    }

    foreach (var item in collection)
    {
        foreach (var tuple in gettersSetters)
        {
            var getter = tuple.Item1;
            var setter = tuple.Item2;

            var value = getter(item);
            value = Math.Round(value, 1);
            setter(item, value);
        }
    }
}

Hope that helps!

\$\endgroup\$
  • \$\begingroup\$ You could combine the branches for property and field using Expression.PropertyOrField(). \$\endgroup\$ – svick Feb 22 '14 at 21:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.