Jamie Rees
Jamie Rees

Reputation: 8183

Dynamically sort collection navigation property using linq

I am attempting to sort a collection based on a property.

So I do not know the property until runtime that I want to sort on.

The following works on the primary object but not any child objects

var prop = TypeDescriptor.GetProperties(typeof(TvRequests)).Find(sortProperty, tru

if (sortProperty.Contains('.'))
{
    // This is a navigation property currently not supported
    prop = TypeDescriptor.GetProperties(typeof(TvRequests)).Find("Title", true);
}
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
    ? allRequests.OrderBy(x => prop.GetValue(x)).ToList()
    : allRequests.OrderByDescending(x => prop.GetValue(x)).ToList();

So the sortProperty is passed into my method and is a string and can be something like title or date and it works. But if I attempt to access a child property of my TvRequests object it will not work e.g. requestedUser.username

This is a trimmed down version of my objects that I'm referring to in this question:

public class TvRequests
{
    public string Title { get; set; }
    [ForeignKey(nameof(RequestedUserId))]
    public OmbiUser RequestedUser { get; set; }
}

public class OmbiUser
{
    public string Username;
}

My question is how would I be able to access any children properties like the above dynamically?

Upvotes: 1

Views: 601

Answers (2)

levent
levent

Reputation: 3634

you can try something like this..

public static class QueryableExtensions
{
    public enum SortDirection { ASC,DESC}
    static LambdaExpression CreateExpression(Type type, string propertyName)
    {
        var param = Expression.Parameter(type, "x");
        Expression body = param;
        body = propertyName.Split('.')
            .Select(prop => body = Expression.PropertyOrField(body, prop))
            .Last();
        return Expression.Lambda(body, param);
    }
    public static IQueryable<T> SortBy<T>(this IQueryable<T> source,string expressionField,SortDirection sortDirection = SortDirection.ASC)
    {
        var lambdaExpression = CreateExpression(typeof(T), expressionField) as dynamic;
        return sortDirection == SortDirection.ASC ? Queryable.OrderBy(source,lambdaExpression) : Queryable.OrderByDescending(source, lambdaExpression);
    }

}

types

public class TvRequests
{
    public string Title { get; set; }
    public OmbiUser RequestedUser { get; set; }

    public DateTime Date { get; set; }
}

public class OmbiUser
{
    public string Username;
    public DateTime Date { get; set; }
}

using

    List<TvRequests> reqList = new List<TvRequests>();
    reqList.Add(new TvRequests {
        Title = "A",
        Date = DateTime.Now.AddDays(-1),
        RequestedUser = new OmbiUser
        {
            Username = "A",
            Date = DateTime.Now.AddDays(-1)
        }
    });
    reqList.Add(new TvRequests
    {
        Title = "C",
        Date = DateTime.Now.AddDays(1),
        RequestedUser = new OmbiUser
        {
            Username = "C",
            Date = DateTime.Now.AddDays(1)
        }
    });
    reqList.Add(new TvRequests
    {
        Title = "B",
        Date = DateTime.Now,
        RequestedUser = new OmbiUser
        {
            Username = "B",
            Date = DateTime.Now
        }
    });
    foreach (var item in reqList.AsQueryable().SortBy("Date", SortDirection.DESC))
        Debug.WriteLine(item.Title);
    foreach (var item in reqList.AsQueryable().SortBy("RequestedUser.Date"))
        Debug.WriteLine(item.Title);
    foreach (var item in reqList.AsQueryable().SortBy("RequestedUser.UserName",SortDirection.DESC))
        Debug.WriteLine(item.Title);

Upvotes: 1

Tseng
Tseng

Reputation: 64259

Use EF.Property.

// Get the string name of the property here
string propertyName = "Title";
allRequests = sortOrder.Equals("asc", StringComparison.InvariantCultureIgnoreCase)
    ? allRequests.OrderBy(x => EF.Property<string>(x, propertyName)).ToList()
    : allRequests.OrderByDescending(x => EF.Property<string>(x, propertyName)).ToList();

Something along the lines may work (untested, but compiles)

// static for caching & performance 
static private MethodInfo efPropertyGenericMethod = typeof(EF).GetTypeInfo().GetDeclaredMethod("Property");

Expression SortBy<TEntity>(Type type, string propertyName)
{
    var xParam = Expression.Parameter(typeof(TEntity), "x");

    // set T generic type here for EF.Property<T>
    var efPropertyMethod = efPropertyGenericMethod.MakeGenericMethod(type);
    // Creates a Lambda
    Expression lambda = Expression.Lambda(
        // Calls a method. First parameter is null for static calls
        Expression.Call(null,
            efPropertyMethod, // our cosntructed generic Version of EF.Property<T>
            xParam, // Pass the x Parameter
            Expression.Constant(propertyName, typeof(string)) // the propertyName asconstant
        ),
        xParam
    );

    return lambda;
};

To be used as

allRequests.OrderBy(SortBy<TvRequests>(propertyType, propertyName))

Please note that that SortBy isn't called within the lambda. The below would be wrong (there's no x => in the line above).

allRequests.OrderBy(x => SortBy<TvRequests>(propertyType, propertyName))

What it does? SortBy generates an expression tree equivalent of x => EF.Property<T>(x, "MyPropertyName").

Edit:

Updated the method, so x is also passed to EF.Property(x, propertyName)

Upvotes: 1

Related Questions