diff --git a/README.md b/README.md
index 93a38bd..4d1c015 100644
--- a/README.md
+++ b/README.md
@@ -106,7 +106,8 @@ var workflow = new Workflow
}
};
-workflow.Compile(compiler, new[] { compileParam });
+// Workflow owns its own compiler — pass parameters only (no compiler argument)
+workflow.Compile(new[] { compileParam });
// Execute with real data
var results = workflow.Execute(new[] { param });
@@ -132,9 +133,9 @@ using RoslynRules.Json;
var workflow = JsonRuleLoader.LoadWorkflowFromFile("rules.json");
-// Compile before executing
+// Compile before executing (Workflow.Compile takes parameters only)
var compileParam = new RuleParameter("customer", typeof(Customer));
-workflow.Compile(compiler, new[] { compileParam });
+workflow.Compile(new[] { compileParam });
// Execute
var customer = new Customer { Name = "Alice", Age = 25 };
@@ -202,23 +203,26 @@ Most rules engines parse expressions into trees and walk them every execution. R
The `ExpressionCompiler` caches delegates in a `ConcurrentDictionary`. Compile a rule, and you get back the same delegate on subsequent calls. No recompilation, no assembly bloat.
**Rules can depend on each other.**
-Use `DependsOnRuleId` to build pipelines where one rule reads another's output. Dependencies are validated at compile time (no missing references) and resolved with topological sorting.
+Use `DependsOnRuleId` to build pipelines where a dependent rule runs after its dependency. The dependency's `Action` mutates the shared parameter, and the dependent rule reads that value. Dependencies are validated at compile time (no missing references) and resolved with topological sorting.
```csharp
var validate = new Rule
{
Description = "Validate",
- Expression = "customer.IsActive"
+ Expression = "customer.IsActive",
+ Action = "customer.Validated = true" // write to the shared parameter
};
var process = new Rule
{
Description = "Process",
- DependsOnRuleId = validate.Id,
- Expression = "context.GetResult(validate.Id).Success"
+ DependsOnRuleId = validate.Id, // runs after 'validate'
+ Expression = "customer.Validated" // reads what 'validate' wrote
};
```
+> Expressions and actions can only reference the parameters you pass to `Compile`/`Execute`. The `RuleContext` (`context.GetResult(...)`) is available to your host code, not inside expression strings — chain data by mutating the shared parameter object.
+
**Async without ceremony.**
Expressions with `await` are auto-detected and compiled to async delegates. No manual `Task` wrapping.
diff --git a/docs/aot-compatibility.md b/docs/aot-compatibility.md
index 13d137b..e16646b 100644
--- a/docs/aot-compatibility.md
+++ b/docs/aot-compatibility.md
@@ -17,7 +17,6 @@ RoslynRules supports AOT deployment via **pre-compiled snapshots**. Rules cannot
|-----------|----------|----------|
| Load rules from JSON/EF | ✅ Yes | ✅ Yes |
| `Workflow.Validate()` | ✅ Yes | ✅ Yes |
-| `Workflow.GetExecutionOrder()` | ✅ Yes | ✅ Yes |
| `Rule.Compile()` (from string) | ✅ Yes | ❌ **Not supported** |
| `Rule.Execute()` without snapshot | ✅ Yes | ❌ Throws error |
| `Rule.Execute()` with snapshot | ✅ Yes | ✅ Yes |
@@ -129,14 +128,20 @@ Your app can also detect mode:
```csharp
bool isAot = !RuntimeFeature.IsDynamicCodeSupported;
+var serializer = new JsonSnapshotSerializer();
+
if (isAot)
{
- workflow.LoadSnapshots("snapshots/");
+ // Load a pre-compiled snapshot and restore the workflow.
+ var snapshot = SnapshotManager.LoadSnapshot(serializer, "snapshots/workflow.snap.json");
+ var workflow = SnapshotManager.RestoreWorkflow(snapshot);
}
else
{
- workflow.Compile(parameters);
- workflow.SaveSnapshots("snapshots/");
+ // Compile and save a snapshot for later AOT execution.
+ var compiled = CompiledWorkflow.Compile(workflow, parameters);
+ var snapshot = compiled.ToSnapshot();
+ SnapshotManager.SaveSnapshot(snapshot, serializer, "snapshots/workflow.snap.json");
}
```
@@ -148,7 +153,6 @@ These work in AOT without snapshots:
- `Workflow` / `Rule` model creation
- `Workflow.Validate()` — syntax validation
-- `Workflow.GetExecutionOrder()` — dependency resolution
- `RuleResult` creation and inspection
- `RuleContext` result storage
- `GraphAlgorithms.TopologicalSort()`
@@ -170,7 +174,7 @@ The `.github/workflows/aot.yml` workflow validates AOT-safe APIs compile without
dotnet test RoslynRules.Tests --filter "FullyQualifiedName~AotCompatibilityTests"
```
-This runs 8 tests covering model creation, validation, `RuleResult`, `RuleContext`, and execution order — all without runtime compilation.
+These tests cover model creation, validation, `RuleResult`, `RuleContext`, and execution order — all without runtime compilation.
---
diff --git a/docs/api-reference.md b/docs/api-reference.md
index 34388fc..0aa0a6f 100644
--- a/docs/api-reference.md
+++ b/docs/api-reference.md
@@ -24,7 +24,6 @@ Complete reference for all RoslynRules public APIs, organized by component.
| RuleParameter | Parameter definition (name, type, value) |
| RuleDiagnostics | Diagnostics, logging, and auditing model |
| RuleLifecycleEvents | OnRuleExecuting/OnRuleExecuted event args |
- | CompiledDelegate | Fast invocation wrapper for compiled delegates |
diff --git a/docs/api/assemblyreferenceprovider.md b/docs/api/assemblyreferenceprovider.md
index aba8909..fa29e8f 100644
--- a/docs/api/assemblyreferenceprovider.md
+++ b/docs/api/assemblyreferenceprovider.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 17
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# AssemblyReferenceProvider
@@ -19,23 +19,43 @@ public class AssemblyReferenceProvider
## Default Behavior
-The default provider includes a safe whitelist of common assemblies and excludes dangerous ones.
+The default provider includes a safe whitelist of common assemblies (`DefaultWhitelist`) and
+excludes a set of known dangerous assemblies (`KnownDangerousAssemblies`). Matching is a
+case-insensitive substring (`Contains`) match against each loaded assembly's name.
-**Included by default:**
-- `System`
+**Included by default (`DefaultWhitelist`):**
+- `System.Runtime`
+- `System.Private.CoreLib`
+- `mscorlib`
+- `netstandard`
- `System.Core`
- `System.Linq`
+- `System.Linq.Expressions`
- `System.Collections`
-- `System.Text`
+- `System.Text.Json`
- `System.Text.RegularExpressions`
+- `System.ComponentModel.Annotations`
+- `RoslynRules`
+- `Microsoft.Extensions.Logging.Abstractions`
+- `Microsoft.CodeAnalysis`
+- `Microsoft.CodeAnalysis.CSharp`
- `Microsoft.CSharp`
+- `Microsoft.CodeAnalysis.CSharp.Scripting`
-**Excluded (dangerous):**
+**Excluded (`KnownDangerousAssemblies`, always blocked):**
- `System.IO` — file system access
-- `System.Net` — network access
-- `System.Reflection` — reflection abuse
+- `System.IO.FileSystem` — file system access
- `System.Diagnostics.Process` — process execution
-- `System.Security` — security manipulation
+- `System.Net.Http` — network access
+- `System.Net.Sockets` — network access
+- `System.Net.Security` — network access
+- `System.Security.Cryptography` — cryptography
+- `System.Reflection.Emit` — runtime code generation
+- `System.Runtime.Loader` — assembly loading
+- `System.Data.SqlClient` — database access
+- `System.Data.OleDb` — database access
+- `System.Data.Odbc` — database access
+- `Microsoft.Win32.Registry` — registry access
---
@@ -43,7 +63,7 @@ The default provider includes a safe whitelist of common assemblies and excludes
```csharp
var provider = new AssemblyReferenceProvider();
-provider.AddAssembly(typeof(MyCustomType).Assembly);
+provider.AllowAssembly("MyCompany.Models");
var del = compiler.Compile>(
"customer.Name.Length > 0",
@@ -56,11 +76,11 @@ var del = compiler.Compile>(
## Security Note
-Even with a whitelist, never compile expressions from untrusted sources without validation. See [SECURITY.md](../SECURITY.md) for full details.
+Even with a whitelist, never compile expressions from untrusted sources without validation. See [Security](../security.md) for full details.
---
## Related
- [ExpressionCompiler](expressioncompiler.md) — Uses provider
-- [SECURITY.md](../SECURITY.md) — Security guide
+- [Security](../security.md) — Security guide
diff --git a/docs/api/compiled-delegate.md b/docs/api/compiled-delegate.md
index 21629c2..1aa8c73 100644
--- a/docs/api/compiled-delegate.md
+++ b/docs/api/compiled-delegate.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 9
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# CompiledDelegate
@@ -207,23 +207,22 @@ var result = compiled.Invoke(new[]
### Integration with Rule execution
+> **Note:** `CompiledDelegate`, its wrappers, and `CompiledDelegateFactory` are `internal`.
+> They are not part of the public API; the snippet below is conceptual and illustrates how the
+> engine wraps a delegate internally. Application code should call `Rule.Compile(...)` /
+> `Rule.Execute(...)` instead of constructing these types directly.
+
+`ExpressionCompiler.Compile` is generic and takes the parameter **names** as a
+`string[]` (not a `RuleParameter[]`). For a single-parameter `Customer` expression:
+
```csharp
-public class RuleExecutor
-{
- private readonly CompiledDelegate _compiledExpression;
-
- public RuleExecutor(Rule rule, ExpressionCompiler compiler)
- {
- var rawDelegate = compiler.Compile(rule.Expression, rule.Parameters);
- _compiledExpression = CompiledDelegateFactory.Wrap(rawDelegate);
- }
-
- public bool Evaluate(object parameter)
- {
- var result = _compiledExpression.Invoke(parameter);
- return (bool)result!;
- }
-}
+// Conceptual — illustrates internal wrapping.
+var rawDelegate = compiler.Compile>(
+ "customer.IsActive",
+ new[] { "customer" });
+
+var compiled = CompiledDelegateFactory.Wrap(rawDelegate); // internal
+var result = compiled.Invoke(customer); // true / false (as object?)
```
---
@@ -247,4 +246,4 @@ For best performance, prefer **single-parameter** expressions in rules. Multi-pa
- [ExpressionCompiler](expressioncompiler.md) — Compiles C# expressions to delegates
- [Delegate Types](delegate-types.md) — Supported expression signatures
- [Rule](rule.md) — Uses `CompiledDelegate` internally for expression evaluation
-- [AOT Compatibility](aot-compatibility.md) — Notes on trimming and AOT limitations
+- [AOT Compatibility](../aot-compatibility.md) — Notes on trimming and AOT limitations
diff --git a/docs/api/delegate-types.md b/docs/api/delegate-types.md
index d2d843a..592ba61 100644
--- a/docs/api/delegate-types.md
+++ b/docs/api/delegate-types.md
@@ -5,16 +5,20 @@ parent: API Reference
nav_order: 7
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Delegate Types
-RoslynRules supports exactly **one input parameter**. Return multiple values by wrapping them in a struct or class.
+RoslynRules supports **up to 16 input parameters** per expression or action. Single-parameter
+signatures take a fast, strongly-typed path; multi-parameter signatures (2–16 parameters) are
+supported as well. To return multiple values from a single rule, wrap them in a struct or class.
---
## Supported Signatures
+Single-parameter signatures (fastest path):
+
| Type | Delegate | Example |
|------|----------|---------|
| **Expression** | `Func` | `Func` |
@@ -23,6 +27,15 @@ RoslynRules supports exactly **one input parameter**. Return multiple values by
| **Async Expression** | `Func>` | `Func>` |
| **Async Action** | `Func` | `Func` |
+Multi-parameter signatures (up to 16 parameters) follow the same shapes:
+
+| Type | Delegate | Example |
+|------|----------|---------|
+| **Expression** | `Func` | `Func` |
+| **Action** | `Action` | `Action` |
+| **Async Expression** | `Func>` | `Func>` |
+| **Async Action** | `Func` | `Func` |
+
---
## Auto-Detection
diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md
index f372a15..6770507 100644
--- a/docs/api/exceptions.md
+++ b/docs/api/exceptions.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 5
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Exceptions
@@ -22,14 +22,14 @@ public abstract class RulesException : Exception
| Exception | When Thrown |
|-----------|-------------|
| `RuleValidationException` | Rule has no Expression, Action, or active children |
-| `CircularReferenceException` | Circular reference in child rules or `DependsOnRuleId` chain |
-| `SyntaxErrorException` | Invalid C# syntax in expression or action |
+| `CircularReferenceException` | Circular reference in the child rule tree (extends `RuleValidationException`) |
+| `SyntaxErrorException` | Invalid C# syntax in expression or action (extends `RuleValidationException`) |
| `RuleCompilationException` | Roslyn compilation failure |
-| `NotCompiledException` | `Execute` called before `Compile` |
-| `RuleExecutionException` | Runtime error in compiled code |
+| `NotCompiledException` | `Execute` called before `Compile` (extends `RuleCompilationException`) |
| `RuleTimeoutException` | Rule exceeded configured `Timeout` |
| `WorkflowException` | Workflow has no active rules |
-| `DuplicateRuleIdException` | Duplicate rule IDs in same workflow |
+| `DuplicateRuleIdException` | Duplicate rule IDs in same workflow (extends `WorkflowException`) |
+| `AotCompatibilityException` | A JIT-only API was called in an AOT/trimming environment |
---
diff --git a/docs/api/expressioncompiler.md b/docs/api/expressioncompiler.md
index 5283c12..4cf21ca 100644
--- a/docs/api/expressioncompiler.md
+++ b/docs/api/expressioncompiler.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 6
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# ExpressionCompiler
diff --git a/docs/api/graph-algorithms.md b/docs/api/graph-algorithms.md
index 910b112..276e338 100644
--- a/docs/api/graph-algorithms.md
+++ b/docs/api/graph-algorithms.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 7
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# GraphAlgorithms
diff --git a/docs/api/iruleengine.md b/docs/api/iruleengine.md
index e03e5dd..7793912 100644
--- a/docs/api/iruleengine.md
+++ b/docs/api/iruleengine.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 12
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# IRuleEngine
@@ -46,7 +46,7 @@ services.AddSingleton();
```csharp
var mock = new Mock();
mock.Setup(x => x.Execute(It.IsAny()))
- .Returns(new[] { new RuleResult { Success = true } });
+ .Returns(new[] { new RuleResult(true) });
var service = new MyService(mock.Object);
```
diff --git a/docs/api/json-serialization.md b/docs/api/json-serialization.md
index 3695238..6030d52 100644
--- a/docs/api/json-serialization.md
+++ b/docs/api/json-serialization.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 8
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# JSON Serialization
@@ -67,15 +67,6 @@ JsonRuleLoader.SaveWorkflowToFile(workflow, "rules.json");
Custom `JsonSerializerOptions` with camelCase naming and indented output.
```csharp
-
----
-
-## Related
-
-- [Rule](rule.md) — Serialized model
-- [Workflow](workflow.md) — Serialized container
-- [Rule Templates](rule-templates.md) — Serialize templates before instantiation
-
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
@@ -89,9 +80,10 @@ var json = JsonRuleLoader.Serialize(workflow, options);
---
-## See Also
+## Related
-- [Getting Started](getting-started.md)
-- [API Reference: Workflow](workflow.md)
-- [API Reference: Rule](rule.md)
+- [Rule](rule.md) — Serialized model
+- [Workflow](workflow.md) — Serialized container
+- [Rule Templates](rule-templates.md) — Serialize templates before instantiation
+- [Getting Started](../getting-started.md)
- [NuGet Package](https://www.nuget.org/packages/RoslynRules.Json)
diff --git a/docs/api/lifecycle-events.md b/docs/api/lifecycle-events.md
index ce1696b..993b40e 100644
--- a/docs/api/lifecycle-events.md
+++ b/docs/api/lifecycle-events.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 15
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Lifecycle Events
diff --git a/docs/api/result-caching.md b/docs/api/result-caching.md
index a17cd2a..5a99b2c 100644
--- a/docs/api/result-caching.md
+++ b/docs/api/result-caching.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 16
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Result Caching (Memoization)
diff --git a/docs/api/rule-diagnostics.md b/docs/api/rule-diagnostics.md
index ea8859b..3c87469 100644
--- a/docs/api/rule-diagnostics.md
+++ b/docs/api/rule-diagnostics.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 6
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleDiagnostics
diff --git a/docs/api/rule-lifecycle-events.md b/docs/api/rule-lifecycle-events.md
index cabf48b..34f1893 100644
--- a/docs/api/rule-lifecycle-events.md
+++ b/docs/api/rule-lifecycle-events.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 8
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleLifecycleEvents
diff --git a/docs/api/rule-metrics.md b/docs/api/rule-metrics.md
index 8d3821d..d49c7e4 100644
--- a/docs/api/rule-metrics.md
+++ b/docs/api/rule-metrics.md
@@ -6,7 +6,7 @@ parent: API Reference
nav_order: 17
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Rule Metrics
diff --git a/docs/api/rule-predicates.md b/docs/api/rule-predicates.md
index 657e4d6..30ebdf3 100644
--- a/docs/api/rule-predicates.md
+++ b/docs/api/rule-predicates.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 10
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Rule Predicates
@@ -19,34 +19,61 @@ using RoslynRules.Predicates;
## Available Predicates
+All methods are `static` and return a `Rule`. Every method accepts an optional trailing
+`string? description = null` argument (omitted below for brevity). The `path` argument is a
+parameter name and may be a dotted member-access path (e.g. `"order.CustomerId"`).
+
+### Null / Empty Checks
+
| Predicate | Description |
|-----------|-------------|
| `IsNotNull(path)` | Value is not null |
-| `IsNull(path)` | Value is null |
-| `Equals(path, value)` | Equality comparison |
-| `NotEquals(path, value)` | Inequality comparison |
-| `GreaterThan(path, value)` | `>` comparison |
-| `GreaterThanOrEqual(path, value)` | `>=` comparison |
-| `LessThan(path, value)` | `<` comparison |
-| `LessThanOrEqual(path, value)` | `<=` comparison |
-| `Between(path, min, max)` | Inclusive range check |
+| `IsNotNullOrEmpty(path)` | String is not null or empty |
+| `IsNotNullOrWhiteSpace(path)` | String is not null or whitespace |
+| `IsNotEmpty(path)` | Collection has any elements (`.Any()`) |
+| `IsEmpty(path)` | Collection has no elements (`!.Any()`) |
+
+### Comparison
+
+| Predicate | Description |
+|-----------|-------------|
+| `GreaterThan(path, value)` | `>` comparison (`T : struct`) |
+| `GreaterThanOrEqual(path, value)` | `>=` comparison (`T : struct`) |
+| `LessThan(path, value)` | `<` comparison (`T : struct`) |
+| `LessThanOrEqual(path, value)` | `<=` comparison (`T : struct`) |
+| `Equals(path, value)` | `==` comparison |
+| `NotEquals(path, value)` | `!=` comparison |
+| `InRange(path, min, max)` | Inclusive range check (`T : struct`) |
+| `NotInRange(path, min, max)` | Outside exclusive range (`T : struct`) |
+
+### String
+
+| Predicate | Description |
+|-----------|-------------|
| `MatchesRegex(path, pattern)` | Regex match |
-| `Contains(path, value)` | String/collection contains |
+| `Contains(path, value)` | String contains substring |
| `StartsWith(path, value)` | String prefix |
| `EndsWith(path, value)` | String suffix |
-| `IsEmpty(path)` | String/collection empty |
-| `IsNotEmpty(path)` | String/collection not empty |
-| `HasLength(path, min, max)` | Length in range |
-| `IsIn(path, values)` | Value in set |
-| `IsNotIn(path, values)` | Value not in set |
-| `IsTrue(path)` | Boolean true |
-| `IsFalse(path)` | Boolean false |
-| `IsDateBefore(path, date)` | Date comparison |
-| `IsDateAfter(path, date)` | Date comparison |
-| `IsDateBetween(path, start, end)` | Date range |
-| `IsGuid(path)` | Valid GUID format |
-| `IsEmail(path)` | Valid email format |
-| `IsUrl(path)` | Valid URL format |
+| `HasLength(path, length)` | String has exact length |
+| `HasMinLength(path, minLength)` | String length `>=` minLength |
+| `HasMaxLength(path, maxLength)` | String length `<=` maxLength |
+
+### Collection
+
+| Predicate | Description |
+|-----------|-------------|
+| `CountEquals(path, count)` | Collection count `==` count |
+| `CountGreaterThan(path, count)` | Collection count `>` count |
+| `CountLessThan(path, count)` | Collection count `<` count |
+| `Contains(path, value)` | Collection contains element |
+
+### Boolean / Type
+
+| Predicate | Description |
+|-----------|-------------|
+| `IsTrue(path)` | Boolean is true |
+| `IsFalse(path)` | Boolean is false |
+| `IsOfType(path)` | Value is of type `T` |
---
@@ -60,7 +87,8 @@ var workflow = new Workflow
RulePredicates.IsNotNull("customer"),
RulePredicates.GreaterThan("customer.Age", 18),
RulePredicates.MatchesRegex("customer.Email", @"^[^@]+@[^@]+\.[^@]+$"),
- RulePredicates.IsIn("customer.Status", new[] { "Active", "Pending" })
+ RulePredicates.InRange("customer.Score", 0, 100),
+ RulePredicates.HasMinLength("customer.Name", 2)
}
};
```
diff --git a/docs/api/rule-priority.md b/docs/api/rule-priority.md
index 673cac6..90226b4 100644
--- a/docs/api/rule-priority.md
+++ b/docs/api/rule-priority.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 14
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Rule Priority
diff --git a/docs/api/rule-templates.md b/docs/api/rule-templates.md
index d3fbe74..02038a3 100644
--- a/docs/api/rule-templates.md
+++ b/docs/api/rule-templates.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 9
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Rule Templates
@@ -34,9 +34,11 @@ public class RuleTemplate
### Methods
-#### `Instantiate(Dictionary, ExpressionCompiler, RuleParameter[], string[]?)`
+#### `Instantiate(Dictionary, ExpressionCompiler, RuleParameter[], string[], AssemblyReferenceProvider?)`
-Creates a compiled `Rule` from the template with placeholder values substituted.
+Creates a compiled `Rule` from the template with placeholder values substituted. The trailing
+`AssemblyReferenceProvider? referenceProvider = null` argument is optional and controls
+compilation sandboxing.
```csharp
var template = new RuleTemplate
@@ -50,12 +52,18 @@ var values = new Dictionary { ["minAge"] = 18 };
var rule = template.Instantiate(values, compiler, parameters, Array.Empty());
```
-#### `ExtractPlaceholders(string)`
+#### `ExtractPlaceholders()`
-Extracts placeholder names from an expression string.
+Instance method that extracts placeholder names from this template's `Expression` (no
+parameters). Returns `IReadOnlyList`.
```csharp
-var names = RuleTemplate.ExtractPlaceholders("customer.Age >= {minAge} && customer.Score >= {minScore}");
+var template = new RuleTemplate
+{
+ Expression = "customer.Age >= {minAge} && customer.Score >= {minScore}"
+};
+
+IReadOnlyList names = template.ExtractPlaceholders();
// ["minAge", "minScore"]
```
diff --git a/docs/api/rule-visualization.md b/docs/api/rule-visualization.md
index cf5a8bb..ff53273 100644
--- a/docs/api/rule-visualization.md
+++ b/docs/api/rule-visualization.md
@@ -47,12 +47,14 @@ var workflow = new Workflow
{
Description = "Check inventory",
Expression = "inventory.HasStock",
+ Id = Guid.Parse("22222222-2222-2222-2222-222222222222"),
DependsOnRuleId = Guid.Parse("11111111-1111-1111-1111-111111111111")
},
new Rule
{
Description = "Apply discount",
Expression = "order.Total > 100",
+ Id = Guid.Parse("33333333-3333-3333-3333-333333333333"),
DependsOnRuleId = Guid.Parse("22222222-2222-2222-2222-222222222222")
}
}
@@ -61,7 +63,8 @@ var workflow = new Workflow
Console.WriteLine(RuleGraphVisualizer.ToMermaid(workflow));
```
-**Output:**
+**Output (illustrative — node ids correspond to the rule ids above; the `:::inactive`
+styling is shown only to demonstrate how inactive rules render):**
```mermaid
graph TD
R11111111111111111111111111111111[Validate customer]
diff --git a/docs/api/rule.md b/docs/api/rule.md
index 8cdde49..a70e37f 100644
--- a/docs/api/rule.md
+++ b/docs/api/rule.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 1
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Rule
diff --git a/docs/api/rulebatch.md b/docs/api/rulebatch.md
index ac1f33c..ecced4c 100644
--- a/docs/api/rulebatch.md
+++ b/docs/api/rulebatch.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 13
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleBatch
diff --git a/docs/api/rulecontext.md b/docs/api/rulecontext.md
index 865a382..d3b09b5 100644
--- a/docs/api/rulecontext.md
+++ b/docs/api/rulecontext.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 11
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleContext
@@ -21,13 +21,14 @@ public class RuleContext
### `GetResult(Guid)`
-Gets the `RuleResult` for a specific rule ID.
+Gets the `RuleResult` for a specific rule ID. Returns `RuleResult?` (`null` if the rule has
+not executed yet).
```csharp
-var dependencyResult = context.GetResult(validateCustomer.Id);
-if (dependencyResult.Success)
+RuleResult? dependencyResult = context.GetResult(validateCustomer.Id);
+if (dependencyResult is { Success: true })
{
- // dependency passed
+ // dependency executed and passed
}
```
@@ -68,20 +69,39 @@ context.StoreResult(rule.Id, result);
## Usage with DependsOnRuleId
+`RuleContext` is a **host-side** API. There is no `context` variable in scope inside an
+`Expression` or `Action` string — those strings may only reference declared `RuleParameter`s.
+The host code reads dependency results from the context after (or between) rule execution.
+
```csharp
var taxRule = new Rule
{
Description = "Calculate tax",
- Action = "customer.TaxAmount = customer.Amount * 0.08"
+ // Expressions/actions reference declared parameters only — never "context".
+ Action = "customer.TaxAmount = customer.Amount * 0.08m"
};
var totalRule = new Rule
{
Description = "Calculate total",
DependsOnRuleId = taxRule.Id,
- Expression = "context.TryGetValue(taxRule.Id, out var tax)",
- Action = "customer.Total = customer.Amount + tax"
+ Action = "customer.Total = customer.Amount + customer.TaxAmount"
};
+
+// Host code inspects dependency results via RuleContext.
+var context = new RuleContext();
+// ... engine stores each rule's result into the context as it runs ...
+
+if (context.TryGetValue(taxRule.Id, out var tax))
+{
+ Console.WriteLine($"Tax computed by dependency: {tax}");
+}
+
+RuleResult? taxResult = context.GetResult(taxRule.Id);
+if (taxResult is { Success: true })
+{
+ // proceed knowing the tax rule succeeded
+}
```
---
diff --git a/docs/api/ruleparameter.md b/docs/api/ruleparameter.md
index 1b592ef..d40d94c 100644
--- a/docs/api/ruleparameter.md
+++ b/docs/api/ruleparameter.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 4
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleParameter
diff --git a/docs/api/ruleresult.md b/docs/api/ruleresult.md
index 5c0ecaf..bb03a3b 100644
--- a/docs/api/ruleresult.md
+++ b/docs/api/ruleresult.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 3
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# RuleResult
@@ -34,7 +34,7 @@ public readonly record struct RuleResult
| `Exception` | `Exception?` | Exception if execution failed |
| `ChildResults` | `IReadOnlyList` | Nested results from child rules |
| `FirstFailure` | `RuleResult?` | First failing child (or `null` if all passed) |
-| `AllFailures` | `IEnumerable` | All failing children (recursive) |
+| `AllFailures` | `IEnumerable` | Direct failing children (not recursive) |
---
@@ -78,4 +78,4 @@ foreach (var child in result.ChildResults)
## Related
- [Rule](rule.md) — Produces RuleResult
-- [RuleTest](rule-test.md) — Testing framework assertions
+- [Testing Framework](../examples/testing-framework.md) — Testing framework assertions
diff --git a/docs/api/workflow.md b/docs/api/workflow.md
index 04b7ffd..93fdfe2 100644
--- a/docs/api/workflow.md
+++ b/docs/api/workflow.md
@@ -5,7 +5,7 @@ parent: API Reference
nav_order: 2
---
-[← Back to API Reference](api-reference.md)
+[← Back to API Reference](../api-reference.md)
# Workflow
diff --git a/docs/architecture.md b/docs/architecture.md
index 6323367..9428002 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -102,30 +102,33 @@ Subsequent calls with the same signature return the cached delegate instantly.
## Execution Model
+The compiled delegate held by each rule is an internal implementation detail (the
+`CompiledDelegate` hierarchy is `internal`). Single-parameter rules use a direct typed
+invoke; multi-parameter rules dispatch via `DynamicInvoke`. The snippets below show the
+**public** execution surface that drives that machinery.
+
### Sequential
```csharp
-foreach (var rule in workflow.Rules)
+foreach (var result in workflow.Execute(parameters))
{
- var result = rule.CompiledDelegate.Invoke(parameters);
- context.StoreResult(rule.Id, result);
+ // Results are yielded in dependency order.
}
```
### Parallel
```csharp
-Parallel.ForEach(rules, rule => {
- var result = rule.CompiledDelegate.Invoke(parameters);
- // Thread-safe result storage
-});
+RuleResult[] results = workflow.ExecuteParallel(parameters);
```
### Async
```csharp
-var asyncDel = (Func>)rule.CompiledDelegate;
-var result = await asyncDel(customer);
+await foreach (var result in workflow.ExecuteAsync(parameters))
+{
+ // Awaitable rules run asynchronously.
+}
```
---
diff --git a/docs/examples/index.md b/docs/examples/index.md
index 1d97fc0..f590a31 100644
--- a/docs/examples/index.md
+++ b/docs/examples/index.md
@@ -174,7 +174,7 @@ var rule = new Rule
}"
};
-// Access result.Data as ValidationResult
+// Access result.Value as ValidationResult
```
## Workflow with Multiple Rules
@@ -204,7 +204,7 @@ var results = workflow.ExecuteParallel(parameters);
## Dependency Chaining
-Chain rules with `DependsOnRuleId` so downstream rules access upstream results via `RuleContext`.
+Chain rules with `DependsOnRuleId` so each upstream rule's `Action` mutates the shared parameter object and downstream rules read those mutated properties. Compiled expressions can only reference the declared parameters — there is no `context` variable inside expression strings.
```csharp
var authId = Guid.NewGuid();
@@ -212,6 +212,7 @@ var authRule = new Rule(authId)
{
Description = "Authenticated?",
Expression = "user.IsAuthenticated",
+ Action = "user.IsAuthenticated = true",
IsActive = true
};
@@ -219,7 +220,8 @@ var adminId = Guid.NewGuid();
var adminRule = new Rule(adminId)
{
Description = "Admin?",
- Expression = "context.GetValue(authId) && user.Role == \"Admin\"",
+ Expression = "user.IsAuthenticated && user.Role == \"Admin\"",
+ Action = "user.IsAdmin = user.IsAuthenticated && user.Role == \"Admin\"",
DependsOnRuleId = authId,
IsActive = true
};
@@ -228,7 +230,7 @@ var discountId = Guid.NewGuid();
var discountRule = new Rule(discountId)
{
Description = "VIP discount",
- Expression = "context.GetValue(adminId) && user.IsVip",
+ Expression = "user.IsAdmin && user.IsVip",
DependsOnRuleId = adminId,
IsActive = true
};
@@ -249,7 +251,8 @@ var results = workflow.Execute(new[]
**Key points:**
- `DependsOnRuleId` ensures execution order
-- `RuleContext.GetValue(ruleId)` retrieves upstream results
+- Upstream rules pass data downstream by mutating the shared parameter object in their `Action`; downstream rules read the mutated properties
+- The host-side `RuleContext.GetValue(ruleId)` API can inspect results from C# code, but is not available inside expression strings
- Cyclic dependencies throw `CircularReferenceException` on `Validate()`
## Logging with Serilog
@@ -317,7 +320,7 @@ Expression = "((dynamic)customer).Age != null && ((dynamic)customer).Age >= 18"
Store rules in JSON files for configuration-driven setups:
```csharp
-using RoslynRules.Extensions;
+using RoslynRules.Json;
// Load from JSON
var workflow = JsonRuleLoader.LoadWorkflowFromFile("customer-rules.json");
@@ -353,7 +356,7 @@ Combine rules from multiple sources into a single batch:
```csharp
using RoslynRules.Batch;
-using RoslynRules.Extensions;
+using RoslynRules.Json;
var batch = new RuleBatch();
@@ -365,7 +368,7 @@ batch.AddRule(new Rule
});
// From JSON file
-var jsonWorkflow = JsonRuleLoader.LoadFromFile("compliance-rules.json");
+var jsonWorkflow = JsonRuleLoader.LoadWorkflowFromFile("compliance-rules.json");
batch.AddRules(jsonWorkflow.Rules);
// From database (EF Core)
@@ -533,5 +536,5 @@ foreach (var error in errors)
{
Console.WriteLine($"[{error.ErrorType}] {error.Message}");
}
-// ErrorType values: NoActiveRules, EmptyRule, SyntaxError, CircularReference, DuplicateRuleId, MissingDependency
+// ErrorType values: NoActiveRules, EmptyRule, CircularReference, SyntaxError, DuplicateRuleId, MissingDependency, General
```
diff --git a/docs/examples/rule-action-chaining.md b/docs/examples/rule-action-chaining.md
index 1bbc72b..30d5d3f 100644
--- a/docs/examples/rule-action-chaining.md
+++ b/docs/examples/rule-action-chaining.md
@@ -85,7 +85,7 @@ var checkCredit = new Rule
1. Validate customer
2. Check credit (only after validate customer completes)
-**Failure behavior:** If validate customer fails, check credit still runs. Check credit can use `RuleContext` to see the dependency's result and decide what to do.
+**Failure behavior:** If validate customer fails, check credit still runs. The upstream rule's `Action` can mutate the shared parameter object, and check credit reads those mutated properties in its `Expression`/`Action`.
### Comparison Table
@@ -172,8 +172,8 @@ var finalDecision = new Rule
{
Description = "Final decision",
DependsOnRuleId = evaluateRisk.Id,
- Expression = "true",
- Action = "customer.Approved = context.GetResult(evaluateRisk.Id).Success",
+ Expression = "customer.CreditScore >= 600",
+ Action = "customer.Approved = true",
IsActive = true
};
@@ -192,9 +192,11 @@ var results = workflow.Execute(parameters).ToList();
## Accessing Dependency Results
-Use `RuleContext` to read the results of previously executed rules.
+To pass data between dependent rules, have the upstream rule's `Action` mutate the shared parameter object, and have the downstream rule read those mutated properties in its `Expression`/`Action`. Compiled expressions and actions can only reference the declared `RuleParameter` objects — there is no `context` variable in scope inside an expression string.
+
+`RuleContext` is a host-side (C#) API for inspecting results after execution. Its `GetResult`/`GetValue`/`TryGetValue` methods can only be called from your own C# code, never from inside an expression or action string.
-### RuleContext API
+### RuleContext API (host-side only)
```csharp
public class RuleContext
@@ -219,14 +221,16 @@ public class RuleContext
}
```
-### Reading Values in Expressions
+### Passing Values Between Rules
+
+The upstream rule mutates a property on the shared parameter object; the downstream rule reads it.
```csharp
var calculateTax = new Rule
{
Description = "Calculate tax",
- Expression = "true",
- Action = "customer.TaxAmount = customer.Amount * 0.08",
+ Expression = "customer.Amount > 0",
+ Action = "customer.TaxAmount = customer.Amount * 0.08m",
IsActive = true
};
@@ -234,13 +238,15 @@ var calculateTotal = new Rule
{
Description = "Calculate total",
DependsOnRuleId = calculateTax.Id,
- Expression = "true",
- Action = @"customer.Total = customer.Amount + context.GetValue(calculateTax.Id)",
+ Expression = "customer.TaxAmount > 0",
+ Action = "customer.Total = customer.Amount + customer.TaxAmount",
IsActive = true
};
```
-### Using TryGetValue for Safe Access
+### Guarding Against Unset Values
+
+If the upstream rule may not have set a value, guard the downstream read against the property's default.
```csharp
var calculateTotal = new Rule
@@ -248,26 +254,23 @@ var calculateTotal = new Rule
Description = "Calculate total",
DependsOnRuleId = calculateTax.Id,
Expression = "true",
- Action = @"
- if (context.TryGetValue(calculateTax.Id, out var taxAmount))
- {
- customer.Total = customer.Amount + taxAmount;
- }
- else
- {
- customer.Total = customer.Amount; // No tax applied
- }",
+ Action = @"customer.Total = customer.TaxAmount > 0
+ ? customer.Amount + customer.TaxAmount
+ : customer.Amount;",
IsActive = true
};
```
### Conditional Logic Based on Dependencies
+The upstream rule records its outcome on the shared object; the downstream rule reads it.
+
```csharp
var checkAuthentication = new Rule
{
Description = "Check authentication",
Expression = "customer.IsAuthenticated",
+ Action = "customer.IsAuthenticated = true",
IsActive = true
};
@@ -275,7 +278,7 @@ var checkAuthorization = new Rule
{
Description = "Check authorization",
DependsOnRuleId = checkAuthentication.Id,
- Expression = @"context.GetResult(checkAuth.Id).Success && customer.HasAdminRole",
+ Expression = @"customer.IsAuthenticated && customer.HasAdminRole",
IsActive = true
};
```
@@ -355,7 +358,8 @@ Use a dependency as a gatekeeper. The dependent rule always runs but checks the
var gatekeeper = new Rule
{
Description = "Feature enabled",
- Expression = "FeatureFlags.IsEnabled("PremiumFeature")",
+ Expression = "FeatureFlags.IsEnabled(\"PremiumFeature\")",
+ Action = "customer.FeatureEnabled = FeatureFlags.IsEnabled(\"PremiumFeature\")",
IsActive = true
};
@@ -363,7 +367,7 @@ var premiumCheck = new Rule
{
Description = "Premium validation",
DependsOnRuleId = gatekeeper.Id,
- Expression = "context.GetResult(gatekeeper.Id).Success && customer.IsPremium",
+ Expression = "customer.FeatureEnabled && customer.IsPremium",
IsActive = true
};
```
@@ -408,6 +412,7 @@ var eligibilityCheck = new Rule
{
Description = "Eligibility",
Expression = "customer.Age >= 18 && customer.Income > 30000",
+ Action = "customer.IsEligible = customer.Age >= 18 && customer.Income > 30000",
IsActive = true
};
@@ -415,7 +420,7 @@ var standardOffer = new Rule
{
Description = "Standard offer",
DependsOnRuleId = eligibilityCheck.Id,
- Expression = "context.GetResult(eligibility.Id).Success && customer.Income < 100000",
+ Expression = "customer.IsEligible && customer.Income < 100000",
IsActive = true
};
@@ -423,7 +428,7 @@ var premiumOffer = new Rule
{
Description = "Premium offer",
DependsOnRuleId = eligibilityCheck.Id,
- Expression = "context.GetResult(eligibility.Id).Success && customer.Income >= 100000",
+ Expression = "customer.IsEligible && customer.Income >= 100000",
IsActive = true
};
```
@@ -445,7 +450,8 @@ var validateAge = new Rule
{
Description = "Validate age",
DependsOnRuleId = parseInput.Id,
- Expression = @"context.GetResult(parse.Id).Success && customer.ParsedAge >= 18",
+ Expression = @"customer.ParsedAge >= 18",
+ Action = "customer.IsAdult = customer.ParsedAge >= 18",
IsActive = true
};
@@ -453,8 +459,8 @@ var processAdult = new Rule
{
Description = "Process adult",
DependsOnRuleId = validateAge.Id,
- Expression = @"context.GetResult(validateAge.Id).Success",
- Action = "customer.IsAdult = true",
+ Expression = @"customer.IsAdult",
+ Action = "customer.Processed = true",
IsActive = true
};
```
@@ -473,11 +479,11 @@ var processAdult = new Rule
**Fix:** Redesign rules to break the cycle. Use parent-child relationships for tight coupling instead.
-### "Expected dependency result but context was empty"
+### Downstream rule reads a stale or default value
-**Cause:** Rule is trying to access `context.GetResult()` but the dependency hasn't executed yet.
+**Cause:** The downstream rule ran before the upstream rule mutated the shared parameter object, or the upstream `Action` never set the property.
-**Fix:** Ensure `DependsOnRuleId` is set correctly. The workflow engine executes dependencies first automatically.
+**Fix:** Ensure `DependsOnRuleId` is set correctly so the dependency runs first, and confirm the upstream rule's `Action` assigns the property the downstream rule reads. Remember: compiled expressions/actions can only reference the declared parameters — there is no `context` available inside expression strings. Use the host-side `RuleContext` only from C# code after execution.
### Priority not respected
diff --git a/docs/examples/testing-framework.md b/docs/examples/testing-framework.md
index 738ec43..ea6675e 100644
--- a/docs/examples/testing-framework.md
+++ b/docs/examples/testing-framework.md
@@ -32,9 +32,11 @@ var test = RuleTest.For(adultRule)
.ExpectAllChildrenPass()
.ExpectValue(true);
+// Run() throws RuleAssertionException if any expectation fails,
+// otherwise returns the RuleResult.
var result = test.Run();
-// result.Passed = true
-// result.ErrorMessage = null
+// result.Success = true
+// result.Exception = null
```
### Testing Failure Cases
@@ -42,12 +44,11 @@ var result = test.Run();
```csharp
var test = RuleTest.For(adultRule)
.WithInput("customer", new Customer { Age = 16, Name = "Bob" })
- .ExpectFailure()
- .ExpectNoException();
+ .ExpectFailure();
var result = test.Run();
-// result.Passed = true
-// result.RuleResult.Success = false
+// result.Success = false
+// result.Exception = null
```
### Testing with Multiple Inputs
@@ -107,6 +108,8 @@ var parent = new Rule
}
};
+var compiler = new ExpressionCompiler();
+parent.Compile(compiler, parameters);
var result = parent.Execute(parameters);
// All children passed
@@ -129,10 +132,11 @@ result.ShouldHaveChild("Child 2").ShouldFail();
var badRule = new Rule
{
Description = "Bad rule",
- Expression = "throw new ArgumentException("test")"
+ Expression = "throw new ArgumentException(\"test\")"
};
-badRule.Compile(parameters);
+var compiler = new ExpressionCompiler();
+badRule.Compile(compiler, parameters);
var result = badRule.Execute(parameters);
// Assert exception type
@@ -189,15 +193,15 @@ Console.WriteLine(result.ToString());
// Rule Test Suite: 3 passed, 0 failed (3 total)
// ✅ PASS Adult check
// ✅ PASS Name check
-// ✅ FAIL Inactive rule (expected)
+// ✅ PASS Inactive rule
// Detailed results
-foreach (var testResult in result.Results)
+foreach (var testResult in result.Tests)
{
- Console.WriteLine($"{testResult.RuleDescription}: {(testResult.Passed ? "PASS" : "FAIL")}");
+ Console.WriteLine($"{testResult.TestName}: {(testResult.Passed ? "PASS" : "FAIL")}");
if (!testResult.Passed)
{
- Console.WriteLine($" Error: {testResult.ErrorMessage}");
+ Console.WriteLine($" Error: {testResult.Exception?.Message}");
}
}
```
@@ -251,7 +255,7 @@ var test = RuleTest.For(rule)
var test = RuleTest.For(rule)
.WithInput("customer", customer)
.ExpectSuccess()
- .ExpectValueOfType()
+ .Assert(r => r.ShouldHaveValueOfType())
.Assert(r =>
{
var result = (ValidationResult)r.Value;
@@ -353,14 +357,15 @@ public void AdultRule_WithAdultCustomer_ShouldPass()
new RuleParameter("customer", typeof(Customer), new Customer { Age = 25 })
};
- rule.Compile(parameters);
+ var compiler = new ExpressionCompiler();
+ rule.Compile(compiler, parameters);
var result = rule.Execute(parameters);
// Framework assertions
result.ShouldPass();
// FluentAssertions for complex checks
- result.ElapsedMilliseconds.Should().BeLessThan(1);
+ result.Success.Should().BeTrue();
result.RuleDescription.Should().Be("Adult check");
result.ChildResults.Should().BeEmpty();
}
@@ -406,7 +411,8 @@ public class AgeRuleTests
IsActive = true
};
_parameters = new[] { new RuleParameter("customer", typeof(Customer), default) };
- _rule.Compile(_parameters);
+ var compiler = new ExpressionCompiler();
+ _rule.Compile(compiler, _parameters);
}
[Fact]
@@ -452,8 +458,9 @@ public void AllValidationRules_ShouldPassWithValidCustomer()
public void Rule_ShouldCompileSuccessfully()
{
var rule = new Rule { Expression = "customer.Age >= 18" };
-
- var act = () => rule.Compile(parameters);
+ var compiler = new ExpressionCompiler();
+
+ var act = () => rule.Compile(compiler, parameters);
act.Should().NotThrow();
}
@@ -461,9 +468,10 @@ public void Rule_ShouldCompileSuccessfully()
public void Rule_WithSyntaxError_ShouldThrow()
{
var rule = new Rule { Expression = "customer.Age >= " }; // Syntax error
-
- var act = () => rule.Compile(parameters);
- act.Should().Throw();
+ var compiler = new ExpressionCompiler();
+
+ var act = () => rule.Compile(compiler, parameters);
+ act.Should().Throw();
}
```
diff --git a/docs/examples/when-to-use-what.md b/docs/examples/when-to-use-what.md
index eea6065..5927dbe 100644
--- a/docs/examples/when-to-use-what.md
+++ b/docs/examples/when-to-use-what.md
@@ -41,7 +41,8 @@ var ageCheck = new Rule
IsActive = true
};
-ageCheck.Compile(parameters);
+var compiler = new ExpressionCompiler();
+ageCheck.Compile(compiler, parameters);
var result = ageCheck.Execute(parameters);
// result.Success tells you pass/fail
@@ -67,7 +68,7 @@ var workflow = new Workflow
Rules = new List
{
new Rule { Description = "Age", Expression = "customer.Age >= 18" },
- new Rule { Description = "Email", Expression = "customer.Email.Contains("@")" },
+ new Rule { Description = "Email", Expression = "customer.Email.Contains(\"@\")" },
new Rule { Description = "Country", Expression = "customer.Country != null" }
}
};
diff --git a/docs/getting-started.md b/docs/getting-started.md
index 2a55675..f052021 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -96,6 +96,7 @@ var parameters = new[]
RuleParameter.ForCompile("quantity", typeof(int))
};
+var compiler = new ExpressionCompiler();
rule.Compile(compiler, parameters);
```
diff --git a/docs/index.md b/docs/index.md
index 66ce736..eb0cc02 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -19,7 +19,7 @@ High-performance .NET rules engine with Roslyn compilation, typed delegates, and
| Roslyn Compilation | Expressions compile to IL once, execute as native code |
- | Typed Delegates | Direct Func<T,R> calls — no DynamicInvoke overhead |
+ | Typed Delegates | Single-parameter rules invoke Func<T,R> directly — no DynamicInvoke overhead (multi-parameter rules use DynamicInvoke) |
| Single Parameter | One input, one output — no array allocation per call |
| Immutable Rules | Lock after compile — zero thread contention |
| Parallel Execution | Parallel.For for independent rule evaluation |
diff --git a/docs/security.md b/docs/security.md
index e1e5a1e..b4c19bc 100644
--- a/docs/security.md
+++ b/docs/security.md
@@ -137,13 +137,21 @@ var rule = new Rule
## Dependency Patterns
-Use `DependsOnRuleId` to chain rules securely — data flows through `RuleContext`, not shared state:
+Use `DependsOnRuleId` to chain rules securely. Expressions and actions can only reference declared `RuleParameter`s — there is no `context` variable in scope. To pass data between rules, have the upstream rule's `Action` mutate a shared parameter object and have the downstream rule read the mutated property, ordering them with `DependsOnRuleId`:
```csharp
-var auth = new Rule { Id = authId, Expression = "user.IsAuthenticated" };
+// Upstream rule writes the authorization result onto the shared parameter.
+var auth = new Rule
+{
+ Id = authId,
+ Expression = "user.IsAuthenticated",
+ Action = "session.IsAuthorized = user.IsAuthenticated"
+};
+
+// Downstream rule reads the mutated property; ordering is enforced via DependsOnRuleId.
var admin = new Rule
{
- Expression = "context.GetValue(authId)",
+ Expression = "session.IsAuthorized",
DependsOnRuleId = authId
};
```
@@ -157,7 +165,7 @@ var admin = new Rule
- [ ] Set `Timeout` on untrusted rules
- [ ] Monitor `CompileCount` and ALC recycling
- [ ] Never include `System.IO`, `System.Net`, `System.Diagnostics`
-- [ ] Use `RuleContext` for dependency data, not shared mutable state
+- [ ] Pass cross-rule data through declared `RuleParameter`s mutated by upstream actions, ordered via `DependsOnRuleId`
- [ ] Log compilation failures for audit trail
---
diff --git a/docs/snapshots.md b/docs/snapshots.md
index b79c740..18c5252 100644
--- a/docs/snapshots.md
+++ b/docs/snapshots.md
@@ -71,16 +71,14 @@ var workflow = new Workflow
}
};
-// 2. Compile the workflow (JIT only)
+// 2. Define compile-time parameters (JIT only)
var parameters = new[]
{
new RuleParameter("order", typeof(Order), null),
new RuleParameter("customer", typeof(Customer), null)
};
-workflow.Compile(parameters);
-
-// 3. Create a snapshot
+// 3. Create a snapshot (CompiledWorkflow.Compile compiles the workflow internally)
var snapshot = SnapshotManager.CreateSnapshot(
CompiledWorkflow.Compile(workflow, parameters));
@@ -158,7 +156,6 @@ public static void Main(string[] args)
var workflow = JsonRuleLoader.LoadWorkflowFromFile(ruleFile);
var parameters = GetParametersFor(ruleFile);
- workflow.Compile(parameters);
var snapshot = SnapshotManager.CreateSnapshot(
CompiledWorkflow.Compile(workflow, parameters));
@@ -258,5 +255,3 @@ if (!snapshot.Version.IsCompatibleWith(expectedVersion))
- [AOT Compatibility](./aot-compatibility.md) — Detailed AOT detection and troubleshooting
- [Performance Tuning](./performance-tuning.md) — Optimize rule execution
-- [API Reference: SnapshotManager](../api/snapshotmanager.md)
-- [API Reference: ISnapshotSerializer](../api/isnapshotserializer.md)
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 5734a29..5e8c408 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -66,6 +66,7 @@ Expression = "awaiting > 0" // This is sync
**Fix:** Compile once before executing:
```csharp
+var compiler = new ExpressionCompiler();
rule.Compile(compiler, parameters);
var result = rule.Execute(parameters); // Now works
```