I recently had the situation where I had a class similar to the following:
public class Item
{
public Guid ItemID { get; set; }
public string ItemName { get; set; }
public IList Categories { get; set; }
}
public class Category
{
public Guid CategoryID { get; set; }
public string CategoryName { get; set; }
}
Data for the application was pulled out of a Linq to SQL class, working off a SQL database. (I don’t want to get into a discussion on the semantics of my design choice – perhaps Items should be members of Categories instead of the other way around. Just enjoy the show for now…)
What I wanted to do was to filter a Linq query from the database (containing IQueryable<Item>) for items that contained Categories with a given CategoryID. No problems! Just run up a Linq to SQL query to get the Category instance matching that CategoryID as follows:
var myCategory = (from c in datacontext.dbCategories
where c.ID == myCategoryID
select new Category
{
CategoryID = c.ID,
CategoryName = c.Name
}).Single();
Note that the filter as shown below won’t work on an IQueryable<Item> (due to the Contains() clause), so we’ll evaluate the incoming IQueryable<Item> as it comes in, and (for convenience) convert it back to IQueryable on the way out. We’ll implement the method as an extension method, because we can.
public IQueryable FilterByCategory(this IQueryable qry, Category cat)
{
return (from i in qry.ToList() // Evaluate the qry
where i.Categories.Contains(cat)
select i).AsQueryable();
}
Now, this is all fine, as far as it goes. Except for one small thing.
It didn’t work.
My result set was empty. Always.
So what’s going on? Basically, here’s the thing. Have a look at the following code fragments:
int x = 7;
int y = 7;
Debug.Assert(x == y); // Assert is true
class MyObject
{
public int Value;
public MyObject(int val)
{
Value = val;
}
}
MyObject a = new MyObject(7);
MyObject b = new MyObject(7);
Debug.Assert(a == b); // Assert fails!
It may seem obvious, but the reason the second Assert fails is that, despite having the same internal value, a and b are not the same! And this is the problem with my filter query above. Despite the Category instance I received from the SQL query having the same internal values as (perhaps) a Category in the Categories property of an Item, they are not the same object and so the comparison will fail.
What we need is some way of telling .NET that two instances of an object are the same, even if they are different objects, and this is where the IEqualityComparer comes in. We can create the following class:
class CategoryComparer : IEqualityComparer
{
#region IEqualityComparer Members
public bool IsEqual(Category x, Category y)
{
return x.CategoryID == y.CategoryID;
}
public int GetHashCode(Category obj)
{
return obj.CategoryID.GetHashCode();
}
#endregion
}
We have created a class that says that two instances of Category are equal when their CategoryID values match, and have based their Hash Code on the CategoryID value rather than the instance itself. All we need to do now is to plug it in to the query, as follows:
public IQueryable FilterByCategory(this IQueryable qry, Category cat)
{
return (from i in qry.ToList() // Evaluate the qry
where i.Categories.Contains(cat, new CategoryComparer())
select i).AsQueryable();
}
Voila! .NET is a wonderful environment, but there are always a few gotchas lying around for the unsuspecting. Work through probelms methodically, and you’ll find there’s always a way.