Introduction
In this article I'll show you how to load an assembly dynamically on demand. Recently I worked on a project where I needed to load the same assembly from different code bases. Assemblies were located in different locations as per the environment (dev/qa/uat/prod). Also I wanted to load them into a different application domain, so they will work in their own domain and will not interfere with any of the other tasks. When I am done with the assembly, I can unload them.
Current AppDomain vs new AppDomain
The Current application domain is the AppDomain, where your current application is running. If you load assemblies in the same AppDomain, they will be locked, and cannot be unloaded. It will remain there until you exit from the application. This situation fails the requirement of loading multiple assemblies. If an assembly is loaded in a different AppDomain then it will allow us to unload the AppDomain at any point of time and release memory.
Instantiating Class
If an assembly is loaded into the same AppDomain, then the class can be instantiated in the usual way. But if an assembly is loaded into a different AppDomain then it can be instantiated using reflection. Another way is an interface. However this interface should be implemented in the assembly which is being loaded dynamically. Here I am going over a second method where, I created an interface in an assembly which is being referered by both assemblies; the current appdomain assembly and the assembly which I want to load dynamically.
I wanted to create an AddIn for outlook which pushes a meeting from outlook to my application. I wanted to allow pushing the meeting to a different environment based on the option the user selects. These options are available based on the user's group. I am not going to discuss an Outlook AddIn here. I will discuss that in another article dedicated to Outlook.
I created a an assembly AddInCommon where I created an interface IMyAddIn and implemented it in a MyAddIn abstract class.
public interface IMyAddin
{
void ShowMessage();
void Increment();
}
public abstract class MyAddIn : MarshalByRefObject, IMyAddin
{
public abstract void ShowMessage();
public abstract void Increment();
}
This class will be inherited in the class of the dynamically loaded assembly.
[Serializable]
public class MyClass
{
public int counter = 0;
public string ShowMessage()
{
Console.WriteLine("Assembply Path : " + Assembly.GetExecutingAssembly().CodeBase);
Console.WriteLine("MDLibOne.MyClass.ShowMessage");
return "Assembply Path : " + Assembly.GetExecutingAssembly().CodeBase;
}
public void Increment()
{
Console.WriteLine("Counter : " + ++counter);
}
}
MyClass is Serializable because this class will be used in the currentAppDomain, while it is defined in its own assembly. An object needs to be serialized and deserialized back in the currentAppDomain. Both application domains have different memory allocations and can't share the object. Create an AppDomainSetup for each codebase you want to load. The following example creates the AppDomainSetup and stores them in a dictionary, so I don't do setup again to create an application domain each time when we want to instantiate a class. This also allows me to unload.
AssemblyLoader class
This class is responsible for managing the lifetime of AppDomains.
AppDomainSetup domainSetup = new AppDomainSetup();
domainSetup.ApplicationName = Path.GetFileNameWithoutExtension( file.Value.ToString() );
domainSetup.ConfigurationFile = Path.GetFileName(file.Value.ToString()) + ".Config";
domainSetup.ApplicationBase = Path.GetDirectoryName(file.Value.ToString());
AppDomain appDomain = AppDomain.CreateDomain(domainSetup.ApplicationName, null, domainSetup);
_appDomains.Add(file.Key.ToString(), appDomain);
MyClass addin = appDomain.CreateInstanceAndUnwrap(domainSetup.ApplicationName, domainSetup.ApplicationName + ".MyClass") as MyClass;
_addIns.Add(file.Key, addin);
Calling the AddIn:
private void button1_Click(object sender, EventArgs e)
{
MDLibOne.MyClass c = new MDLibOne.MyClass();
MessageBox.Show(c.ShowMessage());
AssemblyLoader addInLoader = new AssemblyLoader();
foreach (KeyValuePair addIn in addInLoader.AddIns)
{
MessageBox.Show(addIn.Value.ShowMessage());
}
}
Wrapping Up
This example will show that it is loading from appropriate location. But still it has an issue that the object created has a short time span. The object created with this method will soon expire and will throw an exception. The object will be timed out after some time. The lifetime will be decided by the source assembly. I am going to discuss this in my next article.