目录
介绍
软件架构师最重要的任务
软件架构师最重要的软件开发任务如下:
- 创建和维护插件基础架构,允许各个开发人员几乎彼此独立地创建、测试、调试和扩展插件。插件基础架构应该照顾
- 托管插件
- 排列、显示、保存和恢复可视化插件的布局
- 允许轻松模拟尚未准备好的插件
- 查找并分解可在多个位置重用的代码和概念。
当然,架构师还需要收集需求,估计实现功能需要多长时间,与客户交互,选择软件和测试框架等等,但上面,我们只谈论“软件开发”任务。
最小的插件接口和插件框架
在由重构的NP.IoCy和AutofacAdapter容器实现的通用最小反转控制/依赖注入接口容器,我提供了IoC(插件)容器的最小接口及其作为NP的实现。IoCy插件框架以及AutofacAdapter——建立在Autofac之上的实现。
基于可视化插件——Gidon——Avalonia的MVVM插件IoC容器的多平台实现仍在进行中,完成后,将允许
- 托管Python Shell和可视化页面
- 托管C#脚本和可视页面
- 托管网页
所有这些都将在多个桌面平台(Windows,Linux和Mac)上运行。
安装插件
有一个如何将插件安装到应用程序中的问题。
事实证明,每个插件都可以变成一个特殊的nuget包,然后在安装时通过一些简单的特殊处理作为nuget包安装。
插件包的创建和安装应遵循以下原则:
- 插件NuGet应包括所有DLL——主插件DLL及其依赖的DLL(包括nuget依赖项)。
- 主项目不应依赖于插件DLL——所有插件DLL都应动态加载。
Nuget文档(或缺少文档)
正如你们中的许多人可能已经了解到的那样,无论是使用nuget命令还是csproj文件和MSBuild命令,都很难找到有关将文件打包和解包的信息。
在我尝试的许多网页中,只有两个真正有用:
- 使用 MSBuild和Github示例改进.NET构建设置的提示和技巧,位于 rider-msbuild-webinar-2020 上。我没有全部看完——只是第6节。
- 如何从MSBuild查找NuGet 包路径,说明如何将nuget包内的路径转换为csproj变量,然后使用该路径将文件从包中复制到所选的磁盘文件夹中。
新版本的MSBuild和csproj匹配或超过了所有功能nuget实用程序,因此,我正在移动我的所有包(包括插件包)在没有nuget和没有nuspec文件的情况下构建,只需在Visual Studio中构建相应的C#项目。
代码位置
示例代码位于 PluginPackageSamples文件夹下的NP.Samples 存储库中。
打包/拆包示例
插件功能的简要说明
插件是可以动态加载到框架中的软件。插件不应该相互依赖,也不应该依赖于插件框架,相反,它们可以依赖于一组通用接口并通过这些接口进行通信。这样的插件基础设施将增加关注点的分离,并允许插件几乎独立于彼此和插件框架进行开发、调试和扩展。
在这里,我们只简要说明我们的测试插件的作用。有关插件实现的完整说明,请查看多插件测试链接。
涉及两个非常简单的插件:
- DoubleManipulationPlugin——提供两种操作方法:doublesPlus(...)两个数字相加和Times(...)两个数字相乘。
- StringManipulationPlugins还提供了两种string操作方法:——Concat(...)用于连接两个string和Repeat(...)重复string几次。
这两个插件不相互依赖,主项目也不依赖它们。相反,插件和主项目都依赖于包含两个接口的PluginInterfaces项目,每个接口对应一个接口:
public interface IDoubleManipulationsPlugin
{
double Plus(double number1, double number2);
double Times(double number1, double number2);
}
public interface IStringManipulationsPlugin
{
string Concat(string str1, string str2);
string Repeat(string str, int numberTimesToRepeat);
}
这些接口在位于插件接口文件夹中的公共NP.PackagePluginsTest.PluginInterfaces项目中定义。
打包示例
解决方案PackagePluginsTest.sln(将插件创建为nuget包)位于 PluginPackageSamples\PackagePlugins 文件夹中。它包含三个项目:
- NP.PackagesPluginsTest.DoubleManipulationsPlugin用于创建包NP.PackagesPluginsTest.DoubleManipulationsPlugin.nupkg
- NP.PackagesPluginsTest.StringManipulationsPlugin用于创建包NP.PackagesPluginsTest.StringManipulationsPlugin.nupkg
- NP.PackagePluginsTest.PluginInterfaces其引用在上述项目之间共享。
让我们看一下NP.PackagesPluginsTest.DoubleManipulationsPlugin项目(另一个非常相似,只是它的方法不同,指的是用string而不是double进行操作)。
DoubleManipulationsPlugin通过定义两个方法——double Plus(double number1, double number2)和double Times(double number1, double number2)来实现IDoubleManipulationsPlugin接口。
该类被标记为RegisterTypeAttribute以便NP.IoCy框架知道如何读取它并在其容器中注册它:
[RegisterType]
public class DoubleManipulationsPlugin : IDoubleManipulationsPlugin
{
// sums two numbers
public double Plus(double number1, double number2)
{
return number1 + number2;
}
// multiplies two numbers
public double Times(double number1, double number2)
{
return number1 * number2;
}
}
现在看看项目的csproj文件——NP.PackagePluginsTest.DoubleManipulationsPlugin.csproj.
在顶部的<PropertyTag>标记中,我们添加了一些nuget包属性(包括版本——1.0.4、版权——Nick Polyak 2023、PackageLicenseExpression - MIT)。
其中定义的最重要的属性是:
- <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>——此属性会将所有DLL文件(包括来自依赖nuget包的文件)复制到输出文件夹中,以成为包的一部分。
- <GeneratePackageOnBuild>true</GeneratePackageOnBuild>——此属性在每次生成项目时创建nuget包。创建的 .nupkg 文件将位于目标文件夹上方的一个文件夹中(例如,如果目标文件夹位于项目文件夹下的bin/Debug/net6.0 中,则 .nupkg文件将位于 bin/Debug 文件夹中)。
在PropertyGroup之后,有两个引用——一个PackageReference——对NP.DependencyInjection项目获得IoC属性,另一个——对已经提到的NP.PackagePluginsTest.PluginInterfaces项目的项目引用,以获取我们实现的接口。
在文件的末尾——有两个Target标签:
<Target Name="ClearTarget" BeforeTargets="Build">
<RemoveDir Directories="$(TargetDir)\**" />
</Target>
<Target Name="IncludeAllFilesInTargetDir" AfterTargets="Build">
<ItemGroup>
<Content Include="$(TargetDir)\**">
<Pack>true</Pack>
<PackagePath>Content</PackagePath>
</Content>
</ItemGroup>
</Target>
第一个目标在生成(BeforeTargets="Build")之前触发,从$(TargetDir)中删除所有文件或子文件夹。
第二个目标在构建后触发。它通过将目标目录中的所有文件和子文件夹包含在Nuget包的内容文件夹中来创建 nuget 包。
以下是使用以下NuGetPackageExplorer查看的结果插件的内容:
所有文件都包含在nuget包的内容文件夹下,只有一个——NP.PackagePluginsTest.DoubleManipulationPlugin.dll 是通常位置的容器——lib/net6.0。我不知道如何摆脱那里的最后一个文件,但如果我知道消费端的更改会简单一些(虽然没什么激进的)。
创建两个nuget包文件后,必须将它们上传到nuget.org或其他一些nuget服务器(例如,本地服务器)。我已经上传了两个插件文件,NP.PackagePluginsTest.DoubleManipulationsPlugin.1.0.4.nupkg和NP.PackagePluginsTest.StringManipulationsPlugin.1.0.4.nupkg到nuget.org 服务器上。所以你不必这样做——你可以简单地使用我已经存在于nuget.org上的文件。
包消费示例
演示如何从上传的nuget文件创建插件的示例位于 PluginPackageSamples\PluginConsumer\PluginConsumer.sln 解决方案中。该解决方案有两个项目:
- PluginsConsumer——下载nuget包的主项目,从中创建插件并使用它们为接口提供实现。
- NP.PackagePluginsTest.PluginInterfaces——包含通用接口的项目
Program.cs文件的内容与上一篇文章多插件测试部分描述的主文件内容几乎相同。因此,我将只描述动态加载插件的主程序的开头,创建IoC容器并从IoC容器解析doublemanipulatesPlugin:
// create container builder
IContainerBuilder<string?> builder = new ContainerBuilder<string?>();
// load plugins dynamically from sub-folders of Plugins folder
// located under the same folder as the executable
builder.RegisterPluginsFromSubFolders("Plugins");
// build the container
IDependencyInjectionContainer<string?> container = builder.Build();
// get the plugin for manipulating double numbers
IDoubleManipulationsPlugin doubleManipulationsPlugin =
container.Resolve<IDoubleManipulationsPlugin>();
// get the result of 4 * 5
double timesResult =
doubleManipulationsPlugin.Times(4.0, 5.0);
// check that 4 * 5 == 20
timesResult.Should().Be(20.0);
这里的关键行是builder.RegisterPluginsFromSubFolders("Plugins");,其尝试从$(TargetDir)/Plugins的所有子文件夹中动态加载插件。
最有趣的代码是在csproj文件PluginsConsumer.csproj中。此文件有ItemGroup,其包含对所有软件包(包括插件包)的包引用:
<ItemGroup>
...
<PackageReference Include="NP.PackagePluginsTest.DoubleManipulationsPlugin"
Version="1.0.4" GeneratePathProperty="true">
<ExcludeAssets>All</ExcludeAssets>
</PackageReference>
<PackageReference Include="NP.PackagePluginsTest.StringManipulationsPlugin"
Version="1.0.4" GeneratePathProperty="true">
<ExcludeAssets>All</ExcludeAssets>
</PackageReference>
</ItemGroup>
请注意,这两个包都排除了所有资产。这样做是为了使主程序不会静态地依赖于 NP.PackagePluginsTests.DoubleManipulationsPlugin.dll 和 NP.PackagePluginsTests.StringManipulationsPlugin.dll 文件,以便动态加载它们(与DLL程序集的其余部分相同)。
另外,请注意,两个插件的PackageReference都有eneratePathProperty="true"属性。此属性自动生成一个设置为插件根目录路径的变量。变量名称始终以“Pkg”为前缀,包名称中的每个句点都替换为下划线——“_”。例如,NP. PackagePluginsTest.DoubleManipulationPlugin 的变量名称根文件夹将是PkgNP_PackagePluginsTest_DoubleManipulationPlugin。
我们在下个ItemGroup中使用这些变量名称,我们在其中设置需要从包中复制的所有文件和子文件夹:
<ItemGroup> <!-- setting up the variable for convenience -->
<DoubleManipPluginPackageFiles
Include="$(PkgNP_PackagePluginsTest_DoubleManipulationsPlugin)\Content\**\*.*" />
<StringManipPluginPackageFiles
Include="$(PkgNP_PackagePluginsTest_StringManipulationsPlugin)\Content\**\*.*" />
</ItemGroup>
现在我们来到目标,在build后将文件从<nuget_package_root>\Content复制到$(TargetDir)/Plugins/<PluginName>文件夹:
<Target Name="CopyPluginsFromNugetPackages" AfterTargets="Build">
<PropertyGroup>
<DoublePluginFolder>$(TargetDir)\Plugins\DoubleManipulationPlugin
</DoublePluginFolder>
<StringPluginFolder>$(TargetDir)\Plugins\StringManipulationPlugin
</StringPluginFolder>
</PropertyGroup>
<RemoveDir Directories="$(DoublePluginFolder)" />
<Copy SourceFiles="@(DoubleManipPluginPackageFiles)"
DestinationFolder="$(DoublePluginFolder)%(RecursiveDir)" />
<RemoveDir Directories="$(StringPluginFolder)" />
<Copy SourceFiles="@(StringManipPluginPackageFiles)"
DestinationFolder="$(StringPluginFolder)%(RecursiveDir)" />
</Target>
在Target标签的顶部,我们定义变量DoublePluginFolder,并在以后在同一目标中的多个位置使用StringPluginFolder:
<PropertyGroup>
<DoublePluginFolder>$(TargetDir)\Plugins\DoubleManipulationPlugin
</DoublePluginFolder>
<StringPluginFolder>$(TargetDir)\Plugins\StringManipulationPlugin
</StringPluginFolder>
</PropertyGroup>
然后,对于每个插件,我们:
- 首先——删除插件文件夹,例如<RemoveDir Directories="$(DoublePluginFolder)" />
- 然后——将文件从nuget包复制到插件文件夹,例如:<Copy SourceFiles="@(DoubleManipPluginPackageFiles)" DestinationFolder="$(DoublePluginFolder)%(RecursiveDir)" />
%(RecursiveDir)在路径的末尾,我们可以保留与插件的NuGet包文件中相同的文件夹结构。有时,这很重要,例如,当包有一个runtime子文件夹,其中包含与其中每个本机平台对应的多个文件夹时。
现在您应该能够构建PluginConsumer项目并运行它。构建后,请验证 bin\Debug\net6.0 文件夹中是否有插件文件夹,并且该文件夹有两个子文件夹,即DoublePluginFolder和StringPluginFolder,每个子文件夹都填充了相应的插件文件。运行项目没有错误将证明容器确实具有那些动态加载的插件,并且它们正确解析为相应的接口,并且所有插件功能都在工作。
请注意,当您通过Visual Studio升级相应插件的版本时,所有与csproj文件相关的添加都会被保留。唯一需要编辑csproj文件的时间是添加或删除插件时。
那些彻底阅读代码的人可能会注意到,在programe.cs文件的末尾,我正在测试一些神秘的“MethodNames”:
var methodNames = container.Resolve<IEnumerable<string>>("MethodNames");
methodNames.Count().Should().Be(4);
methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Plus));
methodNames.Should().Contain(nameof(IDoubleManipulationsPlugin.Times));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Concat));
methodNames.Should().Contain(nameof(IStringManipulationsPlugin.Repeat));
对于那些对那里发生的事情感兴趣的人,请阅读上一篇文章——MultiCells和具有多单元格的插件。
结论
本文介绍了如何创建和安装插件作为nuget包。这使我们能够主要依靠嵌入到Visual Studio中的MSBuild功能来创建和安装插件。从本质上讲,我们不需要创建任何(或几乎任何)特殊的安装机制来创建和安装插件。
我计划在以后的文章中使用这种方法来创建和安装插件,将插件添加到Google RPC服务器。
https://www.codeproject.com/Articles/5352287/Creating-and-Installing-Plugins-as-Nuget-Packages