natemcmaster - McMaster.NETCore.Plugins 1.2.0
Provides API for dynamically loading assemblies into a .NET application.
This package should be used by the host application which needs to load plugins. See https://github.com/natemcmaster/DotNetCorePlugins/blob/master/README.md for more samples and documentation.
PM> Install-Package McMaster.NETCore.Plugins -Version 1.2.0 -Source https://www.myget.org/F/natemcmaster/api/v3/index.json
> nuget.exe install McMaster.NETCore.Plugins -Version 1.2.0 -Source https://www.myget.org/F/natemcmaster/api/v3/index.json
> dotnet add package McMaster.NETCore.Plugins --version 1.2.0 --source https://www.myget.org/F/natemcmaster/api/v3/index.json
source https://www.myget.org/F/natemcmaster/api/v3/index.json
nuget McMaster.NETCore.Plugins ~> 1.2.0
Copy to clipboard
> choco install McMaster.NETCore.Plugins --version 1.2.0 --source https://www.myget.org/F/natemcmaster/api/v2
Import-Module PowerShellGet
Register-PSRepository -Name "natemcmaster" -SourceLocation "https://www.myget.org/F/natemcmaster/api/v2"
Install-Module -Name "McMaster.NETCore.Plugins" -RequiredVersion "1.2.0" -Repository "natemcmaster"
Copy to clipboard
.NET Core Plugins
This project provides API for loading .NET Core assemblies dynamically, executing them as extensions to the main application, and finding and isolating the dependencies of the plugin from the main application. This library supports .NET Core 2, but works best in .NET Core 3 and up. It allows fine-grained control over assembly isolation and type sharing. Read more details about type sharing below.
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 = (.NET Core 3+ only). 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
Typically, it is best to implement plugins by targeting net5.0
or higher. They can target netstandard2.0
as well, but using net5.0
is better because it reduces the number of redundant System.* assemblies in the plugin output.
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 Core 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.
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.
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. In .NET Core 3+, 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 Core 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);
Changes:
- @thoemmi - Debounce file system events when using hot reload and add API to control the debounce delay (default = 200 ms) (PR #129)
-
.NETCoreApp 2.0
- Microsoft.DotNet.PlatformAbstractions (>= 3.1.0)
- Microsoft.Extensions.DependencyModel (>= 3.1.0)
- System.Text.Json (>= 4.7.0)
-
.NETCoreApp 3.0
- Microsoft.DotNet.PlatformAbstractions (>= 3.1.0)
- Microsoft.Extensions.DependencyModel (>= 3.1.0)
- .NETCoreApp 2.0: 2.0.0.0
- .NETCoreApp 3.0: 3.0.0.0
Signature validation information
Informational
Signature Hash Algorithm: SHA256 Timestamp: 3/10/2020 6:16:57 AM Verifying author primary signature's timestamp with timestamping service certificate: Subject Name: CN=TIMESTAMP-SHA256-2019-10-15, O="DigiCert, Inc.", C=US SHA1 hash: 0325BD505EDA96302DC22F4FA01E4C28BE2834C5 SHA256 hash: 481F4373272D98586C5364B6C115E82425675AEBFD9FACF7ADC464FA2FFFB8F0 Issued by: CN=DigiCert SHA2 Assured ID Timestamping CA, OU=www.digicert.com, O=DigiCert Inc, C=US Valid from: 10/1/2019 12:00:00 AM to 10/17/2030 12:00:00 AM 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: 43BEB98AA1E8C9EC89B3930C59FF34173044A3B8 SHA256 hash: BC51EBF05EB96010FF3EEED195DE3EC028884A5B4DD9EABF233A16E048401878 Issued by: CN=DigiCert SHA2 Assured ID Code Signing CA, OU=www.digicert.com, O=DigiCert Inc, C=US Valid from: 6/7/2019 12:00:00 AM to 6/9/2020 12:00:00 PM
OwnersNate McMaster |
AuthorsNate McMaster |
Project URLhttps://github.com/natemcmaster/DotNetCorePlugins |
LicenseApache-2.0 |
Tags.NET Core plugins |
SignatureValidation: Valid |
Info133 total downloads |
2 downloads for version 1.2.0 |
Download (58.5 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 | 2 | ||||
1.2.0-rc.179 | 58.54 KB | Tue, 10 Mar 2020 06:17:53 GMT | 2 | ||||
1.1.0 | 57.31 KB | Fri, 17 Jan 2020 05:10:36 GMT | 2 | ||||
1.1.0-rc.178 | 58.54 KB | Tue, 10 Mar 2020 05:55:06 GMT | 1 | ||||
1.1.0-beta.171 | 57.24 KB | Fri, 10 Jan 2020 16:00:22 GMT | 2 | ||||
1.0.0 | 56.56 KB | Thu, 02 Jan 2020 06:59:47 GMT | 1 | ||||
1.0.0-rc.165 | 56.6 KB | Thu, 02 Jan 2020 06:30:17 GMT | 2 | ||||
1.0.0-rc.160 | 56.66 KB | Thu, 02 Jan 2020 06:07:33 GMT | 2 | ||||
1.0.0-beta.154 | 56.68 KB | Sun, 29 Dec 2019 21:42:26 GMT | 2 | ||||
1.0.0-beta.152 | 56.6 KB | Sun, 29 Dec 2019 18:45:54 GMT | 2 | ||||
1.0.0-beta.148 | 55.85 KB | Mon, 16 Dec 2019 00:40:24 GMT | 2 | ||||
1.0.0-beta.141 | 55.83 KB | Sun, 08 Dec 2019 21:38:47 GMT | 2 | ||||
1.0.0-beta.139 | 55.83 KB | Sun, 08 Dec 2019 20:52:40 GMT | 2 | ||||
1.0.0-beta.138 | 55.83 KB | Sun, 08 Dec 2019 20:27:12 GMT | 2 | ||||
1.0.0-beta.135 | 55.81 KB | Sat, 07 Dec 2019 23:33:23 GMT | 2 | ||||
1.0.0-beta.131 | 55.81 KB | Sat, 07 Dec 2019 23:10:16 GMT | 1 | ||||
1.0.0-beta.129 | 55.83 KB | Sat, 07 Dec 2019 23:05:55 GMT | 1 | ||||
1.0.0-beta.121 | 55.82 KB | Sat, 07 Dec 2019 22:16:58 GMT | 1 | ||||
1.0.0-beta.116 | 55.83 KB | Fri, 01 Nov 2019 05:11:46 GMT | 1 | ||||
1.0.0-beta.114 | 55.79 KB | Tue, 29 Oct 2019 04:23:38 GMT | 1 | ||||
0.3.2 | 56.79 KB | Sun, 15 Dec 2019 04:00:24 GMT | 1 | ||||
0.3.1 | 56.76 KB | Mon, 14 Oct 2019 04:25:10 GMT | 1 | ||||
0.3.1-rc.126 | 55.68 KB | Sat, 07 Dec 2019 22:49:18 GMT | 0 | ||||
0.3.1-rc.124 | 56.94 KB | Sat, 07 Dec 2019 22:46:16 GMT | 1 | ||||
0.3.1-rc.122 | 56.79 KB | Sat, 07 Dec 2019 22:40:21 GMT | 0 | ||||
0.3.1-rc.113 | 55.69 KB | Tue, 29 Oct 2019 04:17:40 GMT | 1 | ||||
0.3.1-rc.111 | 56.79 KB | Mon, 14 Oct 2019 04:42:41 GMT | 1 | ||||
0.3.1-rc.109 | 56.79 KB | Mon, 14 Oct 2019 04:18:14 GMT | 0 | ||||
0.3.0 | 56.91 KB | Tue, 24 Sep 2019 04:31:37 GMT | 1 | ||||
0.3.0-rc.108 | 56.96 KB | Mon, 14 Oct 2019 04:09:45 GMT | 1 | ||||
0.3.0-rc.103 | 56.96 KB | Tue, 24 Sep 2019 04:24:05 GMT | 1 | ||||
0.3.0-rc.100 | 56.94 KB | Tue, 24 Sep 2019 04:13:07 GMT | 1 | ||||
0.3.0-rc.89 | 56.71 KB | Sat, 14 Sep 2019 04:33:09 GMT | 1 | ||||
0.3.0-rc.86 | 54.93 KB | Thu, 05 Sep 2019 06:47:33 GMT | 1 | ||||
0.3.0-beta.85 | 54.94 KB | Thu, 05 Sep 2019 06:41:54 GMT | 14 | ||||
0.3.0-beta.83 | 54.96 KB | Thu, 05 Sep 2019 06:26:37 GMT | 2 | ||||
0.3.0-beta.82 | 54.78 KB | Thu, 05 Sep 2019 04:45:18 GMT | 0 | ||||
0.3.0-beta.81 | 54.77 KB | Fri, 30 Aug 2019 05:56:41 GMT | 0 | ||||
0.3.0-beta.80 | 54.77 KB | Fri, 30 Aug 2019 05:53:48 GMT | 0 | ||||
0.3.0-beta.79 | 54.77 KB | Fri, 30 Aug 2019 05:50:20 GMT | 1 | ||||
0.3.0-beta.78 | 54.76 KB | Fri, 30 Aug 2019 05:48:06 GMT | 1 | ||||
0.3.0-beta.77 | 54.55 KB | Fri, 30 Aug 2019 05:43:42 GMT | 0 | ||||
0.3.0-beta.76 | 54.53 KB | Wed, 28 Aug 2019 06:02:28 GMT | 10 | ||||
0.3.0-beta.75 | 54.52 KB | Thu, 15 Aug 2019 05:19:20 GMT | 1 | ||||
0.3.0-beta.73 | 53.15 KB | Wed, 14 Aug 2019 05:14:11 GMT | 0 | ||||
0.3.0-beta.69 | 52.99 KB | Wed, 24 Jul 2019 04:55:19 GMT | 1 | ||||
0.3.0-beta.68 | 52.99 KB | Wed, 24 Jul 2019 04:54:27 GMT | 0 | ||||
0.3.0-beta.67 | 52.99 KB | Wed, 24 Jul 2019 01:12:44 GMT | 1 | ||||
0.3.0-beta.50 | 53.03 KB | Tue, 16 Jul 2019 05:07:17 GMT | 1 | ||||
0.3.0-beta.49 | 52.98 KB | Tue, 16 Jul 2019 04:36:41 GMT | 0 | ||||
0.3.0-beta.48 | 53.08 KB | Mon, 15 Jul 2019 22:27:32 GMT | 1 | ||||
0.3.0-beta.45 | 53.08 KB | Fri, 21 Jun 2019 21:38:41 GMT | 3 | ||||
0.3.0-beta.44 | 53.08 KB | Fri, 21 Jun 2019 21:27:52 GMT | 1 | ||||
0.3.0-beta.40 | 52.95 KB | Fri, 21 Jun 2019 20:45:03 GMT | 1 | ||||
0.3.0-beta.36 | 52.95 KB | Thu, 09 May 2019 05:55:21 GMT | 12 | ||||
0.3.0-beta.35 | 52.06 KB | Thu, 25 Apr 2019 05:21:21 GMT | 3 | ||||
0.3.0-beta.34 | 52.12 KB | Thu, 25 Apr 2019 05:12:02 GMT | 2 | ||||
0.3.0-beta.29 | 53.06 KB | Sat, 20 Apr 2019 19:51:11 GMT | 1 | ||||
0.3.0-beta.27 | 53.07 KB | Thu, 11 Apr 2019 15:48:39 GMT | 1 | ||||
0.3.0-beta.26 | 53.07 KB | Tue, 26 Mar 2019 05:17:08 GMT | 2 | ||||
0.2.4 | 31.45 KB | Fri, 01 Feb 2019 03:20:03 GMT | 0 | ||||
0.2.4-preview.190130.2 | 31.5 KB | Thu, 31 Jan 2019 07:15:46 GMT | 0 | ||||
0.2.3 | 31.43 KB | Wed, 30 Jan 2019 15:53:34 GMT | 1 | ||||
0.2.3-preview.190130.2 | 31.49 KB | Wed, 30 Jan 2019 15:45:27 GMT | 1 | ||||
0.2.3-preview.190130.1 | 31.48 KB | Wed, 30 Jan 2019 14:56:21 GMT | 1 | ||||
0.2.2 | 31.5 KB | Fri, 25 Jan 2019 04:00:05 GMT | 1 | ||||
0.2.2-rtm.41 | 42.3 KB | Tue, 15 Jan 2019 07:04:49 GMT | 1 | ||||
0.2.2-rtm.40 | 41.94 KB | Tue, 15 Jan 2019 04:45:47 GMT | 2 | ||||
0.2.2-preview.190130.1 | 31.56 KB | Wed, 30 Jan 2019 14:53:58 GMT | 1 | ||||
0.2.2-preview.190124.10 | 31.47 KB | Fri, 25 Jan 2019 03:27:06 GMT | 1 | ||||
0.2.1 | 41.67 KB | Sun, 21 Oct 2018 03:28:54 GMT | 1 | ||||
0.2.1-rtm.36 | 41.69 KB | Wed, 05 Dec 2018 16:08:32 GMT | 0 | ||||
0.2.1-rtm.34 | 41.7 KB | Tue, 23 Oct 2018 01:25:55 GMT | 1 | ||||
0.2.1-rtm.32 | 41.69 KB | Sun, 21 Oct 2018 03:26:04 GMT | 0 | ||||
0.2.0 | 41.23 KB | Wed, 26 Sep 2018 06:18:01 GMT | 1 | ||||
0.2.0-rtm.29 | 41.25 KB | Sat, 06 Oct 2018 17:31:48 GMT | 1 | ||||
0.2.0-rtm.28 | 41.24 KB | Sat, 06 Oct 2018 17:29:46 GMT | 1 | ||||
0.2.0-rtm.27 | 41.25 KB | Sat, 06 Oct 2018 17:27:28 GMT | 1 | ||||
0.2.0-rtm.25 | 41.24 KB | Wed, 26 Sep 2018 06:15:46 GMT | 0 | ||||
0.2.0-rtm.24 | 41.15 KB | Wed, 26 Sep 2018 06:13:37 GMT | 0 | ||||
0.1.1 | 38.5 KB | Wed, 22 Aug 2018 06:01:56 GMT | 3 | ||||
0.1.1-rtm.23 | 39.34 KB | Wed, 05 Sep 2018 05:32:44 GMT | 1 | ||||
0.1.1-rtm.21 | 38.51 KB | Wed, 22 Aug 2018 05:59:17 GMT | 1 | ||||
0.1.1-rtm.19 | 38.5 KB | Wed, 22 Aug 2018 05:34:25 GMT | 1 | ||||
0.1.0 | 37.11 KB | Tue, 24 Jul 2018 04:14:39 GMT | 1 | ||||
0.1.0-rtm.17 | 37.12 KB | Wed, 25 Jul 2018 15:58:34 GMT | 1 | ||||
0.1.0-rtm.16 | 37.13 KB | Wed, 25 Jul 2018 15:56:45 GMT | 1 | ||||
0.1.0-rtm.15 | 37.14 KB | Wed, 25 Jul 2018 15:55:21 GMT | 1 | ||||
0.1.0-rtm.14 | 37.14 KB | Wed, 25 Jul 2018 14:45:46 GMT | 0 | ||||
0.1.0-rtm.10 | 37.13 KB | Wed, 25 Jul 2018 06:21:22 GMT | 0 | ||||
0.1.0-rtm.8 | 37.13 KB | Tue, 24 Jul 2018 04:13:41 GMT | 2 | ||||
0.1.0-rtm.7 | 36.33 KB | Tue, 24 Jul 2018 04:10:09 GMT | 1 | ||||
0.1.0-alpha1.6 | 36.35 KB | Tue, 24 Jul 2018 04:06:02 GMT | 0 | ||||
0.1.0-alpha1.5 | 36.35 KB | Tue, 24 Jul 2018 04:00:13 GMT | 2 | ||||
0.1.0-alpha1.4 | 36.1 KB | Tue, 24 Jul 2018 03:17:57 GMT | 0 |