Rule Action Chaining
Connect rules so the output of one rule feeds into another. Use DependsOnRuleId to declare data-flow dependencies between independent rules.
Table of Contents
- Parent-Child vs DependsOn
- Basic Dependency
- Multi-Stage Pipeline
- Accessing Dependency Results
- Parallel Execution with Dependencies
- Validation
- Common Patterns
Parent-Child vs DependsOn
Rules engine supports two types of relationships between rules. Understanding the difference is critical for correct rule design.
Parent-Child (Structural Nesting)
A parent rule contains child rules. Children are evaluated as part of the parent’s evaluation. The parent succeeds only if all active children succeed.
Use for: Sub-conditions that compose a larger check.
var adultCheck = new Rule
{
Description = "Valid adult customer",
Expression = "customer.IsAdult && customer.HasName",
IsActive = true
};
adultCheck.ChildRules.Add(new Rule
{
Description = "Age check",
Expression = "customer.Age >= 18"
});
adultCheck.ChildRules.Add(new Rule
{
Description = "Name check",
Expression = "!string.IsNullOrEmpty(customer.Name)"
});
Execution order:
- Age check (child)
- Name check (child)
- Valid adult customer (parent expression)
Failure behavior: If Age check fails, adultCheck fails immediately. Name check still runs (all children are evaluated).
DependsOn (Data-Flow Dependency)
Rule B depends on Rule A’s output. Rule B can read Rule A’s result and makes its own decision. The rules remain independent — Rule A doesn’t know about Rule B.
Use for: Multi-stage pipelines where later stages need earlier outputs.
var validateCustomer = new Rule
{
Description = "Validate customer",
Expression = "customer != null && customer.IsActive",
IsActive = true
};
var checkCredit = new Rule
{
Description = "Check credit",
DependsOnRuleId = validateCustomer.Id,
Expression = "customer.CreditScore >= 700",
IsActive = true
};
Execution order:
- Validate customer
- Check credit (only after validate customer completes)
Failure behavior: If validate customer fails, check credit still runs. Check credit can use RuleContext to see the dependency’s result and decide what to do.
Comparison Table
| Aspect | Parent-Child | DependsOn |
|---|---|---|
| Relationship | Structural containment | Data-flow dependency |
| Direction | Parent knows children | Dependent knows dependency |
| Execution | Children first, then parent | Dependency first, then dependent |
| Failure impact | Parent fails if any child fails | Dependent still runs regardless |
| Access to results | Parent cannot read child values | Dependent can read dependency values |
| Use case | Composite conditions | Multi-stage pipelines |
| Cycle detection | Child tree validation | Dependency graph validation |
Basic Dependency
Create two rules where the second depends on the first.
var ruleA = new Rule
{
Description = "Calculate tax",
Expression = "customer.Amount > 0",
Action = "customer.TaxAmount = customer.Amount * 0.08",
IsActive = true
};
var ruleB = new Rule
{
Description = "Calculate total",
DependsOnRuleId = ruleA.Id,
Expression = "customer.TaxAmount > 0",
Action = "customer.Total = customer.Amount + customer.TaxAmount",
IsActive = true
};
var workflow = new Workflow
{
Description = "Order calculation",
Rules = new List<Rule> { ruleB, ruleA } // Any order
};
workflow.Validate();
workflow.Compile(parameters);
var results = workflow.Execute(parameters).ToList();
// results[0] = Calculate tax (dependency)
// results[1] = Calculate total (dependent)
Multi-Stage Pipeline
Build a processing pipeline where each stage depends on the previous.
// Stage 1: Validate input
var validateInput = new Rule
{
Description = "Validate input",
Expression = "customer != null && customer.Id > 0",
IsActive = true
};
// Stage 2: Enrich data (fetch additional info)
var enrichData = new Rule
{
Description = "Enrich data",
DependsOnRuleId = validateInput.Id,
Expression = "true",
Action = "customer.CreditScore = CreditService.GetScore(customer.Id)",
IsActive = true
};
// Stage 3: Evaluate risk
var evaluateRisk = new Rule
{
Description = "Evaluate risk",
DependsOnRuleId = enrichData.Id,
Expression = "customer.CreditScore >= 600",
IsActive = true
};
// Stage 4: Approve or reject
var finalDecision = new Rule
{
Description = "Final decision",
DependsOnRuleId = evaluateRisk.Id,
Expression = "true",
Action = "customer.Approved = context.GetResult(evaluateRisk.Id).Success",
IsActive = true
};
var workflow = new Workflow
{
Description = "Loan approval pipeline",
Rules = new List<Rule> { finalDecision, evaluateRisk, enrichData, validateInput }
};
workflow.Validate();
workflow.Compile(parameters);
// Execute: Validate → Enrich → Evaluate → Decide
var results = workflow.Execute(parameters).ToList();
Accessing Dependency Results
Use RuleContext to read the results of previously executed rules.
RuleContext API
public class RuleContext
{
// Store a result
void StoreResult(Guid ruleId, RuleResult result);
// Retrieve a result
RuleResult? GetResult(Guid ruleId);
// Check if a rule has been executed
bool HasResult(Guid ruleId);
// Get typed value from a result (returns default(T) if not found/failed)
T? GetValue<T>(Guid ruleId);
// Get typed value safely — returns true only if rule succeeded and value is of type T
bool TryGetValue<T>(Guid ruleId, out T? value);
// Clear all results
void Clear();
}
Reading Values in Expressions
var calculateTax = new Rule
{
Description = "Calculate tax",
Expression = "true",
Action = "customer.TaxAmount = customer.Amount * 0.08",
IsActive = true
};
var calculateTotal = new Rule
{
Description = "Calculate total",
DependsOnRuleId = calculateTax.Id,
Expression = "true",
Action = @"customer.Total = customer.Amount + context.GetValue<decimal>(calculateTax.Id)",
IsActive = true
};
Using TryGetValue for Safe Access
var calculateTotal = new Rule
{
Description = "Calculate total",
DependsOnRuleId = calculateTax.Id,
Expression = "true",
Action = @"
if (context.TryGetValue<decimal>(calculateTax.Id, out var taxAmount))
{
customer.Total = customer.Amount + taxAmount;
}
else
{
customer.Total = customer.Amount; // No tax applied
}",
IsActive = true
};
Conditional Logic Based on Dependencies
var checkAuthentication = new Rule
{
Description = "Check authentication",
Expression = "customer.IsAuthenticated",
IsActive = true
};
var checkAuthorization = new Rule
{
Description = "Check authorization",
DependsOnRuleId = checkAuthentication.Id,
Expression = @"context.GetResult(checkAuth.Id).Success && customer.HasAdminRole",
IsActive = true
};
Parallel Execution with Dependencies
When using ExecuteParallelAsync, independent rules run concurrently while dependency chains run sequentially.
var ruleA = new Rule { Description = "A", Expression = "true", IsActive = true };
var ruleB = new Rule { Description = "B", DependsOnRuleId = ruleA.Id, Expression = "true", IsActive = true };
var ruleC = new Rule { Description = "C", Expression = "true", IsActive = true }; // Independent
var ruleD = new Rule { Description = "D", DependsOnRuleId = ruleB.Id, Expression = "true", IsActive = true };
var workflow = new Workflow
{
Rules = new List<Rule> { ruleA, ruleB, ruleC, ruleD }
};
workflow.Validate();
workflow.Compile(parameters);
// Execution:
// Phase 1 (parallel): ruleA, ruleC
// Phase 2 (parallel): ruleB (after A completes), ruleC already done
// Phase 3 (parallel): ruleD (after B completes)
var results = await workflow.ExecuteParallelAsync(parameters);
Performance tip: Design your rules to maximize independent rules for better parallelization.
Validation
Validate() performs comprehensive dependency checking.
Missing Dependency Detection
var rule = new Rule
{
Description = "Invalid rule",
DependsOnRuleId = Guid.NewGuid(), // Non-existent rule
Expression = "true",
IsActive = true
};
var workflow = new Workflow { Rules = new List<Rule> { rule } };
// Throws: RuleValidationException
// "Rule 'Invalid rule' (Id: ...) depends on rule ... which does not exist or is inactive."
workflow.Validate();
Circular Dependency Detection
var ruleA = new Rule { Description = "A", Expression = "true", IsActive = true };
var ruleB = new Rule { Description = "B", DependsOnRuleId = ruleA.Id, Expression = "true", IsActive = true };
// Create cycle: A depends on B, B depends on A
ruleA.DependsOnRuleId = ruleB.Id;
var workflow = new Workflow { Rules = new List<Rule> { ruleA, ruleB } };
// Throws: CircularReferenceException
// "Dependency chain on rule 'A'"
workflow.Validate();
Common Patterns
Pattern 1: Gatekeeper Pattern
Use a dependency as a gatekeeper. The dependent rule always runs but checks the gatekeeper’s result.
var gatekeeper = new Rule
{
Description = "Feature enabled",
Expression = "FeatureFlags.IsEnabled("PremiumFeature")",
IsActive = true
};
var premiumCheck = new Rule
{
Description = "Premium validation",
DependsOnRuleId = gatekeeper.Id,
Expression = "context.GetResult(gatekeeper.Id).Success && customer.IsPremium",
IsActive = true
};
Pattern 2: Data Enrichment Pipeline
Each stage adds data to the parameter object for subsequent stages.
var fetchProfile = new Rule
{
Description = "Fetch profile",
Expression = "true",
Action = "customer.Profile = ProfileService.Get(customer.Id)",
IsActive = true
};
var validateProfile = new Rule
{
Description = "Validate profile",
DependsOnRuleId = fetchProfile.Id,
Expression = "customer.Profile != null && customer.Profile.IsComplete",
IsActive = true
};
var processOrder = new Rule
{
Description = "Process order",
DependsOnRuleId = validateProfile.Id,
Expression = "true",
Action = "OrderService.Process(customer)",
IsActive = true
};
Pattern 3: Branching Logic
One dependency, multiple dependents that make different decisions.
var eligibilityCheck = new Rule
{
Description = "Eligibility",
Expression = "customer.Age >= 18 && customer.Income > 30000",
IsActive = true
};
var standardOffer = new Rule
{
Description = "Standard offer",
DependsOnRuleId = eligibilityCheck.Id,
Expression = "context.GetResult(eligibility.Id).Success && customer.Income < 100000",
IsActive = true
};
var premiumOffer = new Rule
{
Description = "Premium offer",
DependsOnRuleId = eligibilityCheck.Id,
Expression = "context.GetResult(eligibility.Id).Success && customer.Income >= 100000",
IsActive = true
};
Pattern 4: Error Handling Chain
Propagate errors through the dependency chain.
var parseInput = new Rule
{
Description = "Parse input",
Expression = "int.TryParse(customer.AgeInput, out var age)",
Action = "customer.ParsedAge = age",
IsActive = true
};
var validateAge = new Rule
{
Description = "Validate age",
DependsOnRuleId = parseInput.Id,
Expression = @"context.GetResult(parse.Id).Success && customer.ParsedAge >= 18",
IsActive = true
};
var processAdult = new Rule
{
Description = "Process adult",
DependsOnRuleId = validateAge.Id,
Expression = @"context.GetResult(validateAge.Id).Success",
Action = "customer.IsAdult = true",
IsActive = true
};
Troubleshooting
“Rule depends on rule … which does not exist”
Cause: DependsOnRuleId points to a rule not in the workflow, or the dependency is inactive.
Fix: Ensure the dependency rule is added to the workflow and IsActive = true.
“Dependency cycle detected”
Cause: Circular dependency chain (A → B → C → A).
Fix: Redesign rules to break the cycle. Use parent-child relationships for tight coupling instead.
“Expected dependency result but context was empty”
Cause: Rule is trying to access context.GetResult() but the dependency hasn’t executed yet.
Fix: Ensure DependsOnRuleId is set correctly. The workflow engine executes dependencies first automatically.
Priority not respected
Cause: Dependency ordering overrides priority.
Fix: This is by design. Dependencies always execute first. Priority only affects ordering within the same dependency level.
See Also
- Parent-Child Rules — Bottom-up evaluation patterns (see section above)
- Rule Testing Framework — Testing chained rules
- API Reference: RuleContext