Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I dynamically store expressions used for Linq Orderby?

I am trying to create a quick class so that I can make writing sorting code for a grid much easier to work with and maintain, and to keep code repetition down. To do this I came up with the following class:

public class SortConfig<TSource, TRelatedObject> where TSource : class where TRelatedObject : class
{
    public IList<SortOption> Options { get; protected set; }
    public SortOption DefaultOption { get; set; }

    public SortConfig()
    {
        Options = new List<SortOption>();
    }

    public void Add(string name, Expression<Func<TSource, object>> sortExpression, TRelatedObject relatedObject, bool isDefault = false)
    {
        var option = new SortOption
        {
            FriendlyName = name,
            SortExpression = sortExpression,
            RelatedObject = relatedObject
        };

        Options.Add(option);

        if (isDefault)
            DefaultOption = option;
    }

    public SortOption GetSortOption(string sortName)
    {
        if (sortName.EndsWith("asc", StringComparison.OrdinalIgnoreCase))
            sortName = sortName.Substring(0, sortName.LastIndexOf("asc", StringComparison.OrdinalIgnoreCase));
        else if (sortName.EndsWith("desc", StringComparison.OrdinalIgnoreCase))
            sortName = sortName.Substring(0, sortName.LastIndexOf("desc", StringComparison.OrdinalIgnoreCase));

        sortName = sortName.Trim();

        var option = Options.Where(x => x.FriendlyName.Trim().Equals(sortName, StringComparison.OrdinalIgnoreCase))
                            .FirstOrDefault();
        if (option == null)
        {
            if (DefaultOption == null)
                throw new InvalidOperationException(
                    string.Format("No configuration found for sort type of '{0}', and no default sort configuration exists", sortName));

            option = DefaultOption;
        }

        return option;
    }

    public class SortOption
    {
        public string FriendlyName { get; set; }
        public Expression<Func<TSource, object>> SortExpression { get; set; }
        public TRelatedObject RelatedObject { get; set; }
    }
}

The idea is that you create a quick configuration of the different sort options, what OrderBy expression is used for that, and optionally an object that is related to that sort option. This allows my code to look like:

    protected void InitSortConfig()
    {
        _sortConfig = new SortConfig<xosPodOptimizedSearch, HtmlAnchor>();
        _sortConfig.Add("name", (x => x.LastName), lnkSortName, true);
        _sortConfig.Add("team", (x => x.SchoolName), lnkSortTeam);
        _sortConfig.Add("rate", (x => x.XosRating), lnkSortRate);
        _sortConfig.Add("pos", (x => x.ProjectedPositions), null);
        _sortConfig.Add("height", (x => x.Height), lnkSortHeight);
        _sortConfig.Add("weight", (x => x.Weight), lnkSortWeight);
        _sortConfig.Add("city", (x => x.SchoolCity), lnkSortCity);
        _sortConfig.Add("state", (x => x.SchoolState), lnkSortState);
    }

and then I can sort by just doing

        // Get desired sorting configuration
        InitSortConfig();
        var sortOption = _sortConfig.GetSortOption(sort);
        bool isDescendingSort = sort.EndsWith("desc", StringComparison.OrdinalIgnoreCase);

        // Setup columns
        InitSortLinks();
        if (sortOption.RelatedObject != null)
        {
            // Make modifications to html anchor
        }

        // Form query
        var query = PodDataContext.xosPodOptimizedSearches.AsQueryable();

        if (isDescendingSort)
            query = query.OrderByDescending(sortOption.SortExpression);
        else
            query = query.OrderBy(sortOption.SortExpression);

This works great when the sorted variable is a string, but when it is not a string I get the following exception: Cannot order by type 'System.Object'.

I'm assuming this is because I am storing the expression as Expression<Func<TSource, object>> and not being more specific about that 2nd generic. I don't understand how I can keep all my different sort options (even for non-string properties) in one class.

I guess one of the issues is that the Linq.OrderBy() clause takes Expression<Func<TSource, TKey>> as it's parameter, but I am not wrapping my head around how Linq.OrderBy() is able to infer what TKey should be, and therefore I cannot understand how to take advantage of that inference to store these expressions with the proper TKey.

Any ideas?

like image 791
KallDrexx Avatar asked May 07 '12 18:05

KallDrexx


People also ask

Can you use LINQ on dynamic?

The Dynamic source file includes a helper library that allows you to express LINQ queries using extension methods that take string arguments instead of type safe operators. To use the Dynamic Expression API, you could simply copy/paste the Dynamic source file in your project.

How do you do OrderBy in LINQ?

LINQ includes five sorting operators: OrderBy, OrderByDescending, ThenBy, ThenByDescending and Reverse. LINQ query syntax does not support OrderByDescending, ThenBy, ThenByDescending and Reverse. It only supports 'Order By' clause with 'ascending' and 'descending' sorting direction.

What is difference between OrderBy and ThenBy in LINQ?

The OrderBy() Method, first sort the elements of the sequence or collection in ascending order after that ThenBy() method is used to again sort the result of OrderBy() method in ascending order.


1 Answers

The generic argument is inferred like so:

IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(this IEnumerable<TSource> enumerable, Func<TSource, TKey> expression)

When you have an IEnumerable<T>, the compiler is able to infer that the TSource is T in this situation because of the extension method declaration; so the extension method is already added on knowing what TSource is. For example:

Enumerable.Range(0, 10).OrderBy(x => x)

Since we start with an IEnumerable<int>, the compiler can infer that the expression it expects is Func<int, TKey>, because the extension is affecting the IEnumerable<int>. Next, because your expression returns a value, the compiler can infer the remaining type, in this situation int, so it becomes a Func<int, int> with this example.

Now, germane to your particular problem, you could easily set up your expression to work appropriately if you genericize your SortConfig object appropriately. It looks like your SortConfig takes a Func<TSource, object> delegate right now. If you genericize your SortConfig to use another type, you gain specificity. Example:

 void Add<TSource, TKey>(string name, Func<TSource, TKey> expression)

The next problem here is how to store your backing methods in some format. And your class declaration looks like so:

 public class SortConfig<TSource>

Then all the data types should line up when you invoke the OrderBy extension.

EDIT: Here's a working example of what I think you want to do:

    static void Main(string[] args)
    {
        var list = Enumerable.Range(0, 10).Reverse().Select(x => new SampleClass { IntProperty = x, StringProperty = x + "String", DateTimeProperty = DateTime.Now.AddDays(x * -1) });

        SortContainer<SampleClass> container = new SortContainer<SampleClass>();
        container.Add("Int", x => x.IntProperty);
        container.Add("String", x => x.StringProperty);
        container.Add("DateTime", x => x.DateTimeProperty);

        var sorter = container.GetSorterFor("Int");

        sorter.Sort(list).ForEach(x => Console.WriteLine(x.IntProperty));
        Console.ReadKey();
    }

    public class SampleClass
    {
        public int IntProperty { get; set; }
        public string StringProperty { get; set; }
        public DateTime DateTimeProperty { get; set; }
    }

    public class SortContainer<TSource>
    {
        protected Dictionary<string, ISorter<TSource>> _sortTypes = new Dictionary<string, ISorter<TSource>>();

        public void Add<TKey>(string name, Func<TSource, TKey> sortExpression)
        {
            Sorter<TSource, TKey> sorter = new Sorter<TSource, TKey>(sortExpression);
            _sortTypes.Add(name, sorter);
        }

        public ISorter<TSource> GetSorterFor(string name)
        {
            return _sortTypes[name];
        }
    }

    public class Sorter<TSource, TKey> : ISorter<TSource>
    {
        protected Func<TSource, TKey> _sortExpression = null;

        public Sorter(Func<TSource, TKey> sortExpression)
        {
            _sortExpression = sortExpression;
        }

        public IOrderedEnumerable<TSource> Sort(IEnumerable<TSource> sourceEnumerable)
        {
            return sourceEnumerable.OrderBy(_sortExpression);
        }
    }

    public interface ISorter<TSource>
    {
        IOrderedEnumerable<TSource> Sort(IEnumerable<TSource> sourceEnumerable);
    }
like image 110
Tejs Avatar answered Oct 17 '22 11:10

Tejs