Stop Hardcoding Business Rules: Building a Dynamic Workflow Engine in C Sharp
Every growing C# application eventually hits a wall: business logic becomes a tangle of deeply nested if/else checks scattered across service layers. Every time marketing tweaks a promotion or compliance updates an age restriction, developers have to modify compiled C#, run regression tests, and push a new deployment.
What if your code handled the execution flow while your database controlled the rules?
By combining the Chain of Responsibility pattern, Keyed Dependency Injection, and Microsoft’s open-source `RulesEngine`, you can build a flexible pipeline that evaluates dynamic business rules on the fly.
The Architecture: Code vs. Configuration
To keep systems maintainable, separate procedural execution (what your system does) from business constraints (what your system allows):
[ Incoming Request ]
│
▼
[ Dynamic Workflow Engine ]
│
├── 1. Fetch Steps & JSON Rules from Database
│
├── 2. Evaluate Dynamic Rules (Microsoft.RulesEngine)
│ ├── Passed ──► Proceed
│ └── Failed ──► Abort & Return Reason
│
└── 3. Execute Step Handler (via Keyed DI)
- Chain of Responsibility: Keeps each processing step (Inventory, Payment, Shipping) modular and isolated.
- Microsoft RulesEngine: Parses readable expression strings (like
Amount > 5000 && CreditScore < 700) stored as JSON in your database and evaluates them against your C# objects at runtime.
1. Defining the Workflow Context
First, create a state object passed down the execution chain. This context carries order details and tracks pipeline status.
public class OrderContext
{
public string OrderId { get; set; } = string.Empty;
public decimal Amount { get; set; }
public int CustomerAge { get; set; }
public int CreditScore { get; set; }
public bool IsAborted { get; private set; }
public string AbortReason { get; private set; } = string.Empty;
public void Abort(string reason)
{
IsAborted = true;
AbortReason = reason;
}
}
2. Setting Up Dynamic Rules in JSON
Instead of writing hardcoded conditions in C#, store rules as JSON objects—either in SQL tables, a NoSQL store, or configuration files.
[
{
"WorkflowName": "PaymentRules",
"Rules": [
{
"RuleName": "HighValueCreditCheck",
"ErrorMessage": "Orders over $5,000 require a Credit Score of at least 700.",
"Expression": "Amount <= 5000 || CreditScore >= 700"
}
]
}
]
3. Building the Pipeline Engine
Using .NET 8/9 Keyed Services, handlers are resolved dynamically using string keys fetched from your database schema.
public interface IWorkflowHandler
{
Task HandleAsync(OrderContext context, Func<Task> next);
}
public class DynamicWorkflowEngine
{
private readonly IServiceProvider _serviceProvider;
public DynamicWorkflowEngine(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;
public async Task ExecuteAsync(OrderContext context, List<DbWorkflowStep> steps)
{
foreach (var step in steps)
{
if (context.IsAborted) break;
// Step 1: Evaluate DB-stored Rules
if (!string.IsNullOrWhiteSpace(step.StepRulesJson))
{
var workflowList = JsonConvert.DeserializeObject<List<WorkflowRules>>(step.StepRulesJson);
var re = new RulesEngine.RulesEngine(workflowList.ToArray());
var results = await re.ExecuteAllRulesAsync(workflowList[0].WorkflowName, context);
if (results.Any(r => !r.IsSuccess))
{
var failedRule = results.First(r => !r.IsSuccess);
context.Abort($"Rule failed at {step.HandlerKey}: {failedRule.Rule.ErrorMessage}");
break;
}
}
// Step 2: Execute Handler from DI
var handler = _serviceProvider.GetKeyedService<IWorkflowHandler>(step.HandlerKey);
if (handler != null)
{
await handler.HandleAsync(context, () => Task.CompletedTask);
}
}
}
}
Why This Pattern Wins
- Zero-Downtime Rule Updates: Non-developers or admin dashboards can edit business expressions in the database. Updated rules apply instantly without recompiling C#.
- Auditable Failure Reasons:
RulesEngine explicitly reports which rule failed and why, giving clear feedback to users and log aggregators.
- Expression Safety: Unlike executing raw string scripts,
RulesEngine safely compiles expressions into Abstract Syntax Trees (ASTs), avoiding code injection vulnerabilities.
Evaluating JSON rules introduces lightweight parsing overhead. In production, wrap DB calls and RulesEngine instances in an IMemoryCache. Invalidate the cache only when an administrator updates a rule in your database.
By shifting fast-changing business rules into dynamic configuration, your codebase stays lean, maintainable, and resilient to change.
Source code :
https://github.com/stevsharp/DynamicWorkflowConsole