Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Dynamic Load and Execution of Workflow Activities in C#

5.00/5 (1 vote)
14 Jul 2022CPOL1 min read 7K  
Solution using reflection to dynamically load and execute C# code in a workflow context
In the execution context of a Workflow, activities from different business units need to be executed. This solution simplifies and unifies it by using reflection and object oriented-programming.

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:

  1. know workflow basic concepts: Activities, execution flow, parameters and Instances
  2. be familiarized with the basic concepts of object oriented-programming: Interfaces, and inheritance
  3. 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
C#
//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
C#
//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
C#
 //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
C#
// 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
C#
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
C#
 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
XML
<!-- 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)