There is a lot of great technology coming from Microsoft in this year – there is almost not enough time to take it all in. That said, there are some areas where we can try to anticipate where some issues are going to occur, be they fast access to objects (i4o), or in this latest installment, enabling bi-directional binding to work correctly on results from LINQ to Objects operations.
You might ask, why is this an issue? Well, as Rocky Lhotka as pointed out before, the results of a query from LINQ to Objects do not return a filtered view of the original collection, but a whole new object (something called a Sequence) that implements IEnumerable<T>, so you can iterate over it and, at least in a read-only sense, bind to it. Now, presumably you could, in theory, add or remove to the result (though I don’t know for sure – have not tried yet)… but the problem is that even if you could, you would be adding or removing from a different collection, as well as losing anything that was implemented for you in the IEnumerable<T> you start with in the first place. Which means that, if you are using CSLA.net, you not only will have wierd things happen on add/remove, but you will also break features that framework provides, like N-level undo.
Needless to say, this is the kind of thing that might rain on the LINQ to objects parade for certain kinds of use cases… if LINQ were not extensible that is :). Writers of frameworks – especially the kind of frameworks that have custom collections, will want to implement IQueryable<T> on their custom collections in order to allow for bi-directional binding LINQ generated subsets. IQueryable<T> allows you to, in its CreateQuery<TElement> method, to specify what exactly comes back from the different LINQ methods (i.e. Where, Select, GroupBy, etc.). Of particular interest for filtering is handling your Where call such that it returns an IQueryable<T> whose concrete implementation you can control, rather than using the default that LINQ gives you.
The technique I am using to do this relies on two classes. The first class is a simple collection class that implements ICollection<T> and IQueryable<T>, that I am calling CollectionExtendingIQueryable<T>. The second class is designed to be a read/write view of the first one that is derived from the first one – called ViewOnCollectionExtendingIQueryable<T>. This second class (the view) also implements IQueryable<T>. The result of a LINQ query that projects an identity projection – that is – a projection of the whole objects of the enumerable we are enumerating – will now be typed to ViewOnCollectionExtendingIQueryable<T>, which can have all the stuff behavior the parent has, and the same data, but is assigned a different expression that IEnumerable<T>.GetEnumerator() will use when generating it’s result from the where clause.
More simply, we generate a read/write view collection that differs from the original collection only in it’s GetEnumerator implementation. In most other respects (most importantly the underlying concrete collection) – it is the same object. If you add or remove from the filtered version, you remove from the concrete collection, which potentially, removes it from other filtered views.
Of course, there is a lot of other work you have to do to fully implement IQueryable<T>. I have done some of it, such as making sure non-identity projections work like normal LINQ projections – but I am sure there is other work needed to do a full implementation. That said, the important part of this is that it proves the concept that you can have LINQ to Objects and support filter style projections, if you are willing to dive into supporting IQueryable<T>.
Source code is below, with a demo console implementation:
FilteredCollection.cs:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Linq.Expressions;
namespace TestExtendingIQueryable
{
//NOTE: This is a proof of concept – not designed to be production code
public class RandomThing
{
public int SomeVal;
public RandomThing(int x) { SomeVal = x; }
}
public class CollectionExtendingIQueryable<T> : ICollection<T>, IQueryable<T>
{
public CollectionExtendingIQueryable()
{ _internalList = new List<T>(); _ex = System.Linq.Expressions.Expression.Constant(this); }
protected Expression _ex;
protected List<T> _internalList;
internal List<T> UnderlyingList { get { return _internalList; } }
public void RemoveBottomItem()
{
_internalList.RemoveAt(0);
}
public void Add(T item)
{
_internalList.Add(item);
}
public int FilteredCount
{
get
{
int cnt = 0;
foreach (T item in this)
cnt++;
return cnt;
}
}
public int UnfilteredCount
{
get
{
int cnt = 0;
foreach (T item in _internalList)
cnt++;
return cnt;
}
}
#region IQueryable<T> Members
IQueryable<TElement> IQueryable<T>.CreateQuery<TElement>(Expression expression)
{
MethodCallExpression mex = expression as MethodCallExpression;
switch(mex.Method.Name)
{
case "Where":
return (IQueryable<TElement>) new ViewOnCollectionExtendingIQueryable<T>(expression, this);
case "Select":
UnaryExpression selectHolder = mex.Arguments[1] as UnaryExpression;
LambdaExpression theSelect = selectHolder.Operand as LambdaExpression;
Expression<Func<T, TElement>> selectorLambda
= Expression.Lambda<Func<T, TElement>>(theSelect.Body,theSelect.Parameters);
Func<T, TElement> selector = selectorLambda.Compile();
return this.Select<T, TElement>(selector).AsQueryable<TElement>();
default:
return null;
}
}
TResult IQueryable<T>.Execute<TResult>(Expression expression)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
#region IEnumerable<T> Members
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
MethodCallExpression mex = _ex as MethodCallExpression;
UnaryExpression whereHolder = mex.Arguments[1] as UnaryExpression;
LambdaExpression theWhere = whereHolder.Operand as LambdaExpression;
Expression<Func<T, bool>> theParmedWhere
= Expression.Lambda<Func<T, bool>>(theWhere.Body, theWhere.Parameters);
Func<T, bool> filter = theParmedWhere.Compile();
//if we had indexes in this collection, they would be used here
foreach (T item in _internalList)
if (filter(item))
yield return item;
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
MethodCallExpression mex = _ex as MethodCallExpression;
UnaryExpression whereHolder = mex.Arguments[1] as UnaryExpression;
LambdaExpression theWhere = whereHolder.Operand as LambdaExpression;
Expression<Func<T, bool>> theParmedWhere = Expression.Lambda<Func<T, bool>>(theWhere.Body, theWhere.Parameters);
Func<T, bool> filter = theParmedWhere.Compile();
foreach (T item in this)
if (filter(item))
yield return item;
}
#endregion
#region IQueryable Members
IQueryable IQueryable.CreateQuery(Expression expression)
{
_ex = expression;
return this;
}
Type IQueryable.ElementType
{
get { return typeof(T); }
}
object IQueryable.Execute(Expression expression)
{
throw new Exception("The method or operation is not implemented.");
}
Expression IQueryable.Expression
{
get { return _ex; }
}
#endregion
#region ICollection<T> Members
void ICollection<T>.Add(T item)
{
_internalList.Add(item);
}
void ICollection<T>.Clear()
{
_internalList.Clear();
}
bool ICollection<T>.Contains(T item)
{
return _internalList.Contains(item);
}
void ICollection<T>.CopyTo(T[] array, int arrayIndex)
{
_internalList.CopyTo(array,arrayIndex);
}
int ICollection<T>.Count
{
get { return _internalList.Count; }
}
bool ICollection<T>.IsReadOnly
{
get { return false; }
}
bool ICollection<T>.Remove(T item)
{
return(_internalList.Remove(item));
}
#endregion
}
public class ViewOnCollectionExtendingIQueryable<T> : CollectionExtendingIQueryable<T>, IQueryable<T>
{
protected Expression _specificEx;
public ViewOnCollectionExtendingIQueryable(Expression ex, CollectionExtendingIQueryable<T> baseCollection)
{
_internalList = baseCollection.UnderlyingList;
_specificEx = ex;
}
IEnumerator<T> IEnumerable<T>.GetEnumerator()
{
MethodCallExpression mex = _specificEx as MethodCallExpression;
UnaryExpression whereHolder = mex.Arguments[1] as UnaryExpression;
LambdaExpression theWhere = whereHolder.Operand as LambdaExpression;
Expression<Func<T, bool>> theParmedWhere
= Expression.Lambda<Func<T, bool>>(theWhere.Body, theWhere.Parameters);
Func<T, bool> filter = theParmedWhere.Compile();
//if we had indexes in this collection, they would be used here
foreach (T item in _internalList)
if (filter(item))
yield return item;
}
#region IQueryable<T> Members
IQueryable<TElement> IQueryable<T>.CreateQuery<TElement>(Expression expression)
{
MethodCallExpression mex = expression as MethodCallExpression;
switch (mex.Method.Name)
{
case "Where":
_specificEx = expression;
return (IQueryable<TElement>) this;
case "Select":
UnaryExpression selectHolder = mex.Arguments[1] as UnaryExpression;
LambdaExpression theSelect = selectHolder.Operand as LambdaExpression;
Expression<Func<T, TElement>> selectorLambda
= Expression.Lambda<Func<T, TElement>>(theSelect.Body, theSelect.Parameters);
Func<T, TElement> selector = selectorLambda.Compile();
return this.Select<T, TElement>(selector).AsQueryable<TElement>();
default:
return null;
}
}
TResult IQueryable<T>.Execute<TResult>(Expression expression)
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
#region IEnumerable Members
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
throw new Exception("The method or operation is not implemented.");
}
#endregion
#region IQueryable Members
IQueryable IQueryable.CreateQuery(Expression expression)
{
throw new Exception("The method or operation is not implemented.");
}
Type IQueryable.ElementType
{
get { return typeof(T); }
}
object IQueryable.Execute(Expression expression)
{
throw new Exception("The method or operation is not implemented.");
}
Expression IQueryable.Expression
{
get { return _specificEx; }
}
#endregion
}
}
Program.cs:
using System;
using System.Linq;
using System.Collections.Generic;
namespace TestExtendingIQueryable
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Demonstration of using LINQ to generate filtered results, using a");
Console.WriteLine("collection class specifically designed to filter rather than project");
Console.WriteLine("by default.");
Console.WriteLine("");
Console.WriteLine("We will generate a collection with 100 random numbers, between 1 and 300");
Console.WriteLine("We will then generate two views on the numbers, one for those below 100,");
Console.WriteLine("the other for those below 200. Removing the bottom most item, which is");
Console.WriteLine("the only item that wont be random (fixed at 42, to fit in both ranges)");
Console.WriteLine("will affect the count of all three collections (original, filteredview,");
Console.WriteLine("and different filtered view.");
Console.WriteLine("");
Console.WriteLine("Lastly, we will do a typical projection, which will demonstrate that");
Console.WriteLine("the filtering logic gets out of the way when you select something that");
Console.WriteLine("is not amenable to filtering.");
CollectionExtendingIQueryable<RandomThing> random = new CollectionExtendingIQueryable<RandomThing>();
Random rnd = new Random();
random.Add(new RandomThing(42)); //first one has to be under 100 to run our removal tests correctly
for (int i = 0; i < 99; i++)
random.Add(new RandomThing(rnd.Next(300)));
var filteredResult = from r in random
where r.SomeVal < 100
select r;
var differentFilteredResult = from r in random
where r.SomeVal < 200
select r;
Console.WriteLine("Filtered results (random numbers under 100)");
foreach (var x in filteredResult)
Console.Write(x.SomeVal + ",");
Console.WriteLine("");
Console.WriteLine("———————————-");
Console.WriteLine("Filtered result Count = " + ((CollectionExtendingIQueryable<RandomThing>)filteredResult).FilteredCount);
Console.WriteLine("Different filtered result Count = " + ((CollectionExtendingIQueryable<RandomThing>)differentFilteredResult).FilteredCount);
Console.WriteLine("Now we are going to remove the bottom item from the first filtered result, which should reduce the count of all filtered results by one");
Console.WriteLine("Press any key to continue…");
Console.ReadKey();
Console.WriteLine("Count of original collection = " + random.UnfilteredCount);
((CollectionExtendingIQueryable<RandomThing>)filteredResult).RemoveBottomItem();
Console.WriteLine("Count of original collection after removal from filtered list = " + random.UnfilteredCount);
Console.WriteLine("Count in the filtered list = " + ((CollectionExtendingIQueryable<RandomThing>)filteredResult).FilteredCount);
Console.WriteLine("Count in the different filtered result = " + ((CollectionExtendingIQueryable<RandomThing>)differentFilteredResult).FilteredCount);
Console.WriteLine("Press any key to test projection…");
Console.ReadKey();
var projectedResult = from r in random
where r.SomeVal < 100
select r.SomeVal;
foreach (var x in projectedResult)
Console.Write(x + ",");
Console.WriteLine("");
Console.WriteLine("———————");
Console.WriteLine("Press any key to exit the demo…");
Console.ReadKey();
}
}
}