Universal method chaining in C# 3.5
04/07/2009
Phil Gilmore
Here I will present a solution that provides method chaining in C# 3.5. I hope it helps you and I hope that we will see it added as part of the next update to the .NET.
Is it really a big deal?
I've been working more and more with jQuery and have become accustomed to the convenience of method chaining. But when I come back to C# I find that I'm annoyed that method chaining seems to have been left out of the picture, even as many of the newer extension methods were implemented in C# 3.5. It is especially noteworthy that so many of the methods in the IEnumerable<> and List<> classes return void. I have always wanted to do method chaining on the Sort() method for example. Without chaining, a method requires a variable to hold the reference and in many situations this can be a hassle. Here is a useless example of the standard means of using some of these methods:
Standard syntax:
List<int> myList = (new int[] { 5, 4, 3, 2, 1 }).ToList();
myList.AddRange(new int[] { 6, 7, 8, 9, 10 });
myList.Sort();
myList.Reverse();
myList.ForEach(i => i + 1);
myList.Insert(0, 0);
return myList;
You can see in the example above that every operation must be done independently and that a variable declaration is required when chaining isn't allowed. Given the flow of chainable extension methods introduced in .Net 3.5, (take the ToList() method, for example), I would like all methods of the List<> class to be chainable. Then my could would look like this:
Ideal syntax:
return (new int[] { 5, 4, 3, 2, 1 })
.ToList()
.AddRange(new int[] { 6, 7, 8, 9, 10 })
.Sort()
.Reverse()
.ForEach(i => i + 1)
.Insert(0, 0);
Wouldn't that be cool? We can make this possible using extension methods. The problem with this is that you have to write an extension method for every method of every class you want to make chainable. Well, I decided to start with a few of the more useful methods of the List<> class. I quickly realized that they all looked the same and some of the code could be further generalized and reused. Before long, I had a single extension method that all of my extension methods were calling. Here it is:
The Chain method:
public static T Chain<T>(this T source, Action<T> operation) where T: class
{
operation(source);
return source;
}
Simple enough. Remember to put this into a STATIC class as is required for every extension method.
So my Sort() method can now return the result from an internal call to this Chain<> method and the consuming code could look just like the hypothetical code I presented above. At this point, I have written all the extension methods that I expected to write. But given the way the implementation worked out, I can now call ANY method on ANY object as a chained method with just a little effort. This was an unexpected side effect. Anyone who has written a few extension methods probably knows that you could go on forever writing extension methods for every type to do any number of things that you may or may not ever use and still you would never run out of methods to write. Since I can chain anything with this single method, I no longer have to worry about thinking of every contingency I may need to add to the List<> class. For example. If someone else writes an extension method to encrypt list data, for example,
List<string> data = LoadStringsFromDatabase().ToList();
data.Encrypt();
return data;
I would normally have to write an extension method to make it chainable if the method returns void. But this is not necessary if you use the Chain<> method.
return LoadStringsFromDatabase().ToList().Chain(s => s.Encrypt());
While a tad more verbose than ideal, it will work under any circumstance with any void method of any object type. So using just the Chain method, let's take another crack at that first example:
Much better:
return (new int[] { 5, 4, 3, 2, 1 })
.ToList()
.Chain(s => s.AddRange(new int[] { 6, 7, 8, 9, 10 }))
.Chain(s => s.Sort())
.Chain(s => s.Reverse())
.Chain(s => s.ForEach(i => i + 1))
.Chain(s => s.Insert(0, 0));
This is not far off from what I wanted. As before, it is only slightly more verbose than using method-specific chaining extension methods, but not much. You will seldom chain so many things or perform such a mundane operation as my example here, but I have found it very useful in common situations, such as ...
Inserting an element into a list at index 0 inline:
drpState.DataSource = Data.GetStates()
.Chain(lst => lst.Sort())
.Chain(lst => lst.Insert(0, new State(" --- ")));
Operating on collections of composite anonymous types:
This is useful because you can't reasonably declare a variable or list variable of anonymous types. I know this looks like a bad example because it could be done using Linq-To-Sql, but this is a fair example because TDS has a 2100 element limit which the .Contains() clause can violate if not handled carefully and this is one solution that may be used:
List<object> idList = (
from od in db.OrderDetails
where o.OrderID.Equals(orderID)
select new { ID = od.OrderDetailID, Name = od.Name, Number = od.DetailNumber }
).ToList().Chain(
lst => lst.RemoveAll(
i => subpoenaIDList.Contains(i)
)
).Cast<object>().ToList();
Modifying properties of a data entity when passing it as a parameter:
db.Orders.Add(myOrder.Chain(o => o.LastUpdated = System.DateTime.Now));
Target ambiguity:
An issue that deserves mention is target ambiguity. There are two ways that chaining can be implemented. The first is where the source is modified and then returned (such as is the case in my method), and the second is where the source is duplicated, modified, and the new copy is returned, leaving the original data intact. Both can be implemented using the method I outlined in this post.
The important point to make is that it should be obvious to the user which behavior your chain method is using. I have failed in this regard and encourage you to improve on the concepts I have outlined here.
Many developers subscribe to a school of thought that one method should be used and the other should not. Feel free to do it either way you like. I personally prefer to return a copy rather than a modified original source and may yet modify my method to do so.
Phil Gilmore ( www.interactiveasp.net )