Lightweight, developer-friendly rule engine for .NET
RuleFlow supports loading rule set definitions from JSON and mapping them to runtime rules via condition/action registries. Playground: PersistenceScenario.cs.
RuleDefinition and RuleSetDefinitionPersisted rules are data only (no expression parsing in JSON):
RuleDefinition — name, optional ConditionKey, optional structured Condition (ConditionNode), ActionKeys, optional reason, priority, stop-processing flag, metadataRuleSetDefinition — name, list of rules, nested groups (each group is another RuleSetDefinition)These types live in RuleFlow.Abstractions and serialize with System.Text.Json.
Implement IRuleRegistry<T> (default: RuleRegistry<T>) to map string keys to C# delegates:
var registry = new RuleRegistry<Order>();
registry.RegisterCondition("HighAmount", (order, _) => order.Amount > 500);
registry.RegisterAction("RequireApproval", (order, _) => order.RequiresApproval = true);
Func<T, IRuleContext, bool>Action<T, IRuleContext>Keep business logic in code when using registry keys; JSON only references keys. Alternatively, store a structured condition in JSON (see Dynamic conditions).
Use RuleDefinitionMapper<T> to build Rule<T> / RuleSet<T> from definitions:
var definition = JsonSerializer.Deserialize<RuleSetDefinition>(json);
var mapper = new RuleDefinitionMapper<Order>(registry);
var executableRuleSet = mapper.MapRuleSet(definition);
var engine = new RuleEngine();
var result = engine.Evaluate(order, executableRuleSet);
When RuleDefinition.Condition is set, the mapper does not use ConditionKey. It validates the tree and wires the rule’s When to IConditionEvaluator<T> (typically ConditionEvaluator<T> with ReflectionFieldResolver<T>, DefaultOperatorRegistry, and DefaultValueConverter).
ConditionLeaf — one comparison: field, operator, and either a literal value or compareToField (field-to-field).ConditionGroup — operator: AND or OR, plus nested conditions (leaves or groups).Field names support dotted paths for nested properties, e.g. Customer.Name, Customer.Address.City. See Dynamic conditions.
Registry-only mapping (unchanged):
var mapper = new RuleDefinitionMapper<Order>(registry);
With structured conditions (supply an evaluator):
var fieldResolver = new ReflectionFieldResolver<Order>();
var evaluator = new ConditionEvaluator<Order>(
fieldResolver,
new DefaultOperatorRegistry(),
new DefaultValueConverter());
var mapper = new RuleDefinitionMapper<Order>(registry, evaluator);
Example leaf comparing two fields (e.g. amount vs budget):
{
"field": "Amount",
"operator": "greater_than",
"compareToField": "MaxOrderValue"
}
The sample scenario embeds JSON similar to:
{
"name": "ApprovalRules",
"rules": [
{
"name": "High amount",
"conditionKey": "HighAmount",
"actionKeys": [ "RequireApproval" ],
"reason": "Amount exceeds threshold",
"priority": 10,
"stopProcessing": false,
"metadata": {}
}
],
"groups": []
}
When the JSON schema or mapper behavior changes, update:
PersistenceScenario.cs (registry keys) and/or DynamicConditionsScenario.cs (structured Condition)