Visual Studio 2010 was recently released, and it brought a new version of the TFS client libraries. One of my goals for git-tfs was to have it detect and use the newest available TFS client libraries at runtime. I don’t want to have to build a separate version of git-tfs for each TFS version.

I looked at duck typing, .NET 4’s dynamic features, and the solution I ended up with.

To prepare, I pulled all of the TFS calls into wrapper classes. This isolated the problem so that I could then try to eliminate it. This was the common starting point for all of the solutions I tried.

Duck typing

My first try was to use deft flux’s Duck Typing library. My approach was going to be to have a small amount of reflection that loaded the right TFS assemblies and instantiated the root objects, and then hand off the objects to dynamic wrappers created at runtime by the duck typing library. The problem I ran into was that the library had a hard time figuring out how to unpack duck-typed objects that were parameters to other duck-typed interface methods. For example, let’s say that the original type was

public class Workspace
{
  public void Shelve(Shelveset s, PendingChange [] p, ShelvingOptions o) { /*...*/ }
}

and my duck-type was

public interface IWorkspace
{
  void Shelve(IShelveset s, IPendingChange [] p, TfsShelvingOptions o);
}

The duck-typing library wasn’t able to figure out how to un-duck-type s and p. If I had more uses for the duck-typing library, I would definitely be interested in making that work.

dynamic

Instead, I decided instead to try the new, shiny “dynamic” keyword. This worked out OK, except for enums. I was also annoyed at the complete lack of intellisense in dynamic expressions, and I was surprised by the IL that was generated for dynamic expressions.

In general, “dynamic” in C# is quite a bit less shiny to me now. I had expected the CLR to explicitly handle dynamic invocations at runtime, but it turned out that the dynamic keyword is mostly a compiler trick. It makes more sense to me now why the DLR could be built to run on .NET 2, but it’s a little less magical, too. Also, the dynamic keyword feels clunky. It’s nice to start using it when types get in the way, but you can tell it’s shoe-horned in.

Polymorphic plugins FTW!

The approach I ended up using was polymorphism and plugins. Because the TFS libs all have the same names, I can’t reference them statically from a single assembly. So I have one assembly per TFS version. I moved out the isolated wrapper classes into the plugin assembly for VS2008. I cloned and tweaked the VS2008 assembly to make a VS2010 assembly. And I added a little bit of plumbing to find and load the correct plugin. I haven’t extensively tested this, but it does what I expect when I have all of the client libs or none of the client libs installed.

Other ideas

In the course of working on this, I came up with a couple other ideas that I didn’t try.

One idea was to write my own dynamic invoker using reflection. This would have been something like a home-made duck-typing library or dynamic evaluation. It seemed like a lot of work and not very elegant. And it would have involved a lot of strings.

The other idea is to reimplement git-tfs in ruby. I’d thought about using ruby when I started working on git-tfs, but I figured the convenience of having the TFS client libraries was worth the heavyweight cost of C#. Some recent developments have made ruby a much more viable option: MS acquired the assets of Teamprise and made them available to TFS users, so jruby would be able to use solid TFS client libraries; ironruby hit 1.0, so I would feel more comfortable targeting it; and, MS released VS2010, which precipated by desire to make git-tfs work with multiple TFS versions out of the box.

I’m trying to get started with Ragel, and found a hello world example for ruby that got me past Go. Since the C# example was a bit different, I thought I’d share what I came up with.


 1 %%{
 2   machine hello;
 3   expr = ‘h’;
 4   main := expr @ { Console.Out.WriteLine("greetings!"); } ;
 5 }%%
 6
 7 using System;
 8
 9 namespace Mab.Test
10 {
11   public class Hello
12   {
13 %% write data;
14     public static void Main(string [] args)
15     {
16       foreach(var arg in args)
17       {
18         Console.WriteLine("***** " + arg + " ******");
19         Run(arg);
20       }
21     }
22
23     private static void Run(String data)
24     {
25       int cs;
26       int p = 0;
27       int pe = data.Length;
28       // init:
29       %% write init;
30       // exec:
31       %% write exec;
32     }
33   }
34 }

To see how it works, save the following code to Hello.rl and run these commands:

ragel -A Hello.rl
csc /t:exe /out:test.exe Hello.cs
test.exe a h z