Introduction
This article will give an idea of what's involved in interacting with a running MEF based application and how to either remove a Composition part or replace it with a new version, without having to shut down your application for an upgrade of a part. This can be particularly useful when replacing or removing components from a Windows service that is MEF based and has an administration component to interact with the service.
Background
The question came up last week about swapping out an MEF enabled DLL on the fly. Because .NET locks the assembly even in an MEF enabled application, you can't replace the DLL when you release the MEF parts in your code. The only way to replace a DLL without a little elbow grease, is to shut down the application, swap out the DLL, and then restart the application. So, after researching and finding a couple of decent examples and after applying the elbow grease, I came up with this solution. It's not pretty and it doesn't do much except prove how to do this. This is a copy of my blog entry of the same title.
Using the Code
This example will only work with .NET 4.5 and above and assumes you already have an understanding of how MEF works and can get around with it without going into a tutorial on that.
We will be using the AppDomain
class to create an application domain for the MEF components to run in. This will allow us to access the components at run time. I'll explain more of what's going on as we progress.
First, create a console application project called AppDomainTest
. And in your Program
class, insert the code below. I have a couple of paths set up here that point to where the MEF DLLs are found and where the AppDomainSetup will cache the DLLs while running. I'll explain more of that later.
using System;
using System.IO;
namespace AppDomainTest {
internal class Program {
private static AppDomain domain;
[STAThread]
private static void Main() {
var cachePath = Path.Combine
(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "ShadowCopyCache");
var pluginPath = Path.Combine
(AppDomain.CurrentDomain.SetupInformation.ApplicationBase, "Plugins");
if (!Directory.Exists(cachePath)) {
Directory.CreateDirectory(cachePath);
}
if (!Directory.Exists(pluginPath)) {
Directory.CreateDirectory(pluginPath);
}
var setup = new AppDomainSetup {
CachePath = cachePath,
ShadowCopyFiles = "true",
ShadowCopyDirectories = pluginPath
};
}
}
}
Now, create a class library project called AppDomainTestInterfaces. This library will contain the contract interface for the MEF libraries and the main application. Add a reference to this library in the main application. Delete the class file in there and create an interface called IExport
.
namespace AppDomainTestInterfaces {
public interface IExport {
void InHere();
}
}
Next, create a couple of MEF class library projects. Add references to AppDomainTestInterfaces and System.ComponentModel.Composition in each library. Delete the Class1
file and create an Import
class in each project.
You'll want to set the build output to the bin\Debug folder for the main application as shown. I put the compiled DLLs into a folder called Plugins under the main application bin\Debug folder so that they were easy to find and I could set up my code to be simple for this example. Use your own folder as needed.
Finally, create a class library project called AppDomainTestRunner
and set a reference to it in the main application. Add references to System.ComponentModel.Composition
, System.ComponentModel.Composition.Registration
, and System.Reflection.Context
to add access to the necessary MEF components used in the rest of the example. And lastly, add a reference to AppDomainTestInterfaces
.
Now we can get to the meat of this project.
In the AppDomainTestRunner library project, delete the Class1
file and add a Runner
class. Add the following code to Runner
. This is the class that will deal with the MEF imports and exports, and we will see that the entire class runs in a separate AppDomain
.
using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.ComponentModel.Composition.Registration;
using System.IO;
using System.Linq;
using AppDomainTestInterfaces;
namespace AppDomainTestRunner {
public class Runner : MarshalByRefObject {
private CompositionContainer container;
private DirectoryCatalog directoryCatalog;
private IEnumerable<iexport> exports;
private static readonly string pluginPath = Path.Combine
(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
"Plugins");
public void DoWorkInShadowCopiedDomain() {
var regBuilder = new RegistrationBuilder();
regBuilder.ForTypesDerivedFrom
<IExport>().Export<IExport>();
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new AssemblyCatalog
(typeof(Runner).Assembly, regBuilder));
directoryCatalog = new DirectoryCatalog(pluginPath, regBuilder);
catalog.Catalogs.Add(directoryCatalog);
container = new CompositionContainer(catalog);
container.ComposeExportedValue(container);
exports = container.GetExportedValues<iexport>();
Console.WriteLine("{0} exports in AppDomain {1}",
exports.Count(), AppDomain.CurrentDomain.FriendlyName);
}
public void Recompose() {
directoryCatalog.Refresh();
container.ComposeParts(directoryCatalog.Parts);
exports = container.GetExportedValues<IExport>();
}
public void DoSomething() {
exports.ToList().ForEach(e => e.InHere(););
}
}
}
Next, set up the MEF library code as shown below. This just shows that we actually are running in the DLLs. I created two of the exact same libraries, just naming the second AppDomainTestLib2.
using System;
using AppDomainTestInterfaces;
namespace AppDomainTestLib {
public class Import : MarshalByRefObject, IExport {
public void InHere() {
Console.WriteLine("In MEF Library1: AppDomain: {0}",
AppDomain.CurrentDomain.FriendlyName);
}
}
}
Note the use of MarshalByRefObject
, this will, in essence, mark this class as Serializable
, and enables access to objects across AppDomain boundaries, thereby gaining necessary access to methods and properties in the class residing in the hosted AppDomain
.
Finally, set up the Main()
method as follows. What we see here is the use of an AppDomainSetup
object to define our AppDomain
configuration. This establishes the shadow copying of the DLLs and where to shadow copy to. The CachePath
parameter is optional, and only shown here as proof of what is happening. The parameter ShadowCopyFiles
is a string
parameter and accepts true
or false
. The ShadowCopyDirectories
parameter establishes which directory to shadow copy from.
using System;
using System.IO;
using AppDomainTestRunner;
namespace AppDomainTest {
internal class Program {
private static AppDomain domain;
[STAThread]
private static void Main() {
var cachePath = Path.Combine
(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
"ShadowCopyCache");
var pluginPath = Path.Combine
(AppDomain.CurrentDomain.SetupInformation.ApplicationBase,
"Plugins");
if (!Directory.Exists(cachePath)) {
Directory.CreateDirectory(cachePath);
}
if (!Directory.Exists(pluginPath)) {
Directory.CreateDirectory(pluginPath);
}
var setup = new AppDomainSetup {
CachePath = cachePath,
ShadowCopyFiles = "true",
ShadowCopyDirectories = pluginPath
};
domain = AppDomain.CreateDomain("Host_AppDomain",
AppDomain.CurrentDomain.Evidence, setup);
var runner = (Runner)domain.CreateInstanceAndUnwrap
(typeof(Runner).Assembly.FullName, typeof(Runner).FullName);
Console.WriteLine("The main AppDomain is:
{0}", AppDomain.CurrentDomain.FriendlyName);
runner.DoWorkInShadowCopiedDomain();
runner.DoSomething();
Console.WriteLine("\nHere you can remove a
DLL from the Plugins folder.");
Console.WriteLine("Press any key when ready...");
Console.ReadKey();
runner.Recompose();
runner.DoSomething();
Console.WriteLine("Press any key when ready...");
Console.ReadKey();
AppDomain.Unload(domain);
}
}
}
About shadow copying: ShadowCopyFiles
will take a copy of the DLLs that are actually used in the AppDomain
and put them in a special folder, then reference them from there. This allows the DLL in the Plugins (or any other configured folder) to be deleted or replaced during runtime. The original DLL will remain in the cache folder until either the next start up of the application or, in the example we will see, the DirectoryCatalog
is refreshed and the CompositionContainer
is recomposed and re-exported.
Now, when you run the application, you see the MEF DLLs run within Host_AppDomain
.
At this point, you can go into the Plugins folder and delete a DLL, then press any key in the console window to see what happens when runner.Recompose()
is called. We then get proof that the recompose released our DLL, but only because of the ShadowCopyFiles
parameter.
Now, open another instance of Visual Studio and create a class library called AppDomainTestLib3
. Add the same references as before and don't set the output directory, we'll want to copy that in by hand. Set up its Import
class code just the same as the previous AppDomainTestLib
classes. Go ahead and compile it.
Next, run the application in the previous Visual Studio instance and stop at the first Console.ReadKey()
. Delete a DLL from the Plugins folder and copy the new one in place. Press any key to continue...
Finally, to actually replace a DLL, you must delete the previous DLL prior to implementing the new one. We can demonstrate the manual way of doing this by inserting the following code into Program
. As noted in the comments and in this article, the assemblies now need to be signed and the AssemblyVersionAttribute
in AssemblyInfo.cs
needs to be set to a new version. (The assembly versioning is a good practice anyway...) This will require the interfaces assembly and the other plugin assemblies to also be signed.
<a href="http://stackoverflow.com/a/14842417">
runner.Recompose();
runner.DoSomething();
Console.WriteLine("\nHere we will begin to replace Lib3 with an updated version.
\nDelete the old one first DLL from the Plugins folder.");
Console.WriteLine("Press any key when ready...");
Console.ReadKey();
Console.WriteLine("Now showing that Lib3 is deleted.");
runner.Recompose();
runner.DoSomething();
Console.WriteLine("\nNext drop the new Lib3 in the Plugins folder.");
Console.WriteLine("Press any key when ready...");
Console.ReadKey();
runner.Recompose();
runner.DoSomething();
Console.WriteLine("Press any key when ready...");
Console.ReadKey();
AppDomain.Unload(domain);</a>
Leave the current AppDomainTestLib3
in the Plugins folder and run the application. Now, follow the prompts and when you get to "Here we will begin to replace Lib3
with an updated version.", make an observable change to the AppDomainTestLib3
, and compile it. After completing the deletion of the old DLL, press any key to recompose the DLLs. Now, when you get the next prompt, drop the new DLL into the Plugins folder. Hit any key as usual. You should now see the response from your changed DLL.
The reason for the double runner.Recompose()
calls is that the Exports signature for the DLL matches the previous version and MEF doesn't see a change since it doesn't look at FileInfo for differences. This then tells the AppDomain
that the DLL hasn't changed either and the ShadowCopyFiles
doesn't kick in to make that change. The simple workaround is to delete the original, recompose, put the new one in place, and recompose one more time. The only disadvantage I can see in this is the performance of the application will drop momentarily during the recompose.
I've added a GitHub repository with the source. I also included in that source the ability to pass data between AppDomains. The source can be downloaded from here.