aboutsummaryrefslogtreecommitdiff
path: root/third-party/DotNetCorePlugins/README.md
blob: 0fa5d578a5b238a5cdc68ea6ff715b19d5702341 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
.NET Core Plugins
=================

[![Build Status][ci-badge]][ci] [![Code Coverage][codecov-badge]][codecov]
[![NuGet][nuget-badge] ![NuGet Downloads][nuget-download-badge]][nuget]

[ci]: https://github.com/natemcmaster/DotNetCorePlugins/actions?query=workflow%3ACI+branch%3Amain
[ci-badge]: https://github.com/natemcmaster/DotNetCorePlugins/workflows/CI/badge.svg
[codecov]: https://codecov.io/gh/natemcmaster/DotNetCorePlugins
[codecov-badge]: https://codecov.io/gh/natemcmaster/DotNetCorePlugins/branch/main/graph/badge.svg?token=l6uSsHZ8nA
[nuget]: https://www.nuget.org/packages/McMaster.NETCore.Plugins/
[nuget-badge]: https://img.shields.io/nuget/v/McMaster.NETCore.Plugins.svg?style=flat-square
[nuget-download-badge]: https://img.shields.io/nuget/dt/McMaster.NETCore.Plugins?style=flat-square


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](#shared-types).

Blog post introducing this project, July 25, 2018: [.NET Core Plugins: Introducing an API for loading .dll files (and their dependencies) as 'plugins'](https://natemcmaster.com/blog/2018/07/25/netcore-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][plugin-tutorial]" 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.

[plugin-tutorial]: https://docs.microsoft.com/dotnet/core/tutorials/creating-app-with-plugin-support

## Getting started

Install the [`McMaster.NETCore.Plugins` NuGet package.][nuget]

```
dotnet add package McMaster.NETCore.Plugins
```

The main API to use is `PluginLoader.CreateFromAssemblyFile`.

```csharp
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?](#shared-types))
* 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/](./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/](./samples/hello-world/) .

### The plugin contract

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

```csharp
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/](./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.

```csharp
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/](./samples/).

```csharp
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()}'.");
    }
}
```

<a id="shared-types"></a>

### 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/](./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.

![DefaultConfigDiagram](https://i.imgur.com/fHEMBO6.png)

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](https://i.imgur.com/sTGqPxa.png)

Read [even more details about shared types here](./docs/what-are-shared-types.md).

## 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.

```c#
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/](./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:
```c#
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](https://github.com/dotnet/coreclr/blob/v3.0.0/Documentation/design-docs/AssemblyLoadContext.ContextualReflection.md) for even more details on contextual reflection.

## Overriding the Default Load Context

Under the hood, DotNetCorePlugins is using a .NET Core API called [ApplicationLoadContext][alc-api].
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.

[alc-api]: https://docs.microsoft.com/dotnet/api/system.runtime.loader.assemblyloadcontext

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


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