使用插件创建 .NET Core 应用程序

本教程展示了如何创建自定义的  AssemblyLoadContext  来加载插件。AssemblyDependencyResolver  用于解析插件的依赖项。该教程正确地将插件依赖项与主机应用程序隔离开来。将了解如何执行以下操作:

  • 构建支持插件的项目。

  • 创建自定义  AssemblyLoadContext  加载每个插件。

  • 使用  System.Runtime.Loader.AssemblyDependencyResolver  类型允许插件具有依赖项。

  • 只需复制生成项目就可以轻松部署的作者插件。

系统必备

安装  .NET 5 SDK  或更高版本。  备注

示例代码针对 .NET 5,但它使用的所有功能都已在 .NET Core 3.0 中推出,并且在此后所有 .NET 版本中都可用。

创建应用程序

  • 第一步是创建应用程序:

创建新文件夹,并在该文件夹中运行以下命令:

.NET CLI
dotnet new console -o AppWithPlugin
  • 为了更容易生成项目,请在同一文件夹中创建一个 Visual Studio 解决方案文件。运行以下命令:

.NET CLI
dotnet new sln
  • 运行以下命令,向解决方案添加应用项目:

.NET CLI
dotnet sln add AppWithPlugin/AppWithPlugin.csproj

现在,我们可以填写应用程序的主干。使用下面的代码替换 AppWithPlugin/Program.cs 文件中的代码:

using PluginBase;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;namespace AppWithPlugin
{class Program{static void Main(string[] args){try{if (args.Length == 1 && args[0] == "/d"){Console.WriteLine("Waiting for any key...");Console.ReadLine();}// Load commands from plugins.if (args.Length == 0){Console.WriteLine("Commands: ");// Output the loaded commands.}else{foreach (string commandName in args){Console.WriteLine($"-- {commandName} --");// Execute the command with the name passed as an argument.Console.WriteLine();}}}catch (Exception ex){Console.WriteLine(ex);}}}
}

创建插件接口

使用插件生成应用的下一步是定义插件需要实现的接口。我们建议创建类库,其中包含计划用于在应用和插件之间通信的任何类型。此部分允许将插件接口作为包发布,而无需发布完整的应用程序。

在项目的根文件夹中,运行  dotnet new classlib -o PluginBase。并运行  dotnet sln add PluginBase/PluginBase.csproj  向解决方案文件添加项目。删除  PluginBase/Class1.cs  文件,并使用以下接口定义在名为  ICommand.cs  的  PluginBase  文件夹中创建新的文件:

namespace PluginBase
{public interface ICommand{string Name { get; }string Description { get; }int Execute();}
}

此  ICommand  接口是所有插件将实现的接口。

由于已定义  ICommand  接口,所以应用程序项目可以填写更多内容。使用根文件夹中的  dotnet add AppWithPlugin/AppWithPlugin.csproj reference PluginBase/PluginBase.csproj  命令将引用从  AppWithPlugin  项目添加到  PluginBase  项目。

使用以下代码片段替换  // Load commands from plugins  注释,使其能够从给定文件路径加载插件:

string[] pluginPaths = new string[]
{// Paths to plugins to load.
};IEnumerable<ICommand> commands = pluginPaths.SelectMany(pluginPath =>
{Assembly pluginAssembly = LoadPlugin(pluginPath);return CreateCommands(pluginAssembly);
}).ToList();

然后用以下代码片段替换 // Output the loaded commands 注释:

foreach (ICommand command in commands)
{Console.WriteLine($"{command.Name}\t - {command.Description}");
}

使用以下代码片段替换 // Execute the command with the name passed as an argument 注释:

ICommand command = commands.FirstOrDefault(c => c.Name == commandName);
if (command == null)
{Console.WriteLine("No such command is known.");return;
}

command.Execute(); 最后,将静态方法添加到名为  LoadPlugin  和  CreateCommands  的  Program  类,如下所示:

static Assembly LoadPlugin(string relativePath)
{throw new NotImplementedException();
}static IEnumerable<ICommand> CreateCommands(Assembly assembly)
{int count = 0;foreach (Type type in assembly.GetTypes()){if (typeof(ICommand).IsAssignableFrom(type)){ICommand result = Activator.CreateInstance(type) as ICommand;if (result != null){count++;yield return result;}}}if (count == 0){string availableTypes = string.Join(",", assembly.GetTypes().Select(t => t.FullName));throw new ApplicationException($"Can't find any type which implements ICommand in {assembly} from {assembly.Location}.\n" +$"Available types: {availableTypes}");}
}

加载插件

现在,应用程序可以正确加载和实例化来自已加载的插件程序集的命令,但仍然无法加载插件程序集。使用以下内容在 AppWithPlugin 文件夹中创建名为 PluginLoadContext.cs 的文件:

using System;
using System.Reflection;
using System.Runtime.Loader;namespace AppWithPlugin
{class PluginLoadContext : AssemblyLoadContext{private AssemblyDependencyResolver _resolver;public PluginLoadContext(string pluginPath){_resolver = new AssemblyDependencyResolver(pluginPath);}protected override Assembly Load(AssemblyName assemblyName){string assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);if (assemblyPath != null){return LoadFromAssemblyPath(assemblyPath);}return null;}protected override IntPtr LoadUnmanagedDll(string unmanagedDllName){string libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);if (libraryPath != null){return LoadUnmanagedDllFromPath(libraryPath);}return IntPtr.Zero;}}
}

PluginLoadContext  类型派生自  AssemblyLoadContext。AssemblyLoadContext  类型是运行时中的特殊类型,该类型允许开发人员将已加载的程序集隔离到不同的组中,以确保程序集版本不冲突。此外,自定义  AssemblyLoadContext  可以选择不同路径来加载程序集格式并重写默认行为。PluginLoadContext  使用 .NET Core 3.0 中引入的  AssemblyDependencyResolver  类型的实例将程序集名称解析为路径。AssemblyDependencyResolver  对象是使用 .NET 类库的路径构造的。它根据类库的 .deps.json 文件(其路径传递给  AssemblyDependencyResolver  构造函数)将程序集和本机库解析为它们的相对路径。自定义  AssemblyLoadContext  使插件能够拥有自己的依赖项,AssemblyDependencyResolver  使正确加载依赖项变得容易。

由于  AppWithPlugin  项目具有  PluginLoadContext  类型,所以请使用以下正文更新  Program.LoadPlugin  方法:

static Assembly LoadPlugin(string relativePath)
{// Navigate up to the solution rootstring root = Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(Path.GetDirectoryName(typeof(Program).Assembly.Location)))))));string pluginLocation = Path.GetFullPath(Path.Combine(root, relativePath.Replace('\\', Path.DirectorySeparatorChar)));Console.WriteLine($"Loading commands from: {pluginLocation}");PluginLoadContext loadContext = new PluginLoadContext(pluginLocation);return loadContext.LoadFromAssemblyName(new AssemblyName(Path.GetFileNameWithoutExtension(pluginLocation)));
}

通过为每个插件使用不同的  PluginLoadContext  实例,插件可以具有不同的甚至冲突的依赖项,而不会出现问题。

不具有依赖项的简单插件

返回到根文件夹,执行以下步骤:

运行以下命令,新建一个名为  HelloPlugin  的类库项目:

.NET CLI
dotnet new classlib -o HelloPlugin

运行以下命令,将项目添加到  AppWithPlugin  解决方案中:

.NET CLI
dotnet sln add HelloPlugin/HelloPlugin.csproj

使用以下内容将 HelloPlugin/Class1.cs 文件替换为名为 HelloCommand.cs 的文件:

using PluginBase;
using System;namespace HelloPlugin
{public class HelloCommand : ICommand{public string Name { get => "hello"; }public string Description { get => "Displays hello message."; }public int Execute(){Console.WriteLine("Hello !!!");return 0;}}
}

现在,打开 HelloPlugin.csproj 文件 。它应类似于以下内容:

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><TargetFramework>net5</TargetFramework></PropertyGroup></Project>

在    标记之间添加以下元素:

<EnableDynamicLoading>true</EnableDynamicLoading>
<EnableDynamicLoading>true</EnableDynamicLoading>

准备项目,使其可用作插件。此外,这会将其所有依赖项复制到项目的输出中。有关更多详细信息,请参阅  EnableDynamicLoading。

在    标记之间添加以下元素:

<ItemGroup><ProjectReference Include="..\PluginBase\PluginBase.csproj"><Private>false</Private><ExcludeAssets>runtime</ExcludeAssets></ProjectReference>
</ItemGroup>

false  元素很重要。它告知 MSBuild 不要将 PluginBase.dll 复制到 HelloPlugin 的输出目录 。如果 PluginBase.dll 程序集出现在输出目录中,PluginLoadContext  将在那里查找到该程序集并在加载 HelloPlugin.dll 程序集时加载它。此时,HelloPlugin.HelloCommand  类型将从  HelloPlugin  项目的输出目录中的 PluginBase.dll 实现  ICommand  接口,而不是加载到默认加载上下文中的  ICommand  接口。因为运行时将这两种类型视为不同程序集的不同类型,所以  AppWithPlugin.Program.CreateCommands  方法找不到命令。因此,对包含插件接口的程序集的引用需要  false  元数据。

同样,如果  PluginBase  引用其他包,则  runtime  元素也很重要。此设置与  false  的效果相同,但适用于  PluginBase  项目或它的某个依赖项可能包括的包引用。

因为  HelloPlugin  项目已完成,所以应该更新  AppWithPlugin  项目,以确认可以找到  HelloPlugin  插件的位置。在  // Paths to plugins to load  注释后,添加  @"HelloPlugin\bin\Debug\netcoreapp3.0\HelloPlugin.dll"(根据所使用的 .NET Core 版本,此路径可能有所不同)作为  pluginPaths  数组的元素。

具有库依赖项的插件

几乎所有插件都比简单的“Hello World”更复杂,而且许多插件都具有其他库上的依赖项。示例中的  JsonPlugin  和  OldJsonPlugin  项目显示了具有  Newtonsoft.Json  上的 NuGet 包依赖项的两个插件示例。因此,所有插件项目都应将  true  添加到项目属性,以便它们将其所有依赖项复制到  dotnet build  的输出中。使用  dotnet publish  发布类库也会将其所有依赖项复制到发布输出。

从 NuGet 包引用插件接口

假设存在应用 A,它具有 NuGet 包(名为  A.PluginBase)中定义的插件接口。如何在插件项目中正确引用包?对于项目引用,使用项目文件的  ProjectReference  元素上的  false  元数据会阻止将 dll 复制到输出。

若要正确引用  A.PluginBase  包,应将项目文件中的    元素更改为以下内容:

<PackageReference Include="A.PluginBase" Version="1.0.0"><ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>

此操作会阻止将  A.PluginBase  程序集复制到插件的输出目录,并确保插件将使用 A 版本的  A.PluginBase。

插件目标框架建议

因为插件依赖项加载使用 .deps.json 文件,所以存在一个与插件的目标框架相关的问题 。具体来说,插件应该以运行时为目标,比如 .NET 5,而不是某一版本的 .NET Standard。.deps.json 文件基于项目所针对的框架生成,而且由于许多与 .NET Standard 兼容的包提供了用于针对 .NET Standard 进行生成的引用程序集和用于特定运行时的实现程序集,因此 .deps.json 可能无法正确查看实现程序集,或者它可能会获取 .NET Standard 版本的程序集,而不是期望的 .NET Core 版本的程序集。

插件框架引用

插件当前无法向该过程引入新的框架。例如,无法将使用  Microsoft.AspNetCore.App  框架的插件加载到只使用根  Microsoft.NETCore.App  框架的应用程序中。主机应用程序必须声明对插件所需的全部框架的引用。

使用插件创建 .NET Core 应用程序相关推荐

  1. 创建在Linux上运行的 .NET Core 应用程序(matlab封装的dll文件)

    matlab社区:https://ww2.mathworks.cn/help/releases/R2019b/compiler_sdk/dotnet/create-a-dotnet-core-appl ...

  2. 使用Jenkins在Azure Web App上进行ASP.NET Core应用程序的持续集成和部署(CI/CD)–第1天

    目录 介绍 路线图 DevOps 持续集成 持续部署 Jenkins Azure Web App服务 主题 先决条件 安装Jenkins 先决条件 下载并安装 Jenkins插件 让它运行 安装自定义 ...

  3. ASP .NET Core Web MVC系列教程一:创建一个Web应用程序

    系列文章目录:ASP .NET Core Web MVC系列教程:使用ASP .NET Core创建MVC Web应用程序 从Visual Studio中选择" 创建新项目". 选 ...

  4. ASP .NET Core Web MVC系列教程:使用ASP .NET Core创建MVC Web应用程序

    本系列教程翻译自微软官方教程,官方教程地址:Get started with ASP.NET Core MVC | Microsoft Docs 本系列教程介绍了构建MVC Web应用程序的基础知识. ...

  5. ASP.NET Core 配置 - 创建自定义配置提供程序

    ASP.NET Core 配置 - 创建自定义配置提供程序 在本文中,我们将创建一个自定义配置提供程序,从数据库读取我们的配置.我们已经了解了默认配置提供程序的工作方式,现在我们将实现我们自己的自定义 ...

  6. 《从零开始学ASP.NET CORE MVC》:VS2019创建ASP.NET Core Web程序(三)

    创建ASP.NET Core Web应用程序 如果您使用的是VS2017请看 VS2017创建ASP.NET Core Web程序(三) 在这个视频中我们将讨论 可用的不同项目模板及其功能 预制的项目 ...

  7. VS2017创建ASP.NET Core Web程序

    创建ASP.NET Core Web应用程序 如果您使用的是VS2019 请看 VS2019创建ASP.NET Core Web程序 在这个视频中我们将讨论 可用的不同项目模板及其功能 预制的项目模板 ...

  8. 用 Visual Studio Code 在 macOS 上创建首个 ASP.NET Core 应用程序

    原文:Your First ASP.NET Core Application on a Mac Using Visual Studio Code 作者:Daniel Roth.Steve Smith  ...

  9. linux运行core控制台程序,VisualStudioCode创建的asp.net core控制台程序部署到linux

    1.asp.net core控制台程序 static void Main(string[] args) {int times=10;while(times>=0) { Console.Write ...

最新文章

  1. Linux平台Oracle安装脚本
  2. 为什么程序员都不喜欢使用switch,而是大量的 if……else if ?
  3. 电脑运行adb闪退_adb命令调试工具
  4. install g++ 出现“g++ : Depends: g++-4.8 (= 4.8.2-5~) but it is not going to be installed...解决方法
  5. css3的自定义字体
  6. 【渝粤题库】陕西师范大学200831 编译原理 作业
  7. Linux驱动开发常用头文件
  8. php 共享内存列队,php中对共享内存,消息队列的操作
  9. ubuntu保存_Arch与Ubuntu安装软件对比
  10. NET中的三种Timer的区别和用法
  11. linux c获取网卡ip,linux c获取IP地址
  12. 劝学篇翻译软件测试,汪洙《神童诗劝学篇》原文与译文
  13. hadoop环境新手安装教程
  14. 2022暑期项目实训(一)
  15. 花了2小时,搭建了一个物联网项目,值了 ~
  16. OSChina 娱乐弹弹弹——假期就是睡睡睡
  17. 八旗旗主,八大贝勒(亦称人王)
  18. LeetCode #77 组合
  19. oracle中句柄,释放句柄 – 提供7*24专业数据库(Oracle,SQL Server,MySQL等)恢复和Oracle技术服务@Tel:+86 13429648788 - 惜分飞...
  20. 《软件工具》手把手教你使用Visual Studio Code开发C/C++(Windows)

热门文章

  1. WEB API:语音识别
  2. oracle 时间转化函数及常见函数 .
  3. JMS : Java Message Service (Java消息服务)之一 [转]
  4. 如何下载python2.7.16_CENTOS6.5 安装PYTHON2.7.16
  5. springboot入门(一)--快速搭建一个springboot框架
  6. es6拼接字符串的方式。
  7. 速达5000出现计算成本数据溢出的问题
  8. 全向轮底盘磁导轨寻迹
  9. MVC中提交表单的4种方式
  10. Linux下Gcc 的编译过程