Drop support for configuring plugins via xml file and refactor PluginLoader.Create* APIs

This removes support for configuring the plugin via XML. It's better to do most of the plugin configuration programmatically. Some settings, such as assemblies to share, have to be expressed via code and can't be expressed in XML. There weren't enough use cases for changing the plugin config via file post-publish, so I'm simplifying the library by removing this.
This commit is contained in:
Nate McMaster 2019-04-24 22:00:18 -07:00
parent 808b134f5c
commit 0bd41c4b3a
No known key found for this signature in database
GPG Key ID: A778D9601BD78810
42 changed files with 179 additions and 437 deletions

View File

@ -33,8 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReferencedLibv2", "test\Tes
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "XunitSample", "test\TestProjects\XunitSample\XunitSample.csproj", "{3CEF9F32-5887-4977-AB8A-1BBC48875A21}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "McMaster.NETCore.Plugins.Sdk", "src\Plugins.Sdk\McMaster.NETCore.Plugins.Sdk.csproj", "{652D3662-DEB4-4FF6-8904-E581A84C040D}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Banana", "test\TestProjects\Banana\Banana.csproj", "{8797AFCA-1EE8-4270-8FC4-F93A85F9C825}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Strawberry", "test\TestProjects\Strawberry\Strawberry.csproj", "{78D2B20D-B173-4CF2-A763-F2AA5347947E}"
@ -202,18 +200,6 @@ Global
{3CEF9F32-5887-4977-AB8A-1BBC48875A21}.Release|x64.Build.0 = Release|Any CPU
{3CEF9F32-5887-4977-AB8A-1BBC48875A21}.Release|x86.ActiveCfg = Release|Any CPU
{3CEF9F32-5887-4977-AB8A-1BBC48875A21}.Release|x86.Build.0 = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|x64.ActiveCfg = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|x64.Build.0 = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|x86.ActiveCfg = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Debug|x86.Build.0 = Debug|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|Any CPU.Build.0 = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|x64.ActiveCfg = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|x64.Build.0 = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|x86.ActiveCfg = Release|Any CPU
{652D3662-DEB4-4FF6-8904-E581A84C040D}.Release|x86.Build.0 = Release|Any CPU
{8797AFCA-1EE8-4270-8FC4-F93A85F9C825}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8797AFCA-1EE8-4270-8FC4-F93A85F9C825}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8797AFCA-1EE8-4270-8FC4-F93A85F9C825}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -326,7 +312,6 @@ Global
{CC37B369-BA9E-4B82-A14D-68B58924885D} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}
{50F86C8F-E052-448C-84CE-08763945FCC7} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}
{3CEF9F32-5887-4977-AB8A-1BBC48875A21} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}
{652D3662-DEB4-4FF6-8904-E581A84C040D} = {471CA3C0-0BC9-4C0C-A013-D9356DBD0F38}
{8797AFCA-1EE8-4270-8FC4-F93A85F9C825} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}
{78D2B20D-B173-4CF2-A763-F2AA5347947E} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}
{C0BA460C-C1AC-4C54-9C88-60E4B8681E94} = {98E964A2-55DA-4740-9F2E-B64FDF6715DB}

View File

@ -138,28 +138,3 @@ public class Program
}
}
```
## Plugin config file
This also supports using a [config file](./docs/plugin-config.md) to control the settings of the loader per-plugin. This plugin config file can be hand-crafted, or generated using `McMaster.NETCore.Plugins.Sdk`.
```xml
<!-- 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.
```csharp
PluginLoader.CreateFromConfigFile(
filePath: "./plugins/MyPlugin/plugin.config",
sharedTypes: new [] { typeof(IPlugin), typeof(IServiceCollection), typeof(ILogger) })
```

View File

@ -11,7 +11,7 @@ variables:
- name: DOTNET_SKIP_FIRST_TIME_EXPERIENCE
value: 1
- name: DotNetCoreVersion
value: 3.0.100-preview3-010431
value: 3.0.100-preview5-011555
- name: BuildNumber
value: $[counter('buildnumber')]

View File

@ -56,10 +56,12 @@ public class Program
{
public static void Main(string[] args)
{
foreach (var pluginFile in Glob("plugins/*/plugin.config"))
foreach (var pluginDirectory in Directory.GetDirectories("plugins/"))
{
var loader = PluginLoader.CreateFromConfigFile(
configFile: pluginFile,
var pluginDirName = Path.GetFileName(pluginDir);
var assemblyFile = Path.Combine(pluginDir, pluginDirName + ".dll");
var loader = PluginLoader.CreateFromAssemblyFile(
assemblyFile: pluginFile,
sharedTypes: new [] { typeof(IFruit) });
var plugin = loader.LoadDefaultAssembly();
@ -79,9 +81,7 @@ A plugin author could implement the shared abstraction and distribute a plugin w
```xml
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="McMaster.NETCore.Plugins.Sdk" />
<PropertyGroup>
<IsPlugin>true</IsPlugin>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
</Project>

View File

@ -1,31 +0,0 @@
Plugin Config File
==================
Plugins can be configured with an xml file, conventionally named 'plugin.config'. This file is optional,
and allows plugins to control some settings about how they load.
```xml
<PluginConfig MainAssembly="Banana, Version=1.0.0.0, Culture=neutral, PublicKeyToken=abc12341873">
<PrivateDependency Identity="MyPrivateDependency, Version=2.0.0.0" />
</PluginConfig>
```
## Elements
The following elements are supported.
### `<PluginConfig>`
The root element of the config file is `PluginConfig`.
The following attributes are supported:
* `MainAssembly` - **required**. This value defines the default assembly for the plugin. Its value should be a valid assembly name.
### `<PrivateDependency>`
This element defines assemblies which the plugin will prefer to load as private versions instead of attempting
to unify with the version with the host application.
The following attributes are supported:
* `Identity` - **required**. The assembly identity of the private dependency.

5
global.json Normal file
View File

@ -0,0 +1,5 @@
{
"sdk": {
"version": "3.0.100-preview5-011555"
}
}

View File

@ -4,6 +4,11 @@
Changes:
* .NET Core 3.0 support
* Support unloading plugins from memory (only .NET Core &gt;3.0)
Breaking changes:
* Support for loading plugin config from an XML file has been dropped. The APIs for PluginLoader.CreateFromConfigFile were removed
in favor of PluginLoader.CreateFromAssemblyFile
* Merged PluginLoaderOptions with the PluginConfig class
</PackageReleaseNotes>
<PackageReleaseNotes Condition="'$(VersionPrefix)' == '0.2.4'">
Bug fix:

View File

@ -0,0 +1,3 @@
<!-- Intentionally empty to prevent samples from inheriting from the other repo targests -->
<Project />

View File

@ -23,7 +23,9 @@ namespace MvcWebApp
var plugin = PluginLoader.CreateFromAssemblyFile(
Path.Combine(dir, pluginName + ".dll"), // create a plugin from for the .dll file
sharedTypes: new [] { typeof(Controller) }); // this ensures that the version of MVC is shared between this app and the plugin
config =>
// this ensures that the version of MVC is shared between this app and the plugin
config.PreferSharedTypes = true);
var pluginAssembly = plugin.LoadDefaultAssembly();
Console.WriteLine($"Loading application parts from plugin {pluginName}");

View File

@ -18,9 +18,11 @@ namespace MainWebApp
public Startup()
{
foreach (var pluginFile in Directory.GetFiles(AppContext.BaseDirectory, "plugin.config", SearchOption.AllDirectories))
foreach (var pluginDir in Directory.GetDirectories(Path.Combine(AppContext.BaseDirectory, "plugins")))
{
var loader = PluginLoader.CreateFromConfigFile(pluginFile,
var dirName = Path.GetFileName(pluginDir);
var pluginFile = Path.Combine(pluginDir, dirName + ".dll");
var loader = PluginLoader.CreateFromAssemblyFile(pluginFile,
// this ensures that the plugin resolves to the same version of DependencyInjection
// and ASP.NET Core that the current app uses
sharedTypes: new[]

View File

@ -23,10 +23,11 @@ This plugin uses AutoMapper, Version=7.0.1.0, Culture=neutral, PublicKeyToken=be
There are some important types, however, which must share the same identity between the plugins and the host.
To ensure type exchange works between the host and the plugins, the MainWebApp project uses the `sharedTypes`
parameter on `PluginLoader.CreateFromConfigFile`.
parameter on `PluginLoader.CreateFromAssemblyFile`.
```csharp
var loader = PluginLoader.CreateFromConfigFile(pluginFile,
var loader = PluginLoader.CreateFromAssemblyFile(
pluginAssembly,
sharedTypes: new[]
{
typeof(IApplicationBuilder),

View File

@ -1,9 +0,0 @@
<Project>
<!--
Required for the samples in this repo only.
Normally, you should replace this with a PackageReference to McMaster.NETCore.Plugins.Sdk.
-->
<Import Project="$(RepoRoot)src\Plugins.Sdk\sdk\Sdk.targets" />
</Project>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -1,9 +0,0 @@
<Project>
<!--
Required for the samples in this repo only.
Normally, you should replace this with a PackageReference to McMaster.NETCore.Plugins.Sdk.
-->
<Import Project="$(RepoRoot)src\Plugins.Sdk\sdk\Sdk.targets" />
</Project>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -1,24 +0,0 @@
<Project>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageDescription>Generates the plugin configuration files required to load .dll files.</PackageDescription>
<PackageTags>.NET Core;plugins;Sdk</PackageTags>
<IncludeBuildOutput>false</IncludeBuildOutput>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<IncludeSymbols>false</IncludeSymbols>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<Content Include="sdk/*" PackagePath="sdk/" />
<Content Include="build/*" PackagePath="build/" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Target Name="Compile" />
<Target Name="CopyFilesToOutputDirectory" />
</Project>

View File

@ -1,3 +0,0 @@
<Project>
<Import Project="..\sdk\Sdk.props" />
</Project>

View File

@ -1,3 +0,0 @@
<Project>
<Import Project="..\sdk\Sdk.targets" />
</Project>

View File

@ -1,7 +0,0 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<PluginConfigFileName>plugin.config</PluginConfigFileName>
</PropertyGroup>
</Project>

View File

@ -1,42 +0,0 @@
<Project>
<PropertyGroup>
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
<IsPlugin Condition=" '$(IsPlugin)' == '' ">true</IsPlugin>
<IntermediatePluginConfigFile>$(IntermediateOutputPath)$(MSBuildProjectName).$(PluginConfigFileName)</IntermediatePluginConfigFile>
<GeneratePluginConfigFile Condition="'$(IsPlugin)' == 'true' AND '$(TargetFramework)' != ''">true</GeneratePluginConfigFile>
</PropertyGroup>
<ItemGroup>
<Content Include="$(IntermediatePluginConfigFile)" Condition="'$(GeneratePluginConfigFile)' == 'true'">
<Visible>false</Visible>
<Link>$(PluginConfigFileName)</Link>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
<Target Name="GeneratePluginConfig"
BeforeTargets="CoreCompile"
Inputs="$(MSBuildAllProjects);$(TargetPath)"
Outputs="$(IntermediatePluginConfigFile)"
Condition="'$(GeneratePluginConfigFile)' == 'true'">
<PropertyGroup>
<_AssemblyIdentity>$(AssemblyName)</_AssemblyIdentity>
<_AssemblyIdentity Condition="'$(AssemblyVersion)' != ''">$(_AssemblyIdentity), Version=$(AssemblyVersion)</_AssemblyIdentity>
<PluginConfigContent>
<![CDATA[
<PluginConfig MainAssembly="$(_AssemblyIdentity)">
@(PrivateDependency->'<PrivateDependency Identity="%(Identity)" />', '%0A ')
</PluginConfig>
]]>
</PluginConfigContent>
</PropertyGroup>
<WriteLinesToFile Lines="$(PluginConfigContent)" Overwrite="true" File="$(IntermediatePluginConfigFile)" />
<ItemGroup>
<FileWrites Include="$(IntermediatePluginConfigFile)" />
</ItemGroup>
</Target>
</Project>

View File

@ -23,7 +23,7 @@ namespace McMaster.NETCore.Plugins.Loader
private readonly Dictionary<string, NativeLibrary> _nativeLibraries = new Dictionary<string, NativeLibrary>(StringComparer.Ordinal);
private readonly HashSet<string> _privateAssemblies = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _defaultAssemblies = new HashSet<string>(StringComparer.Ordinal);
private string _basePath;
private string _mainAssemblyPath;
private bool _preferDefaultLoadContext;
#if FEATURE_UNLOAD
@ -46,7 +46,7 @@ namespace McMaster.NETCore.Plugins.Loader
}
return new ManagedLoadContext(
_basePath,
_mainAssemblyPath,
_managedLibraries,
_nativeLibraries,
_privateAssemblies,
@ -62,12 +62,12 @@ namespace McMaster.NETCore.Plugins.Loader
}
/// <summary>
/// Set the base directory for the context. This is used as the starting point for loading
/// assemblies. Also known as the 'app local' directory.
/// Set the file path to the main assembly for the context. This is used as the starting point for loading
/// other assemblies. The directory that contains it is also known as the 'app local' directory.
/// </summary>
/// <param name="path">The file path. Must not be null or empty. Must be an absolute path.</param>
/// <returns>The builder.</returns>
public AssemblyLoadContextBuilder SetBaseDirectory(string path)
public AssemblyLoadContextBuilder SetMainAssemblyPath(string path)
{
if (string.IsNullOrEmpty(path))
{
@ -79,7 +79,7 @@ namespace McMaster.NETCore.Plugins.Loader
throw new ArgumentException("Argument must be a full path.", nameof(path));
}
_basePath = path;
_mainAssemblyPath = path;
return this;
}

View File

@ -52,7 +52,6 @@ namespace McMaster.NETCore.Plugins.Loader
using (var file = File.OpenRead(depsFilePath))
{
var deps = reader.Read(file);
builder.SetBaseDirectory(Path.GetDirectoryName(depsFilePath));
builder.AddDependencyContext(deps);
}

View File

@ -18,6 +18,7 @@ namespace McMaster.NETCore.Plugins.Loader
internal class ManagedLoadContext : AssemblyLoadContext
{
private readonly string _basePath;
private readonly string _mainAssemblyPath;
private readonly IReadOnlyDictionary<string, ManagedLibrary> _managedAssemblies;
private readonly IReadOnlyDictionary<string, NativeLibrary> _nativeLibraries;
private readonly IReadOnlyCollection<string> _privateAssemblies;
@ -26,7 +27,7 @@ namespace McMaster.NETCore.Plugins.Loader
private readonly bool _preferDefaultLoadContext;
private readonly string[] _resourceRoots;
public ManagedLoadContext(string baseDirectory,
public ManagedLoadContext(string mainAssemblyPath,
IReadOnlyDictionary<string, ManagedLibrary> managedAssemblies,
IReadOnlyDictionary<string, NativeLibrary> nativeLibraries,
IReadOnlyCollection<string> privateAssemblies,
@ -44,7 +45,8 @@ namespace McMaster.NETCore.Plugins.Loader
throw new ArgumentNullException(nameof(resourceProbingPaths));
}
_basePath = baseDirectory ?? throw new ArgumentNullException(nameof(baseDirectory));
_mainAssemblyPath = mainAssemblyPath ?? throw new ArgumentNullException(nameof(mainAssemblyPath));
_basePath = Path.GetDirectoryName(mainAssemblyPath);
_managedAssemblies = managedAssemblies ?? throw new ArgumentNullException(nameof(managedAssemblies));
_privateAssemblies = privateAssemblies ?? throw new ArgumentNullException(nameof(privateAssemblies));
_defaultAssemblies = defaultAssemblies ?? throw new ArgumentNullException(nameof(defaultAssemblies));

View File

@ -5,8 +5,6 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Xml;
using System.Xml.Linq;
namespace McMaster.NETCore.Plugins
{
@ -16,78 +14,52 @@ namespace McMaster.NETCore.Plugins
public class PluginConfig
{
/// <summary>
/// Initialize an instance of <see cref="PluginConfig" />.
/// Initializes a new instance of <see cref="PluginConfig" />
/// </summary>
/// <param name="mainAssembly">The name of the main assembly.</param>
/// <param name="privateAssembly">A list of assemblies to treat as private, if possible.</param>
protected PluginConfig(AssemblyName mainAssembly, IReadOnlyCollection<AssemblyName> privateAssembly)
/// <param name="mainAssemblyPath">The full file path to the main assembly for the plugin.</param>
public PluginConfig(string mainAssemblyPath)
{
MainAssembly = mainAssembly ?? throw new ArgumentNullException(nameof(mainAssembly));
PrivateAssemblies = privateAssembly ?? throw new ArgumentNullException(nameof(privateAssembly));
}
/// <summary>
/// Create an instance of <see cref="PluginConfig" /> from a file.
/// </summary>
/// <param name="filePath">The path the config file.</param>
/// <returns></returns>
public static PluginConfig CreateFromFile(string filePath)
{
using (var reader = File.OpenText(filePath))
if (string.IsNullOrEmpty(mainAssemblyPath))
{
return PluginConfig.CreateFromReader(reader);
throw new ArgumentException("Value must be null or not empty", nameof(mainAssemblyPath));
}
if (!Path.IsPathRooted(mainAssemblyPath))
{
throw new ArgumentException("Value must be an absolute file path", nameof(mainAssemblyPath));
}
MainAssemblyPath = mainAssemblyPath;
}
/// <summary>
/// Create an instance of <see cref="PluginConfig" /> from a file.
/// The file path to the main assembly.
/// </summary>
/// <param name="reader">The reader containing the config file.</param>
/// <returns></returns>
public static PluginConfig CreateFromReader(TextReader reader)
{
var privateDeps = new HashSet<AssemblyName>();
var doc = XDocument.Load(reader, LoadOptions.SetLineInfo);
if (doc.Root.Name != "PluginConfig")
{
throw new InvalidDataException("Root element should be 'PluginConfig'");
}
var mainAssemblyAttr = doc.Root.Attribute("MainAssembly");
if (mainAssemblyAttr == null || string.IsNullOrEmpty(mainAssemblyAttr.Value))
{
IXmlLineInfo line = doc.Root;
throw new InvalidDataException($"Missing required attribute 'MainAssembly' for PluginConfig on line {line.LineNumber}");
}
var mainAssembly = new AssemblyName(mainAssemblyAttr.Value);
foreach (var dep in doc.Root.Descendants("PrivateDependency"))
{
var identity = dep.Attribute("Identity");
if (identity == null || string.IsNullOrEmpty(identity.Value))
{
IXmlLineInfo line = dep;
throw new InvalidDataException($"Missing required attribute 'Identity' for PrivateDependency on line {line.LineNumber}");
}
privateDeps.Add(new AssemblyName(identity.Value));
}
return new PluginConfig(mainAssembly, privateDeps);
}
public string MainAssemblyPath { get; }
/// <summary>
/// A list of assemblies which should be treated as private.
/// </summary>
public IReadOnlyCollection<AssemblyName> PrivateAssemblies { get; protected set; }
public ICollection<AssemblyName> PrivateAssemblies { get; protected set; } = new List<AssemblyName>();
/// <summary>
/// The name of the main assembly.
/// A list of assemblies which should be unified between the host and the plugin.
/// </summary>
public AssemblyName MainAssembly { get; protected set; }
public ICollection<AssemblyName> SharedAssemblies { get; protected set; } = new List<AssemblyName>();
/// <summary>
/// Attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// </summary>
public bool PreferSharedTypes { get; set; }
#if FEATURE_UNLOAD
/// <summary>
/// The plugin can be unloaded from memory.
/// </summary>
public bool IsUnloadable { get; set; }
#endif
}
}

View File

@ -21,134 +21,107 @@ namespace McMaster.NETCore.Plugins
/// </summary>
public class PluginLoader : IDisposable
{
// we have to duplicate a large block of xml code because C# doesn't allow conditional XML elements
#if FEATURE_UNLOAD
/// <summary>
/// Create a plugin loader using the settings from a plugin config file.
/// <seealso cref="PluginConfig" /> for defaults on the plugin configuration.
/// </summary>
/// <param name="filePath">The file path to the plugin config.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="isUnloadable">Enable unloading the plugin from memory.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromConfigFile(string filePath, Type[] sharedTypes = null, bool isUnloadable = false)
{
var loaderOptions = isUnloadable
? PluginLoaderOptions.IsUnloadable
: PluginLoaderOptions.None;
#else
/// <summary>
/// Create a plugin loader using the settings from a plugin config file.
/// <seealso cref="PluginConfig" /> for defaults on the plugin configuration.
/// </summary>
/// <param name="filePath">The file path to the plugin config.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromConfigFile(string filePath, Type[] sharedTypes = null)
{
var loaderOptions = PluginLoaderOptions.None;
#endif
var config = PluginConfig.CreateFromFile(filePath);
var baseDir = Path.GetDirectoryName(filePath);
return new PluginLoader(config,
baseDir,
sharedTypes,
loaderOptions);
}
#if FEATURE_UNLOAD
/// <summary>
/// Create a plugin loader using an existing <see cref="PluginConfig"/> instance.
/// </summary>
/// <param name="config">The <see cref="PluginConfig"/> instance.</param>
/// <param name="baseDir">The base directory from which to load / search for dependencies on disk.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="isUnloadable">Enable unloading the plugin from memory.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromConfigFile(PluginConfig config, string baseDir, Type[] sharedTypes = null, bool isUnloadable = false)
{
var loaderOptions = isUnloadable
? PluginLoaderOptions.IsUnloadable
: PluginLoaderOptions.None;
#else
/// <summary>
/// Create a plugin loader using an existing <see cref="PluginConfig"/> instance.
/// </summary>
/// <param name="config">The <see cref="PluginConfig"/> instance.</param>
/// <param name="baseDir">The base directory from which to load / search for dependencies on disk.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromConfigFile(PluginConfig config, string baseDir, Type[] sharedTypes = null)
{
var loaderOptions = PluginLoaderOptions.None;
#endif
return new PluginLoader(config,
baseDir,
sharedTypes,
loaderOptions);
}
#if FEATURE_UNLOAD
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the plugin config.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="isUnloadable">Enable unloading the plugin from memory.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sharedTypes = null, bool isUnloadable = false)
{
var loaderOptions = isUnloadable
? PluginLoaderOptions.IsUnloadable
: PluginLoaderOptions.None;
#else
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, bool isUnloadable, Type[] sharedTypes)
=> CreateFromAssemblyFile(assemblyFile,isUnloadable, sharedTypes, _ => { });
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the plugin config.</param>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="isUnloadable">Enable unloading the plugin from memory.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sharedTypes = null)
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, bool isUnloadable, Type[] sharedTypes, Action<PluginConfig> configure)
{
var loaderOptions = PluginLoaderOptions.None;
#endif
return CreateFromAssemblyFile(assemblyFile,
sharedTypes,
loaderOptions);
config =>
{
config.IsUnloadable = isUnloadable;
configure(config);
});
}
#endif
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sharedTypes)
=> CreateFromAssemblyFile(assemblyFile, sharedTypes, _ => { });
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sharedTypes, Action<PluginConfig> configure)
{
return CreateFromAssemblyFile(assemblyFile,
config =>
{
if (sharedTypes != null)
{
foreach (var type in sharedTypes)
{
config.SharedAssemblies.Add(type.Assembly.GetName());
}
}
configure(config);
});
}
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the plugin config.</param>
/// <param name="sharedTypes">A list of types which should be shared between the host and the plugin.</param>
/// <param name="loaderOptions">Options for the loader</param>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Type[] sharedTypes, PluginLoaderOptions loaderOptions)
public static PluginLoader CreateFromAssemblyFile(string assemblyFile)
=> CreateFromAssemblyFile(assemblyFile, _ => { });
/// <summary>
/// Create a plugin loader for an assembly file.
/// </summary>
/// <param name="assemblyFile">The file path to the main assembly for the plugin.</param>
/// <param name="configure">A function which can be used to configure advanced options for the plugin loader.</param>
/// <returns>A loader.</returns>
public static PluginLoader CreateFromAssemblyFile(string assemblyFile, Action<PluginConfig> configure)
{
var config = new FileOnlyPluginConfig(assemblyFile);
var baseDir = Path.GetDirectoryName(assemblyFile);
return new PluginLoader(config, baseDir, sharedTypes, loaderOptions);
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
var config = new PluginConfig(assemblyFile);
configure(config);
return new PluginLoader(config);
}
private class FileOnlyPluginConfig : PluginConfig
{
public FileOnlyPluginConfig(string filePath)
: base(new AssemblyName(Path.GetFileNameWithoutExtension(filePath)), Array.Empty<AssemblyName>())
{ }
}
private readonly string _mainAssembly;
private readonly PluginConfig _config;
private readonly AssemblyLoadContext _context;
private volatile bool _disposed;
internal PluginLoader(PluginConfig config,
string baseDir,
Type[] sharedTypes,
PluginLoaderOptions loaderOptions)
/// <summary>
/// Initialize an instance of <see cref="PluginLoader" />
/// </summary>
/// <param name="config">The configuration for the plugin.</param>
public PluginLoader(PluginConfig config)
{
_mainAssembly = Path.Combine(baseDir, config.MainAssembly.Name + ".dll");
_context = CreateLoadContext(baseDir, config, sharedTypes, loaderOptions);
_config = config ?? throw new ArgumentNullException(nameof(config));
_context = CreateLoadContext(config);
}
/// <summary>
@ -174,7 +147,7 @@ namespace McMaster.NETCore.Plugins
public Assembly LoadDefaultAssembly()
{
EnsureNotDisposed();
return _context.LoadFromAssemblyPath(_mainAssembly);
return _context.LoadFromAssemblyPath(_config.MainAssemblyPath);
}
/// <summary>
@ -229,49 +202,44 @@ namespace McMaster.NETCore.Plugins
}
}
private static AssemblyLoadContext CreateLoadContext(
string baseDir,
PluginConfig config,
Type[] sharedTypes,
PluginLoaderOptions loaderOptions)
private static AssemblyLoadContext CreateLoadContext(PluginConfig config)
{
var depsJsonFile = Path.Combine(baseDir, config.MainAssembly.Name + ".deps.json");
var builder = new AssemblyLoadContextBuilder();
if (File.Exists(depsJsonFile))
{
builder.AddDependencyContext(depsJsonFile);
}
builder.SetBaseDirectory(baseDir);
builder.SetMainAssemblyPath(config.MainAssemblyPath);
foreach (var ext in config.PrivateAssemblies)
{
builder.PreferLoadContextAssembly(ext);
}
if (loaderOptions.HasFlag(PluginLoaderOptions.PreferSharedTypes))
if (config.PreferSharedTypes)
{
builder.PreferDefaultLoadContext(true);
}
#if FEATURE_UNLOAD
if (loaderOptions.HasFlag(PluginLoaderOptions.IsUnloadable))
if (config.IsUnloadable)
{
builder.EnableUnloading();
}
#endif
if (sharedTypes != null)
foreach (var assemblyName in config.SharedAssemblies)
{
foreach (var type in sharedTypes)
{
builder.PreferDefaultLoadContextAssembly(type.Assembly.GetName());
}
builder.PreferDefaultLoadContextAssembly(assemblyName);
}
var pluginRuntimeConfigFile = Path.Combine(baseDir, config.MainAssembly.Name + ".runtimeconfig.json");
var baseDir = Path.GetDirectoryName(config.MainAssemblyPath);
var assemblyFileName = Path.GetFileNameWithoutExtension(config.MainAssemblyPath);
var depsJsonFile = Path.Combine(baseDir, assemblyFileName + ".deps.json");
if (File.Exists(depsJsonFile))
{
builder.AddDependencyContext(depsJsonFile);
}
var pluginRuntimeConfigFile = Path.Combine(baseDir, assemblyFileName + ".runtimeconfig.json");
builder.TryAddAdditionalProbingPathFromRuntimeConfig(pluginRuntimeConfigFile, includeDevConfig: true, out _);

View File

@ -1,37 +0,0 @@
// Copyright (c) Nate McMaster.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace McMaster.NETCore.Plugins
{
/// <summary>
/// Options for how <see cref="PluginLoader"/> behaves.
/// </summary>
[Flags]
public enum PluginLoaderOptions
{
/// <summary>
/// Use the default behavior.
/// </summary>
None = 0,
/// <summary>
/// Attempt to unify all types from a plugin with the host.
/// <para>
/// This does not guarantee types will unify.
/// </para>
/// </summary>
PreferSharedTypes = 1 << 0,
#if FEATURE_UNLOAD
/// <summary>
/// If the platform supports it, allow unloading the plugin. This requires .NET Core 3.0 or higher.
/// <para>
/// Setting this option does not guarantee that the plugin can be unloaded.
/// </para>
/// </summary>
IsUnloadable = 1 << 1,
#endif
}
}

View File

@ -32,7 +32,7 @@ namespace McMaster.NETCore.Plugins.Tests
[MethodImpl(MethodImplOptions.NoInlining)] // ensure no local vars are create
private void ExecuteAndUnload(string path, out WeakReference weakRef)
{
var loader = PluginLoader.CreateFromConfigFile(path, isUnloadable: true);
var loader = PluginLoader.CreateFromAssemblyFile(path, c => { c.IsUnloadable = true; });
var assembly = loader.LoadDefaultAssembly();
var method = assembly
@ -57,7 +57,7 @@ namespace McMaster.NETCore.Plugins.Tests
public void LoadsNetCoreProjectWithNativeDeps()
{
var path = TestResources.GetTestProjectAssembly("PowerShellPlugin");
var loader = PluginLoader.CreateFromConfigFile(path);
var loader = PluginLoader.CreateFromAssemblyFile(path);
var assembly = loader.LoadDefaultAssembly();
var method = assembly
@ -74,7 +74,7 @@ namespace McMaster.NETCore.Plugins.Tests
// SqlClient has P/invoke that calls "sni.dll" on Windows. This test checks
// that native libraries can still be resolved in this case.
var path = TestResources.GetTestProjectAssembly("SqlClientApp");
var loader = PluginLoader.CreateFromConfigFile(path);
var loader = PluginLoader.CreateFromAssemblyFile(path);
var assembly = loader.LoadDefaultAssembly();
var method = assembly
@ -88,7 +88,7 @@ namespace McMaster.NETCore.Plugins.Tests
public void LoadsNetCoreApp20Project()
{
var path = TestResources.GetTestProjectAssembly("NetCoreApp20App");
var loader = PluginLoader.CreateFromConfigFile(path);
var loader = PluginLoader.CreateFromAssemblyFile(path);
var assembly = loader.LoadDefaultAssembly();
var method = assembly
@ -102,7 +102,7 @@ namespace McMaster.NETCore.Plugins.Tests
public void LoadsNetStandard20Project()
{
var path = TestResources.GetTestProjectAssembly("NetStandardClassLib");
var loader = PluginLoader.CreateFromConfigFile(path);
var loader = PluginLoader.CreateFromAssemblyFile(path);
var assembly = loader.LoadDefaultAssembly();
var type = assembly.GetType("NetStandardClassLib.Class1", throwOnError: true);
@ -119,7 +119,7 @@ namespace McMaster.NETCore.Plugins.Tests
// In this case, the host will pick the rid-specific version
var path = TestResources.GetTestProjectAssembly("DrawingApp");
var loader = PluginLoader.CreateFromConfigFile(path);
var loader = PluginLoader.CreateFromAssemblyFile(path);
var assembly = loader.LoadDefaultAssembly();
var type = assembly.GetType("Finder", throwOnError: true);
@ -151,7 +151,7 @@ namespace McMaster.NETCore.Plugins.Tests
private IFruit GetPlátano()
{
var path = TestResources.GetTestProjectAssembly("Plátano");
var loader = PluginLoader.CreateFromConfigFile(path,
var loader = PluginLoader.CreateFromAssemblyFile(path,
#if NETCOREAPP3_0
isUnloadable: true,
#endif

View File

@ -14,6 +14,7 @@ namespace McMaster.NETCore.Plugins.Tests
{
var samplePath = TestResources.GetTestProjectAssembly("XunitSample");
var context = new AssemblyLoadContextBuilder()
.SetMainAssemblyPath(samplePath)
.AddProbingPath(samplePath)
.AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath), "XunitSample.deps.json"))
.PreferDefaultLoadContext(true)
@ -28,6 +29,7 @@ namespace McMaster.NETCore.Plugins.Tests
var samplePath = TestResources.GetTestProjectAssembly("XunitSample");
var context = new AssemblyLoadContextBuilder()
.SetMainAssemblyPath(samplePath)
.AddProbingPath(samplePath)
.AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath), "XunitSample.deps.json"))
.Build();
@ -41,12 +43,14 @@ namespace McMaster.NETCore.Plugins.Tests
var samplePath = TestResources.GetTestProjectAssembly("NetCoreApp20App");
var defaultLoader = new AssemblyLoadContextBuilder()
.SetMainAssemblyPath(samplePath)
.AddProbingPath(samplePath)
.PreferDefaultLoadContext(false)
.AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath), "NetCoreApp20App.deps.json"))
.Build();
var unifedLoader = new AssemblyLoadContextBuilder()
.SetMainAssemblyPath(samplePath)
.AddProbingPath(samplePath)
.PreferDefaultLoadContext(true)
.AddDependencyContext(Path.Combine(Path.GetDirectoryName(samplePath), "NetCoreApp20App.deps.json"))

View File

@ -10,9 +10,9 @@ namespace McMaster.NETCore.Plugins.Tests
[Fact]
public void EachContextHasPrivateVersions()
{
var json9context = PluginLoader.CreateFromConfigFile(TestResources.GetTestProjectAssembly("JsonNet9"));
var json10context = PluginLoader.CreateFromConfigFile(TestResources.GetTestProjectAssembly("JsonNet10"));
var json11context = PluginLoader.CreateFromConfigFile(TestResources.GetTestProjectAssembly("JsonNet11"));
var json9context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet9"));
var json10context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet10"));
var json11context = PluginLoader.CreateFromAssemblyFile(TestResources.GetTestProjectAssembly("JsonNet11"));
// Load newest first to prove we can load older assemblies later into the same process
var json11 = GetJson(json11context);

View File

@ -17,7 +17,7 @@ namespace McMaster.NETCore.Plugins.Tests
var loaders = new List<PluginLoader>();
foreach (var name in pluginsNames)
{
var loader = PluginLoader.CreateFromConfigFile(
var loader = PluginLoader.CreateFromAssemblyFile(
TestResources.GetTestProjectAssembly(name),
sharedTypes: new[] { typeof(IFruit) });
loaders.Add(loader);

View File

@ -24,7 +24,7 @@
<ItemGroup>
<AssemblyAttribute Include="McMaster.NETCore.Plugins.Tests.TestProjectReferenceAttribute">
<_Parameter1>%(_ResolvedTestProjectReference.FileName)</_Parameter1>
<_Parameter2>%(_ResolvedTestProjectReference.RootDir)%(_ResolvedTestProjectReference.Directory)plugin.config</_Parameter2>
<_Parameter2>%(_ResolvedTestProjectReference.RootDir)%(_ResolvedTestProjectReference.Directory)%(_ResolvedTestProjectReference.FileName).dll</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
@ -36,7 +36,7 @@
<ItemGroup>
<AssemblyAttribute Include="McMaster.NETCore.Plugins.Tests.TestProjectReferenceAttribute">
<_Parameter1>%(PublishedTestProject.FileName)</_Parameter1>
<_Parameter2>$(TargetDir)%(PublishedTestProject.FileName)/plugin.config</_Parameter2>
<_Parameter2>$(TargetDir)%(PublishedTestProject.FileName)/%(PublishedTestProject.FileName).dll</_Parameter2>
</AssemblyAttribute>
</ItemGroup>
</Target>

View File

@ -6,7 +6,6 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
</Project>

View File

@ -1,6 +1,5 @@
<Project>
<Import Project="..\..\Directory.Build.props" />
<Import Project="..\..\src\Plugins.Sdk\sdk\Sdk.props" />
<PropertyGroup>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>

View File

@ -1,4 +0,0 @@
<Project>
<Import Project="..\..\src\Plugins.Sdk\sdk\Sdk.targets" />
<Import Project="..\..\Directory.Build.targets" />
</Project>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -3,7 +3,6 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>

View File

@ -2,7 +2,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
</Project>

View File

@ -6,7 +6,6 @@
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
</Project>

View File

@ -6,7 +6,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
</Project>

View File

@ -3,7 +3,6 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<IsPlugin>true</IsPlugin>
</PropertyGroup>
<ItemGroup>