[BREAKGLASS] Append-only mirror of github.com/btcpayserver/DotNetCorePlugins
Go to file
Nate McMaster fad7d2f5a4
Fix #12 - Give base-path the lowest precedence when searching for managed libraries
This fixes assembly loading for System.Drawing.Common. This package was a rare case of a package with both runtime agnostic and runtime-specific versions of the managed library. This means the .NET Core SDK will dump System.Drawing.Common.dll in the plugin base path *as well as* a version in the runtimes/ folder. In this case, corehost actually treats the runtime specific asset with higher priority.

This change updates the ManagedLoadContext to imitate this behavior.
2018-10-20 16:49:07 -07:00
.github Create stale.yml 2018-10-06 10:26:29 -07:00
.vscode Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
docs Add PluginLoader.CreateFromAssemblyFile and update README and docs 2018-07-23 20:58:51 -07:00
samples/aspnetcore Update README.md (#2) 2018-07-25 07:44:30 -07:00
src Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
test Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
.appveyor.yml Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
.editorconfig Update target framework to netcoreapp2.0 2018-03-26 20:01:28 -07:00
.gitignore Tidy up release notes for 0.1.1 2018-08-21 22:57:16 -07:00
build.ps1 Rename package to McMaster.NETCore.Plugins 2018-07-23 20:15:44 -07:00
Directory.Build.props Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
Directory.Build.targets Reorganize files and prepare to release the prototype as a package 2018-07-22 22:54:11 -07:00
DotNetCorePlugins.sln Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
LICENSE.txt Add license and copyright 2018-07-23 21:12:22 -07:00
README.md Update README.md 2018-07-24 23:20:08 -07:00
releasenotes.props Fix #12 - Give base-path the lowest precedence when searching for managed libraries 2018-10-20 16:49:07 -07:00
version.props Bump version to 0.2.0 2018-09-25 23:10:38 -07:00

.NET Core Plugins

AppVeyor build status

NuGet MyGet

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.

Unlike other approaches to dynamic assembly loading, like Assembly.LoadFrom, this API attempts to imitate the behavior of .deps.json and runtimeconfig.json files to probe for dependencies, load native (unmanaged) libraries, and to find binaries from runtime stores or package caches. In addition, it allows for fine-grained control over which types should be unified between the loader and the plugin, and which can remain isolated from the main application.

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

Getting started

Pre-release builds and symbols: https://www.myget.org/gallery/natemcmaster/

You can install the plugin loading API using 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) })
  • assemblyFile = the file path to the main .dll of the plugin
  • sharedTypes = a list of types which the loader should ensure are unified

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 abstractions project which defines the interaction between the plugin and the host.

The plugin abstraction

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

public interface IPlugin
{
    string GetName();
}

The plugins

Typically, it is best to implement plugins by targeting netcoreapp2.0 or higher. They can target netstandard2.0 as well, but using netcoreapp2.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";
}

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

For example, you could 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:

using McMaster.NETCore.Plugins;

public class Program
{
    public static void Main(string[] args)
    {
        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.Exist(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()}'.");
            }
        }
    }
}

Plugin config file

This also supports using a config file to control the settings of the loader per-plugin. This plugin config file can be hand-crafted, or generated using McMaster.NETCore.Plugins.Sdk.

<!-- A project that produces the plugin. -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
    <IsPlugin>true</IsPlugin>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="McMaster.NETCore.Plugins.Sdk" Version="*" />
  </ItemGroup>
</Project>

You can then use PluginLoader.CreateFromConfigFile to load the plugin from the configuration file.

PluginLoader.CreateFromConfigFile(
    filePath: "./plugins/MyPlugin/plugin.config",
    sharedTypes: new [] { typeof(IPlugin), typeof(IServiceCollection), typeof(ILogger) })