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:
RulesEngineexplicitly reports which rule failed and why, giving clear feedback to users and log aggregators. -
Expression Safety: Unlike executing raw string scripts,
RulesEnginesafely compiles expressions into Abstract Syntax Trees (ASTs), avoiding code injection vulnerabilities.
Production Tip: Performance
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 :
Top comments (0)