目录
Project ActivityExample.csproj
介绍
在通过工作流处理我们应用程序的业务逻辑时,会执行来自不同业务部门和服务的逻辑,这也容易受到持续扩展和修改的影响。为了促进工作流服务的开发、增长和维护,拥有一个集中式系统作为代理并允许调用任何活动非常方便,而无需在开发生命周期中添加的每个业务单元都涉及耦合、重新编码和重新编译。
以下解决方案通过反射动态加载DLL解决了这个问题,同时统一了活动执行周期中的实现、运行、状态和错误处理。
背景
为了应用这些技术,程序员应该:
- 了解工作流基本概念:活动、执行流程、参数和实例
- 熟悉面向对象编程的基本概念:接口和继承
- 了解有关C#中反射的基础知识:激活器、调用、方法和属性
代码和解决方案结构摘要
解决方案的结构分为三个C#项目,其中包含以下文件:
Project Activites.Runtime
IActivity
必须实现所有活动类的接口
ActivityBase
所有活动类都必须从中继承的父类
Reflector
具有所有必需的反射方法的类,可在运行时查找、加载和执行活动
RuntimeService
具有运行任何活动的方法的工作流活动服务类
Project ActivityExample
GetCustomerActivity
用于实现活动示例的活动类
Project Console.Test
Program
控制台中动态加载和运行GetCustomer活动的执行示例
使用代码
完整代码解释
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>
https://www.codeproject.com/Tips/5333178/Dynamic-Load-and-Execution-of-Workflow-Activities