0

I am writing an extension method that allows me to do an OrderBy on an IEnumerable list object using a string instead of a Lambda expression. It works okay for simple properties. However, I'm trying to figure out how to allow for nested properties.

If my Models looks like this:

public class Submission
{
    public int SubmissionId {get; set;}
    public string Description {get; set;}
    public int ProjectId {get; set;}
    // Parent object
    public Project ParentProject {get; set;}
}

public class Project
{
    public int ProjectId {get; set;}
    public string FullTitle {get; set;}
}

I can do an OrderBy using this:

public static class MkpExtensions
{
    public static IEnumerable<T> OrderByField<T>(this IEnumerable<T> list, string sortExpression)
    {
        sortExpression += "";
        string[] parts = sortExpression.Split(' ');
        bool descending = false;
        string fullProperty = "";

        if (parts.Length > 0 && parts[0] != "")
        {
            fullProperty = parts[0];

            if (parts.Length > 1)
            {
                descending = parts[1].ToLower().Contains("esc");
            }

             ParameterExpression inputParameter = Expression.Parameter(typeof(T), "p");
            Expression propertyGetter = inputParameter;
            foreach (string propertyPart in fullProperty.Split('.'))
            {
                PropertyInfo prop = propertyGetter.Type.GetProperty(propertyPart);
                if (prop == null)
                    throw new Exception("No property '" + fullProperty + "' in + " + propertyGetter.Type.Name + "'");
                propertyGetter = Expression.Property(propertyGetter, prop);
            }

            // This line was needed
            Expression conversion = Expression.Convert(propertyGetter, typeof(object));
            var getter = Expression.Lambda<Func<T, object>>(propertyGetter, inputParameter).Compile();

            if (descending)
                return list.OrderByDescending(x => prop.GetValue(x, null));
            else
                return list.OrderBy(x => prop.GetValue(x, null));
        }

        return list;
    }
}

And my code would have this:

public List<Submission> SortedSubmissions (bool simple = true) {
    var project1 = new Project { ProjectId = 1, FullTitle = "Our Project"};
    var project2 = new Project { ProjectId = 2, FullTitle = "A Project"};

    List<Submission> listToSort = new List<Submission> 
    {
        new Submission { SubmissionId = 1, Description = "First Submission", 
                        ProjectId = project1.ProjectId, ParentProject = project1 } ,
        new Submission { SubmissionId = 2, Description = "Second Submission", 
                        ProjectId = project1.ProjectId, ParentProject = project1 } ,
        new Submission { SubmissionId = 3, Description = "New Submission", 
                        ProjectId = project2.ProjectId, ParentProject = project2 }
    };

    var simpleField = "Description";
    // This would have the submissions sorted (1, 3, 2)
    var simpleSort = listToSort.OrderByField(simpleField + " asc").ToList();


    // Need to see if I can get this to work
    var nestedField = "Project.FullTitle";
    // This would have the submissions sorted (3, 1, 2)
    return listToSort.OrderByField(nestedField + " asc").ToList();
}

I hope I'm explaining myself clearly. Can this be done?

Update: I used André Kops code and adjusted above, but get this error: System.Nullable'1[System.Int32]' cannot be used for return type 'System.Object'

M Kenyon II
  • 4,136
  • 4
  • 46
  • 94
  • I think it's help if you could give an example of what kind of a string would you like to send and what will be the result. – A. Abramov Jan 06 '16 at 14:40
  • The last piece of code shows how I would send the string "Project.FullTitle" and I would want the result of the list to be sorted by the parent property. – M Kenyon II Jan 06 '16 at 14:42
  • If I understand you correctly, you're trying to access the fields in the string by their name? If so, I dont think there's a method to reflect like that in C#. Instead, I'd set a few conditions (if you know what will the fields look like initially). – A. Abramov Jan 06 '16 at 14:44
  • 1
    Maybe you should have a look at Dynamic Linq for inspiration http://weblogs.asp.net/scottgu/dynamic-linq-part-1-using-the-linq-dynamic-query-library – Mikael Nitell Jan 06 '16 at 14:47
  • What sort of object are you expecting in the input lists? Are they generic, or lists of something you created yourself? – Harris Jan 06 '16 at 14:51
  • Dynamic Linq looks promising, but not sure it's still supported: http://stackoverflow.com/questions/5163147/dynamic-linq-is-there-a-net-4-version – M Kenyon II Jan 06 '16 at 14:58
  • Am I right public Project ParentProject {get; set;} should actually be public Project Project {get; set;} ? – André Kops Jan 06 '16 at 15:45

2 Answers2

2

It's a rather big change in your code, but expression trees are perfect for this:

public static class MkpExtensions
{
    public static IEnumerable<T> OrderByField<T>(this IEnumerable<T> list, string sortExpression)
    {
        sortExpression += "";
        string[] parts = sortExpression.Split(' ');
        bool descending = false;
        string fullProperty = "";

        if (parts.Length > 0 && parts[0] != "")
        {
            fullProperty = parts[0];

            if (parts.Length > 1)
            {
                descending = parts[1].ToLower().Contains("esc");
            }

            ParameterExpression inputParameter = Expression.Parameter(typeof(T), "p");
            Expression propertyGetter = inputParameter;
            foreach (string propertyPart in fullProperty.Split('.'))
            {
                PropertyInfo prop = propertyGetter.Type.GetProperty(propertyPart);
                if (prop == null)
                    throw new Exception("No property '" + fullProperty + "' in + " + propertyGetter.Type.Name + "'");
                propertyGetter = Expression.Property(propertyGetter, prop);
            }

            Expression conversion = Expression.Convert(propertyGetter, typeof(object));
            var getter = Expression.Lambda<Func<T, object>>(conversion, inputParameter).Compile();

            if (descending)
                return list.OrderByDescending(getter);
            else
                return list.OrderBy(getter);
        }

        return list;
    }
}

This example also allows nesting deeper than 2 properties.

And it's probably faster for large lists.

André Kops
  • 2,533
  • 1
  • 10
  • 9
  • That looks great, but... in my actual code, I have some fields that are nullable ints. I get this: 'Additional information: Expression of type 'System.Nullable`1[System.Int32]' cannot be used for return type 'System.Object'' – M Kenyon II Jan 06 '16 at 16:04
  • Found a fix for that issue, and updated your post. Thanks so much! – M Kenyon II Jan 06 '16 at 20:06
  • Strange for that error to only occur for nullable ints. How did you fix it? I don't see any update? – André Kops Jan 07 '16 at 07:24
  • Tried to edit your response to add the fix, but needs review. I added the extra line to my code above. – M Kenyon II Jan 07 '16 at 15:30
  • Yes that's correct, although you need to use the "conversion" in the line below it, not "propertyGetter". I have updated my answer. – André Kops Jan 07 '16 at 16:21
0

How's this?

    public static IEnumerable<T> OrderByField<T>(this IEnumerable<T> list, string sortExpression)
    {
        sortExpression += "";
        string[] parts = sortExpression.Split(' ');
        bool descending = false;
        string fullProperty = "";

        if (parts.Length > 0 && parts[0] != "")
        {
            fullProperty = parts[0];

            if (parts.Length > 1)
            {
                descending = parts[1].ToLower().Contains("esc");
            }

            string fieldName;

            PropertyInfo parentProp = null;
            PropertyInfo prop = null;

            if (fullProperty.Contains("."))
            {
                // A nested property
                var parentProperty = fullProperty.Remove(fullProperty.IndexOf("."));
                fieldName = fullProperty.Substring(fullProperty.IndexOf("."));

                parentProp = typeof(T).GetProperty(parentProperty);
                prop = parentProp.PropertyType.GetProperty(fieldName);
            }
            else
            {
                // A simple property
                prop = typeof(T).GetProperty(fullProperty);
            }

            if (prop == null)
            {
                throw new Exception("No property '" + fullProperty + "' in + " + typeof(T).Name + "'");
            }

            if (parentProp != null)
            {
                if (descending)
                    return list.OrderByDescending(x => prop.GetValue(parentProp.GetValue(x, null), null));
                else
                    return list.OrderBy(x => prop.GetValue(parentProp.GetValue(x, null), null));
            }
            else
            {
                if (descending)
                    return list.OrderByDescending(x => prop.GetValue(x, null));
                else
                    return list.OrderBy(x => prop.GetValue(x, null));
            }
        }

        return list;
    }
Steve Harris
  • 5,014
  • 1
  • 10
  • 25