Building simple plug-ins system for ASP.NET Core

Recently I built plug-ins support to my TemperatureStation IoT solution web site. 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 to ASP.NET Core web application.

After some searching in web and some small experimenting I came out with 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 CalculatorsLoader class. This a static class that creates list of calculator types. Rule is simple: class must implement ICalculator interface and must have CalculatorAttribute. Consider this class as internal matter of 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 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 calcutor 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 full .NET Framework. Although this solution is not perfect it was still pretty easy to find information and make 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 in to web application. For my IoT system this solution today is good enough to go with.

Gunnar Peipman

Gunnar Peipman is ASP.NET, Azure and SharePoint fan, Estonian Microsoft user group leader, blogger, conference speaker, teacher, and tech maniac. Since 2008 he is Microsoft MVP specialized on ASP.NET.

    11 thoughts on “Building simple plug-ins system for ASP.NET Core

    • January 25, 2017 at 4:19 pm
      Permalink

      Hi,

      Great article – simple and easy to follow.

      To make things even clearer, you may want to add an example of the usage of the CalculatorAttribute on DummyCalculator

      [Calculator(DisplayLabel=”Dummy Freezing Point”]
      public class DummyCalculator : ICalculator

    • January 25, 2017 at 4:41 pm
      Permalink

      Why not use an IoC fw?

    • January 25, 2017 at 10:50 pm
      Permalink

      Antonio, the example here uses IoC in form of dependency injection. All Calculator plug-in types are registered with ASP.NET Core framework-level dependency injection and they are also created through framework-level dependency injection in code that uses these.

    • February 2, 2017 at 7:23 am
      Permalink

      Nice write up. Thanks for sharing.

      Did you ever consider using MEF since it (well, parts of it) are available for .NET Core?

    • February 2, 2017 at 10:17 am
      Permalink

      Yes, MEF is also good but also “bigger” option. Currently I needed some good way of adding new plug-ins in same project but will move later to MEF or some similar component for sure.

    • Pingback:Szumma #077 – 2017 5. hét | d/fuel

    • March 30, 2017 at 10:57 am
      Permalink

      If you register the calculators as ICalculator (services.AddTransient(typeof(ICalculator), calcType);) you could do
      var calculators = _serviceProvider.GetServices()
      in CalculatorProvider to get instances of all available calculators. It’s questionable if this is even an improvement but to me it feels cleaner.

    • March 31, 2017 at 7:29 am
      Permalink

      Thanks for good suggestion, Spetsen. Call to GetServices() would fit better with framework level DI concept. Before using it one must be sure that the order of calculators will remain the same as in calculator types array.

    • July 11, 2017 at 12:45 pm
      Permalink

      Great article and really excellent straight to point writing

    • September 26, 2017 at 5:16 am
      Permalink

      Nice article?
      I am getting the below error when I try to load the referenced assemblies,
      “System.IO.FileNotFoundException: ‘Could not load file or assembly ‘Libuv, Culture=neutral, PublicKeyToken=null’. The system cannot find the file specified.’

      Any idea?

    • September 26, 2017 at 9:35 am
      Permalink

      You can ignore these errors like I did in my code. Not all reference assemblies can be loaded this way.

    Leave a Reply

    Your email address will not be published. Required fields are marked *