Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Building a Simple Plug-ins System for ASP.NET Core

DZone's Guide to

Building a Simple Plug-ins System for ASP.NET Core

In this post, we take a look at how to add plug-in support to your ASP.NET Core project. Read on for details and some example code!

· Web Dev Zone
Free Resource

Prove impact and reduce risk when rolling out new features. Optimizely Full Stack helps you experiment in any application.

Recently I built plug-ins support for my TemperatureStation IoT solution website. The code for .NET Core is different from what we have seen on full .NET Framework (application domains etc.) but there’s still nothing complex. This blog post describes how to build simple plug-ins support for ASP.NET Core web applications.

After some searching in web and some small experimenting I came out with a simple solution that covers the following:

  1. Loading types from assemblies
  2. Registering types automatically with built-in dependency injection
  3. Getting instances through built-in dependency injection

The code shown here is also kind of experimental and it is taken from my open-source IoT solution called TemperatureStation.

Calculator Plug-ins

TemperatureStation has plug-ins called Calculators. Calculators are classes that can be bound to measurements and when readings are reported then Calculators are run on readings. They provide different calculations like finding freezing point of liquid, estimating the time it takes for liquid to get to freezing point etc.

For calculators, there is ICalculator interface shown below.


public interface ICalculator
{
    double Calculate(SensorReadings readings, Measurement measurement);
    string DisplayValue(double value);
    bool ReturnsReading { get; }
    void SetParameters(string parameters);
}

Calculators use CalculatorAttribute to define some metadata.


[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class CalculatorAttribute : Attribute
{
    public CalculatorAttribute()
    {
        Order = -1;
        ShowOnChart = true;
    }public string Name { get; set; }
    public int Order { get; set; }
    public bool ShowOnChart { get; set; }
    public string DisplayLabel { get; set; }
}

And here is the example of dummy calculator.


public class DummyCalculator : ICalculator
{
    public bool ReturnsReading
    {
        get { return true; }
    }public double Calculate(SensorReadings readings, Measurement measurement)
    {
        return readings.Readings.First().Reading + 10f;
    }       public string DisplayValue(double value)
    {
        return value.ToString();
    }public void SetParameters(string parameters)
    {
    }
}

Finding Plug-in Types

To detect plug-ins I wrote the CalculatorsLoader class. This a static class that creates a list of calculator types. The rule is simple: the class must implement ICalculator interface and must have CalculatorAttribute. Consider this class as an internal matter of the application.


public static class CalculatorsLoader
{
    private static IList<Type> _calculatorTypes;public static IList<Type> CalculatorTypes
    {
        get
        {
            if(_calculatorTypes == null)
            {
                LoadCalculatorTypes();
            }               return _calculatorTypes.ToList();
        }            
    }private static void LoadCalculatorTypes()
    {
        if (_calculatorTypes != null)
        {
            return;
        }           var calcs = from a in GetReferencingAssemblies()
                    from t in a.GetTypes()
                    where t.GetTypeInfo().GetCustomAttribute<CalculatorAttribute>() != null
                          && t.GetTypeInfo().ImplementedInterfaces.Contains(typeof(ICalculator))
                    select t;           _calculatorTypes = calcs.OrderBy(t => t.GetTypeInfo().GetCustomAttribute<CalculatorAttribute>().Order).ToList();
    }private static IEnumerable<Assembly> GetReferencingAssemblies()
    {
        var assemblies = new List<Assembly>();
        var dependencies = DependencyContext.Default.RuntimeLibraries;           foreach (var library in dependencies)
        {
            try
            {
                var assembly = Assembly.Load(new AssemblyName(library.Name));
                assemblies.Add(assembly);
            }
            catch (FileNotFoundException)
            { }
        }
        return assemblies;
    }
}

Automatic Registering of Plug-in Types

To use ASP.NET Core dependency injection I wrote a class that provides extension method for IServiceCollection. Dependency injection is needed because Calculators may use constructor injection to access services and database.


public static class CalculatorExtensions
{
    public static void AddCalculators(this IServiceCollection services)
    {
        foreach(var calcType in CalculatorsLoader.CalculatorTypes)
        {
            services.AddTransient(calcType);
        }
    }
}

This is how to use AddCalculators extension method in Startup class of web application.


public void ConfigureServices(IServiceCollection services)
{
    // ...

    services.AddMvc();       services.AddSingleton<ICalculatorProvider, CalculatorProvider>();
    services.AddCalculators();// ...
}

When web application starts then ConfigureServices method is called and Calculators are automatically registered.

Plug-in Provider

To access calculators I wrote ICalculatorProvider interface and CalculatorProvider class. This class with CalculatorExtensions are the only classes that access CalculatorsLoader directly. All other classes in system use provider to query the calculator types. ICalculatorProvider is introduced to ASP.NET Core dependency injection in application Startup class.

GetCalculators() method uses framework level dependency injection to create instances of ICalculator.


public interface ICalculatorProvider
{
    IDictionary<string, ICalculator> GetCalculators();
    IEnumerable<string> GetNames();
    IEnumerable<Type> GetTypes();
}

public class CalculatorProvider : ICalculatorProvider
{
    private IServiceProvider _serviceProvider;public CalculatorProvider(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }       public IDictionary<string,ICalculator> GetCalculators()
    {
        var result = new Dictionary<string, ICalculator>();foreach(var type in CalculatorsLoader.CalculatorTypes)
        {
            var calc = (ICalculator)_serviceProvider.GetService(type);
            var name = type.GetTypeInfo().GetCustomAttribute<CalculatorAttribute>().Name;               result.Add(name, calc);
        }return result;
    }       public IEnumerable<string> GetNames()
    {
        return CalculatorsLoader.CalculatorTypes
                .Select(t => t.GetTypeInfo().GetCustomAttribute<CalculatorAttribute>()?.Name)
                .Where(t => !string.IsNullOrWhiteSpace(t));            
    }public IEnumerable<Type> GetTypes()
    {
        return CalculatorsLoader.CalculatorTypes;
    }
}

Using Plug-ins in Code

Here is the example about how I use ICalculatorProvider in controller code to output the list of available calculators.


public class DummyController : Controller
{
    private ICalculatorProvider _calculatorProvider;public DummyController(ICalculatorProvider calculatorProvider)
    {
        _calculatorProvider = calculatorProvider;
    }       public IActionResult GetCalculators()
    {
        var calculators = _calculatorProvider.GetCalculators();
        var output = "";foreach (var calculator in calculators)
        {
            output += calculator.Key + "\r\n";
        }           return Content(output, "text/plain");
    }
}

The real usage in my solution is more complex but this example gives the point.

Wrapping Up

On .NET Core, things work a little bit differently compared to the full .NET Framework. Although this solution is not perfect, it was still pretty easy to find information and make the code work. It was also easy to automatically register plug-in types with ASP.NET Core framework-level dependency injection and come out with simple classes that architecturally fit into a web application. For my IoT system, this solution is good enough to go with for today.

With SDKs for all major client and server side platforms, you can experiment on any platform with Optimizely Full Stack.

Topics:
plug-in ,web application ,asp.net core ,code

Published at DZone with permission of Gunnar Peipman, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}