C# – How to load assemblies at runtime using Microsoft Extensibility Framework (MEF)

You can use Microsoft Extensibility Framework (MEF) to load assemblies at runtime. This is an alternative to implementing dynamic assembly loading with a more manual approach (like using AssemblyLoadContext).

Here’s an example of using MEF to load an instance of IMessageProcessorPlugin from some assembly located in the C:\Plugins directory:

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

//Step 1 - Create aggregate catalog
var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Plugins"));

//Step 2 - Create container
var container = new CompositionContainer(catalog);

//Step 3 - Get instance of the exported type
try
{
	var plugin = container.GetExportedValue<IMessageProcessorPlugin>();
	plugin.Process("Hello World");
}
catch (CompositionException ex)
{
	Console.WriteLine(ex);
}
Code language: C# (cs)

MEF looks for exported types in the loaded assemblies. To make it so MEF will create an instance of a class, use the Export attribute, like this:

using System.ComponentModel.Composition;

[Export(typeof(IMessageProcessorPlugin))]
public class MessageProcessor : IMessageProcessorPlugin
Code language: C# (cs)

In this article, I’ll go into more details about using MEF. At the end, I’ll show a full example of using MEF to load multiple plugins (including one with a dependency).

Lazy vs eager initialized instances

Lazy initialization is an optimization that defers initialization until you actually need to use the instance. Just like with any optimization, only use this if you definitely know you need it and will benefit from it. Otherwise stick with the simpler eager initialization approach.

To get lazy initialized instances, use the GetExport<T>() method (and variants), like this:

Lazy<IMessageProcessorPlugin> lazyPlugin = container.GetExport<IMessageProcessorPlugin>();

//use lazy instance somewhere else
lazyPlugin.Value.Process("Hello World");

//Optionally, release it somewhere else
container.ReleaseExport(lazyPlugin);
Code language: C# (cs)

This wraps your instance in a Lazy<T>. When you use .Value for the first time, it triggers initialization.

To get eager initialized instances, use the GetExportedValue<T> method (and variants), like this:

IMessageProcessorPlugin plugin = container.GetExportedValue<IMessageProcessorPlugin>();
plugin.Process("Hello World");
Code language: C# (cs)

Loading a specific assembly

Let’s say you want to only load exported types from a specific assembly. To do that, you can pass in the assembly file name in the searchPattern parameter, like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Plugins", searchPattern: "MessageProcessorPluginLib.dll"));
Code language: C# (cs)

Note: MEF will load dependencies, even if they aren’t part of the search pattern.

The searchPattern parameter supports the wildcard (*) character as well. For example, let’s say all of your plugin DLLs end with “PluginLib.dll.” You could use the wildcard character like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Plugins", searchPattern: "*PluginLib.dll"));
Code language: C# (cs)

Loading from a relative path

You can specify absolute and relative paths in the DirectoryCatalog constructor. Relative paths are resolved based on the application’s current working directory. In some situations, such as when running in a Windows Service, you may need to resolve relative paths yourself.

Here’s a few examples of loading from a relative path. In these examples, assume the app can be deployed anywhere and you have to use a relative path.

Let’s say your app is running in C:\App and your directory structure looks like this:

C:\App
C:\App\PluginsCode language: plaintext (plaintext)

You can load from this plugins subdirectory like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog("Plugins"));
Code language: C# (cs)

Now let’s say your plugins directory is deployed to the same level as your app, so that your folder structure looks like this:

C:\App
C:\PluginsCode language: plaintext (plaintext)

Remember that relative paths are resolved based on the current working directory (C:\App), so to get to the plugins directory, you’d go up one level in the directory hierarchy by using a double dot (..\), like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"..\Plugins"));
Code language: C# (cs)

Loading dependencies

When MEF loads assemblies, it’ll also load their dependencies. When it creates an instance of a type, it can resolve their dependencies as imported types as long as the dependency is an exported type.

Let’s say MessageProcessor (in MessageProcessorPluginLib.dll) depends on IMessageFormatter (in MessageFormatterLib.dll), which has a concrete implementation called MessageReverser.

First, to be able to load MessageReverser as a dependency, it has to be an exported type (by using the Export attribute):

using System.ComponentModel.Composition;

[Export(typeof(IMessageFormatter))]
public class MessageReverser: IMessageFormatter
{
	public string FormatMessage(string message)
	{
		return new string(message.Reverse().ToArray());
	}
}
Code language: C# (cs)

Next, to have MEF dependency inject this type, it needs to be imported into MessageProcessor. There are two ways to do this. You can either use property injection (with a default constructor), or use constructor injection by using the ImportingConstructor attribute. I’ll show examples of both approaches below.

Default constructor and property injection

To use property injection, use the Import attribute on the IMessageFormatter property, like this:

[Export(typeof(IMessageProcessorPlugin))]
public class MessageProcessor : IMessageProcessorPlugin
{
	[Import()]
	public IMessageFormatter MessageFormater { get; set; }
	
	public void Process(string message)
	{
		Console.WriteLine($"Processed message: {MessageFormater.FormatMessage(message)}");
	}
}
Code language: C# (cs)

Constructor injection using ImportingConstructor attribute

To use constructor injection, have IMessageFormatter as a constructor parameter, and then use the ImportingConstructor attribute on the constructor, like this:

[Export(typeof(IMessageProcessorPlugin))]
public class MessageProcessor : IMessageProcessorPlugin
{
	public IMessageFormatter MessageFormater { get; set; }
	
	[ImportingConstructor]
	public MessageProcessor(IMessageFormatter messageFormater)
	{
		MessageFormater = messageFormater;
	}

	public void Process(string message)
	{
		Console.WriteLine($"Processed message: {MessageFormater.FormatMessage(message)}");
	}
}
Code language: C# (cs)

Full example – Loading multiple plugins with MEF

In this example, I’ll show step-by-step how to load and use multiple plugins using MEF.

The following diagram shows all of the assemblies and classes involved:

Component diagram showing the Console App, IMessageProcessorPlugin (in CommonLib.dll), and two plugins in two different assemblies. One of the assemblies has a dependency.

This design is implemented in the code below.

IMessageProcessorPlugin, the plugin interface

In CommonLib.dll, the plugin interface is simply defined as the following:

public interface IMessageProcessorPlugin
{
	void Process(string message);
}
Code language: C# (cs)

Two plugin implementations

There are two plugins in two assemblies. Their implementations are shown below.

MessageReverser plugin and its dependency

Here’s the MessageReverser plugin. It uses the Export attribute to export the plugin interface (IMessageProcessorPlugin). It’s dependent on IMessageFormatter, and is using the property injection approach:

using System.ComponentModel.Composition;

[Export(typeof(IMessageProcessorPlugin))]
public class MessageReverser : IMessageProcessorPlugin
{
	[Import()]
	public IMessageFormatter MessageFormater { get; set; }
	
	public void Process(string message)
	{
		Console.WriteLine($"{nameof(MessageReverser)} - {MessageFormater.FormatMessage(message)}");
	}
}
Code language: C# (cs)

Here’s the IMessageFormatter type and a concrete implementation called ReversesStrings. Because this is imported by MessageReverser, it has to be exported by using the Export attribute:

using System.ComponentModel.Composition;

public interface IMessageFormatter
{
	string FormatMessage(string message);
}

[Export(typeof(IMessageFormatter))]
public class ReversesStrings: IMessageFormatter
{
	public string FormatMessage(string message)
	{
		return new string(message.Reverse().ToArray());
	}
}
Code language: C# (cs)

MessageUpperCaser plugin

Here’s the other plugin. This is located in a second assembly. This is simpler because it has no dependencies.

[Export(typeof(IMessageProcessorPlugin))]
public class MessageUpperCaser : IMessageProcessorPlugin
{
	public void Process(string message)
	{
		Console.WriteLine($"{nameof(MessageUpperCaser)} - {message.ToUpper()}");
	}
}
Code language: C# (cs)

It exports the IMessageProcessorPlugin type.

Loading the plugins in a console app

Bringing it all together, this console app uses MEF to load all of instances of IMessageProcessorPlugin from assemblies in C:/Plugins. It initializes the instances right away so it can centralize error handling in the try/catch block. Then it uses the instances later on.

using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;

static void Main(string[] args)
{
	//Step 1 - Create aggregate catalog
	var catalog = new AggregateCatalog();
	catalog.Catalogs.Add(new DirectoryCatalog(@"C:/Plugins"));

	//Step 2 - Create container
	var container = new CompositionContainer(catalog);

	//Step 3 - Load all instances
	var plugins = new List<IMessageProcessorPlugin>();

	foreach (var lazyPlugin in container.GetExports<IMessageProcessorPlugin>())
	{
		try
		{
			plugins.Add(lazyPlugin.Value);
		}
		catch (CompositionException ex)
		{
			Console.WriteLine(ex);
		}
	}

	//Step 4 - Use the instances elsewhere
	foreach(var plugin in plugins)
	{
		plugin.Process("Hello World");
	}

	Console.ReadKey();
}
Code language: C# (cs)

Note: This doesn’t handle the scenario GetExports() throws an exception. This can happen when it can’t find the assembly of a dependency (this throws a file not found exception from GetExports()).

This outputs the following:

MessageUpperCaser - HELLO WORLD
MessageReverser - dlroW olleHCode language: plaintext (plaintext)

Leave a Comment