Host and reload rules
The engine and a loaded rule set are both immutable and thread-safe, and both are meant to live as long as your process. Build one engine at startup, load your documents once, and swap in a new rule set when a document changes.
Lifetimes
| Object | Lifetime | Why |
|---|---|---|
RuleWrightEngine |
Singleton | Holds the compiled-delegate cache. A new engine recompiles every rule. |
LoadedRuleSet |
Singleton, per document version | Parsed, validated and prepared once. |
EvaluationOptions |
Anything | Immutable, so a shared static instance is fine. |
RuleEvaluationResult |
Per evaluation | Immutable, safe to cache or return. |
Reloading without a restart
Rules change more often than code, which is much of the point of a rule engine. To change them without a restart, load the new document, then swap the reference. A small holder class does it, and it never swaps in a document that didn't load:
using RuleWright.Execution;
/// <summary>
/// The current rule set, swappable while requests are being evaluated. Readers take whatever
/// reference is current; a reload validates and loads the new document before swapping, so a bad
/// file never replaces good rules.
/// </summary>
public sealed class RuleSetHolder
{
private readonly RuleWrightEngine _engine;
private LoadedRuleSet _current;
public RuleSetHolder(RuleWrightEngine engine, string json)
{
_engine = engine;
_current = engine.LoadRuleSet(json);
}
public LoadedRuleSet Current => Volatile.Read(ref _current);
public void Reload(string json)
{
LoadedRuleSet next = _engine.LoadRuleSet(json); // throws on a bad document: nothing swapped
Volatile.Write(ref _current, next);
}
}
using System.Text.RegularExpressions;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;
using RuleWright.Serialization;
RuleWrightEngine engine = new RuleWrightBuilder()
.UseJsonReader(new SystemTextJsonReader())
.Build();
var holder = new RuleSetHolder(engine, File.ReadAllText("discount.json"));
Checkout checkout = Facts.Vip();
Console.WriteLine($"v1: Discount = {engine.Evaluate(holder.Current, checkout).Outputs["Discount"]}");
File.WriteAllText("discount.json", v2); // someone edits the file
holder.Reload(File.ReadAllText("discount.json"));
Console.WriteLine($"v2: Discount = {engine.Evaluate(holder.Current, checkout).Outputs["Discount"]}");
try
{
holder.Reload(broken); // a bad edit is refused...
}
catch (RuleValidationException ex)
{
Console.WriteLine($"Refused: {ex.Errors[0].Path}: {ex.Errors[0].Message}");
}
Console.WriteLine($"Still: Discount = {engine.Evaluate(holder.Current, checkout).Outputs["Discount"]}"); // ...and v2 keeps serving
Output
v1: Discount = 5
v2: Discount = 8
Refused: /condition/operator: Unknown operator 'GraterThan'. Expected one of: Equals, NotEquals, GreaterThan, GreaterThanOrEqual, LessThan, LessThanOrEqual, Contains, StartsWith, EndsWith, MatchesRegex, In, NotIn, IsNull, IsNotNull, custom, Any, All, None.
Still: Discount = 8
An evaluation that already holds the old LoadedRuleSet finishes with it, so a swap never tears a request in half. Trigger Reload however suits you: a FileSystemWatcher, an admin endpoint (the web API tutorial has one), or a poll of your database.
What the cache holds
Compiled delegates are cached on the engine, keyed by fact type and rule content hash:
- A reload recompiles only the rules whose logic changed. Unchanged rules, and rules whose only change is an id, priority, description,
enabledflag or layout, keep their delegates. - Two rules with the same logic share one delegate, even across documents.
- The cache is unbounded. A process that loads an endless stream of distinct rules, such as a multi-tenant service reloading each tenant's rules, grows with them. If that is your shape, build a new engine from time to time and let the old one go.
How RuleWright works explains the hash.
ASP.NET Core
Register the engine and the holder as singletons, and inject them into your endpoints. The web API tutorial builds a complete service this way: evaluate, validate, replace the live rules, and serve the vocabulary to an editor.
Several documents
Load each document into its own LoadedRuleSet and keep them in a dictionary keyed by name. One engine serves all of them, and identical rules across documents share compiled delegates.
Next
Discover the vocabulary: what a rule editor may offer.