I didn't want to believe it, but it's slowly becoming inescapable: all roads lead to Lisp.
Anyway, I did what any lazy C# coder would do; I abstracted the outer logic into a single method that took a C# delegate as a parameter, and called it with two different delegates to handle the two different inner sections of this monstrosity. For the uninitiated, by the way, a C# delegate is an enormously complex and advanced computer science construct that most of the smarter folks in the field call a 'function'. Why they felt it was more efficient to add this new thing instead of just adding first-class functions -- and believe me that their approach is the hard way, because delegates by their design basically require you to think about things like closures and continuations very carefully -- continues to elude me, but if the only screwdriver available is a left-handed electric drill powered by a gerbil in a wheel I know where I'm going if I've got a screw. The main point of this paragraph is that I was conscious of the ugliness of my solution as I was writing it; it was just structurally inelegant. The fact that the C# 2.0 anonymous delegate support (mmm, syntactical sugar) negated the need to define independent functions didn't really help; it was ugly. It would've been much better to simply pass some code into the function directly, somehow.
Today I had to revise a caching system; items can be fetched based on one of two criteria, and storing two copies in the cache was ugly. My solution was to cache them under a single identifier and maintain a map from the first type of identifier to the second. It's not a terribly complex thing, but the vagaries of the .NET caching system and some additional constraints imposed by the app made it relatively difficult to get exactly right. When I realized that I needed to apply this same methodology to several different kinds of things, I sighed deeply and got to work writing several delegate definitions and adding <generic> tags all over. In the end I had a system that could be used for any class fetchable by these two criteria (I didn't feel up to building it generic enough to multicache based on any two criteria). Two delegates, two wrapper methods, etc. Would've been much easier to just pass a block of code in, but you do what you can with what you have where you are, n'est-ce pas?
It turns out that Microsoft has been having the same sort of problem, actually. Far too many people have written off the new features in C# 3.0 as mere syntactical sugar to make LINQ easy, but there's at least one item of massive importance that a lot of people seem to have missed. The new lambda expression in C# 3.0 are more than just a still-terser form of anonymous delegates, because in some situations -- and I'm still not clear as to the when or the why, nor about whether non-Microsoft coders can easily join in the fun -- they aren't sent as delegates, but as parsed expression trees. Trees which the receiving code may then, if it wishes, alter, compose, or mangle in whatever way it sees fit.
That's how LINQ-to-DB works; the syntactical sugar decodes into a bunch of lambda functions which are passed as expressions to the appropriate LINQ-provided method, which can then interpret, parse, and combine them so as to produce a reasonable query (as opposed to the query-all-join-in-code that would be necessary without this change). Suddenly, like a bolt from the blue, .NET supports passing code to a method.
It remains to be seen whether anything useful can actually come of this, of course. If it's only used for LINQ and nothing else then it's just another example of Microsoft embracing and extending, though I guess it's a bit less nefarious when they do it to their own product. But if library-builders can wrap their brains around this -- and if the implementation is robust enough to be useful for anything beyond the narrow case for which it was invented -- this is pretty cool stuff.
Here's hoping .NET 4.0 lets an assembly access itself as an expression tree, eh?