Cortright
Cortright

Reputation: 1174

Better way to accomplish sorting a Generic List of IQueryable?

We're using Entity Framework and a "POCO" generator to create our types that get passed around to various layers and they belong to the (changed to protect the innocent) namespace Company.Application.Project.Module. These POCO objects all inherit from a base class that handles some basic stuff for us.

I would like to write a function that can take a collection of these objects and sort them, by property name.

I have written the following function -- which does the gist of what I want to do, but I don't like it for a few reasons:

1) This won't work on any object type, it has to be an object type that the SortHelper class is aware of (hence the last using statement).

2) The "POCO" objects' Type and BaseType seem to be inconsistent -- depending on from where you call this function within the app (unit test project vs. called from a presenter object of our MVP app) which causes problems with the line I've bolded, because if it grabs the wrong Type, the Property won't be on it, in the following line.

From the presenter object, the .GetType is showing up as: ClassName_96D74E07A154AE7BDD32624F3B5D38E7F50333608A89B561218F854513E3B746 ...within the System.Data.Entity.DynamicProxies namespace.

This is why the code says .GetType().BaseType on that line, which gives me: ClassName ...within Company.Application.Project.Module

But within the unit tests, the .GetType() is showing up as ClassName in Company.Application.Project.Module

and BaseType is showing up as BaseClass in Company.Application.Project.Module

...which makes more sense, but I don't understand the inconsistency -- and the inconsistency scares me.

3) Hate using Reflection to do this in general.

If anyone has a better way of doing this, or even a fix to make the Reflection behave with the namespaces/types -- I would certainly appreciate it!

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using Company.Application.Project.Module;

namespace Company.Application.Project
{
    public static class SortHelper
    {
        public static IOrderedQueryable<T> Sort<T>(this IQueryable<T> source, string propertyName, bool descending)
        {

            // bail out if there's nothing in the list
            if (source == null)
            {
                return null;
            }
            if (source.Count() == 0)
            {
                return source as IOrderedQueryable<T>;
            }

            // get the type -- or should it be the BaseType?  Nobody knows!
            Type sourceType = source.First().GetType().BaseType;

            // this works fine _assuming_ we got the correct type on the line above
            PropertyInfo property = sourceType.GetProperty(propertyName);
            if (descending)
            {
                return source.OrderByDescending(e => property.GetValue(e, null));
            }
            else
            {
                return source.OrderBy(e => property.GetValue(e, null));
            }

        }
    }
}

Upvotes: 3

Views: 3423

Answers (3)

CptRobby
CptRobby

Reputation: 1521

It seems to me that there's no good reason to do this at all. You're using the OrderBy extension method in your function, why not just call that instead of this function? Unless there's something that isn't shown in your example, the function isn't doing anything other than figuring out how to call OrderBy (or OrderByDescending) and then calling it.

If it's just that you want to use a boolean flag to switch between ascending and descending order (without having to use an if statement every place you are trying to sort something), then I would strongly suggest the following method:

public static IOrderedQueryable<TSource> Sort<TSource, TKey>(this IQueryable<TSource> source, Expression<Func<TSource, TKey>> keySelector, bool descending = false)
{
  if (descending) return source.OrderByDescending(keySelector);
  else return source.OrderBy(keySelector);
}

It's neat and clean, you still get to use a lambda expression to define the key without any requirement for a dependency to your model, you get to (optionally) change the sort order to descending using a boolean flag, and NO REFLECTION. That's a WIN WIN WIN! ;)

[Begin edits in response to comments]

OK, so now that I've got VS2010 running again, I was able to figure out a way to encapsulate the SortExpression used when sorting an ASP.NET GridView control, as promised. So here it is.

Since we don't have a model defined for this question, I'm just going to create a simple one.

public class Customer
{
  public int CustomerID { get; set; }
  public string Name { get; set; }
  public virtual List<Order> Orders { get; set; }
}

public class Order
{
  public int OrderID { get; set; }
  public DateTime OrderDate { get; set; }
  public decimal TotalPurchaseAmount { get; set; }
  public string Comments { get; set; }
  public bool Shipped { get; set; }
  public virtual Customer Customer { get; set; }
}

So then let's assume we have a CustomerDetails.aspx page that we want to add a GridView to that would list out the Orders.

<asp:GridView ID="gvOrders" AutoGenerateColumns="false" runat="server"
  AllowSorting="true" OnSorting="gvOrders_Sorting">
  <Columns>
    <asp:BoundField DataField="OrderID" SortExpression="OrderID" HeaderText="Order ID" />
    <asp:BoundField DataField="OrderDate" SortExpression="OrderDate" HeaderText="Order Date" />
    <asp:BoundField DataField="TotalPurchaseAmount" SortExpression="TotalPurchaseAmount" HeaderText="Total Purchase Amount" />
    <asp:BoundField DataField="Comments" SortExpression="Comments" HeaderText="Comments" />
    <asp:BoundField DataField="Shipped" SortExpression="Shipped" HeaderText="Shipped" />
  </Columns>
</asp:GridView>

In the code behind, there is a static dictionary object:

protected static readonly Dictionary<string, Expression<Func<Order, object>>> sortKeys = new Dictionary<string,Expression<Func<Order,object>>>()
{
  { "OrderID", x => x.OrderID },
  { "OrderDate", x => x.OrderDate },
  { "TotalPurchaseAmount", x => x.TotalPurchaseAmount },
  { "Comments", x => x.Comments },
  { "Shipped", x => x.Shipped }
};

And then here's the function that handles sorting:

protected void gvOrders_Sorting(object sender, GridViewSortEventArgs e)
{
  string exp = ViewState["gvOrders_SortExp"] as string;
  if (exp == null || exp != e.SortExpression)
  {
    e.SortDirection = SortDirection.Ascending;
    ViewState["gvOrders_SortExp"] = e.SortExpression;
    ViewState["gvOrders_SortDir"] = "asc";
  }
  else
  {
    string dir = ViewState["gvOrders_SortDir"] as string;
    if (dir == null || dir == "desc")
    {
      e.SortDirection = SortDirection.Ascending;
      ViewState["gvOrders_SortDir"] = "asc";
    }
    else
    {
      e.SortDirection = SortDirection.Descending;
      ViewState["gvOrders_SortDir"] = "desc";
    }
  }
  if (e.SortDirection == SortDirection.Ascending)
  // There's a MyCustomer property on the page, which is used to get to the Orders
  { gvOrders.DataSource = MyCustomer.Orders.OrderBy(sortKeys[e.SortExpression]); }
  else
  { gvOrders.DataSource = MyCustomer.Orders.OrderByDescending(sortKeys[e.SortExpression]); }
  gvOrders.DataBind();
}

There, that does it.

I admit that the gvOrders_Sorting method is really messy, but that's the nature of using custom sorting with the GridView. It could be further encapsulated, but I decided to keep it simple. (I actually started working on creating a helper class that would handle sorting and paging, but custom paging in .NET 4.0 is an even worse beast than custom sorting is!)

Hope you like it. ;)

[Correction]

I just noticed an issue with the code as written. MyCustomer.Orders would be a List<Order> not an IQueryable<Order>. So either change the sortKeys to be a Dictionary<string, Func<Order, object>>, or change the call to MyCustomer.Orders.AsQueryable().OrderBy(sortKeys[e.SortExpression]) or to MyDbContext.Orders.Where(o => o.Customer == MyCustomer).OrderBy(sortKeys[e.SortExpression]). I leave the choice to you. ;)

Upvotes: 3

Keith Payne
Keith Payne

Reputation: 3082

Just project the property to be sorted:

 public static IOrderedQueryable<T> Sort<T, U>(this IQueryable<T> source, Func<T, U> sortExpr, bool descending)

...


source.OrderBy(e => sortExpr(e));

Upvotes: 1

Moo-Juice
Moo-Juice

Reputation: 38825

If they all inherit from a base-class, why not constrain the generic argument?

public static IOrderedQueryable<T> Sort<T>(this IQueryable<T> source, string propertyName, bool descending) where T : MyBase

This way, the compiler won't let you pass a class to this function that does not inherit from MyBase, and you're out of the "nobody knows?!" territory :)

Upvotes: 2

Related Questions