How to load assemblies at runtime using Microsoft Extensibility Framework in C#

By FoxLearn 2/4/2025 4:15:06 AM   91
The Microsoft Extensibility Framework (MEF) is a powerful tool that allows you to load assemblies dynamically at runtime.

MEF simplifies the process by automatically discovering and creating instances of exported types, which is an easier alternative to more manual approaches like using AssemblyLoadContext.

Let’s look at an example where we load an instance of IShapePlugin from an assembly located in the C:\Plugins directory.

Loading and using a shape plugin

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

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

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

// Step 3 - Get an instance of the exported type
try
{
    var plugin = container.GetExportedValue<IShapePlugin>();
    plugin.Draw();
}
catch (CompositionException ex)
{
    Console.WriteLine(ex);
}

Exporting Types with MEF

To let MEF know which types can be used to create instances, you mark the type with the Export attribute. Here’s an example of the shape plugin:

using System.ComponentModel.Composition;

[Export(typeof(IShapePlugin))]
public class Circle : IShapePlugin
{
    public void Draw()
    {
        Console.WriteLine("Drawing a Circle");
    }
}

In this section, I’ll walk you through how to use MEF in various scenarios, from loading specific assemblies to handling dependencies.

Lazy vs Eager Initialization

You can choose between lazy and eager initialization for your exported instances.

Lazy Initialization: The object is only created when accessed for the first time. This can optimize memory usage.

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

// Use the lazy instance later
lazyPlugin.Value.Draw();

Eager Initialization: The object is created immediately upon request.

IShapePlugin plugin = container.GetExportedValue<IShapePlugin>();
plugin.Draw();

Loading a Specific Assembly

You can restrict MEF to load types only from a specific assembly using the searchPattern parameter. For example, if you want to load plugins from the assembly ShapePluginLib.dll, you can do it like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Plugins", searchPattern: "ShapePluginLib.dll"));

You can also use wildcards to match multiple files, like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Plugins", searchPattern: "*ShapePluginLib.dll"));

Loading from a Relative Path

Relative paths can be used to load assemblies. If your plugins are in a subfolder or at a different level in your directory structure, you can load them like this:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog("Plugins"));

If the Plugins folder is located at the same level as your application, you can use ..\ to go up one directory level:

var catalog = new AggregateCatalog();
catalog.Catalogs.Add(new DirectoryCatalog(@"..\Plugins"));

Handling Dependencies

When an exported type has dependencies, MEF will attempt to inject them automatically if they’re also exported types.

Let’s say Circle (in ShapePluginLib.dll) depends on a logging service (ILogger from LoggingLib.dll). First, you export the logger:

using System.ComponentModel.Composition;

[Export(typeof(ILogger))]
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"Log: {message}");
    }
}

Next, you modify the Circle class to import ILogger:

using System.ComponentModel.Composition;

[Export(typeof(IShapePlugin))]
public class Circle : IShapePlugin
{
    [Import]
    public ILogger Logger { get; set; }

    public void Draw()
    {
        Logger.Log("Drawing a Circle");
        Console.WriteLine("Drawing a Circle");
    }
}

Using Constructor Injection

You can use constructor injection to provide dependencies instead of property injection.

Here’s how you can modify Circle to use constructor injection for ILogger:

using System.ComponentModel.Composition;

[Export(typeof(IShapePlugin))]
public class Circle : IShapePlugin
{
    public ILogger Logger { get; set; }

    [ImportingConstructor]
    public Circle(ILogger logger)
    {
        Logger = logger;
    }

    public void Draw()
    {
        Logger.Log("Drawing a Circle");
        Console.WriteLine("Drawing a Circle");
    }
}

Full Example: Loading Multiple Plugins

In this example, we’ll load multiple plugins from assemblies located in C:\Plugins. One of the plugins has a dependency, and the other is independent.

The interface IShapePlugin is defined in CommonLib.dll:

public interface IShapePlugin
{
    void Draw();
}

Here’s how the Rectangle plugin (without dependencies) is defined:

using System.ComponentModel.Composition;

[Export(typeof(IShapePlugin))]
public class Rectangle : IShapePlugin
{
    public void Draw()
    {
        Console.WriteLine("Drawing a Rectangle");
    }
}

Here’s a plugin with a dependency on ILogger:

using System.ComponentModel.Composition;

[Export(typeof(IShapePlugin))]
public class Circle : IShapePlugin
{
    [Import]
    public ILogger Logger { get; set; }

    public void Draw()
    {
        Logger.Log("Drawing a Circle");
        Console.WriteLine("Drawing a Circle");
    }
}

And now, here’s the console application that loads all plugins and uses them:

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<IShapePlugin>();

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

    // Step 4 - Use the instances elsewhere
    foreach (var plugin in plugins)
    {
        plugin.Draw();
    }

    Console.ReadKey();
}

If everything is set up correctly, this is the expected output:

Drawing a Rectangle
Log: Drawing a Circle
Drawing a Circle

This example demonstrates how MEF can dynamically load and instantiate plugins, whether they have dependencies or not, and shows how to configure it to suit different use cases like loading from specific directories or handling dependencies.