Table of Contents

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.

RuleWright net48netstandard2.0net8.0net10.0 MIT licence

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.