65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中工作流活动的动态加载与执行

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2022 年 7 月 14 日

CPOL

1分钟阅读

viewsIcon

7264

使用反射动态加载和执行 C# 代码的工作流解决方案

引言

在通过工作流处理应用程序的业务逻辑时,会执行来自不同业务单元和服务的逻辑,这些逻辑也容易受到持续扩展和修改的影响。为了便于工作流服务的开发、增长和维护,拥有一个充当代理的集中式系统,允许调用任何活动,而无需每个业务单元在开发生命周期中涉及耦合、重编码和重新编译,将非常方便。

以下解决方案通过反射动态加载 DLL,同时统一活动执行周期中的实现、运行、状态和错误处理,从而解决此问题。

背景

为了应用这些技术,程序员应该

  1. 了解工作流的基本概念:活动、执行流程、参数和实例
  2. 熟悉面向对象编程的基本概念:接口和继承
  3. 了解 C# 中的反射基础知识:Activator、Invoke、方法和属性

代码和解决方案结构概述

解决方案的结构分为三个 C# 项目,包含以下文件

项目 Activites.Runtime

IActivity

所有活动类都必须实现的接口

ActivityBase

所有活动类都必须继承的父类

Reflector

包含所有必要的反射方法,用于在运行时查找、加载和执行活动

RuntimeService

工作流活动服务类,包含运行任何活动的方法

项目 ActivityExample

GetCustomerActivity

用于实现活动示例的活动类

项目 Console.Test

程序

在控制台中动态加载和运行 GetCustomer 活动的执行示例

Using the Code

完整代码解释

项目 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 };
    }
 }

项目 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;
     }
 }

项目 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>

历史

  • 2022 年 7 月 14 日:初始版本
© . All rights reserved.