C#-使用属性名称作为字符串按属性排序的代码


92

当属性名称为字符串时,对C#中的属性进行编码的最简单方法是什么?例如,我想允许用户通过他们选择的属性(使用LINQ)对某些搜索结果进行排序。他们将在UI中选择“ order by”属性-当然是字符串值。有没有一种方法可以直接使用该字符串作为linq查询的属性,而不必使用条件逻辑(如果/否则,切换)将字符串映射到属性。反射?

从逻辑上讲,这就是我想要做的:

query = query.OrderBy(x => x."ProductId");

更新:我最初没有指定我正在使用Linq to Entities-看来反射(至少是GetProperty,GetValue方法)不能转换为L2E。


我认为您必须使用反射,而且我不确定您是否可以在lambda表达式中使用反射...好吧,几乎可以肯定,Linq to SQL中没有,但是可能在对列表或其他东西使用Linq时。
CodeRedick

@Telos:没有理由不能在lambda中使用反射(或任何其他API)。如果将代码作为表达式求值并转换为其他形式(如您建议的LINQ-to-SQL),它是否会起作用完全是另一个问题。
亚当·罗宾逊

这就是为什么我发表评论而不是回答的原因。;)主要用于Linq2SQL ...
CodeRedick

1
只是必须克服相同的问题..请参阅下面的答案。stackoverflow.com/a/21936366/775114
马克·鲍威尔

Answers:


129

我将提供其他所有人已发布内容的替代方法。

System.Reflection.PropertyInfo prop = typeof(YourType).GetProperty("PropertyName");

query = query.OrderBy(x => prop.GetValue(x, null));

这样可以避免重复调用反射API以获取该属性。现在唯一的重复调用就是获取值。

然而

我主张使用aPropertyDescriptor代替,因为这将允许将customTypeDescriptor分配给您的类型,从而可以使用轻量级的操作来检索属性和值。在缺少自定义描述符的情况下,无论如何它都会回落到反射上。

PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(YourType)).Find("PropertyName");

query = query.OrderBy(x => prop.GetValue(x));

至于加快速度,请HyperDescriptor在CodeProject上查看Marc Gravel的项目。我已经成功地使用了它。它是针对业务对象进行高性能数据绑定和动态属性操作的救生器。


注意,反射调用(即GetValue)是反射最昂贵的部分。实际上,元数据检索(即GetProperty)的成本较低(一个数量级),因此通过缓存该部分,您实际上并没有节省太多。两种方法的成本几乎相同,而且成本将很高。只是要注意一点。
jrista

1
@jrista:可以肯定,调用是最昂贵的。但是,“成本更低”并不意味着“免费”,甚至与其接近。元数据检索花费了很短的时间,因此缓存它有一个优点,而没有缺点(除非我在这里遗漏了一些东西)。实际上,PropertyDescriptor无论如何,这实际上都应该使用(以考虑自定义类型描述符,这可以使值检索变得轻量级操作)。
亚当·罗宾逊

搜索了几个小时,以查找类似的内容以编程方式处理ASP.NET GridView:PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(ScholarshipRequest))。Find(e.SortExpression,true);
2014年

1
stackoverflow.com/questions/61635636/… 反射存在问题,在EfCore 3.1.3中无法解决。似乎在EfCore 2中引发了错误,需要针对警告进行激活。在下面使用@Mark的答案
armourshield

1
我收到以下信息:InvalidOperationException:LINQ表达式'DbSet <MyObject> .Where(t => t.IsMasterData).OrderBy(t => t.GetType()。GetProperty(“ Address”)。GetValue(obj:t, index:null).GetType())无法翻译。以一种可以翻译的形式重写查询,或者通过插入对AsEnumerable(),AsAsyncEnumerable(),ToList()或ToListAsync()的调用来显式切换到客户端评估。
bbrinck

67

我参加聚会有点晚了,但是,我希望这可以有所帮助。

使用反射的问题在于,几乎可以肯定,除了内部.Net提供程序以外,任何Linq提供程序都不会支持生成的表达式树。这对于内部集合很好,但是在分页之前要在源(例如SQL,MongoDb等)上进行排序的地方将无法使用。

下面的代码示例为OrderBy和OrderByDescending提供了IQueryable扩展方法,可以像这样使用:

query = query.OrderBy("ProductId");

扩展方式:

public static class IQueryableExtensions 
{
    public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderBy(ToLambda<T>(propertyName));
    }

    public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string propertyName)
    {
        return source.OrderByDescending(ToLambda<T>(propertyName));
    }

    private static Expression<Func<T, object>> ToLambda<T>(string propertyName)
    {
        var parameter = Expression.Parameter(typeof(T));
        var property = Expression.Property(parameter, propertyName);
        var propAsObject = Expression.Convert(property, typeof(object));

        return Expression.Lambda<Func<T, object>>(propAsObject, parameter);            
    }
}

问候,马克。


出色的解决方案-我一直在寻找。我真的需要深入研究Expression树。还是很菜鸟。@Mark,做嵌套表达式的解决方案吗?假设我有一个T类型,其TSub类型的属性“ Sub”本身具有一个“ Value”属性。现在,我想为字符串“ Sub.Value”获取表达式Expression <Func <T,object >>。
西蒙·舒勒

4
为什么我们需要将Expression.Convert转换propertyobject?我遇到Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.错误,将其删除似乎可行。
ShuberFu

@Demodave,如果我没记错的话。var propAsObject = Expression.Convert(property, typeof(object));property代替propAsObject
ShuberFu

金。适用于.Net Core 2.0.5。
克里斯·阿梅林克斯

2
得到了错误LINQ to Entities only supports casting EDM primitive or enumeration types
MateuszPuwałowski18年

35

我喜欢@Mark Powell的回答,但正如@ShuberFu所说,它给出了错误LINQ to Entities only supports casting EDM primitive or enumeration types

删除var propAsObject = Expression.Convert(property, typeof(object));不适用于值类型的属性(例如整数),因为它不会将int隐式装到对象中。

通过使用Kristofer AnderssonMarc Gravell的Ideas,我找到了一种使用属性名称构造Queryable函数并使它仍可与Entity Framework一起使用的方法。我还包括一个可选的IComparer参数。警告: IComparer参数不适用于Entity Framework,如果使用Linq to Sql,则应将其忽略。

以下与Entity Framework和Linq to Sql一起使用:

query = query.OrderBy("ProductId");

@Simon Scheurer这也适用:

query = query.OrderBy("ProductCategory.CategoryId");

而且,如果您没有使用Entity Framework或Linq to Sql,则可以这样做:

query = query.OrderBy("ProductCategory", comparer);

这是代码:

public static class IQueryableExtensions 
{    
public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderBy", propertyName, comparer);
}

public static IOrderedQueryable<T> OrderByDescending<T>(this IQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "OrderByDescending", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenBy<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenBy", propertyName, comparer);
}

public static IOrderedQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> query, string propertyName, IComparer<object> comparer = null)
{
    return CallOrderedQueryable(query, "ThenByDescending", propertyName, comparer);
}

/// <summary>
/// Builds the Queryable functions using a TSource property name.
/// </summary>
public static IOrderedQueryable<T> CallOrderedQueryable<T>(this IQueryable<T> query, string methodName, string propertyName,
        IComparer<object> comparer = null)
{
    var param = Expression.Parameter(typeof(T), "x");

    var body = propertyName.Split('.').Aggregate<string, Expression>(param, Expression.PropertyOrField);

    return comparer != null
        ? (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param),
                Expression.Constant(comparer)
            )
        )
        : (IOrderedQueryable<T>)query.Provider.CreateQuery(
            Expression.Call(
                typeof(Queryable),
                methodName,
                new[] { typeof(T), body.Type },
                query.Expression,
                Expression.Lambda(body, param)
            )
        );
}
}

哎呀,老兄,你是微软吗?:)这个Aggregate片段太棒了!Join由于使用了“ T.Property”之类的属性,因此它可以处理使用EF Core模型创建的虚拟视图。否则,之后的订购Join将不可能产生InvalidOperationExceptionNullReferenceException。而且我确实需要对AFTER进行排序Join,因为大多数查询是恒定的,而视图中的顺序则不是。
哈利

哈里 谢谢,但我真的不能为这个Aggregate片段付出太多的功劳。我相信这是Marc Gravell的代码和智能建议的结合。:)
David Specht

@DavidSpecht我正在学习表达式树,所以关于它们的一切现在对我来说都是黑魔法。但是我很快学到了,VS中的C#交互式窗口很有帮助。
哈里

如何使用这个?
Dat Nguyen

@Dat阮相反的products.OrderBy(x => x.ProductId),你可以使用products.OrderBy("ProductId")
大卫的Specht

12

是的,我认为反射是别的方法。

例:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

我收到错误"LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression.",请问任何想法或建议?
弗洛林·维尔多

5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

试图回想起确切的语法,但我认为这是正确的。


2

反思就是答案!

typeof(YourType).GetProperty("ProductId").GetValue(theInstance);

您可以做很多事情来缓存反映的PropertyInfo,检查错误的字符串,编写查询比较函数等,但是从本质上讲,这就是您要做的。



2

比对动态订单商品的反射扩展更具生产力:

public static class DynamicExtentions
{
    public static object GetPropertyDynamic<Tobj>(this Tobj self, string propertyName) where Tobj : class
    {
        var param = Expression.Parameter(typeof(Tobj), "value");
        var getter = Expression.Property(param, propertyName);
        var boxer = Expression.TypeAs(getter, typeof(object));
        var getPropValue = Expression.Lambda<Func<Tobj, object>>(boxer, param).Compile();            
        return getPropValue(self);
    }
}

例:

var ordered = items.OrderBy(x => x.GetPropertyDynamic("ProductId"));

另外,您可能需要缓存符合标准的lambas(例如,在Dictionary <>中)


1

此外动态表达式可以解决这个问题。您可以通过LINQ表达式使用基于字符串的查询,这些查询可以在运行时动态构建。

var query = query
          .Where("Category.CategoryName == @0 and Orders.Count >= @1", "Book", 10)
          .OrderBy("ProductId")
          .Select("new(ProductName as Name, Price)");

0

我认为我们可以使用功能强大的工具名称Expression,在这种情况下,可以将其用作扩展方法,如下所示:

public static IOrderedQueryable<T> OrderBy<T>(this IQueryable<T> source, string ordering, bool descending)
{
    var type = typeof(T);
    var property = type.GetProperty(ordering);
    var parameter = Expression.Parameter(type, "p");
    var propertyAccess = Expression.MakeMemberAccess(parameter, property);
    var orderByExp = Expression.Lambda(propertyAccess, parameter);
    MethodCallExpression resultExp = 
        Expression.Call(typeof(Queryable), (descending ? "OrderByDescending" : "OrderBy"), 
            new Type[] { type, property.PropertyType }, source.Expression, Expression.Quote(orderByExp));
    return (IOrderedQueryable<T>)source.Provider.CreateQuery<T>(resultExp);
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.