Introduction
When it comes to software design patterns, the Chain of Responsibility pattern is a powerful and elegant way to handle complex workflows. It allows a series of tasks to be executed dynamically, with each task deciding whether to pass control to the next one. In this article, we'll explore how to implement a Dynamic Workflow Engine in C# using the Chain of Responsibility pattern, combined with additional design principles like Single Responsibility and Dependency Injection.
Workflow engines are widely used in industries like e-commerce, finance, and healthcare to automate processes such as payment handling, order processing, and approvals. They provide flexibility, reusability, and scalability, making them a must-have tool for dynamic business logic.
In this article, we'll build a Dynamic Workflow Engine for an e-shop system. The workflow will simulate a customer's journey through an order process. It will dynamically branch based on the payment method (e.g., cash or credit card), and customers paying with a credit card will receive a free gift. By the end, you'll have a reusable workflow engine capable of handling dynamic branching, and you'll understand how to extend it further.
What Is a Workflow Engine?
At its core, a workflow engine is a system that:
- Defines Workflows: A sequence of steps to achieve a specific goal.
- Executes Logic Dynamically: Executes tasks in order, often with conditional branching or parallel steps.
- Shares Context: Uses shared data (context) to enable communication between steps.
Design Patterns Used
- Chain of Responsibility: The workflow engine executes tasks in a sequence, passing control to the next step dynamically.
- Single Responsibility Principle: Each step in the workflow performs a single well-defined task.
- Dependency Injection: Steps and branching logic are injected dynamically, enabling loose coupling and better testability.
Dynamic Workflow Engine in Action: E-Shop Example
Full Code Example
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace EShopDynamicWorkflow
{
// 1. Define the IWorkflowStep interface
public interface IWorkflowStep
{
string Name { get; }
Task ExecuteAsync(WorkflowContext context);
}
// 2. Define the WorkflowContext class
public class WorkflowContext
{
public Dictionary<string, object> Data { get; } = new Dictionary<string, object>();
public void SetData(string key, object value)
{
Data[key] = value;
}
public T GetData<T>(string key)
{
return (T)Data[key];
}
}
// 3. Define the Workflow class
public class Workflow
{
public string Name { get; set; }
public List<IWorkflowStep> Steps { get; set; } = new List<IWorkflowStep>();
public void AddStep(IWorkflowStep step)
{
Steps.Add(step);
}
}
// 4. Create the WorkflowEngine class
public class WorkflowEngine
{
public async Task RunWorkflowAsync(Workflow workflow, WorkflowContext context)
{
Console.WriteLine($"Starting workflow: {workflow.Name}");
foreach (var step in workflow.Steps)
{
Console.WriteLine($"Executing step: {step.Name}");
try
{
await step.ExecuteAsync(context);
}
catch (Exception ex)
{
Console.WriteLine($"Error in step {step.Name}: {ex.Message}");
throw;
}
}
Console.WriteLine($"Workflow {workflow.Name} completed.");
}
}
// 5. Example E-Shop Steps
// Step 1: Browse Items
public class BrowseItemsStep : IWorkflowStep
{
public string Name => "BrowseItemsStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Customer is browsing items...");
context.SetData("Cart", new List<string> { "Laptop", "Headphones" });
Console.WriteLine("Items added to cart: Laptop, Headphones");
return Task.CompletedTask;
}
}
// Step 2: Add Items to Cart
public class AddToCartStep : IWorkflowStep
{
public string Name => "AddToCartStep";
public Task ExecuteAsync(WorkflowContext context)
{
var cart = context.GetData<List<string>>("Cart");
cart.Add("Mouse");
Console.WriteLine("Added item to cart: Mouse");
return Task.CompletedTask;
}
}
// Step 3: Dynamic Conditional Branch Step
public class ConditionalBranchStep : IWorkflowStep
{
public string Name => "ConditionalBranchStep";
private readonly Func<WorkflowContext, Task> _branchLogic;
public ConditionalBranchStep(Func<WorkflowContext, Task> branchLogic)
{
_branchLogic = branchLogic;
}
public async Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Executing conditional branch logic...");
await _branchLogic(context);
}
}
// Step 4: Payment Steps
public class CashPaymentStep : IWorkflowStep
{
public string Name => "CashPaymentStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Processing cash payment...");
context.SetData("PaymentStatus", "Success");
Console.WriteLine("Cash payment successful!");
return Task.CompletedTask;
}
}
public class CreditCardPaymentStep : IWorkflowStep
{
public string Name => "CreditCardPaymentStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Processing credit card payment...");
context.SetData("PaymentStatus", "Success");
Console.WriteLine("Credit card payment successful!");
return Task.CompletedTask;
}
}
public class AddGiftStep : IWorkflowStep
{
public string Name => "AddGiftStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Adding a free gift for credit card payment...");
var cart = context.GetData<List<string>>("Cart");
cart.Add("Free Gift");
Console.WriteLine("Free gift added to the cart!");
return Task.CompletedTask;
}
}
// Step 5: Send Confirmation Email
public class ConfirmationEmailStep : IWorkflowStep
{
public string Name => "ConfirmationEmailStep";
public Task ExecuteAsync(WorkflowContext context)
{
var paymentStatus = context.GetData<string>("PaymentStatus");
if (paymentStatus == "Success")
{
Console.WriteLine("Sending confirmation email to the customer...");
}
else
{
Console.WriteLine("Payment failed. Cannot send confirmation email.");
}
return Task.CompletedTask;
}
}
// 6. Putting It All Together
class Program
{
static async Task Main(string[] args)
{
// Create a workflow
var workflow = new Workflow { Name = "E-Shop Workflow with Dynamic Branching" };
workflow.AddStep(new BrowseItemsStep());
workflow.AddStep(new AddToCartStep());
// Add a dynamic conditional branch step
workflow.AddStep(new ConditionalBranchStep(async context =>
{
var paymentMethod = context.GetData<string>("PaymentMethod");
if (paymentMethod == "Cash")
{
await new CashPaymentStep().ExecuteAsync(context);
}
else if (paymentMethod == "CreditCard")
{
await new CreditCardPaymentStep().ExecuteAsync(context);
await new AddGiftStep().ExecuteAsync(context);
}
}));
workflow.AddStep(new ConfirmationEmailStep());
// Set up the context
var context = new WorkflowContext();
context.SetData("PaymentMethod", "CreditCard"); // Change to "Cash" to test the other branch
// Run the workflow
var engine = new WorkflowEngine();
await engine.RunWorkflowAsync(workflow, context);
}
}
}
Explanation of the Code
To help you understand the code more deeply, let's break it into smaller segments and explain how each part works.
1. The Workflow Context
public class WorkflowContext
{
public Dictionary<string, object> Data { get; } = new Dictionary<string, object>();
public void SetData(string key, object value)
{
Data[key] = value;
}
public T GetData<T>(string key)
{
return (T)Data[key];
}
}
-
Purpose: The
WorkflowContextserves as a shared data store, allowing different workflow steps to exchange information during execution. - SetData: Adds or updates data in the context.
- GetData: Retrieves data by key and casts it to the specified type.
-
Example: The
PaymentMethodis stored here, allowing theConditionalBranchStepto determine the branch to execute.
2. The Workflow Step Interface
public interface IWorkflowStep
{
string Name { get; }
Task ExecuteAsync(WorkflowContext context);
}
- Purpose: Defines the contract that all workflow steps must follow.
- Name: Provides a descriptive name for the step.
-
ExecuteAsync: Executes the step's logic and interacts with the
WorkflowContext.
This abstraction ensures that all steps are loosely coupled and interchangeable.
3. The Workflow Engine
public class WorkflowEngine
{
public async Task RunWorkflowAsync(Workflow workflow, WorkflowContext context)
{
Console.WriteLine($"Starting workflow: {workflow.Name}");
foreach (var step in workflow.Steps)
{
Console.WriteLine($"Executing step: {step.Name}");
try
{
await step.ExecuteAsync(context);
}
catch (Exception ex)
{
Console.WriteLine($"Error in step {step.Name}: {ex.Message}");
throw;
}
}
Console.WriteLine($"Workflow {workflow.Name} completed.");
}
}
- Purpose: The engine orchestrates the execution of all workflow steps in sequence.
- Handles Errors: If a step fails, the engine logs the error and stops the workflow.
- Extensibility: The engine can work with any workflow, making it reusable for multiple systems.
4. Dynamic Branching with Conditional Logic
public class ConditionalBranchStep : IWorkflowStep
{
public string Name => "ConditionalBranchStep";
private readonly Func<WorkflowContext, Task> _branchLogic;
public ConditionalBranchStep(Func<WorkflowContext, Task> branchLogic)
{
_branchLogic = branchLogic;
}
public async Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Executing conditional branch logic...");
await _branchLogic(context);
}
}
-
Purpose: This step allows dynamic branching by accepting a
Func<WorkflowContext, Task>delegate, which defines its behavior at runtime. - Dynamic Logic: The branching logic is injected when the step is created, enabling flexible decision-making.
-
Example: The lambda function in
Maindetermines whether to process cash payment, credit card payment, or both.
5. Individual Workflow Steps
Each step implements IWorkflowStep and performs a specific unit of work.
Browse Items Step
public class BrowseItemsStep : IWorkflowStep
{
public string Name => "BrowseItemsStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Customer is browsing items...");
context.SetData("Cart", new List<string> { "Laptop", "Headphones" });
Console.WriteLine("Items added to cart: Laptop, Headphones");
return Task.CompletedTask;
}
}
-
Purpose: Simulates a customer browsing items and adds them to the
Cartin theWorkflowContext.
Add to Cart Step
public class AddToCartStep : IWorkflowStep
{
public string Name => "AddToCartStep";
public Task ExecuteAsync(WorkflowContext context)
{
var cart = context.GetData<List<string>>("Cart");
cart.Add("Mouse");
Console.WriteLine("Added item to cart: Mouse");
return Task.CompletedTask;
}
}
-
Purpose: Adds another item (
Mouse) to the existing cart.
Cash Payment Step
public class CashPaymentStep : IWorkflowStep
{
public string Name => "CashPaymentStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Processing cash payment...");
context.SetData("PaymentStatus", "Success");
Console.WriteLine("Cash payment successful!");
return Task.CompletedTask;
}
}
- Purpose: Processes payment if the customer chooses the cash payment method.
Credit Card Payment Step
public class CreditCardPaymentStep : IWorkflowStep
{
public string Name => "CreditCardPaymentStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Processing credit card payment...");
context.SetData("PaymentStatus", "Success");
Console.WriteLine("Credit card payment successful!");
return Task.CompletedTask;
}
}
- Purpose: Processes payment if the customer chooses the credit card payment method.
Add Gift Step
public class AddGiftStep : IWorkflowStep
{
public string Name => "AddGiftStep";
public Task ExecuteAsync(WorkflowContext context)
{
Console.WriteLine("Adding a free gift for credit card payment...");
var cart = context.GetData<List<string>>("Cart");
cart.Add("Free Gift");
Console.WriteLine("Free gift added to the cart!");
return Task.CompletedTask;
}
}
- Purpose: Adds a free gift to the cart if the customer uses a credit card.
6. Main Workflow Orchestration
var workflow = new Workflow { Name = "E-Shop Workflow with Dynamic Branching" };
workflow.AddStep(new BrowseItemsStep());
workflow.AddStep(new AddToCartStep());
workflow.AddStep(new ConditionalBranchStep(async context =>
{
var paymentMethod = context.GetData<string>("PaymentMethod");
if (paymentMethod == "Cash")
{
await new CashPaymentStep().ExecuteAsync(context);
}
else if (paymentMethod == "CreditCard")
{
await new CreditCardPaymentStep().ExecuteAsync(context);
await new AddGiftStep().ExecuteAsync(context);
}
}));
workflow.AddStep(new ConfirmationEmailStep());
-
Dynamic Branching: The
ConditionalBranchStepuses a lambda function to determine the payment method and execute the corresponding steps. - Extensibility: Additional branches (e.g., digital wallet payments) can be added easily by modifying the lambda function.
Key Takeaways
- WorkflowContext: Centralized data store for sharing information between steps.
- IWorkflowStep: Standard interface for defining reusable and independent workflow steps.
-
Dynamic Branching: Achieved using
ConditionalBranchStepwith runtime-injected logic. - WorkflowEngine: Orchestrates the execution of steps, ensuring scalability and reusability.
This modular design makes the system highly flexible, extensible, and easy to maintain. You can now apply this approach to build workflows for a variety of domains, from e-commerce to approval pipelines.
Sample Output
Case 1: Payment with Cash
Starting workflow: E-Shop Workflow with Dynamic Branching
Executing step: BrowseItemsStep
Customer is browsing items...
Items added to cart: Laptop, Headphones
Executing step: AddToCartStep
Added item to cart: Mouse
Executing step: ConditionalBranchStep
Executing conditional branch logic...
Processing cash payment...
Cash payment successful!
Executing step: ConfirmationEmailStep
Sending confirmation email to the customer...
Workflow E-Shop Workflow with Dynamic Branching completed.
Case 2: Payment with Credit Card
Starting workflow: E-Shop Workflow with Dynamic Branching
Executing step: BrowseItemsStep
Customer is browsing items...
Items added to cart: Laptop, Headphones
Executing step: AddToCartStep
Added item to cart: Mouse
Executing step: ConditionalBranchStep
Executing conditional branch logic...
Processing credit card payment...
Credit card payment successful!
Adding a free gift for credit card payment...
Free gift added to the cart!
Executing step: ConfirmationEmailStep
Sending confirmation email to the customer...
Workflow E-Shop Workflow with Dynamic Branching completed.
Next Steps for Enhancement
- Retry Mechanism: Implement a retry mechanism for steps that fail, with configurable retry limits and delays.
- Parallel Execution: Add support for executing multiple steps in parallel for workflows with independent tasks.
- Configuration-Driven Workflows: Externalize workflow definitions into JSON or XML to allow dynamic changes without recompiling the code.
- State Management: Add step status tracking to resume workflows after interruptions.
- Logging and Monitoring: Integrate logging to track workflow execution and monitor performance in production systems.
Conclusion
By combining the Chain of Responsibility pattern with dynamic branching logic, we've designed a flexible and extensible Workflow Engine in C#. This engine not only simplifies the orchestration of complex processes but also adapts to changing business needs with minimal effort.
By implementing the suggested enhancements, you can take this engine to the next level, making it production-ready for real-world applications. Try building your own workflows today and unlock the power of dynamic automation!
Love design patterns & C#? Happy coding!
Top comments (0)