1

I am working with an open-source system and have been given the requirement to sort a set of entities by relevance to a term.

The set needs be ordered with exact matches first, then the "startswith" matches, then the "contains" matches.

The lambda expression I came up with to achieve this is:

var results = products.OrderBy(p => p.Name.StartsWith(searchTerm) ? Convert.ToString((char)0) : p.Name);

At present, the pre-written system works such that it invokes an OrderBy method with an instance of a lambda expression with reflection, in order to sort the results.

To minimise the amount of time spent on this task, I would like to conform to this convention. I've tried to create an instance of a lambda expression and pass it in as a parameter to MethodInfo.Invoke() but because it's got a variable in it, it throws an exception.

The code for the sort method of this system is:

        private IOrderedQueryable<ReferenceProduct> OrderProducts(IQueryable<ReferenceProduct> filteredProducts, String sortExpression, String sortDirection, out String checkedSortExpression, out String checkedSortedDescending)
    {
        ProductListManager.SortExpression correspondingSortExpression = this.SortExpressions.OrderByDescending(se => String.Equals(se.Code, sortExpression, StringComparison.OrdinalIgnoreCase))
                                                                                            .DefaultIfEmpty(new SortExpression<Guid> { Code = String.Empty, Expression = rp => rp.ProductId })
                                                                                            .FirstOrDefault();
        checkedSortExpression = correspondingSortExpression.Code;
        checkedSortedDescending = String.Equals("desc", sortDirection, StringComparison.OrdinalIgnoreCase) ? "desc" : "asc";
        MethodInfo orderMethod = (checkedSortedDescending.Equals("desc", StringComparison.OrdinalIgnoreCase) ? (Queryable.OrderByDescending) : new Func<IQueryable<Object>, Expression<Func<Object, Object>>, IOrderedQueryable<Object>>(Queryable.OrderBy)).Method.GetGenericMethodDefinition().MakeGenericMethod(typeof(ReferenceProduct), correspondingSortExpression.Expression.ReturnType);
        IOrderedQueryable<ReferenceProduct> orderedProducts = orderMethod.Invoke(null, new Object[] { filteredProducts, correspondingSortExpression.Expression }) as IOrderedQueryable<ReferenceProduct>;
        return orderedProducts;
    }

Has anyone got any ideas?

Thanks in advance.

Ryan

Ryan Penfold
  • 752
  • 2
  • 11
  • 16
  • 2
    What is the reason that makes you want (or need) to use reflection? – Marc Gravell Feb 13 '14 at 12:47
  • The open-source system that this is for does it with reflection for other things. It's not clear why it's written that way. I'm reluctant to change it. – Ryan Penfold Feb 13 '14 at 12:52
  • 1
    In your edit, I notice it is using `IQueryable`. There is a **huge** difference between `IEnumerable` and `IQueryable`. If it is `IQueryable`, you shouldn't be using delegates in the first place; you should be using `Expression`s.... can you clarify? – Marc Gravell Feb 13 '14 at 12:56
  • Can you give an example of what `sortExpression` might contain, so I can understand the scenario? Or what a `ProductListManager.SortExpression` is? – Marc Gravell Feb 13 '14 at 12:58
  • Note: I can refer you to this (http://stackoverflow.com/a/233505/23354) for the general approach of dynamic sorting on `IQueryable`. Creating a slightly more complex lambda isn't a huge difficulty, but I simply don't understand in the question where all the pieces are coming from. – Marc Gravell Feb 13 '14 at 12:59
  • sortExpression is a string = "name" and ProductListManager.SortExpression is the lambda. I don't know why delegates are in use there. – Ryan Penfold Feb 13 '14 at 13:00
  • if it is a delegate, on `IQueryable`, it is *already broken*. Is it an `Expression` ? For, say, `"name"`, what would the `correspondingSortExpression` be? What is the lambda? – Marc Gravell Feb 13 '14 at 13:01
  • Also: where is `searchTerm` meant to come from? – Marc Gravell Feb 13 '14 at 13:06
  • searchTerm is a local string variable (in a test class). correspondingSortExpression is an instance of a type that looks like this: `public abstract class SortExpression { public String Code { get; set; } public String Name { get; set; } public virtual LambdaExpression Expression { get; set; } }` – Ryan Penfold Feb 13 '14 at 13:10
  • what is the scope of the test class? this is critical. Meaning: how should `OrderProducts` resolve that instance? – Marc Gravell Feb 13 '14 at 13:23

2 Answers2

3

This is about as short as I can make it, using dynamic to do some of the obscure method resolution (an evil hack, but very worthwhile here - doing it manually adds nothing useful). Rather than rely on the undefined scope of the "test class" that has the searchTerm, I've hoisted that up to the top level - but you may need something different (hence my continuing questions in comments about the scope of searchTerm):

class ReferenceProduct {
    public string Name { get; set; }

    static readonly Dictionary<string, Func<string, LambdaExpression>> knownSortFactories =
    new Dictionary<string, Func<string,LambdaExpression>> {
        {"name", searchTerm =>(Expression<Func<ReferenceProduct, string>>)(p => p.Name.StartsWith(searchTerm) ? Convert.ToString((char)0) : p.Name) }
        // ... more here
    };

    public static IOrderedQueryable<ReferenceProduct> OrderProducts(IQueryable<ReferenceProduct> filteredProducts, string sortExpression, string sortDirection, string queryTerm)
    {
        Func<string, LambdaExpression> factory;
        if (!knownSortFactories.TryGetValue(sortExpression, out factory))
            throw new InvalidOperationException("Unknown sort expression: " + sortExpression);
        dynamic lambda = factory(queryTerm); // evil happens here ;p
        switch(sortDirection)
        {
            case "asc":
                return Queryable.OrderBy(filteredProducts, lambda);
            case "desc":
                return Queryable.OrderByDescending(filteredProducts, lambda);
            default:
                throw new InvalidOperationException("Unknown sort direction: " + sortDirection);
        }
    }
}

With example usage (here using LINQ-to-Objects as a facade):

static void Main()
{
    var source = new[] {
        new ReferenceProduct { Name = "def" },
        new ReferenceProduct { Name = "fooghi" },
        new ReferenceProduct { Name = "abc" }
    }.AsQueryable();
    var sorted = ReferenceProduct.OrderProducts(source, "name", "asc", "foo");
    var arr = sorted.ToArray(); 
    foreach(var item in arr) {
        Console.WriteLine(item.Name);
    }
}

which outputs:

fooghi
abc
def
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
1

What you want to achieve is possible entirely without the reflection invoke call.

Instead of

var orderedProducts = orderMethod.Invoke(null, new Object[] { filteredProducts, correspondingSortExpression.Expression }) as IOrderedQueryable<ReferenceProduct>;

just use the Query Provider:

var orderedProducts = filteresProducts.Provider.CreateQuery<ReferenceProduct>(Expression.Call(null, orderMethod, Expression.Constant(correspondingSortExpression.Expression)));
Georg
  • 5,626
  • 1
  • 23
  • 44
  • By the way, this is also exactly what the OrderBy/OrderByDescending extension methods of IQueryable would do. – Georg Feb 13 '14 at 13:20