Skip to main content

Dynamic plugin loading

In this tutorial, we'll implement dynamic plugin loading. This allows you to customize the Workflow Engine by placing compiled DLLs with plugins into the plugins folder. Plugins are classes that implement IWorkflowPlugin, enabling the engine to incorporate new Actions, Rules, etc. Learn more about plugins.

GitHub

You can find the code from this tutorial in the GitHub repository.

Motivation

You might be interested in this functionality for a variety of reasons:

  • You distribute your software with a Workflow Engine to your clients and want to provide them with customization options without needing access to the source code.
  • You want to separate dependencies used in the project with the Workflow Engine from those used for plugins.
  • You manage a large number of plugins and want to simplify their modification or installation.

Environment Requirements

  • Application with integrated Workflow Engine.
  • IDE for working with C# code, such as Visual Studio.

Tutorial

In this tutorial, we'll step through implementing dynamic plugin loading, which requires:

  1. Writing a PluginLoader class for dynamically loading assemblies.
  2. Adding an extension method for loading plugins into the WorkflowRuntime.
  3. Creating a new project with plugin implementation and exporting its DLL.
  4. Testing and ensuring everything works well.

PluginLoader

This class will inherit from the System class AssemblyLoadContext, aiming to correctly load DLLs and resolve their dependencies at the specified path.

PluginLoader.cs
using System.Reflection;
using System.Runtime.Loader;

namespace DynamicPluginLoading;

public class PluginLoader : AssemblyLoadContext
{
private readonly AssemblyDependencyResolver _resolver;

public PluginLoader(string path)
{
_resolver = new AssemblyDependencyResolver(path);
}

protected override Assembly? Load(AssemblyName assemblyName)
{
var assemblyPath = _resolver.ResolveAssemblyToPath(assemblyName);

return assemblyPath != null
? LoadFromAssemblyPath(assemblyPath)
: null;
}

protected override IntPtr LoadUnmanagedDll(string unmanagedDllName)
{
var libraryPath = _resolver.ResolveUnmanagedDllToPath(unmanagedDllName);

return libraryPath != null
? LoadUnmanagedDllFromPath(libraryPath)
: IntPtr.Zero;
}
}

Extension Method

The extension method allows us to add dynamic loading of plugins into the Workflow Engine Runtime initialization pipeline alongside other settings.

Extensions.cs
using System.Reflection;
using OptimaJet.Workflow.Core.Runtime;
using OptimaJet.Workflow.Plugins;

namespace DynamicPluginLoading;

public static class Extensions
{
private const string PluginsFolder = "plugins";

public static WorkflowRuntime WithDynamicPlugins(this WorkflowRuntime runtime, params string[] plugins)
{
foreach (var plugin in plugins)
{
var dllPath = Path.Combine(Environment.CurrentDirectory, PluginsFolder, plugin, $"{plugin}.dll");

try
{
var loader = new PluginLoader(dllPath);

var assembly = loader.LoadFromAssemblyName(new AssemblyName(plugin));

var workflowPluginTypes = assembly.GetTypes()
.Where(type => typeof(IWorkflowPlugin).IsAssignableFrom(type));

foreach (var workflowPlugin in workflowPluginTypes)
{
try
{
var instance = Activator.CreateInstance(workflowPlugin) as IWorkflowPlugin;
runtime.WithPlugin(instance);
}
catch (Exception)
{
Console.WriteLine("Create instance failed for plugin: " + workflowPlugin.FullName);
}
}
}
catch (Exception)
{
Console.WriteLine($"Plugin {plugin} not found on path '{dllPath}'");
}
}

return runtime;
}
}

Project with Plugin

To test the functionality, we need to create a test project from which we'll import plugins into the Workflow Engine. This requires several steps:

  1. Create a new project MyPlugin.

    dotnet new classlib --name MyPlugin
    dotnet sln add MyPlugin
    rm MyPlugin\Class1.cs
  2. Add a package reference to WorkflowEngine.NETCore-Core to implement IWorkflowPlugin.

    dotnet add MyPlugin package WorkflowEngine.NETCore-Core
  3. Add a new class MyPlugin implementing the IWorkflowPlugin interface.

    MyPlugin.cs
    using OptimaJet.Workflow.Core.Runtime;
    using OptimaJet.Workflow.Plugins;

    namespace MyPlugin;

    public class MyPlugin : IWorkflowPlugin
    {

    public string Name => nameof(MyPlugin);
    public bool Disabled { get; set; }
    public Dictionary<string, string> PluginSettings => new();

    public void OnPluginAdd(WorkflowRuntime runtime, List<string>? schemes = null)
    {
    // Do nothing
    }

    public Task OnRuntimeStartAsync(WorkflowRuntime runtime)
    {
    // Do nothing
    return Task.CompletedTask;
    }
    }
  4. Build the project and copy its DLL to the plugins/{plugin_name}/… folder.

    dotnet build
    mkdir -p $(YOUR_PROJECT_NAME)/bin/Debug/net8.0/plugins/MyPlugin
    cp -r MyPlugin/bin/Debug/net8.0/* $(YOUR_PROJECT_NAME)/bin/Debug/net8.0/plugins/MyPlugin/

Testing

Finally, we have the ability to connect dynamic plugin loading to the WorkflowRuntime creation pipeline with the plugin name specified.

var runtime = new WorkflowRuntime()
.WithPlugin(new BasicPlugin())
.WithDynamicPlugins("MyPlugin")
.AsSingleServer();

If you've followed the steps exactly, MyPlugin will appear among the plugins in WorkflowRuntime, which you can verify by checking WorkflowRuntime.Plugins.

foreach (var plugin in runtime.Plugins)
{
Console.WriteLine($"- {plugin.Name}");
}

// Output:
// - BasicPlugin
// - MyPlugin

Conclusion

Now you can easily enhance plugin management in your project.