How do I get property names of a type using a lambda expression and anonymous type?
Asked Answered
L

5

5

I am trying to use Expression Trees and anonymous types to achieve the following.

Let's say I have this class:

class Person
{
   public string FirstName {get;set;}
   public string MiddleName {get;set;}
   public string LastName {get;set;}
   public DateTime DateOfBirth {get;set;}
}

Now I want to be able to call the following:

string[] names = Foo<Person>(x=> new { x.LastName, x.DateOfBirth });

I want names to contain 2 items, "LastName" and "DateOfBirth".

I am trying to extend PetaPoco, in a compile time safe way rather than writing string sql, so that I can specify a list of properties/columns I want to include in the SQL, rather than it selecting everything. I have some pretty large entities and there are cases where I do not want to select all the columns for performance reasons.

Laszlo answered 28/3, 2012 at 21:58 Comment(2)
Other than a learning excercise, is there any particular reason to implement your own ORM utility? There are many ORM providers out there and often those which are home grown cause more hassle than they are worth.Mirisola
I just updated the question. I actually trying to extend PetaPoco to support specified columns in a SELECT rather than all of the properties on a data class.Laszlo
T
2

I'm lazy so this code handles only public properties. But it should be a good base to get you started.

public static string[] Foo<T>(Expression<Func<T, object>> func)
{
    var properties = func.Body.Type.GetProperties();

    return typeof(T).GetProperties()
        .Where(p => properties.Any(x => p.Name == x.Name))
        .Select(p =>
        {
            var attr = (ColumnAttribute) p.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault();
            return (attr != null ? attr.Name : p.Name);
        }).ToArray();
}
Tangerine answered 28/3, 2012 at 22:24 Comment(0)
M
4

Try this out for size:

public static string[] Foo<T, TResult>(Expression<Func<T, TResult>> func)
{
    return typeof(TResult).GetProperties().Select(pi => pi.Name).ToArray();
}

As you are returning an anonymous type from your lamda, you are able loop over all the properties of this anonymous type and use the inferred names of the properties. However when using this the syntax would be more like:

Foo((Person x) => new { x.LastName, x.DateOfBirth });

This is because the second generic argument is an anoymous type.

Mirisola answered 28/3, 2012 at 22:28 Comment(3)
Thanks for this answer. I like how it is succinct.Laszlo
@Brady: Did you end up extending Petapoco? If so, did you make a branch for it? I'm interested in your solution.Nucleoside
What does Foo((Person x) => x.FirstName) return? It returns a string array with these elements: { "Chars", "Length" }. Which means one must call Foo using anonymous types, even for single properties. Please take a look at my answer.Flavor
T
2

I'm lazy so this code handles only public properties. But it should be a good base to get you started.

public static string[] Foo<T>(Expression<Func<T, object>> func)
{
    var properties = func.Body.Type.GetProperties();

    return typeof(T).GetProperties()
        .Where(p => properties.Any(x => p.Name == x.Name))
        .Select(p =>
        {
            var attr = (ColumnAttribute) p.GetCustomAttributes(typeof(ColumnAttribute), true).FirstOrDefault();
            return (attr != null ? attr.Name : p.Name);
        }).ToArray();
}
Tangerine answered 28/3, 2012 at 22:24 Comment(0)
D
2

The answers given here work when either only single property is selected, OR when multiple properties are selected. None of them work for both. The answer by Lukazoid only works for multiple properties, the rest for single property, as of writing my answer.

The code below considers both the case, that is, you can use it for selecting single property AND multiple properties. Please note that I haven't added any sanity checking here, so feel free to add your own.

string[] Foo<T>(Expression<Func<Person, T>> func)
{
    if (func.Body is NewExpression)
    {
        // expression selects multiple properties, 
        // OR, single property but as an anonymous object

        // extract property names right from the expression itself
        return (func.Body as NewExpression).Members.Select(m => m.Name).ToArray();

        // Or, simply using reflection, as shown by Lukazoid
        // return typeof(T).GetProperties().Select(p => p.Name).ToArray();
    }
    else
    {
        // expression selects only a single property of Person,
        // and not as an anonymous object.
        return new string[] { (func.Body as MemberExpression).Member.Name };
    }        
}

Or more succinctly, using a ternary operator it all becomes just this:

string[] Foo<T>(Expression<Func<Person, T>> func)
{
    return (func.Body as NewExpression) != null
        ? typeof(T).GetProperties().Select(p => p.Name).ToArray()
        : new string[] { (func.Body as MemberExpression).Member.Name };
}

Download LinkPad file: LinkPad
See it online: Repl.it

Please feel free to point out anything that I may have missed.

Disenthrall answered 3/3, 2018 at 15:9 Comment(0)
V
0

A page of code is a thousand words, so here's how Microsoft does it in Prism:

///<summary>
/// Provides support for extracting property information based on a property expression.
///</summary>
public static class PropertySupport
{
    /// <summary>
    /// Extracts the property name from a property expression.
    /// </summary>
    /// <typeparam name="T">The object type containing the property specified in the expression.</typeparam>
    /// <param name="propertyExpression">The property expression (e.g. p => p.PropertyName)</param>
    /// <returns>The name of the property.</returns>
    /// <exception cref="ArgumentNullException">Thrown if the <paramref name="propertyExpression"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when the expression is:<br/>
    ///     Not a <see cref="MemberExpression"/><br/>
    ///     The <see cref="MemberExpression"/> does not represent a property.<br/>
    ///     Or, the property is static.
    /// </exception>
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }

        var memberExpression = propertyExpression.Body as MemberExpression;
        if (memberExpression == null)
        {
            throw new ArgumentException(Resources.PropertySupport_NotMemberAccessExpression_Exception, "propertyExpression");
        }

        var property = memberExpression.Member as PropertyInfo;
        if (property == null)
        {
            throw new ArgumentException(Resources.PropertySupport_ExpressionNotProperty_Exception, "propertyExpression");
        }

        var getMethod = property.GetGetMethod(true);
        if (getMethod.IsStatic)
        {
            throw new ArgumentException(Resources.PropertySupport_StaticExpression_Exception, "propertyExpression");
        }

        return memberExpression.Member.Name;
    }
}

If you want to take attributes into account it's going to be slightly more complicated, but the general idea of accepting an Expression<Func<T>> and fishing out the name of the property being targeted is the same.

Update: As is, the method will accept only one parameter; I only provided it as a guideline. The idea can be generalized of course:

public static string[] ExtractPropertyNames<T>(
    Expression<Func<T, object>> propertyExpression)

This method will accept an expression that takes a T and returns an anonymous type which you can then reflect upon. You could substitute a second type parameter for object but that doesn't really do anything here because the only thing you want to do is reflect on the type.

Veronikaveronike answered 28/3, 2012 at 22:16 Comment(2)
Won't this only support one member (i.e. x=>x.LastName) ?Laszlo
You are right, it will only support single member. Also that single member must not be as an anonymous object. Take a look at me answer, which covers both cases, that is, single member and multiple member selection.Flavor
P
-1

I guess you have to disassemble code for Html.LabelFor(LabelExtensions.LabelFor<TModel,TValue> from System.Web.Mvc assembly).

For example, look at ExpressionHelper.GetExpressionText

As for replacing member name with attribute member value - you'll have to use old fashioned reflection.

Phosphate answered 28/3, 2012 at 22:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.