natemcmaster - McMaster.NETCore.Plugins 0.2.2-rtm.40

Provides API for consuming .NET Core and .NET Standard assemblies as plugins to a .NET Core application.

This package should be used by the host application which needs to load plugins. Projects which create plugins should use McMaster.NETCore.Plugins.Sdk to generate the plugin configuration files required to load .dll files.

PM> Install-Package McMaster.NETCore.Plugins -Version 0.2.2-rtm.40 -Source https://www.myget.org/F/natemcmaster/api/v3/index.json

Copy to clipboard

> nuget.exe install McMaster.NETCore.Plugins -Version 0.2.2-rtm.40 -Source https://www.myget.org/F/natemcmaster/api/v3/index.json

Copy to clipboard

> dotnet add package McMaster.NETCore.Plugins --version 0.2.2-rtm.40 --source https://www.myget.org/F/natemcmaster/api/v3/index.json

Copy to clipboard
<PackageReference Include="McMaster.NETCore.Plugins" Version="0.2.2-rtm.40" />
Copy to clipboard
source https://www.myget.org/F/natemcmaster/api/v3/index.json

nuget McMaster.NETCore.Plugins  ~> 0.2.2-rtm.40
Copy to clipboard

> choco install McMaster.NETCore.Plugins --version 0.2.2-rtm.40 --source https://www.myget.org/F/natemcmaster/api/v2

Copy to clipboard
Import-Module PowerShellGet
Register-PSRepository -Name "natemcmaster" -SourceLocation "https://www.myget.org/F/natemcmaster/api/v2"
Install-Module -Name "McMaster.NETCore.Plugins" -RequiredVersion "0.2.2-rtm.40" -Repository "natemcmaster" -AllowPreRelease
Copy to clipboard

.NET Plugins

Build Status Code Coverage NuGet NuGet Downloads

This project provides API for loading .NET assemblies dynamically, executing them as extensions to the main application, and finding and isolating the dependencies of the plugin from the main application. It allows fine-grained control over assembly isolation and type sharing. Read more details about type sharing below.

Note

2.0+ of library supports .NET 8. If you still need .NET < 8 support, look for an old 1.* version of this library.

Blog post introducing this project, July 25, 2018: .NET Core Plugins: Introducing an API for loading .dll files (and their dependencies) as 'plugins'.

Since 2018, .NET Core 3 was released, and it added stdlib API to improve assembly loading. If you are interested in understanding that API, see "Create a .NET Core application with plugins" on docs.microsoft.com. The result of this tutorial would be a simpler version of DotNetCorePlugins, but missing some features like an API for unifying types across the load context boundary, hot reload, and .NET Core 2 support.

Getting started

Install the McMaster.NETCore.Plugins NuGet package.

dotnet add package McMaster.NETCore.Plugins

The main API to use is PluginLoader.CreateFromAssemblyFile.

PluginLoader.CreateFromAssemblyFile(
    assemblyFile: "./plugins/MyPlugin/MyPlugin1.dll",
    sharedTypes: new [] { typeof(IPlugin), typeof(IServiceCollection), typeof(ILogger) },
    isUnloadable: true)
  • assemblyFile = the file path to the main .dll of the plugin
  • sharedTypes = a list of types which the loader should ensure are unified. (See What is a shared type?)
  • isUnloadable = Allow this plugin to be unloaded from memory at some point in the future. (Requires ensuring that you have cleaned up all usages of types from the plugin before unloading actually happens.)

See example projects in samples/ for more detailed, example usage.

Usage

Using plugins requires at least two projects: (1) the 'host' app which loads plugins and (2) the plugin, but typically also uses a third, (3) an contracts project which defines the interaction between the plugin and the host.

For a fully functional sample of this, see samples/hello-world/ .

The plugin contract

You can define your own plugin contract. A minimal contract might look like this.

public interface IPlugin
{
    string GetName();
}

There is nothing special about the name "IPlugin" or the fact that it's an interface. This is just here to illustrate a concept. Look at samples/ for additional examples of ways you could define the interaction between host and plugins.

The plugins

Warning

Using netstandard2.0 as the TargetFramework for your plugin project has known issues. Use net8.0 instead.

A minimal implementation of the plugin could be as simple as this.

internal class MyPlugin1 : IPlugin
{
    public string GetName() => "My plugin v1";
}

As mentioned above, this is just an example. This library doesn't require the use of "IPlugin" or interfaces or "GetName()" methods. This code is only here to demonstrates how you can decouple hosts and plugins, but still use interfaces for type-safe interactions.

The host

The host application can load plugins using the PluginLoader API. The host app needs to define a way to find the assemblies for the plugin on disk. One way to do this is to follow a convention, such as:

plugins/
    $PluginName1/
        $PluginName1.dll
        (additional plugin files)
    $PluginName2/
        $PluginName2.dll

It is important that each plugin is published into a separate directory. This will avoid contention between plugins and duplicate dependency issues.

You can prepare the sample plugin above by running

dotnet publish MyPlugin1.csproj --output plugins/MyPlugin1/

An implementation of a host which finds and loads this plugin might look like this. This sample uses reflection to find all types in plugins which implement IPlugin, and then initializes the types' parameter-less constructors. This is just one way to implement a host. More examples of how to use plugins can be found in samples/.

using McMaster.NETCore.Plugins;

var loaders = new List<PluginLoader>();

// create plugin loaders
var pluginsDir = Path.Combine(AppContext.BaseDirectory, "plugins");
foreach (var dir in Directory.GetDirectories(pluginsDir))
{
    var dirName = Path.GetFileName(dir);
    var pluginDll = Path.Combine(dir, dirName + ".dll");
    if (File.Exists(pluginDll))
    {
        var loader = PluginLoader.CreateFromAssemblyFile(
            pluginDll,
            sharedTypes: new [] { typeof(IPlugin) });
        loaders.Add(loader);
    }
}

// Create an instance of plugin types
foreach (var loader in loaders)
{
    foreach (var pluginType in loader
        .LoadDefaultAssembly()
        .GetTypes()
        .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsAbstract))
    {
        // This assumes the implementation of IPlugin has a parameterless constructor
        IPlugin plugin = (IPlugin)Activator.CreateInstance(pluginType);

        Console.WriteLine($"Created plugin instance '{plugin.GetName()}'.");
    }
}

What is a shared type?

By default, each instance of PluginLoader represents a unique collection of assemblies loaded into memory. This can make it difficult to use the plugin if you want to pass information from plugin to the host and vice versa. Shared types allow you define the kinds of objects that will be passed between plugin and host.

For example, let's say you have a simple host app like samples/hello-world/, and two plugins which were compiled with a reference interface IPlugin. This interface comes from Contracts.dll. When the application runs, by default, each plugin and the host will have their own version of Contracts.dll which .NET will keep isolated.

The problem with this isolation is that an object of IPlugin created within the "PluginApple" or "PluginBanana" context does not appear to be an instance of IPlugin in any of the other plugin contexts.

DefaultConfigDiagram

Configuring a shared type of IPlugin allows the .NET to pass objects of this type across the plugin isolation boundary. It does this by ignoring the version of Contracts.dll in each plugin folder, and sharing the version that comes with the Host.

SharedTypes

Read even more details about shared types here.

Support for MVC and Razor

A common usage for plugins is to load class libraries that contain MVC controllers or Razor Pages. You can set up an ASP.NET Core to load controllers and views from a plugin using the McMaster.NETCore.Plugins.Mvc package.

dotnet add package McMaster.NETCore.Plugins.Mvc

The main API to use is .AddPluginFromAssemblyFile(), which can be chained onto the call to .AddMvc() or .AddRazorPages() in the Startup.ConfigureServices method.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        var pluginFile = Path.Combine(AppContext.BaseDirectory, "plugins/MyRazorPlugin/MyRazorPlugin.dll");
        services
            .AddMvc()
            // The AddPluginFromAssemblyFile method comes from McMaster.NETCore.Plugins.Mvc
            .AddPluginFromAssemblyFile(pluginFile);
    }
}

See example projects in samples/aspnetcore-mvc/ for more detailed, example usage.

Reflection

Sometimes you may want to use a plugin along with reflection APIs such as Type.GetType(string typeName) or Assembly.Load(string assemblyString). Depending on where these APIs are used, they might fail to load the assemblies in your plugin. There is an API which you can use to set the ambient context which .NET's reflection APIs will use to load the correct assemblies from your plugin.

Example:

var loader = PluginLoader.CreateFromAssemblyFile("./plugins/MyPlugin/MyPlugin1.dll");

using (loader.EnterContextualReflection())
{
    var myPluginType = Type.GetType("MyPlugin.PluginClass");
    var myPluginAssembly = Assembly.Load("MyPlugin1");
}

Read this post written by .NET Core engineers for even more details on contextual reflection.

Overriding the Default Load Context

Under the hood, DotNetCorePlugins is using a .NET API called ApplicationLoadContext. This creates a scope for resolving assemblies. By default, PluginLoader will create a new context and fallback to a default context if it cannot find an assembly or if type sharing is enabled. The default fallback context is inferred when PluginLoader is instantiated. In certain advanced scenarios, you may need to manually change the default context, for instance, plugins which then load more plugins, or when running .NET in a custom native host.

To override the default assembly load context, set PluginConfig.DefaultContext. Example:

AssemblyLoadContext myCustomDefaultContext = // (something).
PluginLoader.CreateFromAssemblyFile(dllPath,
     config => config.DefaultContext = myCustomDefaultContext);

Bug fix:

  • Fix loading plugins which use SqlClient on Windows.
  • .NETCoreApp 2.0
    • Microsoft.Extensions.DependencyModel (>= 2.0.0)
  • .NETCoreApp 2.0: 2.0.0.0

Signature validation information

Informational

Signature Hash Algorithm: SHA256

Signature type: Author

Verifying the author primary signature with certificate: 
  Subject Name: CN=Nathan McMaster, O=Nathan McMaster, L=Redmond, S=WA, C=US
  SHA1 hash: 6724CA970495F8F364E4CFD47955577E55169DF0
  SHA256 hash: 2BD101C6C436FED4D086F3AC3939C49672360CA777CA3995D455871F71A98B1A
  Issued by: CN=DigiCert SHA2 Assured ID Code Signing CA, OU=www.digicert.com, O=DigiCert Inc, C=US
  Valid from: 7/1/2018 12:00:00 AM to 7/2/2019 12:00:00 PM

Timestamp: 1/15/2019 4:45:35 AM

Verifying author primary signature's timestamp with timestamping service certificate: 
  Subject Name: CN=DigiCert SHA2 Timestamp Responder, O=DigiCert, C=US
  SHA1 hash: 400191475C98891DEBA104AF47091B5EB6D4CBCB
  SHA256 hash: FC834D5BFFDE31DBA5B79BF95F573F7953BCBF9156E8525163E828EB92EA8A93
  Issued by: CN=DigiCert SHA2 Assured ID Timestamping CA, OU=www.digicert.com, O=DigiCert Inc, C=US
  Valid from: 1/4/2017 12:00:00 AM to 1/18/2028 12:00:00 AM

Owners

Nate McMaster

Authors

Nate McMaster

Project URL

https://github.com/natemcmaster/DotNetCorePlugins

License

Apache-2.0

Tags

.NET Core plugins

Signature

Validation: Valid

Info

6859 total downloads
75 downloads for version 0.2.2-rtm.40
Download (41.94 KB)
Found on the current feed only

Package history

Version Size Last updated Downloads Mirrored?
1.2.0 58.5 KB Tue, 10 Mar 2020 06:18:34 GMT 77
1.2.0-rc.179 58.54 KB Tue, 10 Mar 2020 06:17:53 GMT 72
1.1.0 57.31 KB Fri, 17 Jan 2020 05:10:36 GMT 72
1.1.0-rc.178 58.54 KB Tue, 10 Mar 2020 05:55:06 GMT 85
1.1.0-beta.171 57.24 KB Fri, 10 Jan 2020 16:00:22 GMT 71
1.0.0 56.56 KB Thu, 02 Jan 2020 06:59:47 GMT 65
1.0.0-rc.165 56.6 KB Thu, 02 Jan 2020 06:30:17 GMT 71
1.0.0-rc.160 56.66 KB Thu, 02 Jan 2020 06:07:33 GMT 62
1.0.0-beta.154 56.68 KB Sun, 29 Dec 2019 21:42:26 GMT 78
1.0.0-beta.152 56.6 KB Sun, 29 Dec 2019 18:45:54 GMT 65
1.0.0-beta.148 55.85 KB Mon, 16 Dec 2019 00:40:24 GMT 72
1.0.0-beta.141 55.83 KB Sun, 08 Dec 2019 21:38:47 GMT 76
1.0.0-beta.139 55.83 KB Sun, 08 Dec 2019 20:52:40 GMT 85
1.0.0-beta.138 55.83 KB Sun, 08 Dec 2019 20:27:12 GMT 72
1.0.0-beta.135 55.81 KB Sat, 07 Dec 2019 23:33:23 GMT 72
1.0.0-beta.131 55.81 KB Sat, 07 Dec 2019 23:10:16 GMT 64
1.0.0-beta.129 55.83 KB Sat, 07 Dec 2019 23:05:55 GMT 78
1.0.0-beta.121 55.82 KB Sat, 07 Dec 2019 22:16:58 GMT 93
1.0.0-beta.116 55.83 KB Fri, 01 Nov 2019 05:11:46 GMT 75
1.0.0-beta.114 55.79 KB Tue, 29 Oct 2019 04:23:38 GMT 64
0.3.2 56.79 KB Sun, 15 Dec 2019 04:00:24 GMT 76
0.3.1 56.76 KB Mon, 14 Oct 2019 04:25:10 GMT 63
0.3.1-rc.126 55.68 KB Sat, 07 Dec 2019 22:49:18 GMT 71
0.3.1-rc.124 56.94 KB Sat, 07 Dec 2019 22:46:16 GMT 54
0.3.1-rc.122 56.79 KB Sat, 07 Dec 2019 22:40:21 GMT 77
0.3.1-rc.113 55.69 KB Tue, 29 Oct 2019 04:17:40 GMT 73
0.3.1-rc.111 56.79 KB Mon, 14 Oct 2019 04:42:41 GMT 87
0.3.1-rc.109 56.79 KB Mon, 14 Oct 2019 04:18:14 GMT 65
0.3.0 56.91 KB Tue, 24 Sep 2019 04:31:37 GMT 70
0.3.0-rc.108 56.96 KB Mon, 14 Oct 2019 04:09:45 GMT 69
0.3.0-rc.103 56.96 KB Tue, 24 Sep 2019 04:24:05 GMT 55
0.3.0-rc.100 56.94 KB Tue, 24 Sep 2019 04:13:07 GMT 80
0.3.0-rc.89 56.71 KB Sat, 14 Sep 2019 04:33:09 GMT 68
0.3.0-rc.86 54.93 KB Thu, 05 Sep 2019 06:47:33 GMT 70
0.3.0-beta.85 54.94 KB Thu, 05 Sep 2019 06:41:54 GMT 83
0.3.0-beta.83 54.96 KB Thu, 05 Sep 2019 06:26:37 GMT 76
0.3.0-beta.82 54.78 KB Thu, 05 Sep 2019 04:45:18 GMT 70
0.3.0-beta.81 54.77 KB Fri, 30 Aug 2019 05:56:41 GMT 64
0.3.0-beta.80 54.77 KB Fri, 30 Aug 2019 05:53:48 GMT 66
0.3.0-beta.79 54.77 KB Fri, 30 Aug 2019 05:50:20 GMT 75
0.3.0-beta.78 54.76 KB Fri, 30 Aug 2019 05:48:06 GMT 77
0.3.0-beta.77 54.55 KB Fri, 30 Aug 2019 05:43:42 GMT 61
0.3.0-beta.76 54.53 KB Wed, 28 Aug 2019 06:02:28 GMT 67
0.3.0-beta.75 54.52 KB Thu, 15 Aug 2019 05:19:20 GMT 70
0.3.0-beta.73 53.15 KB Wed, 14 Aug 2019 05:14:11 GMT 73
0.3.0-beta.69 52.99 KB Wed, 24 Jul 2019 04:55:19 GMT 72
0.3.0-beta.68 52.99 KB Wed, 24 Jul 2019 04:54:27 GMT 71
0.3.0-beta.67 52.99 KB Wed, 24 Jul 2019 01:12:44 GMT 76
0.3.0-beta.50 53.03 KB Tue, 16 Jul 2019 05:07:17 GMT 74
0.3.0-beta.49 52.98 KB Tue, 16 Jul 2019 04:36:41 GMT 57
0.3.0-beta.48 53.08 KB Mon, 15 Jul 2019 22:27:32 GMT 65
0.3.0-beta.45 53.08 KB Fri, 21 Jun 2019 21:38:41 GMT 77
0.3.0-beta.44 53.08 KB Fri, 21 Jun 2019 21:27:52 GMT 71
0.3.0-beta.40 52.95 KB Fri, 21 Jun 2019 20:45:03 GMT 81
0.3.0-beta.36 52.95 KB Thu, 09 May 2019 05:55:21 GMT 86
0.3.0-beta.35 52.06 KB Thu, 25 Apr 2019 05:21:21 GMT 70
0.3.0-beta.34 52.12 KB Thu, 25 Apr 2019 05:12:02 GMT 71
0.3.0-beta.29 53.06 KB Sat, 20 Apr 2019 19:51:11 GMT 72
0.3.0-beta.27 53.07 KB Thu, 11 Apr 2019 15:48:39 GMT 62
0.3.0-beta.26 53.07 KB Tue, 26 Mar 2019 05:17:08 GMT 78
0.2.4 31.45 KB Fri, 01 Feb 2019 03:20:03 GMT 65
0.2.4-preview.190130.2 31.5 KB Thu, 31 Jan 2019 07:15:46 GMT 69
0.2.3 31.43 KB Wed, 30 Jan 2019 15:53:34 GMT 72
0.2.3-preview.190130.2 31.49 KB Wed, 30 Jan 2019 15:45:27 GMT 72
0.2.3-preview.190130.1 31.48 KB Wed, 30 Jan 2019 14:56:21 GMT 65
0.2.2 31.5 KB Fri, 25 Jan 2019 04:00:05 GMT 84
0.2.2-rtm.41 42.3 KB Tue, 15 Jan 2019 07:04:49 GMT 71
0.2.2-rtm.40 41.94 KB Tue, 15 Jan 2019 04:45:47 GMT 75
0.2.2-preview.190130.1 31.56 KB Wed, 30 Jan 2019 14:53:58 GMT 70
0.2.2-preview.190124.10 31.47 KB Fri, 25 Jan 2019 03:27:06 GMT 77
0.2.1 41.67 KB Sun, 21 Oct 2018 03:28:54 GMT 73
0.2.1-rtm.36 41.69 KB Wed, 05 Dec 2018 16:08:32 GMT 68
0.2.1-rtm.34 41.7 KB Tue, 23 Oct 2018 01:25:55 GMT 72
0.2.1-rtm.32 41.69 KB Sun, 21 Oct 2018 03:26:04 GMT 75
0.2.0 41.23 KB Wed, 26 Sep 2018 06:18:01 GMT 63
0.2.0-rtm.29 41.25 KB Sat, 06 Oct 2018 17:31:48 GMT 71
0.2.0-rtm.28 41.24 KB Sat, 06 Oct 2018 17:29:46 GMT 81
0.2.0-rtm.27 41.25 KB Sat, 06 Oct 2018 17:27:28 GMT 74
0.2.0-rtm.25 41.24 KB Wed, 26 Sep 2018 06:15:46 GMT 76
0.2.0-rtm.24 41.15 KB Wed, 26 Sep 2018 06:13:37 GMT 76
0.1.1 38.5 KB Wed, 22 Aug 2018 06:01:56 GMT 73
0.1.1-rtm.23 39.34 KB Wed, 05 Sep 2018 05:32:44 GMT 69
0.1.1-rtm.21 38.51 KB Wed, 22 Aug 2018 05:59:17 GMT 84
0.1.1-rtm.19 38.5 KB Wed, 22 Aug 2018 05:34:25 GMT 76
0.1.0 37.11 KB Tue, 24 Jul 2018 04:14:39 GMT 74
0.1.0-rtm.17 37.12 KB Wed, 25 Jul 2018 15:58:34 GMT 69
0.1.0-rtm.16 37.13 KB Wed, 25 Jul 2018 15:56:45 GMT 72
0.1.0-rtm.15 37.14 KB Wed, 25 Jul 2018 15:55:21 GMT 64
0.1.0-rtm.14 37.14 KB Wed, 25 Jul 2018 14:45:46 GMT 75
0.1.0-rtm.10 37.13 KB Wed, 25 Jul 2018 06:21:22 GMT 82
0.1.0-rtm.8 37.13 KB Tue, 24 Jul 2018 04:13:41 GMT 73
0.1.0-rtm.7 36.33 KB Tue, 24 Jul 2018 04:10:09 GMT 74
0.1.0-alpha1.6 36.35 KB Tue, 24 Jul 2018 04:06:02 GMT 80
0.1.0-alpha1.5 36.35 KB Tue, 24 Jul 2018 04:00:13 GMT 72
0.1.0-alpha1.4 36.1 KB Tue, 24 Jul 2018 03:17:57 GMT 61