Storing Rules with Rule XML
Code Effects is a decisioning platform: a rule engine, a web-based Rule Editor for the people who write the rules, and the Rule Repository, which takes a business rule from its first draft to production. You do not have to use all of it. The Rule Editor can also be used on its own, as a stand-alone web component inside your application, and then the rules are yours to look after. You need to create the storage for them, and, if your rule authors need them, the versioning that keeps a history of every change, the testing that checks a rule before it goes live, and the control over who may change what. The Rule Repository provides all of these out of the box. If you would rather not build them, see Rule Storage in Rule Repository.
This article is for developers who use the Rule Editor as a stand-alone web component and store the rules themselves. It explains what a stored rule is, and how your code saves, loads, and evaluates it.
What Rule XML Is
The Rule Editor delivers every rule to your server as an XML document called Rule XML. The evaluation classes take the same document to evaluate the rule, and the Rule Editor takes it back when a rule author wants to change the rule. The schema of the document is here. Rules saved by earlier versions of Code Effects are still read as they are, and the editor writes new and updated rules in the current schema.
This is a complete Rule XML document, exactly as the Rule Editor produces it:
<?xml version="1.0" encoding="utf-8"?>
<codeeffects xmlns="https://codeeffects.com/schemas/rule/7" xmlns:ui="https://codeeffects.com/schemas/ui/7">
<rule id="f480a6ce-deb0-4622-b7a3-b87fcca316ff"
webrule="7.0.4.73"
utc="2026-11-05T19:08:57.8341"
type="MyApp.Patient, MyApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"
eval="true">
<name>Senior patients</name>
<description>Patients who qualify for the senior program</description>
<definition>
<condition type="greater">
<property name="Age" />
<value type="System.Int32">64</value>
</condition>
</definition>
<format>
<lines />
</format>
</rule>
</codeeffects>
The <rule> node carries the rule's identity:
id is the rule's unique ID. The Rule Editor creates it, as a GUID, when a rule author starts a new rule, and keeps it for the life of the rule. Use it as the key of your storage.
webrule is the version of the Code Effects library that wrote the rule.
utc is the UTC date and time the rule was written.
type is the name of the source object the rule is written against. It is absent when the source object is not persisted.
eval is true for an evaluation type rule and false for an execution type rule.
Its children are the optional <name> and <description>, which the rule author types into the Toolbar, the <definition>, which holds the rule's logic, and the <format>, which holds the line breaks and indentation of the rule as the author arranged them. The definition names source object members by their declared names, not by their display names, so you can translate or relabel the members without affecting stored rules.
Treat the document as opaque. Store it exactly as you received it, and never edit it by hand: the Rule Editor and the engine validate the rule against the source object, and a document changed outside of them may no longer load. If you must check a document against the schema, pass it to the static Xml.IsRuleValid() method of the CodeEffects.Rule.Editor.Utils namespace.
Rules and Rulesets
You have two options for storing Rule XML.
You can store each rule as a separate document, the way you received it from the Rule Editor. This is the best choice when rule authors manage rules one at a time, because it lets you load, update, and delete a single rule without touching the others.
You can also store multiple rules in a single document. Such a document is known as a ruleset. Its schema is the same as for a single rule; the only difference is that the root <codeeffects> node holds any number of <rule> nodes:
<?xml version="1.0" encoding="utf-8"?>
<codeeffects xmlns="https://codeeffects.com/schemas/rule/7" xmlns:ui="https://codeeffects.com/schemas/ui/7">
<rule id="f480a6ce-deb0-4622-b7a3-b87fcca316ff" eval="true" ...>
<!-- The first rule -->
</rule>
<rule id="0d1484fb-1757-4855-871c-90539a3569eb" eval="true" ...>
<!-- The second rule -->
</rule>
</codeeffects>
All public constructors of all evaluator classes have a string parameter called rulesetXml. It takes an entire ruleset, a single Rule XML document, or even the outer XML of a single <rule> node, which is convenient when you loop through the rules of a ruleset. When a ruleset is evaluated, Evaluate() takes the ID of the rule to run; without one it runs the first rule.
An empty ruleset is one call away. The following example builds a ruleset from rules stored as separate documents:
using System.Collections.Generic;
using System.Xml;
using CodeEffects.Rule.Editor.Utils;
static string BuildRuleset(IEnumerable<string> storedRules)
{
XmlDocument ruleset = Xml.GetEmptyRuleDocument();
foreach(string ruleXml in storedRules)
{
var doc = new XmlDocument();
doc.LoadXml(ruleXml);
// Copy the <rule> node into the ruleset
XmlNode rule = ruleset.ImportNode(doc.DocumentElement.FirstChild, true);
ruleset.DocumentElement.AppendChild(rule);
}
return ruleset.OuterXml;
}
Saving a Rule
When a rule author creates or changes a rule and clicks the Save button on the Toolbar, the Rule Editor does not save anything. It calls the save callback you gave it, and your code sends the rule to your server. In effect, the editor tells your server: "The user wants to create or update a rule. Here is that rule, in the editor's own format, together with its auto-generated ID and its name." What happens next is entirely yours: your server must check that the rule is complete and valid, turn it into Rule XML, and store it in your rule storage, whether that is a database, a file system, or something as complicated as network storage or a remote web service. It must also decide whether the current user has the right to save that rule.
The following ASP.NET Core endpoint does all of it. It creates the editor on the server with the same source object the client uses, hands it the rule, and stores the Rule XML only if the rule is valid:
using CodeEffects.Rule.Editor;
using Microsoft.AspNetCore.Mvc;
api.MapPost("/api/rules", ([FromBody] string clientRule) =>
{
var editor = new Control("divEditor") { SourceType = typeof(Patient) };
editor.LoadRuleData(clientRule);
if(editor.IsEmpty)
return Results.Ok(new { isRuleEmpty = true });
if(!editor.IsValid)
return Results.Ok(new { isRuleValid = false, invalidData = editor.GetInvalidData() });
// The rule is valid. The editor created the ID; use it as your key.
RuleStorage.Save(
id: editor.Rule.ID,
name: editor.Rule.Name,
isEvaluationType: editor.Rule.IsEvalType == true,
ruleXml: editor.GetRuleXml());
return Results.Ok(new { isRuleEmpty = false, isRuleValid = true, id = editor.Rule.ID, name = editor.Rule.Name });
});
Always check IsEmpty and IsValid before you call GetRuleXml(): it throws for an invalid rule. That is what makes every stored rule structurally valid: no Rule XML is ever produced from a rule that does not fit its source object. The client sends the rule from its save callback, shows the invalid elements if there are any, and tells the editor that the rule was saved:
const editor = $rule.init({
id: "divEditor",
globalData: settings.globalData,
editorData: settings.editorData,
saveRuleCallback: async function ()
{
const response = await fetch("/api/rules", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(editor.extract())
});
const data = await response.json();
if(data.isRuleEmpty) return; // the editor has already warned the author
if(data.isRuleValid === false) { editor.loadInvalids(data.invalidData); return; }
// Add the rule to the Rules menu, or update its name there
editor.saved(data.id, data.name);
},
// loadRuleCallback and deleteRuleCallback: see below
});
The Toolbar article describes the three Toolbar callbacks, and the Client-Side API describes every function used here. If your project does not use the Toolbar, you do the same thing from your own Save button: call editor.extract() and send the result to the same endpoint.
A table for the rules
Any storage that holds text works. A relational table needs very little. Keep the rule's ID, name, and type next to the document so you can fill the Rules menu and filter rules without opening the XML:
CREATE TABLE BusinessRule
(
RuleId uniqueidentifier NOT NULL PRIMARY KEY, -- editor.Rule.ID
Name nvarchar(50) NOT NULL, -- the Toolbar allows up to 50 characters
IsEvaluation bit NOT NULL,
RuleXml nvarchar(max) NOT NULL,
ModifiedOn datetime2(3) NOT NULL
);
Saving a rule that already exists is an update of the row with the same ID. The Rule Editor has no notion of versions, drafts, approvals, or history; if you need them, they are yours to build, or you can use the Rule Repository, which has all of them.
Loading a Rule
To edit a stored rule, read its Rule XML from your storage, load it into a server-side editor with LoadRuleXml(), and send the result of GetRuleData() to the client:
api.MapGet("/api/rules/{id}", (string id) =>
{
var editor = new Control("divEditor") { SourceType = typeof(Patient) };
editor.LoadRuleXml(RuleStorage.GetRuleXml(id));
return Results.Ok(new { ruleData = editor.GetRuleData(), invalidData = editor.GetInvalidData() });
});
The client passes the data to the editor with the loadRule() function, from the load callback you gave $rule.init():
loadRuleCallback: async function (id)
{
const response = await fetch(`/api/rules/${encodeURIComponent(id)}`);
const data = await response.json();
editor.loadRule(data.ruleData);
if(data.invalidData) editor.loadInvalids(data.invalidData); // null when every element resolves
}
The editor loads a rule against the current source object, not against the one the rule was written with. If you have since renamed or removed a member, LoadRuleXml() still succeeds, but IsValid is false and GetInvalidData() describes the elements that could no longer be resolved. Passing that data to loadInvalids() marks those elements in the Rule Area, and the rule cannot be saved until the author fixes them.
If a rule calls reusable rules, the editor has to read those as well. Give the server-side editor a RuleResolver, a function that takes a rule ID and returns that rule's Rule XML:
var editor = new Control("divEditor")
{
SourceType = typeof(Patient),
RuleResolver = ruleId => RuleStorage.GetRuleXml(ruleId)
};
Deleting a Rule
The Delete button of the Toolbar sends the ID of the rule to your delete callback. Remove the rule from your storage, then call editor.deleted(id) on the client. Before you delete, check that no other rule calls it as a reusable rule; the editor does not know what your storage holds.
Evaluating a Rule
The string you stored is the string the evaluation classes take. Load the Rule XML and evaluate it against an instance of the source object:
using CodeEffects.Rule.Engine;
string ruleXml = RuleStorage.GetRuleXml("f480a6ce-deb0-4622-b7a3-b87fcca316ff");
var evaluator = new Evaluator<Patient>(ruleXml);
bool isSenior = evaluator.Evaluate(patient);
When the rule calls reusable rules, give the evaluator the same kind of resolver, through the evaluation parameters:
var parameters = new EvaluationParameters
{
RuleResolver = ruleId => RuleStorage.GetRuleXml(ruleId)
};
var evaluator = new Evaluator<Patient>(ruleXml, parameters);
An evaluator caches what it compiles, so create one for a rule and reuse it for as long as the rule does not change. See Rule Evaluation for everything else the evaluators can do.
Please refer to our demo project repos for complete examples of storing and managing Rule XML documents.