Hey,

I am trying to compile a VB or C# project manually, so that I can use Reflection on the compiled assembly to figure out the types in the assembly and their members.

I am running into a whole load of trouble however, and I cannot find much information about this. Perhaps I haven't looked hard enough, but all I can find is some basic compiling information, like how to compile a mathematical expression to be evaluated dynamically. I am not dealing with 'code snippets' like this, I am dealing with real projects, containing several source files, references, and resources.


At the moment, I am reading a project file (either VB Project or C# Project as their format seems to be the same) using XML and storing the information in a Project class I've created. The Project class isn't very interesting, all you need to know for now is that it has a bunch of properties that I need. I started with these:
- Files: the paths to the source files included in this project
- References: the references (dll name) in this project

In a UserControl (which is going to be used to display the types and their members, in a TreeView), I have a method that accepts a Project instance and then compiles this project. This compilation returns an array of Types which I use to populate the TreeView:
vb.net Code:
  1. Public Sub ParseProject(ByVal prj As Project)
  2.             Me.TreeView.Nodes.Clear()
  3.  
  4.             ' Get the source code from the paths
  5.             Dim sources As New List(Of String)
  6.             For Each file As String In prj.Files
  7.                 sources.Add(System.IO.File.ReadAllText(file))
  8.             Next
  9.          
  10.             ' Compile the project (code later)
  11.             Dim types = CodeParsingLibrary.CodeParser.GetTypes(prj.Language, sources, prj.References)
  12.             If types IsNot Nothing Then
  13.                 ' Add types to treeview (code omitted)
  14.             End If
  15.         End Sub

The compilation is done using a few shared methods in the CodeParser class:
vb.net Code:
  1. Imports System.CodeDom.Compiler
  2. Imports System.Reflection
  3. Imports System.Text
  4.  
  5. Public Class CodeParser
  6.  
  7.     Public Shared Function GetTypes(ByVal language As Languages, _
  8.                                     ByVal sources As IEnumerable(Of String), _
  9.                                     ByVal references As IEnumerable(Of String)) As Type()
  10.  
  11.         ' Compile the source files
  12.         Dim asm As Assembly = CodeParser.CompileSource(language, sources, references)
  13.         If asm IsNot Nothing Then
  14.             Return asm.GetTypes
  15.         Else
  16.             Return Nothing
  17.         End If
  18.     End Function
  19.  
  20.     ' Gets the CodeDomProvider from the language (VB or CSharp for now)
  21.     Private Shared Function GetCodeDomProvider(ByVal language As Languages) As CodeDomProvider
  22.         Dim languageString = String.Empty
  23.         If language = Languages.CSharp Then
  24.             languageString = "CSharp"
  25.         ElseIf language = Languages.VB Then
  26.             languageString = "VB"
  27.         End If
  28.  
  29.         If languageString = String.Empty OrElse Not CodeDomProvider.IsDefinedLanguage(languageString) Then
  30.             Throw New ArgumentException("Provided language is invalid.", "language")
  31.         End If
  32.  
  33.         Dim provider = CodeDomProvider.CreateProvider(languageString)
  34.         Return provider
  35.     End Function
  36.  
  37.     Private Shared Function CompileSource(ByVal language As Languages, _
  38.                                           ByVal sources As IEnumerable(Of String), _
  39.                                           ByVal references As IEnumerable(Of String)) As Assembly
  40.  
  41.         ' Get the CodeDomProvider used to compile
  42.         Dim provider = CodeParser.GetCodeDomProvider(language)
  43.  
  44.         ' Set up some parameters
  45.         Dim params As New CompilerParameters()
  46.         params.GenerateInMemory = True
  47.         params.GenerateExecutable = False
  48.  
  49.         ' Add the referenced assemblies found in the project xml file
  50.         params.ReferencedAssemblies.AddRange(references.ToArray())
  51.  
  52.         ' Finally compile the sources and return the compiled assembly
  53.         Dim result = provider.CompileAssemblyFromSource(params, sources.ToArray())
  54.         Return result.CompiledAssembly
  55.     End Function
  56.  
  57. End Class

Here is where the trouble starts.

It only works correctly when I try to compile an extremely simple C# project, containing just a single class with some base types. This proofs that the concept works.

However, when I try to compile a VB project, I run into trouble. I am getting all kinds of errors returned, three which seem common:

1. If the project uses multiple files, they can never seem to 'see' each other. If Class1 uses an instance of Class2 then compilation tells me that Class2 is not declared. I think this is easy to fix: Class2 cannot compile due to points 2 and 3 below, hence Class1 cannot find Class2 either.

2. I am getting errors like the fact that 'My.Settings' is not defined:
Type 'HatchBrushTest.My.MySettings' is not defined.
Also, I keep seeing the '<Default>' namespace returning, in errors such as:
'HatchBrushTest' is not a member of '<Default>'.
(HatchBrushTest is the project name). I've no idea what causes this and how to fix it...

3. I am getting errors that types such as 'Color' and 'Rectangle' and 'Control' are not declared. This seemed strange to me at first: I had already added 'System.Drawing.dll' and 'System.Windows.Forms.dll' to the referenced assemblies..? But then it occurred to me that this is not enough: you also need to use the Imports statement for these namespaces. Since VB can automatically ("implicitly"?) import some namespaces for you (System.Drawing is one of them by default), they do not appear in the source code files, and hence the types aren't found.

For point 3, I had another look in a VB project file, and luckily the file does list these 'invisible' imports. So, I extended my Project class to extract the imports statements as well.

But now I'm "stuck". I now have a list of source files, and a list of imports statements to add to them. At first I thought that wasn't too hard: just add the imports statement to the front of every source file manually. So I passed the imported namespace around to the compile method and did this:
vb.net Code:
  1. Private Shared Function CompileSource(ByVal language As Languages, _
  2.                                           ByVal sources As IEnumerable(Of String), _
  3.                                           ByVal references As IEnumerable(Of String), _
  4.                                           ByVal [imports] As IEnumerable(Of String)) As Assembly
  5.  
  6.         ' Get the CodeDomProvider used to compile
  7.         Dim provider = CodeParser.GetCodeDomProvider(language)
  8.  
  9.         ' Set up some parameters
  10.         Dim params As New CompilerParameters()
  11.         params.GenerateInMemory = True
  12.         params.GenerateExecutable = False
  13.  
  14.         ' Add the referenced assemblies found in the project xml file
  15.         params.ReferencedAssemblies.AddRange(references.ToArray())
  16.  
  17.         ' Add the imports statements to the front of every source file
  18.         Dim sourceFiles As List(Of String)
  19.         sourceFiles = New List(Of String)
  20.         For Each source As String In sources
  21.             Dim sb As New StringBuilder()
  22.             For Each imp As String In [imports]
  23.                 sb.AppendLine("Imports " & imp)
  24.             Next
  25.             sb.AppendLine()
  26.             sb.Append(source)
  27.             sourceFiles.Add(sb.ToString())
  28.         Next
  29.  
  30.         Dim result = provider.CompileAssemblyFromSource(params, sourceFiles.ToArray())
  31.         Return result.CompiledAssembly
  32.     End Function
But that gives me more problems:
- I can't always add them to the top of the file, because there might be 'Option' statements that need to precede the Imports statements.
- I can't add all of them 'blindly', I have to check if they are imported already, because the compilation fails if there is a duplicate imports statement.

So I would have to do quite some string manipulation (or regex possibly) to first figure out which imports are there already, and where I need to put them. That doesn't seem like a very good approach... But I can't find any other way. There doesn't seem to be a way to set the implicit imports via the CompilerParameters or something (like I can set the references)...


Should I really use this cumbersome approach, is it the only way?

Also, am I correct about point 1 resolving itself if points 2 and 3 are resolved, and how can I solve point 2?

Finally, if anyone knows a good 'tutorial' or resource on this subject feel free to post it. I've looked around MSDN obviously but apart from some very simple examples I found nothing to help me.


Thanks!