Value functions
A custom function answers yes or no. A value function computes something — rounding, clamping, a lookup — and a { "call": … } expression invokes it anywhere an expression is valid. The document names the function; the behaviour stays registered C#, bound when the rules load.
Calling a function
{ "call": "RoundTo", "operands": [ { "op": "multiply", "operands": [ { "field": "Order.Total" }, 0.0725 ] }, 2 ] }
The operands are expressions themselves — fields, literals, operators, params, even other calls — evaluated in document order and handed to the function as an array. operands may be omitted for a no-argument call. A call composes with everything else, on either side of a condition:
{
"name": "Value functions",
"rules": [
{
"id": "tax-rounded",
"condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0 },
"actions": [
{
"type": "setOutput",
"target": "Tax",
"value": {
"call": "RoundTo",
"operands": [
{ "op": "multiply", "operands": [ { "field": "Order.Total" }, 0.0725 ] },
2
]
}
}
]
},
{
"id": "big-order",
"condition": {
"expression": { "call": "RoundTo", "operands": [ { "field": "Order.Total" }, 0 ] },
"operator": "GreaterThanOrEqual",
"value": 200
},
"actions": [
{ "type": "setOutput", "target": "BigOrder", "value": true }
]
}
]
}
Registering one
As a delegate
using System.Text.Json;
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Extensions.Functions;
using RuleWright.Json.NewtonsoftJson;
using RuleWright.Json.SystemText;
RuleWrightEngine engine = new RuleWrightBuilder()
.UseJsonReader(new SystemTextJsonReader())
.RegisterValueFunction("RoundTo", args =>
args is [decimal value, long digits] ? decimal.Round(value, (int)digits) : null)
.Build();
LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("value-functions.json"));
foreach (Checkout checkout in new[] { Facts.Vip(), Facts.Newcomer() })
{
RuleEvaluationResult result = engine.Evaluate(rules, checkout);
string big = result.Outputs.TryGetValue("BigOrder", out object? b) ? $"{b}" : "-";
Console.WriteLine($"{checkout.Customer.Name,-6} Tax = {result.Outputs["Tax"],-6} BigOrder = {big}");
}
Output
Aroha Tax = 17.40 BigOrder = True
Ben Tax = 2.54 BigOrder = -
As a class, with metadata
A class can declare a description for rule editors and — unlike a condition function's metadata, which is purely descriptive — a required operand count that the engine enforces when rules load:
using RuleWright.Core;
/// <summary>Rounds a number to a digit count: { "call": "RoundTo", "operands": [ number, digits ] }.</summary>
public sealed class RoundToFunction : IRuleValueFunction, IRuleValueFunctionMetadata
{
public string Name => "RoundTo";
public string? Description => "Rounds a number to a digit count.";
// Declared, so a call with any other operand count fails when the rules load.
public int? RequiredOperandCount => 2;
// Total, like the built-in operators: an argument shape it doesn't understand gives null.
// Rule arithmetic is decimal, and a whole JSON number arrives as long.
public object? Invoke(object?[] arguments) =>
arguments is [decimal value, long digits] ? decimal.Round(value, (int)digits) : null;
}
using System.Text.Json;
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Extensions.Functions;
using RuleWright.Json.NewtonsoftJson;
using RuleWright.Json.SystemText;
RuleWrightEngine engine = new RuleWrightBuilder()
.UseJsonReader(new SystemTextJsonReader())
.RegisterValueFunction(new RoundToFunction())
.Build();
foreach (RuleValueFunctionDescriptor function in engine.ValueFunctionCatalog)
{
Console.WriteLine($"{function.Name,-8} operands: {function.RequiredOperandCount?.ToString() ?? "any"} {function.Description}");
}
A call with the wrong number of operands then fails at LoadRuleSet, naming the rule, the function and both counts:
using System.Text.Json;
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Extensions.Functions;
using RuleWright.Json.NewtonsoftJson;
using RuleWright.Json.SystemText;
try
{
engine.LoadRuleSet("""
{ "id": "x", "condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0 },
"actions": [ { "type": "setOutput", "target": "T", "value": { "call": "RoundTo", "operands": [ 1 ] } } ] }
""");
}
catch (RuleCompilationException ex)
{
Console.WriteLine(ex.Message);
}
Output of both
RoundTo operands: 2 Rounds a number to a digit count.
Rule 'x': value function 'RoundTo' requires exactly 2 operands, but the call supplies 1.
Unknown names fail at load
Like the custom operator, a call binds to its function instance when the rules load — one virtual call per evaluation, no name lookup. An unregistered name is a RuleCompilationException at LoadRuleSet, never a surprise mid-request:
using System.Text.Json;
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Extensions.Functions;
using RuleWright.Json.NewtonsoftJson;
using RuleWright.Json.SystemText;
try
{
engine.LoadRuleSet("""
{ "id": "x", "condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0 },
"actions": [ { "type": "setOutput", "target": "T", "value": { "call": "Truncate", "operands": [ 1 ] } } ] }
""");
}
catch (RuleCompilationException ex)
{
Console.WriteLine(ex.Message);
}
Output
Rule 'x': value function 'Truncate' is not registered. Register it with RuleWrightBuilder.RegisterValueFunction before loading the rule set.
Rules for value functions
- Be total, like the built-in operators. Return
nullfor an argument shape you don't understand rather than throwing; a null then flows through the usual computed-value rules. - Expect
decimalandlong. Rule arithmetic runs indecimal, and a whole JSON number arrives aslong. Pattern-match, asRoundTodoes. - Be thread-safe and stateless. One instance serves every concurrent evaluation.
- Names are case-sensitive, registering a name twice throws, and the
customandcallregistries are independent: the same name may exist in both, and the operator decides which answers. - Discoverable:
engine.RegisteredValueFunctionsandengine.ValueFunctionCataloglist what an engine has, for editors — see Discover the vocabulary.
Next
Custom actions: the same idea for how outputs are written.