Assemblies can be loaded at runtime, through reflection, using the System.Reflection.Assembly class into the current application domain. Unfortunately, there is no way to unload an assembly without unloading the entire application domain that contains it. To allow loading and unloading an assembly, I have created the AssemblyPlugin class which loads and unloads assemblies in an isolated application domain through remoting.
How it Works
When the AssemblyPlugin class is initialized, a dedicated application domain is created to load assembly references, create entities and class instances as well as instantiate an instance of PrivateRemoteAssembly in the new application domain (more on this later):
/// <summary> /// Initializes the plugin from an assembly given it's file name or path. /// </summary> /// <exception cref="System.ArgumentNullException">assemblyFile is null.</exception> /// <exception cref="System.IO.FileNotFoundException">assemblyFile is not found, or the module you are trying to load does not specify a filename extension.</exception> /// <exception cref="System.IO.FileLoadException">A file that was found could not be loaded.</exception> /// <exception cref="System.BadImageFormatException">assemblyFile is not a valid assembly.</exception> /// <exception cref="System.ArgumentException">The assemblyFile parameter is an empty string ("").</exception> /// <exception cref="System.IO.PathTooLongException">The assembly name is too long.</exception> /// <exception cref="System.IO.FileNotFoundException">An assembly was not found.</exception> public AssemblyPlugin(string assemblyFile) { if (assemblyFile == null) throw new ArgumentNullException("assemblyFile"); //get current domain AppDomain current = AppDomain.CurrentDomain; //get existing setup AppDomainSetup currentSetup = current.SetupInformation; //setup domain settings from existing setup AppDomainSetup setup = new AppDomainSetup { ApplicationBase = currentSetup.ApplicationBase, PrivateBinPath = currentSetup.PrivateBinPath, PrivateBinPathProbe = currentSetup.PrivateBinPathProbe }; //create the new domain with a random yet relevant FriendlyName _Domain = AppDomain.CreateDomain( string.Format("{0} - AssemblyPluginContainer[{1}]", current.FriendlyName, Guid.NewGuid()), null, setup); try { //attempts to create the plugin _Plugin = CreateEntity<PrivateRemoteAssembly>(assemblyFile); } catch { //if the assembly failed to load, unload the domain AppDomain.Unload(_Domain); //rethrow the exception throw; } }
To pass a type between application domains, the type must be serializable. To communicate between application domains, a type must derive from MarshalByRefObject. Types are initially created in the new AppDomain through the CreateEntity method to enable access across application domain boundaries:
/// <summary> /// Locates the specified type from this assembly and creates an instance of /// it using the system activator, with optional case-sensitive search and having /// the specified arguments. /// </summary> /// <typeparam name="TReturn">The type of object to return where TReturn : MarshalByRefObject.</typeparam> /// <param name="typeName">The <see cref="System.Type.FullName"/> of the type to locate.</param> /// <param name="args">An array that contains the arguments to be passed to the constructor. This /// array of arguments must match in number, order, and type the parameters of /// the constructor to be invoked. If the default constructor is desired, args /// must be an empty array or null.</param> /// <returns>An instance of the specified type, or null if typeName is not found. The /// supplied arguments are used to resolve the type, and to bind the constructor /// that is used to create the instance.</returns> /// <exception cref="System.ArgumentException">typeName is an empty string ("") or a string beginning with a null character. /// -or-The current assembly was loaded into the reflection-only context.</exception> /// <exception cref="System.ArgumentNullException">typeName is null.</exception> /// <exception cref="System.MissingMethodException">No matching constructor was found.</exception> /// <exception cref="System.NotSupportedException">A non-empty activation attributes array is passed to a type that does not /// inherit from System.MarshalByRefObject.</exception> /// <exception cref="System.IO.FileNotFoundException">typeName requires a dependent assembly that could not be found.</exception> /// <exception cref="System.IO.FileLoadException">typeName requires a dependent assembly that was found but could not be loaded.-or-The /// current assembly was loaded into the reflection-only context, and typeName /// requires a dependent assembly that was not preloaded.</exception> /// <exception cref="System.BadImageFormatException">typeName requires a dependent assembly, but the file is not a valid assembly. /// -or-typeName requires a dependent assembly which was compiled for a version /// of the runtime later than the currently loaded version.</exception> public TReturn CreateInstance<TReturn>(string typeName, params object[] args) where TReturn : MarshalByRefObject { //create and return instance return (TReturn)_Assembly.CreateInstance( typeName, false, BindingFlags.CreateInstance, null, args, null, null); }
An instance of PrivateRemoteAssembly is created in the new application domain and is responsible for communicating between application domains. When PrivateRemoteAssembly is initialized, the PrivateRemoteAssemblyLoader is initialized for loading the assembly and all references in the new application domain.
To load all references, a collection of loaded assembly names or paths and assemblies are maintained. The target assembly is loaded and all references are recursively loaded. If any reference fails to load, the AppDomain.CurrentDomain.AssemblyResolve event is fired and the assembly is loaded from file:
/// <summary> /// Resolves the assembly reference. /// </summary> /// <param name="sender"></param> /// <param name="args"></param> /// <returns>The loaded assembly.</returns> private Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) { //get AssemblyName AssemblyName assemblyName = new AssemblyName(args.Name); //get path to assembly string assemblyPath = Path.Combine(_CurrentAssemblyDirectory, assemblyName.Name + ".dll"); //check if assembly exists if (File.Exists(assemblyPath)) //load assembly return LoadAssembly(assemblyPath); //could not load assembly return null; }
Actual loading of assemblies is facilitated through two loading methods, the first that loads an assembly from file and the other which loads from the AssemblyName:
/// <summary> /// Loads an assembly given it's file name or path. /// </summary> /// <param name="assemblyFile">The name or path of the file that contains the manifest of the assembly.</param> /// <returns>The loaded assembly.</returns> public Assembly LoadAssembly(string assemblyFile) { //get the directory the assembly is located in string assemblyDirectory = Path.GetDirectoryName(assemblyFile); //load the assembly Assembly assembly = Assembly.LoadFrom(assemblyFile); //check if new assembly if (!_Assemblies.ContainsKey(assembly.FullName)) { //add new assembly to cache _Assemblies.Add(assembly.FullName, assembly); //new assembly, load all references foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) { //temp the current directory string lastAssemblyFile = _CurrentAssemblyDirectory; //set new current directory _CurrentAssemblyDirectory = assemblyDirectory; try { //load assembly and assembly references LoadAssemblyWithReferences(reference); } finally { //reset current directory _CurrentAssemblyDirectory = lastAssemblyFile; } } //return new reference return assembly; } else //assembly already added, return reference return _Assemblies[assembly.FullName]; } /// <summary> /// Loads an assembly given it's <see cref="System.Reflection.AssemblyName"/>. /// </summary> /// <param name="assemblyFile">The name or path of the file that contains the manifest of the assembly.</param> /// <returns>The loaded assembly.</returns> public void LoadAssemblyWithReferences(AssemblyName assemblyReference) { //loads the assembly Assembly assembly = Assembly.Load(assemblyReference); //check if new assembly if (!_Assemblies.ContainsKey(assemblyReference.FullName)) { //add new assembly to cache _Assemblies.Add(assemblyReference.FullName, assembly); //new assembly, load all references foreach (AssemblyName reference in assembly.GetReferencedAssemblies()) LoadAssemblyWithReferences(reference); } }
After all assemblies have been successfully loaded, instances can be created through the AssemblyPlugin.CreateInstance method which reaches across the application domains to return a type that derives from MarshalByRefObject coverted to a specified base type.
Available plugin types can be located using the GetTypes method which searches for any type that derives from a specified base type:
/// <summary> /// Searches the assembly for types that derive from <typeparamref name="TBase"/>. /// </summary> /// <typeparam name="TBase">The type of objects to locate where TReturn : MarshalByRefObject.</typeparam> /// <returns>An array of identifiers that derive from <typeparamref name="TBase"/>.</returns> public PluginTypeIdentifier[] GetTypes<TBase>() where TBase : MarshalByRefObject { List<PluginTypeIdentifier> plugins = new List<PluginTypeIdentifier>(); Type tbase = typeof(TBase); //enumerate all types foreach (Type t in _Assembly.GetTypes()) { //look for type that derives from tbase if (t.IsSubclassOf(tbase)) //add type plugins.Add(new PluginTypeIdentifier(t, tbase)); } //return array of identifiers return plugins.ToArray(); }
When the AssemblyPlugin class is disposed, the application domain is unloaded causing the assembly and all references to be unloaded.
Using
Create a base type that derives from MarshalByRefObject and is located in a separate assembly:
/// <summary> /// Base class for class instances in a plugin. /// </summary> public abstract class PluginBaseClass : MarshalByRefObject { public abstract object Process(params object[] args); }
The plugin base assembly should be referenced by both the application that will instantiate AssemblyPlugin and a plugin assembly. In the plugin assembly, create a type that derives from PluginBaseClass that overrides the Process method:
[DisplayName("Simple Concatenation")] [Description("This class simply concatenates string arguments.")] public class StringConcat : PluginBaseClass { public override object Process(params object[] args) { //simply concatenates the arguments return string.Concat(args); } }
This allows the AssemblyPlugin class to create and return an instance of a type, converted to the base type that is loaded in both application domains.
All types that are passed between application domains are expected to be marked Serializable or implement ISerializable.
To use the StringConcat type across application domains, initialize the AssemblyPlugin and call the CreateInstance method to return the PluginBaseClass type:
//loads the demo plugin library using (AssemblyPlugin plugin = new AssemblyPlugin("[Your Plugin Assembly].dll")) { //creates instance of [Your Plugin Namespace].StringConcat converted to base-type: PluginBaseClass instance = plugin.CreateInstance<PluginBaseClass>("[Your Plugin Namespace].StringConcat"); //calls the StringConcat.Process method which concatenates the strings: Console.WriteLine(string.Format("Result: {0}", instance.Process("Hello", " ", "World", "!"))); }
All types that derive from a base type in a plugin can be located using the GetTypes method:
PluginTypeIdentifier[] types = plugin.GetTypes<PluginBaseClass>();
The PluginTypeIdentifier class provides BaseType, FullName, Name, Description and DisplayName properties to identify available plugin types.
Source Code
Download AssemblyPlugin, Supporting Classes and Example Solution