facebook

Blog

Stay updated

Let’s see how adding a plugin mechanism in a .NET Core application
Loading plugins in .NET applications
Wednesday, March 18, 2020

During the development of our own CMS WebRight, we found that people from outside the team also need to develop plugins. I thought it might be interesting to tell you how we dealt with the problem, showing you how to develop a plugin system for your .NET applications.

The first problem to be solved is to recognize plugins among loaded assemblies. We decided to use an interface allowing those who implement it to decide what a plugin is to us. Since WebRight is based on our Framework Raptor, the name of the interface is IRaptorPlugin:

public interface IRaptorPlugin
{
    string Name { get; }
 
    string Version { get; }
 
    void OnLoad();
}

Suppose we want to create a plugin to manage a menu. We create a library containing a class, which implements the interface:

public class MenuPlugin : IRaptorPlugin
{
    public string Name => "Menu";
 
    public string Version => "1.0.0"
 
    public void OnLoad()
    {
         
    }
}

We are already able to load the plugin in the CMS, recognizing this class, thanks to Reflection. We create a plugin manager which does the dirty work for us, and I show you a simplified version of it that highlights main aspects:

public class RaptorPluginManager : IRaptorPluginManager
{
    public ICollection<Assembly> Plugins { get; private set; }
 
 
    public RaptorPluginManager(string path, IServiceCollection services)
    {
        this.Plugins = new List<Assembly>();
        string[] dlls = Directory.GetFiles(path, "*.dll", SearchOption.AllDirectories);
        foreach (var dll in dlls)
        {
            var plugin = Assembly.LoadFrom(dll);
            var pluginClass = plugin.GetTypes().Where(x => typeof(IRaptorPlugin).IsAssignableFrom(x)).SingleOrDefault();
            if (pluginClass != null)
            {
                var pluginInstance = Activator.CreateInstance(pluginClass);
                var loadMethod = pluginClass.GetMethod("OnLoad");
                loadMethod.Invoke(pluginInstance, null);
                this.Plugins.Add(plugin);
            }
        }
    }
}

This code just only searches all DLL in a specific folder, and for any element it finds, verify if there is an IRaptorPlugin implementation. The method OnLoad is executed for each implementation, and it can be used to populate a configuration or to make a data seed, which can be collected in a plugin list.

Once we get the list, we can load the framework’s specific features. Since we are now using a CMS, we defined Page and Widget concepts, using the first one to display a page of contents, and the second one to show a specific section on the content inside a page. Within the implementation of WebRight in ASP.NET Core, a Page is an MVC View, that we can reach thanks to a specific path, and a widget is a ViewComponent (find the definition in my article)

Let’s complete our plugin with the code to insert a Menu widget. Using the same process used for the plugin, we can say that a widget is a class which implements a specific interface; in our case, it is named  IWebRightWidget:

public interface IWebRightWidget
{
    IWebRightBaseViewModel GetViewModel(string language, object[] parameters);
}

This interface not only marks a class as a WebRight widget but also allows you to define how the data needed for its operation are recovered, whose data will then be reverted to a classic ViewModel. For example, if data for a menu widget is defined within a WebRightMenuViewModel class, which will extend a base class of the framework or implement IWebRightBaseViewModel directly, the widget implementation will look something like this:

public class WebRightMenuWidget : IWebRightWidget
{
    public IRaptorBaseViewModel GetViewModel(string language, object[] parameters)
    {
        WebRightMenuViewModel vm = new WebRightMenuViewModel();
         
        … view model fill logic ...
         
        return vm; 
    }
}

How does the framework know that there is a widget in the plugin? WebRight defines a WidgetFactory that contains a method that, given the name of the widget, verifies its existence and returns it to the caller who will display it:

public IWebRightWidget GetWidgetByName(string name)
{
    if (cache.Exists(name))
    {
        return cache.Get<IWebRightWidget>(name);
    }
    else
    {
        var widgetType = pluginManager.Plugins.GetTypes()
            .Where(y => typeof(IWebRightWidget).IsAssignableFrom(y) && y.Name == name)
            .FirstOrDefault();
 
            if (widgetType != null)
            {
                var widget = (IWebRightWidget)this.serviceProvider.GetService(widgetType);
                cache.Add<IWebRightWidget>(widget, name);
                return widget;
            }
 
            foreach (var plugin in this.pluginManager.Plugins)
            {
                widgetType = plugin.GetTypes()
                    .Where(y => typeof(IWebRightWidget).IsAssignableFrom(y) && y.Name == name)
                    .FirstOrDefault();
 
                if (widgetType != null)
                {
                    var widget = (IWebRightWidget)this.serviceProvider.GetService(widgetType);
                    cache.Add<IWebRightWidget>(widget, name);
                    return widget;
                }
            }
 
            throw new WebRightWidgetNotFoundException(name);
        }
    }
}

The method verifies if there is the required widget among the plugins exposed by the PluginManager. If it found it, adds it to a cache to speed up subsequent requests and return it; otherwise, it raises an exception.

As you can see, it is a matter of establishing conventions and using the Reflection of .NET, optimizing performance by using the cache. The code we have in production is obviously a little more complex than that, but the basic idea is the one exposed. If you are curious to know how then the widget is displayed, you can read one of my previous articles.

See you on the next article!