Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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 };
Expand Down Expand Up @@ -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.

Expand Down
16 changes: 10 additions & 6 deletions docs/aot-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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");
}
```

Expand All @@ -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()`
Expand All @@ -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.

---

Expand Down
1 change: 0 additions & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ Complete reference for all RoslynRules public APIs, organized by component.
<tr><td><a href="api/ruleparameter.html">RuleParameter</a></td><td>Parameter definition (name, type, value)</td></tr>
<tr><td><a href="api/rule-diagnostics.html">RuleDiagnostics</a></td><td>Diagnostics, logging, and auditing model</td></tr>
<tr><td><a href="api/rule-lifecycle-events.html">RuleLifecycleEvents</a></td><td>OnRuleExecuting/OnRuleExecuted event args</td></tr>
<tr><td><a href="api/compiled-delegate.html">CompiledDelegate</a></td><td>Fast invocation wrapper for compiled delegates</td></tr>
</tbody>
</table>

Expand Down
44 changes: 32 additions & 12 deletions docs/api/assemblyreferenceprovider.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,31 +19,51 @@ 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

---

## Custom Provider

```csharp
var provider = new AssemblyReferenceProvider();
provider.AddAssembly(typeof(MyCustomType).Assembly);
provider.AllowAssembly("MyCompany.Models");

var del = compiler.Compile<Func<Customer, bool>>(
"customer.Name.Length > 0",
Expand All @@ -56,11 +76,11 @@ var del = compiler.Compile<Func<Customer, bool>>(

## 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
35 changes: 17 additions & 18 deletions docs/api/compiled-delegate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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<TDelegate>` 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<Func<Customer, bool>>(
"customer.IsActive",
new[] { "customer" });

var compiled = CompiledDelegateFactory.Wrap(rawDelegate); // internal
var result = compiled.Invoke(customer); // true / false (as object?)
```

---
Expand All @@ -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
17 changes: 15 additions & 2 deletions docs/api/delegate-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TParam, bool>` | `Func<Customer, bool>` |
Expand All @@ -23,6 +27,15 @@ RoslynRules supports exactly **one input parameter**. Return multiple values by
| **Async Expression** | `Func<TParam, Task<bool>>` | `Func<Customer, Task<bool>>` |
| **Async Action** | `Func<TParam, Task>` | `Func<Customer, Task>` |

Multi-parameter signatures (up to 16 parameters) follow the same shapes:

| Type | Delegate | Example |
|------|----------|---------|
| **Expression** | `Func<T1, …, Tn, bool>` | `Func<Customer, Order, bool>` |
| **Action** | `Action<T1, …, Tn>` | `Action<Customer, Order>` |
| **Async Expression** | `Func<T1, …, Tn, Task<bool>>` | `Func<Customer, Order, Task<bool>>` |
| **Async Action** | `Func<T1, …, Tn, Task>` | `Func<Customer, Order, Task>` |

---

## Auto-Detection
Expand Down
12 changes: 6 additions & 6 deletions docs/api/exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 |

---

Expand Down
2 changes: 1 addition & 1 deletion docs/api/expressioncompiler.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion docs/api/graph-algorithms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/api/iruleengine.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -46,7 +46,7 @@ services.AddSingleton<IRuleEngine, RuleBatch>();
```csharp
var mock = new Mock<IRuleEngine>();
mock.Setup(x => x.Execute(It.IsAny<RuleParameter[]>()))
.Returns(new[] { new RuleResult { Success = true } });
.Returns(new[] { new RuleResult(true) });

var service = new MyService(mock.Object);
```
Expand Down
20 changes: 6 additions & 14 deletions docs/api/json-serialization.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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)
Loading
Loading