简体   繁体   中英

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

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.

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.

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.

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

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM