When I saw MSBuild for the first time I thought - "Yeah, good improvement over the standard makefile - we now have XML (surprise!!) and it is extensible". The real import of word “extensible” struck home when I started writing custom tasks. The concept of being able to call an army of reusable objects from a makefile – well we’ll start giving them more respect and start calling them proj files - is quite empowering. But (ahh there’s the but) there’s one pain point. Writing a task means writing a new class which means more testing, maintenance etc etc. And what if all you wanted to do was to say hey! (Well I can’t really think of 101 reasons to say “hey” in a program … but you get the point). So I wrote this object that executes C# code passed to it as a parameter.
So now to greet someone all you need to do is:
<ExecuteCode Code =""Wotcher!"" />
Yeah I know the " spoils the effect L
You could also get something back from the object. Say you wanted a property in your proj file that had the value of current time:
Code ="DateTime dt = DateTime.Now;
ReturnValue = dt.ToString();">
<Output TaskParameter ="OutputString" PropertyName ="Time" />
Now $(Time) can be used like any other property in your proj file. I had hard-coded a few standard assemblies and “using directives” for ease of use of the object. For the sake of extensibility I take in the assemblies to be referenced and the namespaces that’ll figure in the “using directives” as parameters of the Task. So if you are strictly against hard-coding anything you could take them as parameters like this:
ReturnValue = dt.ToString();"
<Output TaskParameter ="OutputString" PropertyName ="CurrentDate" />
Behind the Scenes
ExecuteCode is an object that adds some boilerplate code to the parameter that gets passed in, compiles it and then uses reflection to load up the assembly and execute the method.
Compiling CSharp code is rather easy:
CSharpCodeProvider csc = new CSharpCodeProvider();
CompilerParameters cp = new CompilerParameters();
cp.GenerateInMemory = true;
string program = ConstructProgram();
CompilerResults cr = csc.CompileAssemblyFromSource(cp, program);
ConstructProgram() is a function that adds the boilerplate code to the code that was passed in (like using directives, class name, method name, closing braces)
We can also set the assemblies to be referenced in the CompilerParameters object like this
if (cr.Errors.HasErrors == false)
Assembly assembly = cr.CompiledAssembly;
Object o = assembly.CreateInstance(ClassName);
Type t = o.GetType();
MethodInfo mi = t.GetMethod(MethodName);
OutputString = (string)mi.Invoke(o, null);
returnValue = true;
foreach (CompilerError err in cr.Errors)
Log.LogError("(" + err.Line + "," + err.Column + "): " + err.ErrorText);
returnValue = false;
If you don’t have any errors in the code that you passed in(fat chance!) then load the assembly and invoke the method or else spew out the errors.
Giving something back
Also notice that the method need not be Main and hence can have any signature. The ConstructProgram() method adds a “return ReturnValue” statement. So to return something from your code you just need to set the ReturnValue variable. Notice that the output property OutputString is set to the return value of the method.