使用 lambda 表达式和匿名类型,如何获取一个类型的属性名称?

5
我是一名能翻译文本的助手。

我正在尝试使用表达式树和匿名类型来实现以下功能。

假设我有这个类:

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

现在我想要能够调用以下内容:
string[] names = Foo<Person>(x=> new { x.LastName, x.DateOfBirth });

我希望名称包含2个项目,“LastName”和“DateOfBirth”。
我正在尝试以编译时安全的方式扩展PetaPoco,而不是编写字符串SQL,以便我可以指定要包括在SQL中的属性/列列表,而不是选择所有内容。 我有一些非常大的实体,有些情况下出于性能原因,我不想选择所有列。

1
除了作为学习练习外,实现自己的ORM实用程序还有什么特别的原因吗?市面上有很多ORM提供商,通常那些自行开发的ORM会带来比它们所值得的更多的麻烦。 - Lukazoid
我刚刚更新了问题。我实际上正在尝试扩展PetaPoco以支持在SELECT中指定列,而不是数据类中的所有属性。 - Brady Holt
5个回答

4

试试这个:

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

由于你从 lambda 返回的是一个匿名类型,你可以遍历此匿名类型的所有属性,并使用推断出的属性名称。然而,在使用时语法更像是:

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

这是因为第二个泛型参数是一个匿名类型。

谢谢你的回答。我喜欢它简洁明了的风格。 - Brady Holt
@Brady:你最终扩展了Petapoco吗?如果是这样,你为此创建了一个分支吗?我对你的解决方案很感兴趣。 - Don
Foo((Person x) => x.FirstName)返回什么?它返回一个字符串数组,其中包含这些元素:{"Chars", "Length"}。这意味着即使针对单个属性也必须使用匿名类型调用Foo。请看一下我的答案。 - Sнаđошƒаӽ

2
以下答案适用于仅选择单个属性或选择多个属性的情况。其中任何一个都不适用于两种情况。截至撰写本文时,Lukazoid答案仅适用于多个属性,其余答案仅适用于单个属性。
下面的代码考虑了这两种情况,即您可以将其用于选择单个属性和多个属性。请注意,我没有添加任何“合理性检查”,因此请随意添加自己的内容。
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 };
    }        
}

或者更简洁地说,使用三元运算符,它就变成了这个样子:
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 };
}

下载LinkPad文件:LinkPad
在线查看:Repl.it

如有遗漏之处,请随时指出。


2

我比较懒,所以这段代码只处理公共属性。但是它应该是一个很好的起点,让你开始。

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

0
一段代码胜过千言万语,所以这里介绍一下微软在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;
    }
}

如果您想考虑属性,那么会稍微复杂一些,但是接受 Expression<Func<T>> 并找出目标属性的名称的一般思路是相同的。

更新:目前该方法只接受一个参数;我只是提供了一个指导方针。当然,这个思路可以推广到其他情况:

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

这个方法将接受一个表达式,该表达式接受一个 T 并返回一个匿名类型,您可以反射该类型。您可以用第二个类型参数替换 object,但这在这里并没有真正做任何事情,因为您想要做的唯一的事情就是反射类型。


1
这样做只支持一个成员(即 x=>x.LastName)吗? - Brady Holt
你说得对,它只支持单个成员。而且这个单个成员不能是匿名对象。看一下我在 answer 中的回答,它涵盖了单个成员和多个成员选择的情况。 - Sнаđошƒаӽ

-1

我猜你需要反汇编来查看System.Web.Mvc程序集中的Html.LabelFor(LabelExtensions.LabelFor<TModel,TValue>代码。

例如,可以查看ExpressionHelper.GetExpressionText

至于用属性成员值替换成员名称,你需要使用老式的反射技术。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接