Create plugins in Dotnet Core or what we call Managed Extensibility Framework

Create plugins in Dotnet Core or what we call Managed Extensibility Framework

2018-11-29 0 By Nordes

In Java we have the Service Loader that can add plugin(s) within your application at startup. It basically means you can extend your application with a set of new features. This same service loader can also at some extent add those plugins at runtime by detecting new files within the plugin folder.

If we look what we have in Dotnet world, we have something very similar that have been existing for a long time. It’s still rare to see such system being used. It will more likely already exists without having anyone really having to maintain it. Since in Dotnet Core 2.1, the namespaces changed a bit, it makes it interesting to explore with you. I guess that if you are on this post, you are most likely a technical guy. I assume that you’ve already done some research on what it is and that you’d like an example (step by step) to really show how easy it is to build. In this article we’ll cover only partially the Manageed Extensibility Framework (MEF).

What is the Managed Extensibility Framework (A.k.a.: MEF)?

Often you will hear about the acronym MEF instead of its long name “Managed Extensibility Framework”.  In the past we were also talking in parallel about the MAF which stands for Managed Add-in Framework (See details here). MAF still exists within the Dotnet Framework with Windows Client (WPF for example), however in Dotnet Core you it is not available “yet”.

In the introduction, I stated that if you’ve used Java, you know the main idea about it. For the others, the general idea is, that when, you or a third party, have finished to create your plugin you drop your JAR in the plugins folder.  In C#/Dotnet world, MEF and MAF are using the same principle, whereas instead of dropping a Jar you drop your “Assemblies”.

Let’s go back to the question, what is MEF? It’s a contracts based extensibility managed by imports and exports within your programs or libraries. You will then make an extensible application by implementing interfaces and then in your main application you will have a provider to provide the implemented contracts (interfaces) implemented. The Assemblies that you want to import in your main application require the exports. Those exports are going to be discovered through “Reflection“.

Let’s take the famous calculator as an example where you could extends the functionalities (plus, minus, division, etc.). Each of those functionalities could be a provided within different “Assemblies” loaded at the startup of your application. There would be no need of project references to those assemblies within your main application. So if you add or remove “Assemblies”, functionalities are going to be added or removed. The magic comes from the exports you would discover. For the calculator example, you could have a “Name” (Square Root) for what it is and the official “Operator” (√) for the discovered feature. You would then have an implementation of the operator receiving “Number 1, …” producing a new result (Contract within an interface shared between all the libraries, a.k.a. interfaces). The Imports are, as stated previously, most likely be in your main executable (WebApi, Console, etc.).

How does it works?

As stated above, the main program will do your discovery of shared contracts API’s. Those Exports/Imports are being assigned using attributes on your classes. Those attributes, to name only a few, will look like [import], [ImportMany], [ExportMetadata(“something”, “here”)], [Export(typeof(IYourInterface))], etc.. As you can see, the import will import only one instance while ImportMany will create an IEnumerable. Those attributes can be found within the Nuget package “System.Composition.AttributedModel“. Keep that package in your mind since it will become handy in a later stage.

If you wonder why the ExportMetadata can become something really cool. I would say that you could have some filtering that could come from there regarding your application. That could also help in order to give some capabilities about your module (Extensibility).

Practice time – Let’s build a simple App (WebApi/SPA)

Of course, we’re not going to be building the famous calculator sample. It’s all over the place already. I don’t say that the calculator is a not a great example, but my point here is not to copy what already exists. In this “exercise”, you will be scratching the surface of the Extensibility. In a real-world scenario, you could have logs that need to strips out content based on rules that are either common or specific. Let’s say, the customer numbers are not the same from customer to customer as such that each customer requires to have different custom rules mixed with general ones. That one could be done using simply a regex, but let’s say it goes to something really complex like the passport numbers. In that case, you could implement custom rules for each customer (per country) and simply drop those rules within the plugin folder. That way, you could even request the customer to build the plugin themselves and if you thrust them enough then simply allow them to update their plugin through a portal (upload, push, and either restart your service or then load at runtime the updated plugin).

The current exercice is to simply create a simple Web Application written in Dotnet Core 2.1 using a basic UI querying the API. It will then show the result in raw of what plugin have discovered.

Scaffold a simple WebApi and Projects

In previous posts, I said that I like to use my own template since it’s quite a good starter. It’s compact and fast enough for anyone with basic knowledge of Dotnet Core and VueJs. The UX is using, instead of the common Bootstrap 4, something lighter called Picnic CSS. You can download/update the template using > dotnet new -i HoNoSoFt.DotNet.Web.Spa.ProjectTemplates(Documentation here). This exercise can also be accomplished within a Console application. However, I think the most common scenario will be around the usage of a WebApplication and then extend it somehow. Let’s create the structure of the project (Command line, it’s easier):

> mkdir Sample.Mef
> cd Sample.Mef
Sample.Mef> dotnet new vuejs-picnic -o Sample.Mef.Web
Sample.Mef> dotnet new classlib -o Sample.Mef.Api
Sample.Mef> dotnet new classlib -o Sample.Mef.GiveMeOne
Sample.Mef> dotnet new classlib -o Sample.Mef.GiveMeFive
Sample.Mef> dotnet new sln
Sample.Mef> dotnet sln add .\Sample.Mef.Web\Sample.Mef.Web.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.Api\Sample.Mef.Api.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.GiveMeOne\Sample.Mef.GiveMeOne.csproj
Sample.Mef> dotnet sln add .\Sample.Mef.GiveMeFive\Sample.Mef.GiveMeFive.csproj
Sample.Mef> cd Sample.Mef.Web
Sample.Mef\Sample.Mef.Web> npm install
Sample.Mef\Sample.Mef.Web> dotnet run

Normally after completing the previous command, you should have the Web Project starting. The URL of the project will be displayed within the console or if you are in Visual Studio, it will open the start page by itself.

For the next part of the blog I will be using Visual Studio Community, but feel free to use VS Code.

Exercise: Structure Application Tree

In the following screen capture, some renaming have already taken place (1) and as indicated in the image, some files needs to be removed (2) or modified (3).

Not marked on the picture:

  • Remove IWeatherProvider.cs 
  • Update ServiceCollectionExtensions.cs (remove the injection of the IWeatherService).

Implement the contract in GiveMeOne & GiveMeFive

Some project will require the reference in order to be able to use IGiveNumber interface. Those relation are shown within one of the previous section (UML like)

  • Add the reference of Sample.Mef.Api project to
    • Sample.Mef.GiveMeOne
    • Sample.Mef.GiveMeFive
    • Sample.Mef.Web

The interface “IGiveNumber.cs” look like:

namespace Sample.Mef.Api
{
    public interface IGiveNumber
    {
        int GiveInt();
    }
}

This contract is going to be implemented within “GiveOne.cs” and “GiveFive.cs

using Sample.Mef.Api;

namespace Sample.Mef.GiveMeOne
{
    public class GiveOne : IGiveNumber
    {
        public int GiveInt()
        {
            return 1;
        }
    }
}
using Sample.Mef.Api;

namespace Sample.Mef.GiveMeFive
{
    public class GiveFive : IGiveNumber
    {
        public int GiveInt()
        {
            return 5;
        }
    }
}

Now you should be able to build with no errors.

Add the Composition/Extensibility to Import/Export

You remember the nuget package from earlier? It gives the simple Import/Export and a few other basic functionalities. In our case, we will be using it simply for exporting the implemented interface. Let’s install it:

  • Add Nuget package “System.Composition.AttributedModel” to
    • Sample.Mef.GiveMeOne
    • Sample.Mef.GiveMeFive

Add the “Export” attribute

Now, let’s update our two previously defined classes (GiveOne, GiveFive) and add our exports. Those exports allow the Reflection, in a later stage, to discover the implementations. Some customization could also be done, but for that, rely on the MSDN documentation.

Within the two previous project mentioned, add the following Export attribute:

using System.Composition;
// Some code...

[Export(typeof(IGiveNumber))]
public class ...// some code...

Add the “Import” matching the previous “Export” within a provider

This part is only modify “Sample.Mef.Web” project. As we’re going to create a new provider, let’s make it properly by using Dependency Injection (DI). Within the provider, we will also be using System.Composition. This is will help doing the discovery of the plugin(s).

  1. Add the Nuget package reference to the project: “System.Composition
  2. Create an IGiveNumberProvider + GiveNumberProvider
  3. Update the startup to inject the new provider

We will put IGiveNumberProvider and GiveNumberProvider within the same file, but it’s a sample and you can also do as you please.

using Sample.Mef.Api;
using System.Collections.Generic;
using System.Composition;
using System.Composition.Hosting;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;

namespace Sample.Mef.Web.Providers
{
    public class GiveNumberProvider : IGiveNumberProvider
    {
        public GiveNumberProvider()
        {
            Compose();
        }

        [ImportMany]
        public IEnumerable<IGiveNumber> Services { get; private set; }

        public IEnumerable<(string Id, int Value)> GetNumberFromAllFoundServices()
        {
            return Services.Select(f => new { Id = f.GetType().ToString(), Value = f.GiveInt()})
                .AsEnumerable()
                .Select(c => (c.Id, c.Value))
                .ToList();
        }

        private void Compose()
        {
            // Catalogs does not exists in Dotnet Core, so you need to manage your own.
            var assemblies = new List<Assembly>() { typeof(Program).GetTypeInfo().Assembly };
            var pluginAssemblies = Directory.GetFiles("d:\\plugins\\", "*.dll", SearchOption.TopDirectoryOnly)
                .Select(AssemblyLoadContext.Default.LoadFromAssemblyPath)
                // Ensure that the assembly contains an implementation for the given type.
                .Where(s => s.GetTypes().Where(p=> typeof(IGiveNumber).IsAssignableFrom(p)).Any());

            assemblies.AddRange(pluginAssemblies);

            var configuration = new ContainerConfiguration()
                .WithAssemblies(assemblies);

            using (var container = configuration.CreateContainer())
            {
                Services = container.GetExports<IGiveNumber>();
            }
        }
    }

    public interface IGiveNumberProvider
    {
        IEnumerable<(string Id, int Value)> GetNumberFromAllFoundServices();
    }
}

The code should be clear enough in order for you to understand what it does. However, let’s explain quickly:

  • It’s important to note that I’ve set the Assemblies for the extensibility available in the “d:\plugins\” (Please adapt)
  • We filter using reflection on the assemblies. This will enforce keeping only those that have at least one implementation from IGiveNumber
  • In case the assembly can be loaded from the container, we take the related “Export” implementing IGiveNumber and push that within the services
  • You can now access the implementations through the “Services” (Property field)
    • I use the Tuple as a return type, some of it is new in the C# 7+. In case you’ve missed that new feature, go on the following Blogs MSDN article

Add the Dependency Injection (DI)

Now that the provider is ready, let’s add the DI within Startup.cs. The usage of Singleton is privileged since we don’t want to rediscover at each HTTP requests.

// Code ...
        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // code ...

            // Example with dependency injection for a data provider.
            services.AddSingleton<IGiveNumberProvider, GiveNumberProvider>();
        }
// Code ...

Alter the existing “api/values” endpoint in order to get the provider “values”.

using Microsoft.AspNetCore.Mvc;
using Sample.Mef.Web.Providers;

namespace Sample.Mef.Web.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {
        private readonly IGiveNumberProvider _giveNumberProvider;

        public ValuesController(IGiveNumberProvider giveNumberProvider)
        {
            _giveNumberProvider = giveNumberProvider;
        }

        // GET api/values
        [HttpGet]
        public IActionResult Get()
        {
            return Ok(_giveNumberProvider.GetNumberFromAllFoundServices().ToList().Select(f=> new { f.Id, f.Value }));
        }

        // Code
}

It’s now mostly complete. I would even say that you must put your project Assemblies (DLL) in “d:\plugins” or where you’ve decided to discover them. The assemblies we’re going to look for are described next.

Time to test by moving your dll’s in the plugins folder

Since you’ve already built, or maybe not yet, your solution, it’s now the time to bring the assemblies (dll) from Sample.Mef.GiveMeOne, Sample.Mef.GiveMeFive available through the bin directory. You then move them to the “d:\plugins”, or the reconfigured folder from previous step. When you copy your bin folder, you need to copy not only the Sample.Mef.GiveMeOne, you also need to copy it’s dependencies (i.e.: Sample.Mef.Api). Since the System.Composition is included in the Web project, it is not mandatory to have a copy in related assemblies plugin folder.

The plugin folder will then contains:

  • Sample.Mef.GiveMeOne.dll
  • Sample.Mef.GiveMeFive.dll
  • Sample.Mef.Api.dll

Start your application and look at the results

Purely speaking, do a dotnet run from the Web application folder, or in Visual Studio push F5.

Go see if your API’s is answering properly before the last step

 

[Optional] Update your UX in order to do the call

The output is going to simply show a list from the homepage. The extensions to your project, after being loaded, will be displayed within the result of the API call.

<template>
  <div>
    <page-title title="Give the Extensions" />
    <p>He you will be finding your loaded extensions:</p>
    <ul>
      <li v-for="(data, idx) in numbers" :key="idx">{{data.id}}
          <ul>
            <li>{{data.value}}</li>
          </ul>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data () {
    return {
      numbers: []
    }
  },

  mounted() {
    this._getNumbers()
  },

  methods: {
    async _getNumbers () {
      let response = await this.$http.get(`api/values`)
      this.numbers = response.data
    }
  }
}
</script>

Simply run the application and go on the home page. In my case, while using IIS Express, it gave me https://localhost:44307

Final result

Extra Resources

Those resource were not used that much during this blog post, but I think it can help people to also understand how it works in a different context.

  • Official: https://docs.microsoft.com/en-us/dotnet/framework/mef/
  • https://dotnetthoughts.net/using-mef-in-dotnet-core/
  • https://blog.softwarepotential.com/porting-to-net-standard-2-0-part-2-porting-mef-1-0-to-mef-2-0-on-net-core/

Thank you!

By the way, all the source code is available in GitHub (personal @Nordes repository). Enjoy 😉

Featured image original is from: https://unsplash.com/photos/l090uFWoPaI