Introduction to RuleWright
RuleWright is a business rule engine for .NET. A rule is a JSON document: a condition over your data and the outputs to write when it holds. RuleWright checks each document once, compiles it to a delegate once per data type, and then evaluates it as a plain method call, with no reflection or string parsing on the hot path.
A rule, and what it does
{
"id": "adult-big-spender",
"description": "Adults spending over 100 get 10% off.",
"condition": {
"type": "group",
"operator": "AND",
"rules": [
{ "field": "Customer.Age", "operator": "GreaterThanOrEqual", "value": 18 },
{ "field": "Order.Total", "operator": "GreaterThan", "value": 100 }
]
},
"actions": [
{ "type": "setOutput", "target": "DiscountPercent", "value": 10 },
{ "type": "setOutput", "target": "Reason", "value": "adult spending over 100" }
]
}
Given a checkout whose customer is 34 and whose order totals 150, this rule fires and writes DiscountPercent = 10 and a Reason into the result. For a 17-year-old it doesn't fire and writes nothing. Your first rule runs exactly this.
What you can do with it
| You want to… | Start here |
|---|---|
| Run your first rule against a C# object | Your first rule |
| Evaluate JSON payloads or dictionaries whose shape you only know at run time | Typed and dictionary facts |
| Price a basket: discounts, shipping, loyalty points | Tutorial: a checkout pricing policy |
| Score and triage orders with decision tables | Tutorial: score orders for fraud review |
| Serve rules from an API and change them without redeploying | Tutorial: host rules behind a web API |
| Test rule documents in CI | Tutorial: test your rule documents |
| Build a visual rule editor | Discover the vocabulary, Validate documents |
How it is organised
Everything goes through one engine, built once by RuleWrightBuilder:
rules.json ──▶ engine.LoadRuleSet(json) parse, validate and prepare, once per document
│
LoadedRuleSet
│
your object ──▶ engine.Evaluate(rules, fact)
│ compiled on the first call for each fact type, then cached
▼
RuleEvaluationResult
├─ FiredRules which rules fired, and which branch
├─ Outputs the merged outputs of every fired rule
└─ Trace why, when you ask for it
A typed fact (any class) runs as compiled expression trees. A dictionary fact (IDictionary<string, object?>, or JSON converted to one) runs through an interpreter with the same semantics. The result always reports which path ran.
Promises worth knowing up front
- Rules are data, never code. The vocabulary is closed: operators, field paths and literals. Nothing in a document is ever compiled or executed as a string. The only extension point is a function your own application registers.
- Evaluation never throws on data. A null field, a missing key, a non-numeric operand or a division by zero each have defined results. Problems with the rules surface when they load, not on a random request.
- Same answer everywhere. String comparisons are ordinal and dates compare as instants, so a rule gives the same answer on every machine, culture and time zone.
- Safe to share. The engine, a loaded rule set and every result are immutable. Build one engine and use it from every thread.
Next steps
Support and contact
Questions, feedback, support or licensing: email salwan@albahadly.com or visit www.albahadly.com. The source is on GitHub.