How can I pass a property as a delegate?

11,339

Solution 1

(Adding a second answer because it's on a completely different approach)

To address your original problem, which is more about wanting a nice API for mapping named values in a datareader to properties on your object, consider System.ComponentModel.TypeDescriptor - an often overlooked alternative to doing reflective dirtywork yourself.

Here's a useful snippet:

var properties = TypeDescriptor.GetProperties(myObject)
    .Cast<PropertyDescriptor>()
    .ToDictionary(pr => pr.Name);

That creates a dictionary of the propertydescriptors of your object.

Now I can do this:

properties["Property1"].SetValue(myObject, rdr["item1"]);

PropertyDescriptor's SetValue method (unlike System.Reflection.PropertyInfo's equivalent) will do type conversion for you - parse strings as ints, and so on.

What's useful about this is one can imagine an attribute-driven approach to iterating through that properties collection (PropertyDescriptor has an Attributes property to allow you to get any custom attributes that were added to the property) figuring out which value in the datareader to use; or having a method that receives a dictionary of propertyname - columnname mappings which iterates through and performs all those sets for you.

I suspect an approach like this may give you the API shortcut you need in a way that lambda-expression reflective trickery - in this case - won't.

Solution 2

I like using expression trees to solve this problem. Whenever you have a method where you want to take a "property delegate", use the parameter type Expression<Func<T, TPropertyType>>. For example:

public void SetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression,
    TProperty value
)
{
    MemberExpression member = (MemberExpression)expression.Body;
    PropertyInfo property = (PropertyInfo)member.Member;
    property.SetValue(obj, value, null);
}

Nice thing about this is that the syntax looks the same for gets as well.

public TProperty GetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression
)
{
    MemberExpression member = (MemberExpression)expression.Body;
    PropertyInfo property = (PropertyInfo)member.Member;
    return (TProperty)property.GetValue(obj, null);
}

Or, if you're feeling lazy:

public TProperty GetPropertyFromDbValue<T, TProperty>(
    T obj,
    Expression<Func<T, TProperty>> expression
)
{
    return expression.Compile()(obj);
}

Invocation would look like:

SetPropertyFromDbValue(myClass, o => o.Property1, reader["field1"]);
GetPropertyFromDbValue(myClass, o => o.Property1);

Solution 3

Ignoring whether this is useful in your specific circumstances (where I think the approach you've taken works just fine), your question is 'is there a way to convert a property into a delegate'.

Well, there kind of might be.

Every property actually (behind the scenes) consists of one or two methods - a set method and/or a get method. And you can - if you can get a hold of those methods - make delegates that wrap them.

For instance, once you've got hold of a System.Reflection.PropertyInfo object representing a property of type TProp on an object of type TObj, we can create an Action<TObj,TProp> (that is, a delegate that takes an object on which to set the property and a value to set it to) that wraps that setter method as follows:

Delegate.CreateDelegate(typeof (Action<TObj, TProp>), propertyInfo.GetSetMethod())

Or we can create an Action<TProp> that wraps the setter on a specific instance of TObj like this:

Delegate.CreateDelegate(typeof (Action<TProp>), instance, propertyInfo.GetSetMethod())

We can wrap that little lot up using a static reflection extension method:

public static Action<T> GetPropertySetter<TObject, T>(this TObject instance, Expression<Func<TObject, T>> propAccessExpression)
{
    var memberExpression = propAccessExpression.Body as MemberExpression;
    if (memberExpression == null) throw new ArgumentException("Lambda must be a simple property access", "propAccessExpression");

    var accessedMember = memberExpression.Member as PropertyInfo;
    if (accessedMember == null) throw new ArgumentException("Lambda must be a simple property access", "propAccessExpression");

    var setter = accessedMember.GetSetMethod();

    return (Action<T>) Delegate.CreateDelegate(typeof(Action<T>), instance, setter);
}

and now I can get a hold of a 'setter' delegate for a property on an object like this:

MyClass myObject = new MyClass();
Action<string> setter = myObject.GetPropertySetter(o => o.Property1);

That's strongly typed, based on the type of the property itself, so it's robust in the face of refactoring and compile-time typechecked.

Of course, in your case, you want to be able to set your property using a possibly-null object, so a strongly typed wrapper around the setter isn't the whole solution - but it does give you something to pass to your SetPropertyFromDbValue method.

Solution 4

No, there's nothing akin to method group conversions for properties. The best you can do is to use a lambda expression to form a Func<string> (for a getter) or an Action<string> (for a setter):

SetPropertyFromDbValue<string>(value => myClass.Property1 = value,
                               rdr["field1"]);

Solution 5

Worth mentioning you can do this with some reflection trickery.. something like...

public static void LoadFromReader<T>(this object source, SqlDataReader reader, string propertyName, string fieldName)
    {
        //Should check for nulls..
        Type t = source.GetType();
        PropertyInfo pi = t.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        object val = reader[fieldName];
        if (val == DBNull.Value)
        {
            val = default(T);
        }
        //Try to change to same type as property...
        val = Convert.ChangeType(val, pi.PropertyType);
        pi.SetValue(source, val, null);
    }

then

myClass.LoadFromReader<string>(reader,"Property1","field1");
Share:
11,339
BenAlabaster
Author by

BenAlabaster

Tinkerer, problem solver, artist, photographer and professional code monkey. Full time software engineer, father and student desperately seeking more hours in the day so I can keep up with life. I'm currently a free agent and looking for my next project, so if you are in need of a strong and dedicated .NET developer, please get in touch. I can be found in the following places: Google Plus: http://gplus.to/BenAlabaster Twitter: @BenAlabaster Blog: http://www.endswithsaurus.com/ LinkedIn: http://www.linkedin.com/in/BenAlabaster Email: BenAlabaster at gmail dot com

Updated on August 02, 2022

Comments

  • BenAlabaster
    BenAlabaster almost 2 years

    This is a theoretical question, I've already got a solution to my problem that took me down a different path, but I think the question is still potentially interesting.

    Can I pass object properties as delegates in the same way I can with methods? For instance:

    Let's say I've got a data reader loaded up with data, and each field's value needs to be passed into properties of differing types having been checked for DBNull. If attempting to get a single field, I might write something like:

    if(!rdr["field1"].Equals(DBNull.Value)) myClass.Property1 = rdr["field1"];
    

    But if I've got say 100 fields, that becomes unwieldy very quickly. There's a couple of ways that a call to do this might look nice:

    myClass.Property = GetDefaultOrValue<string>(rdr["field1"]); //Which incidentally is the route I took
    

    Which might also look nice as an extension method:

    myClass.Property = rdr["field1"].GetDefaultOrValue<string>();
    

    Or:

    SetPropertyFromDbValue<string>(myClass.Property1, rdr["field1"]); //Which is the one that I'm interested in on this theoretical level
    

    In the second instance, the property would need to be passed as a delegate in order to set it.

    So the question is in two parts:

    1. Is this possible?
    2. What would that look like?

    [As this is only theoretical, answers in VB or C# are equally acceptable to me]

    Edit: There's some slick answers here. Thanks all.