C# - code to order by a property using the property name as a string

Viewed 62905

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.

10 Answers

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

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

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

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