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.
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:
- Writing a
PluginLoader
class for dynamically loading assemblies. - Adding an extension method for loading plugins into the
WorkflowRuntime
. - Creating a new project with plugin implementation and exporting its DLL.
- 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.
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.
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:
- Create a new project
MyPlugin
.dotnet new classlib --name MyPlugin
dotnet sln add MyPlugin
rm MyPlugin\Class1.cs - Add a package reference to
WorkflowEngine.NETCore-Core
to implementIWorkflowPlugin
.dotnet add MyPlugin package WorkflowEngine.NETCore-Core
- Add a new class
MyPlugin
implementing theIWorkflowPlugin
interface.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;
}
} - 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.