Skip to content

Dependency-injection based plugin toolkit for all .NET platforms.

Notifications You must be signed in to change notification settings

cav94mat/ExpandR

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ExpandR

A dependency-injection based plugin-system coded against .NET Standard 2.0 and Microsoft.Extensions.DependencyInjection.

⚠️ This project is experimental, and as such caution is advised before its use, especially in production environments.

How does it work?

The host - in general the main application - declares and exposes a set of service interfaces, which can be implemented by one or more plugin(s) (external assemblies), and/or even by the host itself - e.g. in case there is no plugin implementing it first:

using cav94mat.ExpandR;
namespace MyHost {
  public class Program {
    // [...]
    private static void ConfigureServices(IServiceCollection services)
    {    
        // Internal services, not exposed to plugins.
        services.AddSingleton<ISomethingInternal, SomethingInternal>(); 

        services.AddExpandR(pluggableServices =>
        {
            // Expose `IHelpPrinter` as a singleton, accepting only one implementation.
            //  If no plugin implements it, `DefaultHelpPrinter` is used.
            pluggableServices.ExposeSingleton<IHelpPrinter, DefaultHelpPrinter>();        
            
            // Expose `ICommand` as transient, accepting multiple implementations.
            //  At least two implementations (`EchoCommand` and` HelpCommand`) are always registered before
            //  any plugin is loaded.
            pluggableServices.ExposeMultiTransient<ICommand>(typeof(EchoCommand), typeof(HelpCommand));

            // Load plugins from the /plugins sub-directory
            var plugDir = new DirectoryInfo(Directory.GetCurrentDirectory()).CreateSubdirectory("plugins");
            pluggableServices.LoadPlugins(plugDir);
        });
    }
  }
}

💡 For the sake of brevity, the declarations of ISomethingInternal, ICommand, IHelpPrinter, and the relative built-in implementations DefaultHelpPrinter, EchoCommand and HelpCommand are ommitted, since irrelevant.

Plugins(s) are contained in external assembly and loaded at runtime via reflection, provided they have an entry-point class defined via an assembly-level attribute (see the example below). When a plugin is loaded it's entry-point is initialized and its Setup method is called, so that the host's dependency injection container can be further extended with the plugin's service implementations.

It's always the host that decides whether a specific service can have multiple implementations, as well as its lifetime (i.e. singleton, scoped or transient).

using cav94mat.ExpandR;
// ...

// Define the plugin entry-point with an assembly-level attribute:
[assembly: Entrypoint(typeof(MyPlugin.Plugin))]

namespace MyPlugin {
  // ...
  public class Plugin : IEntrypoint
  {
      // The entry-point method:
      public void Setup(IServiceCollectionExtender services)
      {
          // IHelpPrinter is to be implemented by FancyHelpPrinter, if no plugin has provided an
          //  implementation yet. Otherwise, `TryAdd` here simply returns false. On the other hand, if
          //  the alternative `Add` method was used and an implementation already exists, an
          //  exception is thrown.
          services.TryAdd<IHelpPrinter, FancyHelpPrinter>();
          // (Subsequent calls to TryAdd<IHelpPrinter, ...> will always return false.

          // Another implementation for ICommand, residing in the class MyPlugin.ConfCommand, is added.
          //  Since ICommand supports multiple implementation, `TryAdd` always returns true and `Add` is
          //  perfectly safe to be used here.
          services.Add<ICommand, ConfCommand>();
      }
  }
}

💡 For the sake of brevity, the declarations of FancyHelpPrinter and ConfCommand are ommitted, since irrelevant.

💡 The Entrypoint attribute and the IEntrypoint interface used in this example are part of ExpandR. It's possible to derive these types in order to create custom entry-points.