26.03.2010 г.

Using the Microsoft .NET CodeDom Framework

Recently I learned some CodeDom stuff that I consider worth sharing.

CodeDom is an API,included in .NET Framework, that gives us an object oriented way of dealing with the csc.exe (The C# compiler). Visual studio uses this console application for compiling your projects. CSC.exe accepts a whole bunch of parameters. For instance parameters for the path to output to,path for looking for referenced assemblies, parameter that tells the compiler whether to create a library or an executable and so on. The list is really long. Good thing is that you don't actually need all of the parameters. HERE is a reference for the compiler options. I will mention and explain below just the ones I needed in my case.

My task consisted in creating an API for parsing source code that the user of the system creates and compiling that source code in a separate assembly. After that these assemblies are executed on some events in the system.
Example:Lets say your system logs to the windows event log on error. However some of you clients want to send e-mail on every error in the system. So with the "custom code" approach you can just raise event on error in the system. Then for every client wish you can create separate implementation... you know one for writing to the windows event log, another for sending an e-mail to the system administrator etc. And the last step is hooking these implementations to the event. This is a good way for code injection.
To be honest I didn't came to this architecture alone. My project manager gave me this idea and after I developed it and saw it in action, I should admit that it really rocks.

So lets get down to the nitty gritty.

First of all lets see what kind of input I expect for the source code that is going to be compiled.


1: assemblyName:Test.dll
2: className:TestClass
3: include:System.dll
4: include:System.Core.dll
5: include:System.Xml.dll
6: include:NotFrameworkAssembly1.dll
7: include:NotFrameworkAssembly2.dll
8: include:NotFrameworkAssembly3.dll

9: using System;
10: using NamespaceFrom.NotFrameworkAssembly1;
11: using NamespaceFrom.NotFrameworkAssembly2;
12: using NamespaceFrom.NotFrameworkAssembly3;

13: public class TestClass : SomeBaseClass
14: {
15: public override void Method1FromTheBaseClass()
16: {
17: //TODO Add some code that you can execute later
18: }

19: public override void AnotherMethodFromTheBaseClass()
20: {
21: //TODO: Send e-mail
22: }
23: }

Between lines 1 and 8 is the needed information apart from the actual code. There are:
1 - The name of the assembly to be created;
2 - The name of the class ,which name will be used later to create an instance of.
3..8 - List of the assemblies that will be included as referenced assemblies
9..23 - The actual code.

I created a class that represents the raw data above. The class has some properties that I need and I initialize them by passing the raw data to the 'ParseRawSource' method.

public class AssemblyGenerationInfo
{
public List ReferencedAssemblies { get; set; }
public string SourceCode { get; set; }
public string ClassName { get; set; }
public string AssemblyName { get; set; }

public void ParseRawSource(string inputStr)
{
}
//THE MAJORITY OF THE CODE IS OMITTED


Here is finally the actual code for compiling the assembly. There are some key things that are worth of mentioning:
1) Adding a 'lib' param that points to thePathWhereTheReferencedAssembliesReside;
2) Setting the 'CompilerVersion' to 'v3.5';
3) Assigning 'buildOutput' out parameter so that whoever calls the method will get eventual build errors.


public bool CompileAssembly(AssemblyGenerationInfo agi, out string buildOutput)
{
string outputDirectoryPath = "some path";
string thePathWhereTheReferencedAssembliesReside = "some path";
if (agi == null)
{
buildOutput = string.Format("Code to compile was not provided!");
return false;
}

CompilerParameters parameters = new CompilerParameters(agi.ReferencedAssemblies.ToArray());
parameters.IncludeDebugInformation = false;

parameters.CompilerOptions = string.Format(@"/lib:""{0}""", thePathWhereTheReferencedAssembliesReside);
string fullPathToTheCompiledAssembly = Path.Combine(outputDirectoryPath, agi.AssemblyName);

if (!Directory.Exists(outputDirectoryPath))
{
Directory.CreateDirectory(outputDirectoryPath);
}
parameters.OutputAssembly = fullPathToTheCompiledAssembly;


CSharpCodeProvider csc = new CSharpCodeProvider(new Dictionary() { { "CompilerVersion", "v3.5" } });

CompilerResults results = csc.CompileAssemblyFromSource(parameters, agi.SourceCode);
if (results.Errors.Count == 0)
{
buildOutput = null;
return true;
}
else
{
StringBuilder sb = new StringBuilder();
foreach (CompilerError error in results.Errors)
{
sb.Append(error.ToString() + Environment.NewLine);
}
buildOutput = sb.ToString().TrimEnd();
return false;
}
}


I hope this post was interesting and helpful.
Enjoy.