Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Store multi-type OrderBy expression as a property

Tags:

c#

linq

In a generic abstract base class I'm storing a couple of expressions used for ordering:

public Expression<Func<T, string>> OrderByString { get; set; }
public Expression<Func<T, int>> OrderByInt { get; set; }

The get used later on in the generic base class:

if (OrderByString != null)
{
    results = results.OrderBy(OrderByString);
}
else if (OrderByInt != null)
{
    results = results.OrderBy(OrderByInt);
}

Finally one of them will get set in the deriving concrete class's constructer:

this.OrderByString = c => c.CustomerID;

I don't like the fact that I need to have separate expressions based on the property type I want to OrderBy. ToString won't work on the property's because LINQ to Entities doesn't support it. What I'm after is a way of storing an expression that picks any of the properties to order on regardless of type.

If I try something a little more generic such as:

public Expression<Func<T, object>> Order { get; set; }

Unable to cast the type 'System.Int32' to type 'System.Object'. LINQ to Entities only supports casting Entity Data Model primitive types.

Additionally if I try a slight hack this also doesn't work:

public Expression<Func<T, string>> Order { get; set; }
this.Order = c => c.OrderID.ToString();

LINQ to Entities does not recognize the method 'System.String ToString()' method, and this method cannot be translated into a store expression.

like image 421
Daniel Revell Avatar asked Oct 12 '11 08:10

Daniel Revell


3 Answers

Sounds like you want a way to pile up a bunch of Ordering in a list somewhere and apply it. But you can't because each Expression has its own type, which is checked by the compiler when calling OrderBy. You must have those two types when calling OrderBy, but you must have one type to put in the same list.

Hide that second type behind an interface.

public interface IOrderer<T>
{
    IOrderedQueryable<T> ApplyOrderBy(IQueryable<T> source);
    IOrderedQueryable<T> ApplyOrderByDescending(IQueryable<T> source);
    IOrderedQueryable<T> ApplyThenBy(IOrderedQueryable<T> source);
    IOrderedQueryable<T> ApplyThenByDescending(IOrderedQueryable<T> source);
}

public class Orderer<T, U> : IOrderer<T>
{
    private Expression<Func<T, U>> _orderExpr;
    public Orderer(Expression<Func<T, U>> orderExpr) { _orderExpr = orderExpr; }

    public IOrderedQueryable<T> ApplyOrderBy(IQueryable<T> source)
    { return source.OrderBy(_orderExpr); }
    public IOrderedQueryable<T> ApplyOrderByDescending(IQueryable<T> source)
    { return source.OrderByDescending(_orderExpr); }
    public IOrderedQueryable<T> ApplyThenBy(IOrderedQueryable<T> source)
    { return source.ThenBy(_orderExpr); }
    public IOrderedQueryable<T> ApplyThenByDescending(IOrderedQueryable<T> source)
    { return source.ThenByDescending(_orderExpr); }
}

public class OrderCoordinator<T>
{
    public List<IOrderer<T>> Orders { get; set; }

    public OrderCoordinator() { Orders = new List<IOrderer<T>>(); }

    //note, did not return IOrderedQueryable to support ability to return with empty Orders
    public IQueryable<T> ApplyOrders(IQueryable<T> source)
    {
        foreach (IOrderer<T> orderer in Orders)
        {
            source = orderer.ApplyOrderBy(source);
        }
        return source;
    }
}

public class Customer
{
    public string Name { get; set; }
    public int FavNumber { get; set; }
}

public class Tester
{
    public void Test()
    {
        OrderCoordinator<Customer> coord = new OrderCoordinator<Customer>();
        coord.Orders.Add(new Orderer<Customer, string>(c => c.Name));
        coord.Orders.Add(new Orderer<Customer, int>(c => c.FavNumber));

        IQueryable<Customer> query = Enumerable.Empty<Customer>().AsQueryable();

        query = coord.ApplyOrders(query);

        string result = query.Expression.ToString();
    }
}

In the debugger:

result = "OrderingDemo.Customer[].OrderBy(c => c.Name).OrderBy(c => c.FavNumber)"

So in your case, instead of this property:

 public Expression<Func<T, U>> Order { get; set; } 

use this property

 public IOrderer<T> Order { get; set; } 
like image 76
Amy B Avatar answered Nov 11 '22 07:11

Amy B


This is easy to do if you use the DynamicLinq library found at NuGet.org. This allows you to write queries like;

db.People.Where("Id == 8");
db.People.OrderBy("Created ASC");

That way you can save or pass in your where clauses as strings. No fuss, no muss.

http://nuget.org/List/Packages/DynamicLINQ

like image 22
Khalid Abuhakmeh Avatar answered Nov 11 '22 06:11

Khalid Abuhakmeh


Amy B's answer is great, and I based my own solution on it. So my point is more of an improvement for what I needed, which I am likely to improve upon in time.

public interface IOrderer<TItem>
{
    IOrderedQueryable<TItem> Apply(IQueryable<TItem> source);
}

public class OrderBy<TItem, TType> : IOrderer<TItem>
{
    private Expression<Func<TItem, TType>> _orderExpr;
    public OrderBy(Expression<Func<TItem, TType>> orderExpr)
    {
        _orderExpr = orderExpr;
    }

    public IOrderedQueryable<TItem> Apply(IQueryable<TItem> source)
    {
        return source.OrderBy(_orderExpr);
    }
}   

public class ThenBy<TItem, TType> : IOrderer<TItem>
{
    private Expression<Func<TItem, TType>> _orderExpr;
    public ThenBy(Expression<Func<TItem, TType>> orderExpr)
    {
        _orderExpr = orderExpr;
    }

    public IOrderedQueryable<TItem> Apply(IQueryable<TItem> source)
    {
        return ((IOrderedQueryable<TItem>)source).ThenBy(_orderExpr);
    }
}   

public class OrderCoordinator<TItem>
{
    public List<IOrderer<TItem>> Orders { get; private set; } = new List<IOrderer<TItem>>();

    public IQueryable<TItem> ApplyOrder(IQueryable<TItem> source)
    {
        foreach (IOrderer<TItem> orderer in Orders)
        {
            source = orderer.Apply(source);
        }
        return source;
    }

    public OrderCoordinator<TItem> OrderBy<TValueType>(Expression<Func<TItem, TValueType>> orderByExpression)
    {
        Orders.Add(new OrderBy<TItem, TValueType>(orderByExpression));
        return this;
    }

    // Can add more sort calls over time
    public OrderCoordinator<TItem> ThenBy<TValueType>(Expression<Func<TItem, TValueType>> orderByExpression)
    {
        Orders.Add(new ThenBy<TItem, TValueType>(orderByExpression));
        return this;
    }
}   

Specify Coordinator with type:

public OrderCoordinator<MyObjectType> OrderCoordinator { get; private set; } = new OrderCoordinator<MyObjectType>();

Specify sort ordering:

OrderCoordinator.OrderBy(e => e.MyStringProperty).ThenBy(e => e.MyIntProperty);

Apply the ordering:

ordered = OrderCoordinator.ApplyOrder(ordered); 
like image 1
Macuistin Avatar answered Nov 11 '22 05:11

Macuistin