Using Extension Methods in .NET Framework 2.0
The code base I am currently working with has a collection class that inherits from ArrayList. The purpose of this class is to offer a unique list of objects and to provide case insensitive comparisons when adding a unique item to the collection. The collection does not need to be unique, but we a way to add unique things to the list is required.
My task the last week was to become familiar with the code base and identify possible refactorings. Using NDepend, I was able to determine that this class is typically used with strings. One possible refactoring I suggested was to replace this custom class with List(). An out of sprint hit squad was formed to tackle doing just that. We came in on Saturday morning bright and early and got started.
Doing a global search and replace to change this custom ArrayList class with List turned up thousands of compiler warnings where the method AddUnique() was called.
Our first stab at tackling this problem was to create a helper method that added an item to a given list only if that list did not already contain the item. We wrote a macro to quickly turn the old references into the new helper references. The code looked something like this:
tableList.AddUnique("item_table")
ListHelper.AddUnique(tableList, "item_table")
This seemed to get us around the uniqueness hurdle. It was a bit verbose, but it got the job done and would eventually allow us to get rid of the custom collection class altogether. We started hacking away, taking turns at the keyboard with three sets of eyes making sure we were hitting the right items and evaluating between List and ArrayList where appropriate.
After a few hours of locating problem areas and fixing them with a handy macro we created out of sheer boredom, the hit squad decided this was not working. I mentioned that if we were using Visual Studio 2008, we could write an extension method that would simply attach an AddUnique() method to the List class. We knew that Visual Studio 2008 allowed you to target the 2.0 framework. So we could upgrade the solution to VS2k8, leave all our projects targeting 2.0 and be able to use extension methods.
I left the 8 hour marathon triple-programming session with the goal of playing around with implementing extension methods in Visual Studio 2008. Since I was not familiar with actually writing them, I fired up my IDE and wrote a couple tests for what I wanted to happen.
[Test]
public void Can_Add_Unique_String_To_List_Of_String()
{
var list = new List<String>();
list.AddUnique("one");
list.AddUnique("one");
Assert.AreEqual(1, list.Count);
}
[Test]
public void Can_Add_Case_Insensitive_Unique_String_To_List_Of_String()
{
var list = new List<string>();
list.AddUnique("one");
list.AddUnique("ONE");
Assert.AreEqual(1, list.Count);
}
I flipped over to my Extensions library and added a ListOfStringExtensions class. To get the code to compile I quickly stubbed out the method like this:
public static void AddUnique(this List list, string item) { }
Using the ReSharper unit test runner, I verified that my test indeed fail. The moved on to implementing the method logic. My first attempt at implementing the method looked something like this:
public static void AddUnique(this List<string> list, string item)
{
foreach(var s in list)
{
if(s == item)
return;
}
list.Add(item);
}
This allowed my first test to pass, but not the second. So I modified the implementation like so:
public static void AddUnique(this List<string> list, string item)
{
foreach(var s in list)
{
if(String.Compare(s,item,true) > 0)
return;
}
list.Add(item);
}
Now both tests pass. I continued down this TDD path until I had a nice set of extension methods and unit tests that satisfied all the requirements to get rid of our custom collection class. In the end I created AddUnique, AddUniqueRange, CaseInsensitiveContains, IsUnique and ToUniqueList extension methods which all work nicely together with a full suit of unit tests.
The next task at hand was to get the extension methods working in projects that were targeting the 2.0 framework. I set my unit test class to target 2.0 and verified that they still worked. I got an warning as soon as I set the framework target that I had references to projects targeting a different framework. The tests ran fine.
I then changed the target of my extension library to 2.0 and got this nasty error at compile time:
Cannot define a new extension method because the compiler required type 'System.Runtime.CompilerServices.ExtensionAttribute' cannot be found. Are you missing a reference to System.Core.dll?
Hrm.. ExtensionAttribute seems to be a 3.x feature. But I had started out this adventure reading ScottGu's blog where he says that extension methods are language syntactical sugar and should work fine with the 2.0 framework. So I fired up Chrome and hit google.
The first result for my query happened to be my good friend Nate Kohari's blog. Nate is the creator of Ninject the amazing Dependency Injection framework. He also recently release his new web site IdeaVine. He is an awesome guy and as usual he already blazed the path I was walking down.
According to Nate, all I needed to do was add an attribute class in a specific namespace to get around the compiler issue.
//override the .net 3.5 compiler services for .net 2.0 compatibility
//see: http://kohari.org/2008/04/04/extension-methods-in-net-20/
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class ExtensionAttribute : Attribute
{
}
}
Everything compiles and test run fine targeting the 2.0 framework. In morning I plan to verify this by running sample code on a fresh box with only the 2.0 framework installed. As always you can get my full source code here.