95

What's the simplest way to code against a property in C# when I have the property name as a string? For example, I want to allow the user to order some search results by a property of their choice (using LINQ). They will choose the "order by" property in the UI - as a string value of course. Is there a way to use that string directly as a property of the linq query, without having to use conditional logic (if/else, switch) to map the strings to properties. Reflection?

Logically, this is what I'd like to do:

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

Update: I did not originally specify that I'm using Linq to Entities - it appears that reflection (at least the GetProperty, GetValue approach) does not translate to L2E.

Nguyễn Văn Phong
  • 11,572
  • 15
  • 21
  • 43
Jeremy
  • 8,073
  • 17
  • 52
  • 68
  • I think you'd have to use reflection, and I'm not sure you can use reflection in a lambda expression... well, almost certainly not in Linq to SQL but maybe when using Linq against a list or something. – CodeRedick Nov 06 '09 at 17:49
  • @Telos: There's no reason that you can't use reflection (or any other API) in a lambda. Whether or not it will work if the code gets evaluated as an expression and translated into something else (like LINQ-to-SQL, as you suggest) is another question entirely. – Adam Robinson Nov 06 '09 at 17:56
  • This is why I posted a comment instead of an answer. ;) Mostly used to Linq2SQL... – CodeRedick Nov 06 '09 at 18:04
  • 1
    Just had to overcome the same problem.. see my answer below. http://stackoverflow.com/a/21936366/775114 – Mark Powell Feb 21 '14 at 14:25

11 Answers11

137

I would offer this alternative to what everyone else has posted.

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

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

This avoids repeated calls to the reflection API for obtaining the property. Now the only repeated call is obtaining the value.

However

I would advocate using a PropertyDescriptor instead, as this will allow for custom TypeDescriptors to be assigned to your type, making it possible to have lightweight operations for retrieving properties and values. In the absence of a custom descriptor it will fall back to reflection anyhow.

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

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

As for speeding it up, check out Marc Gravel's HyperDescriptor project on CodeProject. I've used this with great success; it's a life saver for high-performance data binding and dynamic property operations on business objects.

Adam Robinson
  • 171,726
  • 31
  • 271
  • 330
  • Note that reflected invocation (i.e. GetValue) is the most costly part of reflection. Metadata retrieval (i.e. GetProperty) is actually less costly (by an order of magnitude), so by caching that part, you are not really saving yourself that much. This is going to cost pretty much the same either way, and that cost is going to be heavy. Just something to note. – jrista Nov 06 '09 at 18:42
  • 1
    @jrista: invocation is the most costly, to be certain. However, "less costly" does not mean "free", or even close to it. Metadata retrieval takes a non-trivial amount of time, so there is an advantage to caching it and no *disadvantage* (unless I'm missing something here). In truth this should really be using a `PropertyDescriptor` anyway (to account for custom type descriptors, which *could* make value retrieval a lightweight operation). – Adam Robinson Nov 06 '09 at 18:46
  • Searched for hours for something like this to handle sorting a ASP.NET GridView programmatically: PropertyDescriptor prop = TypeDescriptor.GetProperties(typeof(ScholarshipRequest)).Find(e.SortExpression, true); – Baxter Mar 19 '14 at 16:22
  • 1
    https://stackoverflow.com/questions/61635636/efcore-3-1-3-throwing-execption-for-orderby?noredirect=1#comment109026507_61635636 Had a problem with reflection it didn't work out in EfCore 3.1.3. It seems to throw error in EfCore 2 which needs to be activated for the warnings. Use the answer of @Mark below – armourshield May 06 '20 at 13:39
  • 1
    I receive the following: InvalidOperationException: The LINQ expression 'DbSet .Where(t => t.IsMasterData) .OrderBy(t => t.GetType().GetProperty("Address").GetValue( obj: t, index: null).GetType())' could not be translated. Either rewrite the query in a form that can be translated, or switch to client evaluation explicitly by inserting a call to either AsEnumerable(), AsAsyncEnumerable(), ToList(), or ToListAsync(). – bbrinck May 12 '20 at 13:56
  • @AdamRobinson In both cases, i receive the error `"LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."` Any thoughts or advices, please? – Florin Vîrdol Nov 06 '20 at 08:13
75

I'm a little late to the party, however, I hope this can be of some help.

The problem with using reflection is that the resulting Expression Tree will almost certainly not be supported by any Linq providers other than the internal .Net provider. This is fine for internal collections, however this will not work where the sorting is to be done at source (be that SQL, MongoDb, etc.) prior to pagination.

The code sample below provides IQueryable extention methods for OrderBy and OrderByDescending, and can be used like so:

query = query.OrderBy("ProductId");

Extension Method:

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

Regards, Mark.

cederlof
  • 6,530
  • 4
  • 39
  • 59
Mark Powell
  • 3,061
  • 1
  • 15
  • 11
  • Excellent solution - I was looking for exactly that. I really need to dig into the Expression trees. Still very rookie at that. @Mark, any solution to do nested expressions? Say I have a type T with a property "Sub" of type TSub that itself has a property "Value". Now I'd like to get the expression Expression> for the string "Sub.Value". – Simon Scheurer Jul 27 '16 at 06:11
  • 5
    Why do we need the `Expression.Convert` to convert `property` to `object`? I'm getting a `Unable to cast the type 'System.String' to type 'System.Object'. LINQ to Entities only supports casting EDM primitive or enumeration types.` error, and removing it seems to work. – ShuberFu Dec 12 '16 at 19:07
  • @Demodave if i remember correctly. `var propAsObject = Expression.Convert(property, typeof(object));` and just use `property` in place of `propAsObject` – ShuberFu Jul 02 '17 at 04:16
  • 1
    Gold. Adapted for a .Net Core 2.0.5. – Chris Amelinckx Feb 08 '18 at 05:39
  • 2
    Got error `LINQ to Entities only supports casting EDM primitive or enumeration types` – Mateusz Puwałowski Jul 09 '18 at 08:47
  • This solution is great because, if you want to order objects in a LINQ to objects query, just call one of the `OrderBy` methods, compile it, store the resulting delegate in a variable, and use it in your LINQ query. – oscilatingcretin Jan 17 '19 at 13:51
  • This is a much cleaner solution! Thank you! – Marisol Gutiérrez Sep 20 '19 at 20:47
  • @SimonScheurer did you ever find an answer to your case? – Simopaa Nov 18 '20 at 13:09
41

I liked the answer from @Mark Powell, but as @ShuberFu said, it gives the error LINQ to Entities only supports casting EDM primitive or enumeration types.

Removing var propAsObject = Expression.Convert(property, typeof(object)); didn't work with properties that were value types, such as integer, as it wouldn't implicitly box the int to object.

Using Ideas from Kristofer Andersson and Marc Gravell I found a way to construct the Queryable function using the property name and have it still work with Entity Framework. I also included an optional IComparer parameter. Caution: The IComparer parameter does not work with Entity Framework and should be left out if using Linq to Sql.

The following works with Entity Framework and Linq to Sql:

query = query.OrderBy("ProductId");

And @Simon Scheurer this also works:

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

And if you are not using Entity Framework or Linq to Sql, this works:

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

Here is the code:

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)
            )
        );
}
}
David Specht
  • 4,196
  • 1
  • 16
  • 22
  • 1
    Geez, man, are you Microsoft? :) That `Aggregate` fragment is awesome! It takes care of virtual views created from EF Core model with `Join`, since I use properties like "T.Property". Otherwise ordering after `Join` would be impossible producing either `InvalidOperationException` or `NullReferenceException`. And I do need to order AFTER `Join`, because most queries are constant, the orders in views are not. – Harry Oct 01 '19 at 12:11
  • @DavidSpecht I'm just learning Expression Trees, so everything about them is now black magic to me yet. But I learn quickly, C# interactive window in VS helps a lot. – Harry Oct 02 '19 at 07:57
  • how to use this? – Dat Nguyen Dec 28 '19 at 06:35
  • 1
    @Dat Nguyen Instead of `products.OrderBy(x => x.ProductId)`, you could use `products.OrderBy("ProductId")` – David Specht Dec 28 '19 at 15:46
13

Yes, I don't think there's another way than Reflection.

Example:

query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));
Alon Gubkin
  • 53,054
  • 52
  • 181
  • 282
  • 1
    I receive the error `"LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method, and this method cannot be translated into a store expression."` Any thoughts or advices, please? – Florin Vîrdol Nov 06 '20 at 08:22
  • 1
    You can check my [answer below](https://stackoverflow.com/a/67630860/9071943) to know the reason why @FlorinVîrdol – Nguyễn Văn Phong May 21 '21 at 06:04
5
query = query.OrderBy(x => x.GetType().GetProperty("ProductId").GetValue(x, null));

Trying to recall exact syntax off the top of my head but I think that is correct.

dkackman
  • 14,361
  • 11
  • 63
  • 118
2

Reflection is the answer!

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

There's lots of things you can do to cache the reflected PropertyInfo, check for bad strings, write your query comparison function, etc., but at its heart, this is what you do.

Sebastian Good
  • 6,091
  • 2
  • 28
  • 54
2

You can use dynamic Linq - check out this blog.

Also check out this StackOverFlow post...

Community
  • 1
  • 1
2

More productive than reflection extension to dynamic order items:

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

Example:

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

Also you may need to cache complied lambas(e.g. in Dictionary<>)

gdbdable
  • 4,065
  • 3
  • 27
  • 42
1

Also Dynamic Expressions can solve this problem. You can use string-based queries through LINQ expressions that could have been dynamically constructed at run-time.

var query = query
          .Where("Category.CategoryName == @0 and Orders.Count >= @1", "Book", 10)
          .OrderBy("ProductId")
          .Select("new(ProductName as Name, Price)");
ali-myousefi
  • 603
  • 2
  • 8
  • 22
1

Warning ⚠

You just can use Reflection in case that data is in-memory. Otherwise, you will see some error like below when you work with Linq-2-EF, Linq-2-SQL, etc.

@Florin Vîrdol's comment

LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object)' method and this method cannot be translated into a store expression.

Why ⁉

Because when you write code to provide a query to Linq query provider. It is first translated into an SQL statement and then executed on the database server.

(See image below, from https://www.tutorialsteacher.com/linq/linq-expression)

enter image description here

Solution ✅

By using Expression tree, you can write a generic method like this

public static IEnumerable<T> OrderDynamic<T>(IEnumerable<T> Data, string propToOrder)
{
    var param = Expression.Parameter(typeof(T));
    var memberAccess = Expression.Property(param, propToOrder);        
    var convertedMemberAccess = Expression.Convert(memberAccess, typeof(object));
    var orderPredicate = Expression.Lambda<Func<T, object>>(convertedMemberAccess, param);

    return Data.AsQueryable().OrderBy(orderPredicate).ToArray();
}

And use it like this

var result = OrderDynamic<Student>(yourQuery, "StudentName"); // string property

or

var result = OrderDynamic<Student>(yourQuery, "Age");  // int property

And it's also working with in-memory by converting your data into IQueryable<TElement> in your generic method return statement like this

return Data.AsQueryable().OrderBy(orderPredicate).ToArray();

See the image below to know more in-depth.

enter image description here

Demo on dotnetfiddle

Nguyễn Văn Phong
  • 11,572
  • 15
  • 21
  • 43
0

I think we can use a powerful tool name Expression an in this case use it as an extension method as follows:

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);
}
Abolfazl
  • 1,591
  • 8
  • 21