Create and install plugins as Nuget packages

Directory

introduce

The Most Important Tasks of a Software Architect

Minimal plugin interface and plugin framework

install plugin

Nuget documentation (or lack thereof)

code location

Packing/unpacking example

A brief description of the plugin’s functionality

Packaging example

Example of package consumption

in conclusion


Introduction

The most important task of a software architect

The most important software development tasks of a software architect are as follows:

  1. Create and maintain a plugin infrastructure that allows individual developers to create, test, debug, and extend plugins almost independently of each other. The plugin infrastructure should take care of
    • Hosted plugin
    • Arrange, display, save and restore the layout of the visual plugin
    • Allows easy mocking of plugins that are not yet ready
  2. Find and factor out code and concepts that can be reused in multiple places.

Of course, architects also need to collect requirements, estimate how long it will take to implement features, interact with customers, select software and testing frameworks, etc., but above, we only talk about “software development” tasks.

Minimal plug-in interface and plug-in frame

In Generic Minimal Inversion of Control/Dependency Injection Interface Container Implemented by Refactored NP.IoCy and AutofacAdapter Containers, I present a minimal interface to an IoC (plugin) container and its implementation as NP. The IoCy plugin framework and AutofacAdapter – an implementation built on top of Autofac.

A multi-platform implementation of the IoC container based on the visualization plugin – Gidon – Avalonia’s MVVM plugin is still in progress, and when complete, will allow

  1. Hosted Python Shell and Visualization Pages
  2. Hosted C# scripts and visual pages
  3. Host web page

All will run on multiple desktop platforms (Windows, Linux and Mac).

Install plugin

There is a question of how to install plugins into the application.

It turns out that each plugin can be turned into a special nuget package, which is then installed as a nuget package at install time with some simple special handling.

The creation and installation of plug-in packages should follow the following principles:

  1. Plugin NuGet should include all DLLs – the main plugin DLL and its dependent DLLs (including nuget dependencies).
  2. The main project should not depend on plugin DLLs – all plugin DLLs should be loaded dynamically.

Nuget documentation (or lack of documentation)

As many of you may have learned, whether using nuget commands or csproj files and MSBuild commands, it can be difficult to find information on packaging and unpacking files.

Of the many pages I tried, only two really worked:

  1. Tips and tricks to improve your .NET build setup using MSBuild and Github samples at rider-msbuild-webinar-2020. I didn’t read it all – just verse 6.
  2. How to find the NuGet package path from MSBuild, explain how to convert the path inside the nuget package into a csproj variable, and then use the path to copy the file from the package to the selected disk folder.

The newer versions of MSBuild and csproj match or exceed all functional nuget utilities, so, I’m moving all my packages (including plugin packages) to build without nuget and without nuspec files, just build in Visual Studio accordingly C# project.

code location

Sample code is in the NP.Samples repository under the PluginPackageSamples folder.

Packing/unpacking example

Brief description of plug-in function

Plugins are software that can be dynamically loaded into the framework. Plugins should not depend on each other, nor on the plugin framework, instead they can depend on and communicate through a common set of interfaces. Such a plugin infrastructure would increase the separation of concerns and allow plugins to be developed, debugged and extended almost independently of each other and the plugin framework.

Here, we only briefly describe what our test plugin does. For a full description of plugin implementation, check out the multi-plugin testing link.

Two very simple plugins are involved:

  1. DoubleManipulationPlugin–provides two operation methods: doublesPlus(…) adds two numbers and Times(…) multiplies two numbers .
  2. StringManipulationPlugins also provides two string manipulation methods: – Concat(…) is used to connect two strings and Repeat(…) Repeat string several times.

The two plugins do not depend on each other, nor does the main project depend on them. Instead, both the plugin and the main project depend on the PluginInterfaces project which contains two interfaces, one for each interface:

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);
}

These interfaces are defined in the public NP.PackagePluginsTest.PluginInterfaces project located in the Plugin Interfaces folder.

package example

The solution PackagePluginsTest.sln (which creates the plugin as a nuget package) is located in the PluginPackageSamples\PackagePlugins folder. It contains three items:

  1. NP.PackagesPluginsTest.DoubleManipulationsPlugin is used to create package NP.PackagesPluginsTest.DoubleManipulationsPlugin.nupkg
  2. NP.PackagesPluginsTest.StringManipulationsPlugin is used to create package NP.PackagesPluginsTest.StringManipulationsPlugin.nupkg
  3. NP.PackagePluginsTest.PluginInterfaces whose references are shared between the above projects.

Let’s take a look at the NP.PackagesPluginsTest.DoubleManipulationsPlugin project (the other one is very similar, except that it has a different approach, referring to manipulating with strings instead of doubles).

DoubleManipulationsPlugin implements IDoubleManipulationsPlugin interface by defining two methods – double Plus(double number1, double number2) and double Times(double number1, double number2).

The class is marked with RegisterTypeAttribute so that the NP.IoCy framework knows how to read it and register it in its container:

[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;
    }
} 

Now look at the csproj file of the project – NP.PackagePluginsTest.DoubleManipulationsPlugin.csproj.

In the top tag, we add some nuget package properties (including Version – 1.0.4, Copyright – Nick Polyak 2023, PackageLicenseExpression – MIT).

The most important properties defined in it are:

  1. true – this property will copy all DLL files (including files from dependent nuget packages) to the output folder , to be part of the package.
  2. true – this property creates a nuget package every time the project is generated. The created .nupkg file will be located in a folder above the target folder (for example, if the target folder is in bin/Debug/net6.0 under the project folder , the .nupkg file will be located in the bin/Debug folder).

After the PropertyGroup, there are two references – a PackageReference – to the NP.DependencyInjection project to get the IoC properties, and another – a project reference to the already mentioned NP.PackagePluginsTest.PluginInterfaces project to get the interfaces we implement.

At the end of the file – there are two Target tags:

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

The first target fires before a build (BeforeTargets=”Build”), removing all files or subfolders from $(TargetDir).

The second target fires after build. It creates a nuget package by including all files and subfolders in the target directory in the content folder of the Nuget package.

Here are the contents of the resulting plugin viewed using the following NuGetPackageExplorer:

All files are included in the content folder of the nuget package , only one – NP.PackagePluginsTest.DoubleManipulationPlugin.dll is the container of the usual location – lib/ net6.0. I don’t know how to get rid of the last file in there, but it would be simpler (not drastic though) if I knew the changes on the consumer side.

After creating the two nuget package files, they must be uploaded to nuget.org or some other nuget server (for example, a local server). I have uploaded two plugin files, NP.PackagePluginsTest.DoubleManipulationsPlugin.1.0.4.nupkg and NP.PackagePluginsTest.StringManipulationsPlugin.1.0.4.nupkg to nuget.org on the server. So you don’t have to – you can simply use my files that already exist on nuget.org.

package consumption example

A sample demonstrating how to create a plugin from an uploaded nuget file is located in the PluginPackageSamples\PluginConsumer\PluginConsumer.sln solution. The solution has two projects:

  1. PluginsConsumer – the main project that downloads nuget packages, creates plugins from it and uses them to provide implementations for interfaces.
  2. NP.PackagePluginsTest.PluginInterfaces – a project containing common interfaces

The content of the Program.cs file is almost the same as that of the main file described in the multi-plugin testing section of the previous article. Therefore, I will just describe the beginning of the main program that dynamically loads the plugin, creates the IoC container and resolves the doublemanipulatesPlugin from the IoC container:

// 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); 

The key line here is builder.RegisterPluginsFromSubFolders(“Plugins”); which tries to dynamically load plugins from all subfolders of $(TargetDir)/Plugins.

The most interesting code is in the csproj file PluginsConsumer.csproj. This file has an ItemGroup that contains package references to all packages, including plugin packages:

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

Note that both packages exclude all assets. This is done so that the main program does not statically depend on the NP.PackagePluginsTests.DoubleManipulationsPlugin.dll and NP.PackagePluginsTests.StringManipulationsPlugin.dll files, so that they are loaded dynamically (with The rest of the DLL assembly is the same).

Also, note that the PackageReference of both plugins has generatePathProperty=”true” property. This property automatically generates a variable that is set to the path to the plugin’s root directory. Variable names are always prefixed with “Pkg”, and each period in the package name is replaced with an underscore – “_”. For example, the variable name root folder for NP.PackagePluginsTest.DoubleManipulationPlugin would be PkgNP_PackagePluginsTest_DoubleManipulationPlugin.

We use these variable names in the next ItemGroup where we set all the files and subfolders that need to be copied from the package:

<ItemGroup> <!-- setting up the variable for convenience -->
    <DoubleManipPluginPackageFiles
     Include="$(PkgNP_PackagePluginsTest_DoubleManipulationsPlugin)\Content\**\*.*" />
    <StringManipPluginPackageFiles
     Include="$(PkgNP_PackagePluginsTest_StringManipulationsPlugin)\Content\**\*.*" />
</ItemGroup> 

Now we come to the target, copy the file from \Content to the $(TargetDir)/Plugins/ folder after build:

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

At the top of the Target tag, we define the variable DoublePluginFolder and later use the StringPluginFolder in multiple places in the same target:

<PropertyGroup>
    <DoublePluginFolder>$(TargetDir)\Plugins\DoubleManipulationPlugin
    </DoublePluginFolder>
    <StringPluginFolder>$(TargetDir)\Plugins\StringManipulationPlugin
    </StringPluginFolder>
</PropertyGroup> 

Then, for each plugin, we:

  1. First – delete the plugins folder, eg
  2. Then – copy the files from the nuget package to the plugins folder, for example:

%(RecursiveDir) at the end of the path, we can keep the same folder structure as in the plugin’s NuGet package file. Sometimes this is important, for example when the package has a runtime subfolder containing multiple folders corresponding to each native platform within it.

Now you should be able to build the PluginConsumer project and run it. After building, please verify that there is a plugins folder in the bin\Debug\\
et6.0
folder, and this folder has two subfolders, Namely, DoublePluginFolder and StringPluginFolder, each subfolder is populated with the corresponding plugin files. Running the project without errors will prove that the container does indeed have those dynamically loaded plugins, and that they resolve correctly to the corresponding interfaces, and that all plugin functionality is working.

Note that all additions related to csproj files are preserved when you upgrade the version of the corresponding plugin through Visual Studio. The only time you need to edit the csproj file is when adding or removing plugins.

Those who read the code thoroughly may notice that at the end of the programe.cs file, I’m testing some cryptic “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)); 

For those interested in what’s going on there, read the previous article – MultiCells and plugins with multicells.

Conclusion

This article describes how to create and install plugins as nuget packages. This allows us to rely primarily on the MSBuild functionality embedded into Visual Studio to create and install plugins. Essentially, we don’t need to create any (or nearly any) special installation mechanism to create and install plugins.

I plan to use this approach in a future article to create and install plugins that add plugins to the Google RPC server.

https://www.codeproject.com/Articles/5352287/Creating-and-Installing-Plugins-as-Nuget-Packages