Plugins¶
Built-in nodes and external nodes use the same mechanism: assemblies are scanned for [PipeNode] and [PipeCredentialType] classes.
An external plugin project¶
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<EnableDynamicLoading>true</EnableDynamicLoading>
</PropertyGroup>
<ItemGroup>
<!-- the contracts come from the host: never ship them with the plugin -->
<PackageReference Include="gView.Pipe.Abstraction" Version="…" ExcludeAssets="runtime" />
</ItemGroup>
</Project>
gView.Pipe.Plugins.Scripts and gView.Pipe.Plugins.Database in the repository are complete examples. They use a project reference with Private="false" ExcludeAssets="runtime".
Build, then copy the output into plugins/<Name>/ of the app, where <Name> is the name of the main assembly:
plugins\
MyCompany.PipeNodes\
MyCompany.PipeNodes.dll
(dependencies, native libraries)
How plugins are loaded¶
- The app folder is scanned for
gView.Pipe.Plugins*.dll(the built-in nodes).Pipe:PluginPathis scanned for plugin folders. - Each plugin folder is loaded into its own
AssemblyLoadContext, with its dependencies resolved from itsdeps.json. Plugins may use different versions of a library without conflicts. - Assemblies named
gView.Pipe.Abstraction,Microsoft.Extensions.*,Microsoft.AspNetCore.*andSystem.*always come from the host, so the plugin and the host share the same types. Don't ship them. - Plugins are loaded when the host configures its services, so they can register services before the container is built. Plugins can't be unloaded: restart the app after an update.
- A problem while loading gives a warning in the log that starts with
Plugins:, and the plugin is skipped. An id used by two classes keeps the first and warns.
Workflows that use a node of a plugin that isn't installed still load, and the node's data is kept. Running such a node fails with "Unknown node type".
Services and options¶
Implement IPipePluginStartup (public, parameterless constructor) to register services:
public sealed class MyPluginStartup : IPipePluginStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddOptions<MyOptions>().Configure<IServiceProvider>((options, provider) =>
{
if (provider.GetService<IConfiguration>() is { } configuration)
{
configuration.GetSection("Pipe:MyPlugin").Bind(options);
}
});
services.AddHttpClient("my-plugin");
}
}
Nodes get services through their constructor. Settings an administrator controls (released folders, servers) belong in options like this, never in a node parameter: that way a workflow can't widen what it is allowed to do.
Credential types¶
A credential type is a POCO with [PipeCredentialType]; its properties are the fields of the form:
[PipeCredentialType("5b7f3c2a-1d4e-4f7a-9c0b-2a6d8e1f3b45", "My Service", Description = "Token and URL of My Service")]
public sealed class MyServiceCredential
{
[Required, PipeParameter(Label = "Base URL")]
public string Url { get; set; } = "";
[Required, Secret, PipeParameter(Label = "Token")]
public string Token { get; set; } = "";
}
A node references it with [PipeCredential("5b7f3c2a-…")] on a string? parameter and reads it with await context.GetCredentialAsync<MyServiceCredential>(parameters.CredentialId). Never log secret values and never copy them into items: items end up in the execution history.
Rules for ids¶
- The GUID of a node or credential type is its identity in stored workflows. Never change it, and don't reuse it.
- Built-in ids are listed in
PipeBuiltIn.csofgView.Pipe.Plugins. - When a node's parameters change incompatibly, increase
Versionand add anIPipeNodeMigrationinstead of a new id.