When to return IOrderedEnumerable?
As Thomas points out, knowing that an object is an IOrderedEnumerable
tells us only that it's been ordered in some way, not that it's been ordered in a way that we will want to maintain.
It's also worth noting that the return type will influence overrides and compile-ability, but not runtime checks:
private static IOrderedEnumerable<int> ReturnOrdered(){return new int[]{1,2,3}.OrderBy(x => x);}
private static IEnumerable<int> ReturnOrderUnknown(){return ReturnOrdered();}//same object, diff return type.
private static void UseEnumerable(IEnumerable<int> col){Console.WriteLine("Unordered");}
private static void UseEnumerable(IOrderedEnumerable<int> col){Console.WriteLine("Ordered");}
private static void ExamineEnumerable(IEnumerable<int> col)
{
if(col is IOrderedEnumerable<int>)
Console.WriteLine("Enumerable is ordered");
else
Console.WriteLine("Enumerable is unordered");
}
public static void Main(string[] args)
{
//Demonstrate compile-time loses info from return types
//if variable can take either:
var orderUnknown = ReturnOrderUnknown();
UseEnumerable(orderUnknown);//"Unordered";
orderUnknown = ReturnOrdered();
UseEnumerable(orderUnknown);//"Unordered"
//Demonstate this wasn't a bug in the overload selection:
UseEnumerable(ReturnOrdered());//"Ordered"'
//Demonstrate run-time will see "deeper" than the return type anyway:
ExamineEnumerable(ReturnOrderUnknown());//Enumerable is ordered.
}
Because of this, if you have a case where there could be either IEnumerable<T>
or IOrderedEnumerable<T>
returned to the caller depending on circumstance, the variable will be typed as IEnumerable<T>
and the information from the return type lost. Meanwhile, no matter what the return type, the caller will be able to determine if the type is really IOrderedEnumerable<T>
.
Either way, return type didn't really matter.
The trade-off with return types are between utility to the caller vs flexibility to the callee.
Consider a method that currently ends with return currentResults.ToList()
. The following return types are possible:
List<T>
IList<T>
ICollection<T>
IEnumerable<T>
IList
ICollection
IEnumerable
object
Let's exclude object and the non-generic types right now as unlikely to be useful (in cases where they would be useful, they are probably no-brainer decisions to use). This leaves:
List<T>
IList<T>
ICollection<T>
IEnumerable<T>
The higher up the list we go, the more convenience we give the caller to make use of the functionality exposed by that type, that is not exposed by the type below. The lower down the list we go, the more flexibility we give to the callee to change the implementation in the future. Ideally therefore, we want to go as high up the list as makes sense in the context of the method's purpose (to expose useful functionality to the caller, and reduce cases where new collections are created to offer functionality we were already offering) but no higher (to allow for future changes).
So, back to our case where we have an IOrderedEnumerable<TElement>
that we can return as either an IOrderedEnumerable<TElement>
or an IEnumerable<T>
(or IEnumerable
or object
).
The question is, is the fact that this is an IOrderedEnumerable
inherently related to the purpose of the method, or is it merely an implementation artefact?
If we had a method ReturnProducts
that happened to order by price to as part of the implementation of removing cases where the same product was offered twice for different prices, then it should return IEnumerable<Product>
, because callers shouldn't care that it's ordered, and certainly shouldn't depend upon it.
If we had a method ReturnProductsOrderedByPrice
where the ordering was part of its purpose, then we should return IOrderedEnumerable<Product>
, because this relates more closely to its purpose, and may reasonably expect that calling CreateOrderedEnumerable
, ThenBy
or ThenByDescending
on it (the only things this really offers) and not have this broken by a subsequent change to the implementation.
Edit: I missed the second part of this.
What about in the case that a repository wraps a stored procedure with an ORDER BY clause. Should the repository return IOrderedEnumerable? And how would that be achieved?
That is quite a good idea when possible (or perhaps IOrderedQueryable<T>
). However, it's not simple.
First, you have to be sure that nothing subsequent to the ORDER BY
could have undone the ordering, this may not be trivial.
Secondly, you have to not undo this ordering in a call to CreateOrderedEnumerable<TKey>()
.
For example, if elements with fields A
, B
, C
and D
are being returned from something that used ORDER BY A DESCENDING, B
resulting in the return of a type called MyOrderedEnumerable<El>
that implements IOrderedEnumerable<El>
. Then, the fact that A
and B
are the fields that were ordered on must be stored. A call to CreateOrderedEnumerable(e => e.D, Comparer<int>.Default, false)
(which is also what ThenBy
and ThenByDescending
call into) must take groups of elements that compare equally for A
and B
, by the same rules for which they were returned by the database (matching collations between databases and .NET can be hard), and only within those groups must it then order according to cmp.Compare(e0.D, e1.D)
.
If you could do that, it could be very useful, and it would totally be appropriate that the return type were IOrderedEnumerable
if ORDER BY
clauses would be present on all queries used by all calls.
Otherwise, IOrderedEnumerable
would be a lie - since you couldn't fulfil the contract it offers - and it would be less than useless.
I don't think it would be a good idea:
Should IOrderedEnumerable be used as a return type purely for semantic value?
For example, when consuming a model in the presentation layer, how can we know whether the collection requires ordering or is already ordered?
What is the point in knowing that a sequence is ordered if you don't know by which key it is ordered? The point of the IOrderedEnumerable
interface is to be able to add a secondary sort criteria, which doesn't make much sense if you don't know what is the primary criteria.
What about in the case that a repository wraps a stored procedure with an ORDER BY clause. Should the repository return IOrderedEnumerable? And how would that be achieved?
This doesn't make sense. As I already said, IOrderedEnumerable
is used to add a secondary sort criteria, but when the data is returned by the stored procedure, it is already sorted and it's too late to add a secondary sort criteria. All you can do is re-sort it completely, so calling ThenBy
on the result wouldn't have the expected effect.