Dynamic Load and Execution of Workflow Activities in C#





5.00/5 (1 vote)
Solution using reflection to dynamically load and execute C# code in a workflow context
Introduction
When handling business logic of our applications through workflows, logic from different business units and services is executed, which is also susceptible to continuous extensions and modifications. To facilitate the development, growth and maintenance of a workflow service, it is convenient to have a centralized system that acts as a proxy and allows calling any activity without the need that each business unit added in the development life cycle involves coupling, recoding, and recompilation.
The following solution solves this problem through the dynamic loading of DLLs via reflection, while unifying the implementation, running, state and error handling in the execution cycle of activities.
Background
In order to apply these techniques, a programmer should:
- know workflow basic concepts: Activities, execution flow, parameters and Instances
- be familiarized with the basic concepts of object oriented-programming: Interfaces, and inheritance
- know basics about reflection in C#: Activator, Invoke, methods and attributes
Summary of the Code and Solution Structure
The structure of the solution is separated in three C# projects with the following files:
Project Activites.Runtime
IActivity
Interface that has to implement all activity classes
ActivityBase
Parent class from which all activity classes have to inherit
Reflector
Class with all needed reflection methods to find, load and execute activities at runtime
RuntimeService
Workflow activities service class with a method to run any activity
Project ActivityExample
GetCustomerActivity
Activity class to implement an example for an activity
Project Console.Test
Program
An execution example in console to dynamically load and run the GetCustomer
Activity
Using the Code
Full code explained
Project Activites.Runtime
IActivity.cs
//Interface IActivity requires:
//Workflow Guid, Input parameters, state, Response and ErrorDetail for every activity
public enum EState { NO_STARTED, STARTED, FINISHED, ERROR, NOT_FOUND }
public interface IActivity
{
EState State { get; set; }
List<KeyValuePair<string, object>> Response { get; set; }
}
ActivityBase.cs
//Parent class for all activities Implements:
//IActivity Properties
//A constructor with default started state
//RunActivity method to execute the activity and encapsule state and error control
//Virtual RunActivityImplementation method,
//which must be overridden by every child class
public class ActivityBase : IActivity
{
public Guid WorkflowId { get; set; }
public EState State { get; set; }
public List<KeyValuePair<string, object>> Response { get; set; }
public List<KeyValuePair<string, object>> InputParameters { get; set; }
public string ErrorDetail { get; set; }
public ActivityBase() { }
public ActivityBase (Guid workflowId,
List<KeyValuePair<string, object>> inputParameters,
EState state = EState.STARTED)
{
WorkflowId = workflowId;
InputParameters = inputParameters;
State = state;
}
public IActivity RunActivity(Guid workflowId,
List<KeyValuePair<string, object>> parameters)
{
var result = new ActivityBase(workflowId, parameters);
this.WorkflowId =workflowId;
this.InputParameters = parameters;
try
{
result.Response = RunActivityImplementation();
result.State = EState.FINISHED;
}
catch (Exception ex)
{
result.ErrorDetail = ex.Message;
result.State = EState.ERROR;
}
return result;
}
public virtual List<KeyValuePair<string, object>> RunActivityImplementation()
{
throw new NotImplementedException();
}
}
Reflector.cs
//Attribute to be used to flag Activity
//to be found dynamically and to be executed at runtime
public class WorkflowActivitesAttribute : Attribute
{
public string ActivityMethodId;
}
//Class that defines a repository for assemblies loaded in memory
public class LoadAssembly
{
public string Key { get; set; }
public DateTime LastModification { get; set; }
public System.Reflection.Assembly Assembly { get; set; }
}
//Reflection class that:
//Dynamically loads assemblies (.DLLs) from a set path
//From loaded assemblies, finds all IActivity classes
//From all IActivity classes, finds specific method that matches
//activityMethodId parameter
public class Reflector
{
//set Path from AppConfig
private static string Path =
System.Configuration.ConfigurationManager.AppSettings
["WorkflowDLLsClientsFolderPath"];
private static object _LockAssembliesList = new object();
private static List<LoadAssembly> LoadAssemblies = new List<LoadAssembly>();
//From loaded assemblies, finds all classes that implement type(IActivity)
public static List<Type> GetClassesFromInterface(Type type)
{
var types = GetAssemblies()
.SelectMany(s => s.GetTypes())
.Where(p => type.IsAssignableFrom(p) &&
p.IsClass)
.ToList();
return types;
}
//From all Loaded IActivity classes,
//returns specific method name that matches activityMethodId parameter
public static string GetWorkflowActivityMethodName
(Type type, string activityMethodId)
{
string result = null;
System.Reflection.MethodInfo[] methods = type.GetMethods();
foreach (System.Reflection.MethodInfo m in methods)
{
object[] attrs = m.GetCustomAttributes(false);
foreach (object attr in attrs)
{
var a = attr as WorkflowActivitesAttribute;
if (a != null && a.ActivityMethodId == activityMethodId)
{
return m.Name;
}
}
}
return result;
}
//Load assemblies from Path and manage a repository
//to load every assembly only once until there are new versions
private static System.Reflection.Assembly[] GetAssembliesFromPath()
{
lock (_LockAssembliesList)
{
var resultList = new List<System.Reflection.Assembly>();
System.Reflection.Assembly assembly;
foreach (string dll in System.IO.Directory.GetFiles(Path, "*.dll"))
{
DateTime modification = System.IO.File.GetLastWriteTime(dll);
var loadedAssembly = LoadAssemblies.FirstOrDefault(a => a.Key == dll);
if (loadedAssembly != null &&
loadedAssembly.LastModification < modification)
{
LoadAssemblies.RemoveAll(a => a.Key == dll);
loadedAssembly = null;
}
assembly = loadedAssembly?.Assembly;
if (assembly == null)
{
assembly = System.Reflection.Assembly.LoadFile(dll);
LoadAssemblies
.Add(new LoadAssembly
{
Key = dll,
LastModification = modification,
Assembly = assembly
});
}
resultList.Add(assembly);
}
var result = resultList.ToArray();
return result;
}
}
}
RuntimeService.cs
// Core to execute Activities from any Service
// Programmer has to integrate a call to this method in its workflow service
public class RuntimeService
{
//Given a workflow processId, an activityMethodId, and inputParameters for activity
//This method uses reflector to:
//Dynamically load DLLs located at Path folder (check out at Reflector class)
//Get all classes that implements interface IActivity
//Create an instance for every class by using Activator
//Find the class that implements the logic to run.
//By matching activityMethodId attribute and parameter
//Invoke method RunActivity from ActivityBase
public ActivityBase RunActivity(Guid processId,
string activityMethodId,
List<KeyValuePair<string, object>> inputParameters)
{
var types = Reflector.GetClassesFromInterface(typeof(IActivity));
foreach (Type t in types)
{
var obj = Activator.CreateInstance(t) as IActivity;
string methodName =
Reflector.GetWorkflowActivityMethodName(t, activityMethodId);
if (methodName != null)
{
System.Reflection.MethodInfo methodInfo = t.GetMethod("RunActivity");
var parameters = new object[] { processId, inputParameters };
try
{
var result = (ActivityBase)methodInfo.Invoke(obj, parameters);
return result;
}
catch (Exception ex)
{
return new ActivityBase(processId, inputParameters, EState.ERROR)
{
ErrorDetail = ex.Message
};
}
}
}
return new ActivityBase { State = EState.NOT_FOUND };
}
}
Project ActivityExample.csproj
GetCustomerActivity.cs
using Activities.Runtime;
//Example for an Activity compiled in a separated project from our Workflow service
//Placing it at Path folder(check out at reflector class),
//it will be dynamically loaded and executed.
public class GetCustomerActivity : ActivityBase, IActivity
{
[WorkflowActivites(ActivityMethodId = "GetCustomer")]
public override List<KeyValuePair<string, object>> RunActivityImplementation()
{
//Get InputParameters (List<KeyValuePair<string, object>>)
//Execute business logic code
//Use any Client/Server communication architecture: Web API, WCF, ..
//Return result in a List<KeyValuePair<string, object>>
var result = new List<KeyValuePair<string, object>>
{
new KeyValuePair<string, object>("customers",
new { Lists = new List<dynamic>{ new { id = 1 } } })
};
return result;
}
}
Project Console.Test
Program.cs
using Activities.Runtime;
//An example that uses RunTimeService to dynamically load ActivityExample DLL
//and run implementation for GetCustomerActivity class
class Program
{
static void Main(string[] args)
{
//Set a new workflow Id
var workflowGuid = System.Guid.NewGuid();
//Set input parameters to call GetCustomer Activity
var inputParameters = new List<KeyValuePair<string, object>>()
{
new KeyValuePair<string, object>("Customer",1)
};
//Run method "GetCustomer" from Activity class
//"GetCustomerActivity" through RuntimeService
//Activity class "GetCustomerActivity" is loaded at runtime by service
ActivityBase result = new RuntimeService().RunActivity
(workflowGuid, "GetCustomer", inputParameters);
//Check result state and response
System.Console.WriteLine("Success...");
System.Console.ReadLine();
}
}
App.Config
<!-- Set path folder to place, find and dynamically run the DLLs for the activities -->
<appSettings>
<add key="WorkflowDLLsClientsFolderPath" value="[DLL_FOLDER_PATH]" />
</appSettings>
History
- 14th July, 2022: Initial version