Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is .ThenBy implemented?

Tags:

c#

linq

Inspired by this I made this:

ISortable

public interface ISortable<T>
{
    IPageable<T> OrderBy<U>(Expression<Func<T, U>> orderBy);
    IPageable<T> OrderByDescending<U>(Expression<Func<T, U>> orderBy);
}

IPageable

public interface IPageable<T> : ISortable<T>, IEnumerable<T>
{
    IPageable<T> Page(int pageNumber, int pageSize);
    List<T> ToList();
    int TotalPages { get; }
    int TotalItemCount { get; }
    int PageNumber { get; }
    int? PageSize { get; }
}

Pageable

public class Pageable<T> : IPageable<T>
{
    private readonly IQueryable<T> _countQuery;
    private IQueryable<T> _sourceQuery;

    /// <summary>
    /// A pageable result
    /// </summary>
    /// <param name="sourceQuery">Query which holdes all relevant items.</param>
    public Pageable(IQueryable<T> sourceQuery)
    {
        _sourceQuery = sourceQuery;
        _countQuery = sourceQuery;
        PageNumber = 1;
    }

    /// <summary>
    /// A pageable result
    /// </summary>
    /// <param name="sourceQuery">Query which holdes all relevant items.</param>
    /// <param name="countQuery">
    /// Alternative query optimized for counting.
    /// <see cref="countQuery"/> is required to give the same count as <see cref="sourceQuery"/> else paging will break. 
    /// <remarks>No checks if <see cref="sourceQuery"/> and <see cref="countQuery"/> return the same count are appiled.</remarks>
    /// </param>
    public Pageable(IQueryable<T> sourceQuery, IQueryable<T> countQuery)
        : this (sourceQuery)
    {
        _countQuery = countQuery;
    }

    #region Implementation of IEnumerable

    /// <summary>
    /// Returns an enumerator that iterates through the collection.
    /// </summary>
    /// <returns>
    /// A <see cref="T:System.Collections.Generic.IEnumerator`1"/> that can be used to iterate through the collection.
    /// </returns>
    /// <filterpriority>1</filterpriority>
    public IEnumerator<T> GetEnumerator()
    {
        return _sourceQuery.GetEnumerator();
    }

    /// <summary>
    /// Returns an enumerator that iterates through a collection.
    /// </summary>
    /// <returns>
    /// An <see cref="T:System.Collections.IEnumerator"/> object that can be used to iterate through the collection.
    /// </returns>
    /// <filterpriority>2</filterpriority>
    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    #region Implementation of ISortable

    public IPageable<T> OrderBy<U>(Expression<Func<T, U>> orderBy)
    {
        _sourceQuery = _sourceQuery.OrderBy(orderBy);
        return this;
    }

    public IPageable<T> OrderByDescending<U>(Expression<Func<T, U>> orderBy)
    {
        _sourceQuery = _sourceQuery.OrderByDescending(orderBy);
        return this;
    }

    #endregion

    #region Implementation of IPageable

    public int PageNumber { get; private set; }
    public int? PageSize { get; private set; }
    public int TotalItemCount
    {
        get { return _countQuery.Count(); }
    }
    public int TotalPages
    {
        get { return (int) (Math.Ceiling((double) TotalItemCount/PageSize ?? 1)); }
    }

    /// <summary>
    /// Chop a query result into pages.
    /// </summary>
    /// <param name="pageNumber">Page number to fetch. Starting from 1.</param>
    /// <param name="pageSize">Items per page.</param>
    /// <returns></returns>
    public IPageable<T> Page(int pageNumber, int pageSize)
    {
        PageNumber = pageNumber;
        PageSize = pageSize;

        _sourceQuery = _sourceQuery
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize);

        return this;
    }

    public List<T> ToList()
    {
        return _sourceQuery.ToList();
    }

    #endregion
}

The above works. Great success! :)

However I've run into a problem implementing the method .ThenBy(). The problem is that .ThenBy() should only be accessible when .OrderBy() has been called.

I noticed that IQueryable.OrderBy returns an IOrderedQueryable and that's where the access to .ThenBy() comes from. But in order to make my current solution work I'd need to make a IOrderedPageable and a new OrderedPagable to go with it. The OrderedPagable would be an almost exact copy of Pageable which is really really bad design.

I highly doubt that's how it's done in LINQ. So my question is, how did they do it? I'm very curious :)

One thing I did notice is that pretty much all the LINQ methods are extension methods, is that part of the "trick" :)?

like image 865
Snæbjørn Avatar asked Aug 10 '13 14:08

Snæbjørn


2 Answers

It sounds like your class OrderedPageable could be a subclass of Pageable and additionally implement the IOrderedPageable interface.

The inheritance approach would seem to make sense, since anything that handles Pageable should likely be able to handle OrderedPageable in the same way.


By slightly changing your interfaces and using an interface inheritance approach similar to the above, you can achieve the functionality you're looking for with immutable queryable classes and extension methods.

In my opinion, the usage is clearer and more consistent with LINQ.

Examples:

query.AsPageable(100).Page(1);
query.AsPageable(100).OrderBy(x => x.Name).ThenBy(x => x.Age).Page(1).ToList();

I haven't tested this but the concept should work. Note that:

  • the total item count is computed from the original query (not any of the sorted queries)
  • the total item count is lazy and only computed once
  • depending on your query provider, you may have to expose a SourceQuery property of IPageableQuery for use within PageableExtensions, as your query provider may not successfully translate queries against this new PageableQuery type.

Interfaces:

public interface IPageableQuery<T> : IQueryable<T>
{
    int TotalPages { get; }
    int TotalItemCount { get; }

    int PageSize { get; }
}

public interface IOrderedPageableQuery<T> : IPageableQuery<T>, IOrderedQueryable<T>
{
}

Implementations:

public class PageableQuery<T> : IPageableQuery<T>
{
    readonly IQueryable<T> _sourceQuery;
    readonly Lazy<int> _totalItemCount; 

    public int TotalPages { get { return (int)Math.Ceiling((double)TotalItemCount / PageSize); } }
    public int TotalItemCount { get { return _totalItemCount.Value; } }
    public int PageSize { get; private set; }

    public PageableQuery(IQueryable<T> sourceQuery, int pageSize)
    {
        _sourceQuery = sourceQuery;
        _totalItemCount = new Lazy<int>(() => _sourceQuery.Count());

        PageSize = pageSize;
    }

    public IEnumerator<T> GetEnumerator() { return _sourceQuery.GetEnumerator();}
    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    public Expression Expression { get { return _sourceQuery.Expression; } }
    public Type ElementType { get { return _sourceQuery.ElementType; } }
    public IQueryProvider Provider { get { return _sourceQuery.Provider; } }
}

public class OrderedPageableQuery<T> : IOrderedPageableQuery<T>
{
    readonly IPageableQuery<T> _sourcePageableQuery;
    readonly IOrderedQueryable<T> _sourceQuery;

    public int TotalPages { get { return (int)Math.Ceiling((double)TotalItemCount / PageSize); } }
    public int TotalItemCount { get { return _sourcePageableQuery.TotalItemCount; } }
    public int PageSize { get { return _sourcePageableQuery.PageSize; } }

    public OrderedPageableQuery(IPageableQuery<T> sourcePageableQuery, IOrderedQueryable<T> newSourceQuery)
    {
        _sourcePageableQuery = sourcePageableQuery;
        _sourceQuery = newSourceQuery;
    }

    public IEnumerator<T> GetEnumerator() { return _sourceQuery.GetEnumerator();}
    IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }

    public Expression Expression { get { return _sourceQuery.Expression; } }
    public Type ElementType { get { return _sourceQuery.ElementType; } }
    public IQueryProvider Provider { get { return _sourceQuery.Provider; } }
}

Extension Methods:

public static class PageableExtension
{
    public static IPageableQuery<T> AsPageable<T>(this IQueryable<T> sourceQuery, int pageSize)
    {
        return new PageableQuery<T>(sourceQuery, pageSize);
    }

    public static IOrderedPageableQuery<T> OrderBy<T, U>(this IPageableQuery<T> sourcePageableQuery, Expression<Func<T, U>> orderBy)
    {
        return new OrderedPageableQuery<T>(sourcePageableQuery, Queryable.OrderBy(sourcePageableQuery, orderBy));
    }

    public static IOrderedPageableQuery<T> OrderByDescending<T, U>(this IPageableQuery<T> sourcePageableQuery, Expression<Func<T, U>> orderBy)
    {
        return new OrderedPageableQuery<T>(sourcePageableQuery, Queryable.OrderByDescending(sourcePageableQuery, orderBy));
    }

    public static IOrderedPageableQuery<T> ThenBy<T, U>(this IOrderedPageableQuery<T> sourcePageableQuery, Expression<Func<T, U>> orderBy)
    {
        return new OrderedPageableQuery<T>(sourcePageableQuery, Queryable.ThenBy(sourcePageableQuery, orderBy));
    }

    public static IOrderedPageableQuery<T> ThenByDescending<T, U>(this IOrderedPageableQuery<T> sourcePageableQuery, Expression<Func<T, U>> orderBy)
    {
        return new OrderedPageableQuery<T>(sourcePageableQuery, Queryable.ThenByDescending(sourcePageableQuery, orderBy));
    }

    public static IQueryable<T> Page<T>(this IPageableQuery<T> sourceQuery, int pageNumber)
    {
        return sourceQuery.Skip((pageNumber - 1) * sourceQuery.PageSize)
                          .Take(sourceQuery.PageSize);
    }
}
like image 170
Michael Petito Avatar answered Oct 06 '22 00:10

Michael Petito


My suggested design for this would be making it explicit that your methods are in fact mutating the source object and deliberately not mirroring LINQ method names to avoid confusion. I omitted the IPageable interface and a bunch of other stuff for clarity, since the code is already a bit lengthy:

public interface ISortable<T>
{
    Pageable<T> ResetOrder();
    Pageable<T> AddOrder(Expression<Func<T, object>> orderBy);
    Pageable<T> AddOrderDescending(Expression<Func<T, object>> orderBy);
}

public class Pageable<T> : ISortable<T>, IEnumerable<T> {
    class SortKey {
        public Expression<Func<T, object>> Expression { get; set; }
        public bool Descending { get; set; }
    }

    List<SortKey> _sortKeys = new List<SortKey>();

    System.Linq.IQueryable<T> _sourceQuery;

    int _pageNumber;
    int _pageSize;

    public Pageable<T> SetPage(int pageNumber, int pageSize) {
        _pageNumber = pageNumber;
        _pageSize = pageSize;
        return this;
    }

    public Pageable<T> ResetOrder()
    {
        _sortKeys.Clear();
        return this;
    }

    public Pageable<T> AddOrder(Expression<Func<T, object>> orderBy)
    {
        _sortKeys.Add(new SortKey {
            Expression = orderBy, 
            Descending = false
        });
        return this;
    }

    public Pageable<T> AddOrderDescending(Expression<Func<T, object>> orderBy)
    {
        _sortKeys.Add(new SortKey {
            Expression = orderBy, 
            Descending = true
        });
        return this;
    }

    IEnumerable<T> SortAndPage()
    {
        if (_sortKeys.Count == 0) 
        {
            return Page(_sourceQuery);
        }

        var firstKey = _sortKeys[0];
        var orderedQuery = firstKey.Descending 
            ? _sourceQuery.OrderByDescending(firstKey.Expression) 
            : _sourceQuery.OrderBy(firstKey.Expression);

        foreach (var key in _sortKeys.Skip(1)) 
        {
            orderedQuery = key.Descending ? orderedQuery.ThenByDescending(key.Expression) : orderedQuery.ThenBy(key.Expression);
        }
        return Page(orderedQuery);
    }

    IEnumerable<T> Page(IQueryable<T> query) 
    {
        return query.Skip((_pageNumber - 1) * _pageSize)
                    .Take (_pageSize);
    }

    public IEnumerator<T> GetEnumerator()
    {
        return SortAndPage().GetEnumerator();
    }

    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}

Note that the source query remains the same so you could change the parameters before enumerating the elements. This would also let you implement chaining behaviour if you wanted by returning a new Pageable based on the current one instead of this in the methods that do that, but it would make the code even clunkier since you'd have create a copy constructor and add code to create those derived objects.

Also I believe the annoying redundant code in SortAndPage() could be refactored (or at least golfed down) using some FP approaches but it's probably more straightforward to read the way it is.

like image 34
millimoose Avatar answered Oct 06 '22 00:10

millimoose