DEV Community

Spyros Ponaris
Spyros Ponaris

Posted on

Stop Hardcoding Business Rules: Building a Dynamic Workflow Engine in C#

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)

Enter fullscreen mode Exit fullscreen mode
  1. Chain of Responsibility: Keeps each processing step (Inventory, Payment, Shipping) modular and isolated.
  2. 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;
    }
}

Enter fullscreen mode Exit fullscreen mode

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"
      }
    ]
  }
]

Enter fullscreen mode Exit fullscreen mode

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);
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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.

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 :

https://github.com/stevsharp/DynamicWorkflowConsole

Top comments (0)