Table of Contents

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, enabled flag 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.