3

I am trying to build a generic query mechanism to access my repository. I wish to use Lambda expressions to filter and sort the query. I am struggling to find a way to pass a list of generic Lambda expressions in, specifically for the order-by, and would appreciate help in doing so.

EDIT: 2 requirements I am trying to meet is, not expose IQueryable beyond the repository, but still be able to carry out some filtering and sorting at database level.

To better illustrate this let me show you the code

public class Query<T>
{
    public class OrderBy<T>
    {
        public Expression<Func<T, **int**>> Clause { set; get; } // Order By clause
        public bool Descending = true;
    }

    public Expression<Func<T, bool>> Where { set; get; } // Where clause
    public IList<OrderBy<T>> OrderBys { set; get; } // Where clause

    public Query()
    {
        OrderBys = new List<OrderBy<T>>();
    }
}    

public IEnumerable<Person> FindBy(Query<Person> query)
{
    IQueryable<Person> Temp = GetObjectSet();
    if (query.Where != null) 
        Temp = Temp.Where(query.Where);
    foreach (var OrderByThis in query.OrderBys)
    {
        if (OrderByThis.Descending) 
            Temp = Temp.OrderByDescending(OrderByThis.Clause);
        else 
            Temp = Temp.OrderBy(OrderByThis.Clause);
    }

    return Temp.ToList<Person>();
}

This is all very nice, BUT Expression< Func< T, int>> is not generic. I need to be able to do something like:

Query<Person> query = new Query<Person>();
Query<Person>.OrderBy<Person> clause1 = new Query<Person>.OrderBy<Person>();
clause1.Clause = m => m.Username;
Query<Person>.OrderBy<Person> clause2 = new Query<Person>.OrderBy<Person>();
clause2.Clause = m => m.DateOfBirth;
query.OrderBys.Add(clause1);
query.OrderBys.Add(clause2);

i.e. adding a number of different fields of different types.

I imagine there must be a way to store these as generic Lambda functions, and then in the repository convert then to the strongly typed Lambda function it needs.

How can I do this?

Hakan Fıstık
  • 16,800
  • 14
  • 110
  • 131
Dale K
  • 25,246
  • 15
  • 42
  • 71
  • .NET already provides mechanisms for ordering and sorting, with extension methods like `.OrderBy` and `.Where` and a much cleaner syntax. Why not just expose something like `repository.Get(Func,IEnumerable queryExpression)`? (or make it `Expression – Jay Aug 16 '12 at 01:31
  • I don't quite follow sorry, can give a concrete example? – Dale K Aug 16 '12 at 01:48
  • Can you use `object` as your type? – Gabe Aug 16 '12 at 01:49
  • From what I have read object causes issues with both not working with value types, and not sorting correctly according to the specific object type. – Dale K Aug 16 '12 at 01:52
  • @DaleBurrell, you can resolve sorting issues by passing in an `IComparable` – smartcaveman Aug 16 '12 at 03:44

2 Answers2

1

Elaborating on my comment, I don't see why you need to construct your own intermediate query object out of Expressions and then reconstruct Expressions from that intermediate object, when you could just skip that translation altogether.

Given your example query:

repository.FindBy(people => people.OrderBy(p => p.Username).ThenBy(p => p.DateOfBirth));

Take note that you can still build up the queries incrementally, if it is being done based on user selections, for example. The following query is equivalent to the above:

Func<IEnumerable<Person>, IEnumerable<Person>> query = people => people.OrderBy(p => p.Username);
 query = query.ThenBy(p => p.DateOfBirth);

I understand that you don't want to expose IQueryable beyond the repository, but you can still use LINQ with a signature such as:

public IEnumerable<Person> FindBy(Func<IEnumerable<Person>, IEnumerable<Person>> query)
{
    return query(GetObjectSet()).ToList();
}

Speaking to your actual question, however, you can achieve your OrderBy task by using Expression<Func<T, object>> for the Clause property type, or if that unsettles you, you could constrain it a bit more by using IComparable instead of object, as it is really all you need for ordering, and strings and numeric types all implement it.

Jay
  • 56,361
  • 10
  • 99
  • 123
  • I just tried your solution, and realised I had forgotten to mention 2 requirements being to not expose IQueryable beyond the repository, but still be able to carry out some filtering and sorting at database level. Your solution doesn't make it to the database using Entity Framework, I assume because its using IEnumerable? – Dale K Aug 16 '12 at 02:26
  • @DaleBurrell Okay, I added the EF tag to the question. Did you try the 2nd option (`FindBy(Func – Jay Aug 16 '12 at 02:31
  • Thanks Jay, yes I was using the second option, and in query profiler it didn't pass any filter/sort information. So that must have been applied in code. Using IComarable compiles nicely! But the code itself doesn't work - which could be the subject for another question unless you have any thoughts on that? – Dale K Aug 16 '12 at 02:48
  • @DaleBurrell If the sorting is not being passed to EF, I suspect that `GetObjectSet()` is actually executing to select all items. I would try replacing it with the entity set reference. – Jay Aug 16 '12 at 02:52
  • GetObjectSet() is the entity set reference e.g. returns DataContextFactory.GetDataContext().People, and stepping through the code the query doesn't happen until I call ToList() which is after having added sort items as per my example code. Its rather strange. – Dale K Aug 16 '12 at 02:55
  • @DaleBurrell It is possible that because the expressions are not explicitly typed (you have `IComparable` instead of `int`), EF is not able to translate them to SQL and is performing the sort in-memory as a result. As a debugging exercise, I would add a lambda expression in the mix (e.g. a sort not taken from your query object) and see if that one executes in the database. – Jay Aug 16 '12 at 03:15
  • The IComparable solution i.e. my original code using IComparable isn't applying the sort/filter EVER! Its not only not performing it in the database, its not performing it in memory either. – Dale K Aug 16 '12 at 03:22
  • OK, found that, I forgot that I need to assign e.g. Temp = Temp.Where() when not chaining. However it turns out that IComparable doesn't work for DateTime for example with ET, so that out. – Dale K Aug 16 '12 at 04:00
1

As I noted in my answer to your other question, I would discourage this approach in general. It makes more sense just to expose IQueryable<T>/IOrderedQueryable<T>.

That being said, there is a solution along the lines of your intention available in the selected answer to How to pass multiple Expressions to OrderBy for EF? .

It allows you to use a syntax like:

var query = context.Users ... ;

var queryWithOrderBy = ApplyOrderBy(query,
    new OrderByExpression<User, string>(expression: u => u.UserName, descending: false),    // a string, asc
    new OrderByExpression<User, int>(expression: u => u.UserId, descending: true));  // an int, desc

var result = queryWithOrderBy.ToList(); // didn't throw an exception for me
Community
  • 1
  • 1
smartcaveman
  • 41,281
  • 29
  • 127
  • 212
  • Thanks @smartcaveman, I am rapidely coming to the same conclusion. I still would like to get this generic query sorted, but I suspect I will end up just exposing IQueryable in the end. – Dale K Aug 16 '12 at 04:02
  • 1
    @DaleBurrell, you will. I went down that road for a while as well before coming to my senses. – smartcaveman Aug 16 '12 at 04:08
  • Well that solution works, I'm sure there is a neater one around. But anyway back to IQueryable :) thanks – Dale K Aug 16 '12 at 05:09