Friday, July 4, 2014

AppDomains from libraries in subfolders

Early 2022 note: The AppDomain class has been basically deprecated after the introduction of .NET Core and later frameworks. This article is therefore only meaningful for the traditional .NET Framework. For more information see .NET Framework technologies unavailable on .NET Core and .NET 5+.


It's a nice deployment pattern to isolate "plug-in" code into library files in subfolders under the application folder and run these plug-ins in a separate AppDomain. By loading libraries and the assemblies they contain into a separate AppDomain it's possible to apply a restrictive security policy them and it's possible to unload them.

The Assembly Load, LoadFile and LoadFrom methods load libraries into one of the contexts in the AppDomain of the caller, and if this is the initial AppDomain of the Process then it cannot be unloaded until the Process terminates.

Organise your projects like this skeleton:

MyApp
MyApp.Plugin.Common
MyApp.Plugin.PluginOne
MyApp.Plugin.PluginTwo

The application and the plugin projects reference the common library, never each other. All of the plugins should implement an interface defined in the common library which defines their public contract. The plugin classes must be derived from MarshalByRefObject to allow strongly-typed communication via Remoting between the AppDomains.

When the application is deployed, arrange the folders like this:

Application Folder
  theapp.exe
  MyApp.Plugin.Common.dll
Application Folder\Subfolder One
  MyApp.Plugin.PluginOne.dll
  MyApp.Plugin.Common.dll
Application Folder\Subfolder Two
  MyApp.Plugin.PluginTwo.dll
  MyApp.Plugin.Common.dll

The application can search subfolders at runtime to find plugin libraries. You might use a folder naming convention or place a special XML file of instructions beside the plugins to identify and describe them. When a plugin is identified it can be loaded, called and unloaded like this:

var folder1 = new FileInfo("Subfolder One");
var ads1 = new AppDomainSetup();
ads1.ApplicationBase = folder1.FullName;
var dom1 = AppDomain.CreateDomain("Domain-1", null, ads1);
string file1 = Path.Combine(folder1.FullName, "MyApp.Plugin.PluginOne.dll");
string class1 = "MyApp.Plugin.PluginOne.ClassName1";
var plug1 = (IPlugin)dom1.CreateInstanceFromAndUnwrap(file1, class1);
Console.WriteLine(plug1.SayHello());
AppDomain.Unload(dom1);

This code creates an AppDomain with its base folder set to the subfolder where the plugin was found. It then uses CreateInstanceFromAndUnwrap passing the full path of the plugin's folder and the name of the class to instantiate in the AppDomain. The returned value (actually a proxy) is cast to the common interface and it can be called like a normal class method. The AppDomainSetup class has many other properties that help configure the AppDomain, such as specifying a config file.

The applications and the plugin projects do not reference each other and at runtime they only communicate using an interface over Remoting between the AppDomains.

While writing the real application that uses the technique described above I accidentally used the CreateInstanceAndUnwrap method (without the From) and I received misleading "Assembly not found" errors. It took me hours of suffering before I realised my dyslexic mistake caused by the similar names. The MSDN documentation describes the subtle differences between the two methods.

The technique described in this article is the manual way of implementing plugin isolation via AppDomains and is suitable for simple scenarios. The Managed Extensibility Framework (MEF) and the Managed Add-In Framework (MAF) are worth exploring for more complex scenarios.

No comments:

Post a Comment