Another GREAT question in MS newsgroups today.
I'm using the CSharpCodeProvider to buils some assemblies @ runtime which are
never saved as files (cp.GenerateInMemory = true;).
The generated assemblies are hierachically dependent on each other so I
generate the "bottom" assemblies first.
How do I add a dependency to another previously loaded (generated) assembly?
I would be happy if CompilerParameters.ReferencedAssemblies.Add could take a
System.Reflection.Assembly reference as parameter.
A possible solution would be to generate dll files and reference them but I
like the idea of not having any files to cleanup when my application exits.
I have seen this one a few times before but googling this subject brings up nothing so I will give a quick write up on it.
Often times when generating code for runtime use the assembly is simply created in memory. This prevents us from having to write out a temporary file for the assembly. Since the assembly is intended to have the same lifespan as our appdomain and the assembly is loaded into memory anyways, there is little use in us writing it to a temporary file as well.
A common issue that people run into is that it is rare that the assembly is completely stand-alone. It often times has to reference other assemblies. This is quite straight forward when you are dealing with assemblies that are on disk, just add the assembly as follows
Parameters.ReferencedAssemblies.Add("System.dll");
Sometimes however we want our assembly to reference another dynamically generated assembly. In this case many resort to writing the dependency out to disk as a temp file then reference the disk based tempory assembly when they generate the main assembly. This does not need to be done, all we need to do is assign an OutputAssembly to our first assembly and it will be loaded into our appdomain as that name. As such when the main assembly is loaded it will be seen that the assembly is in fact already in memory and as such accessible.
This method does not always work though. If the assembly that you are generating is to be hosted in another app domain (lets say it is its own process, take the example and make the example generate an executable for the main assembly that it then tries to run). The in memory assembly will only be within the calling appdomain and as such will not be accessible to the new appdomain.
In the sample code two assemblies are generated. The satellite assembly which contains a single class OtherObject that has a static method SomeMethod and the main assembly which a class MainObject with a method Test that calls into OtherObject from the other assembly. You will notice the two key points of the code are in
GenerateSatelliteAssembly : Parameters.OutputAssembly = "inmemoryassembly.dll"
GenerateMainAssembly : Parameters.ReferencedAssemblies.Add("inmemoryassembly.dll");
That said ... Here's the code!
using System;
using System.Reflection;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
namespace TestInMemoryAssemblies
{
class MainEntryPoint {
private static void CheckResults(CompilerResults Results) {
if(Results.Errors.Count > 0) {
throw new System.Exception("Compile Failed");
}
}
static Assembly GenerateMainAssembly() {
Microsoft.CSharp.CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
ICodeCompiler compiler = codeProvider.CreateCompiler();
CompilerParameters Parameters = new CompilerParameters(new string[] {"mscorlib.dll"});
Parameters.ReferencedAssemblies.Add("System.dll");
Parameters.ReferencedAssemblies.Add("inmemoryassembly.dll");
Parameters.GenerateExecutable = false;
Parameters.GenerateInMemory = true;
string source = "";
source += "public class MainObject {";
source += " public void Test() {";
source += " System.Console.WriteLine(\"Running in main assembly, calling satellite assembly\");";
source += " OtherObject.SomeMethod();";
source += " System.Console.ReadLine();";
source += " }";
source += "}";
CompilerResults results = compiler.CompileAssemblyFromSource(Parameters, source);
CheckResults(results);
return results.CompiledAssembly;
}
static void GenerateSatelliteAssembly() {
CSharpCodeProvider codeProvider = new Microsoft.CSharp.CSharpCodeProvider();
ICodeCompiler compiler = codeProvider.CreateCompiler();
CompilerParameters Parameters = new CompilerParameters(new string[] {"mscorlib.dll"});
Parameters.ReferencedAssemblies.Add("System.dll");
Parameters.OutputAssembly = "inmemoryassembly.dll";
Parameters.GenerateExecutable = false;
Parameters.GenerateInMemory = true;
string source = "";
source += "public class OtherObject {";
source += " public static void SomeMethod() {";
source += " System.Console.WriteLine(\"Hello World from other object located in an in memory assembly\");";
source += " }";
source += "}";
CompilerResults results = compiler.CompileAssemblyFromSource(Parameters, source);
CheckResults(results);
}
static void Main(string[] args) {
//build the satellite assembly
GenerateSatelliteAssembly();
//build the main assembly
Assembly Generated = GenerateMainAssembly();
//Get the main object
object o = Generated.CreateInstance("MainObject");
//Get the type of the main object
Type t = o.GetType();
//Request the Test method from the type
MethodInfo mi = t.GetMethod("Test");
//call the test method
mi.Invoke(o, null);
}
}
}
*UPDATE*
This only gives the appearance of working properly. This will in fact load the satellite assembly into the appdomain twice.
'ConsoleApplication6.vshost.exe' (Managed): Loaded 'C:\Documents and Settings\Greg Young\My Documents\Visual Studio 2005\Projects\ConsoleApplication6\ConsoleApplication6\bin\Debug\ConsoleApplication6.exe', Symbols loaded.
'ConsoleApplication6.vshost.exe' (Managed): Loaded 'inmemoryassembly', No symbols loaded.
'ConsoleApplication6.vshost.exe' (Managed): Loaded 'wessmgnq', No symbols loaded.
'ConsoleApplication6.vshost.exe' (Managed): Loaded 'C:\Documents and Settings\Greg Young\My Documents\Visual Studio 2005\Projects\ConsoleApplication6\ConsoleApplication6\bin\Debug\inmemoryassembly.dll', No symbols loaded.
The reason that this happens appears to be some oddities with the CSharpCompiler. From poking through some reflectored code it appears that the CSharpCompiler always writes out files and the lifespan of these files is what is controlled by the “in memory” flag. It further appears that by giving an assembly a name you force it to be treated as a normal assembly at some points and an in-mmeory assembly at others as opposed to a normal inmemory assembly :( I probably should have figured out this from the fact that it didn't work with the automagically generated names but the final decision on this is that there is no way to do this.