I'm currently working on a workflow system. Simply put, a workflow consists of several steps. Each step has 0 or more transitions to other steps based on the result of the step. If there are no transitions, then it's the end of the workflow.
Background
Steps are stored in the database with a unique id, which is used to instantiate them through reflection. I have the following right now:
IEnumerable<Workflow> workflows = _workflowRepository.GetWorkflows();
foreach(Workflow workflow in workflows) {
// Retrieve the current step that must be execute
WorkflowStep currentStep = getCurrentStep();
var flowRunning = true;
while (flowRunning) {
IWorkflowStep instance = WorkflowStepFactory.GetStep(currentStep.StepId);
Result result = instance.Execute();
// Set currentStep based on defined transitions and the value of result
}
}
The IWorkflowStep
is the interface that steps must inherit:
public interface IWorkflowStep
{
Result Execute();
}
Each class that is a step has a StepIdAttribute placed on the class with the id:
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public sealed class StepIdAttribute : Attribute
{
public StepIdAttribute(int stepId)
{
StepId = stepId;
}
public int StepId { get; }
}
A step must implement IWorkflowStep
and must have the StepIdAttribute
placed on that it:
[StepId(Constants.FIRST_STEP_ID)]
public sealed class FirstStep : IWorkflowStep
{
public Result Execute()
{
// Execute step
}
}
Lastly, the WorkflowStepFactory
creates an instance of the interface through reflection:
public static class WorkflowStepFactory
{
public static IWorkflowStep GetStep(int stepId)
{
var iWorkflowStepType = typeof(IWorkflowStep);
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
var typesThatImplementIWorkflowStep = assembly.GetTypes().Where(t => iWorkflowStepType.IsAssignableFrom(t));
foreach (var type in typesThatImplementIWorkflowStep)
{
var appliedStepIdAttributes = type.GetCustomAttributes(typeof(StepIdAttribute), false).Cast<StepIdAttribute>();
foreach (var appliedAttribute in appliedStepIdAttributes)
{
if (appliedAttribute.StepId == stepId)
{
return (IWorkflowStep)Activator.CreateInstance(type);
}
}
}
}
throw new Exception("No implementation found");
}
}
Problem
My problem is that none of the steps are guaranteed to require the same data. For instance, step one might need the following class:
public class Step1Data {
public string SomeId { get; set; }
}
Whereas step two might need the following class:
public class Step2Data {
public int MinimumThreshold { get; set; }
public int MaximumThreshold { get; set; }
}
What I don't want, is for the main loop to become responsible for getting the data for a variety of steps. I want to make it as easy as possible to add new steps and data, so if possible, I want reflection to be able to care of it all.
What I want, is for the instance itself to request the data, that some class picks it up and gives it back to the instance.
My ideas
I was thinking of accomplishing it through delegates and events. To that end, I introduced a delegate:
public delegate void RequestingDataEventHandler<T>(object source, T e);
And added an event to the IWorkflowStep
interface:
public interface IWorkflowStep<T>
{
event RequestingDataEventHandler<T> RequestingData;
Result Execute();
}
A step could then be like this:
public sealed class FirstStep : IWorkflowStep<Step1Data>
{
public event RequestingDataEventHandler<Step1Data> RequestingData;
public Result Execute()
{
var myData = new Step1Data();
if (RequestingData != null)
{
RequestingData.Invoke(this, myData);
}
// Execute step based on retrieved
}
}
Then, there could be a class that listens to the event, a StepDataBuilder
if you will. There would be one for each T
and T
could be the Step1Data
type or the Step2Data
type, etc.. Essentially: T
represents the type that should hold the data that the step needs. Something like this:
public class Step1DataBuilder<Step1Data> {
public void WireEvent(IWorkflowStep<Step1Data> step) {
step.RequestingData += GetData;
}
public void GetData(object source, Step1Data dataObj) {
dataObj.SomeId = "some id retrieved from somewhere else";
}
}
However, the above setup requires the loop or outer program to know what type T
should be, which I don't think it really doesn't need to at all. I want the outer program to instantiate the classes, wire up the events and to execute the steps. So I'm not really sure what T
I should supply, if any. Maybe I can create the classes use Type.MakeGenericType
but I'm not too sure if it could accomplish what I want.
Question(s)
- What would I need to do to get the above working if I want to supply data using events and delegates?
- Is this even a relatively sane approach? It feels a bit convoluted and I'm wondering if there's an easier way that I'm overlooking. An extra layer of indirection, a long-lost design pattern or something else entirely.
If you need any more information, please let me know.