Examples
Quick-reference code snippets for common scenarios.
On This Page
- Basic Boolean Rule
- Rule with Action
- Parent with Child Rules
- Multiple Parameters
- Async Rule
- Returning Multiple Values
- Workflow with Multiple Rules
- Dependency Chaining
- Logging with Serilog
- ExpandoObject
- JSON Configuration
- Loading Rules into a Batch
- Batch Evaluation
- Rule Priority
- Per-Rule Timeout
- Validation Before Compile
- Semantic Validation (No Rule Instance)
- Non-Throwing Validation
Detailed Guides
| Guide | What It Covers |
|---|---|
| Rule Action Chaining | Parent-Child vs DependsOn, multi-stage pipelines, RuleContext |
| Streaming and Cancellation | ExecuteAsync, ExecuteBufferedAsync, CancellationToken patterns |
| Testing Framework | RuleTest, RuleResult assertions, test suites, custom assertions |
| Real-World Use Cases | Form validation, fraud detection, feature flags, compliance, pricing |
| EF Core Serialization | DbContext setup, storing/loading rules, temporal tables |
| When to Use What | Decision matrix, execution modes, choosing by rule count |
| Localization | i18n with DescriptionKey and IRuleDescriptionProvider |
| Visualization | DOT/Mermaid dependency graphs |
Basic Boolean Rule
var rule = new Rule
{
Description = "Adult check",
Expression = "customer.Age >= 18",
IsActive = true
};
Rule with Action
var rule = new Rule
{
Description = "Mark adult",
Expression = "customer.Age >= 18",
Action = "customer.IsAdult = true",
IsActive = true
};
Parent with Child Rules
Children evaluated bottom-up. Parent only runs if all children pass.
var parent = new Rule
{
Description = "Full validation",
Expression = "customer.IsValid"
};
var child1 = new Rule
{
Description = "Name check",
Expression = "!string.IsNullOrEmpty(customer.Name)"
};
var child2 = new Rule
{
Description = "Age check",
Expression = "customer.Age > 0"
};
parent.ChildRules.Add(child1);
parent.ChildRules.Add(child2);
Multiple Parameters
Pass multiple parameters directly — up to 16.
var rule = new Rule
{
Description = "Price check",
Expression = "price > 0 && quantity > 0",
IsActive = true
};
var parameters = new[]
{
new RuleParameter("price", typeof(decimal), 9.99m),
new RuleParameter("quantity", typeof(int), 5)
};
var compiler = new ExpressionCompiler();
rule.Compile(compiler, parameters);
var result = rule.Execute(parameters);
// result.Success = true
Parameter names in expressions match exactly:
var rule = new Rule
{
Expression = "x > y"
};
var parameters = new[]
{
new RuleParameter("x", typeof(int), 10),
new RuleParameter("y", typeof(int), 5)
};
rule.Compile(compiler, parameters);
rule.Execute(parameters); // Success = true
Async Rule
Auto-detected by await keyword. Compiled to Func<T, Task<bool>>.
var rule = new Rule
{
Description = "Check price from API",
Expression = "await PriceService.GetAsync(customer.ProductId) > 100"
};
// Compile normally
workflow.Compile(parameters);
// Execute async
var results = await workflow.ExecuteParallelAsync(parameters);
Returning Multiple Values
Wrap outputs in a struct:
public struct ValidationResult
{
public bool IsAdult { get; set; }
public bool IsPremium { get; set; }
}
var rule = new Rule
{
Expression = "new ValidationResult {
IsAdult = customer.Age >= 18,
IsPremium = customer.Age >= 65
}"
};
// Access result.Data as ValidationResult
Workflow with Multiple Rules
var workflow = new Workflow
{
Description = "Customer processing",
Rules = new List<Rule>
{
new Rule { Expression = "customer.Age >= 18", Description = "Adult" },
new Rule { Expression = "customer.Name.StartsWith(\"A\")", Description = "Starts with A" },
new Rule {
Expression = "customer.IsActive",
Action = "customer.Processed = true",
Description = "Process active"
}
}
};
workflow.Validate();
workflow.Compile(parameters);
// All rules evaluated, results in order
var results = workflow.ExecuteParallel(parameters);
Dependency Chaining
Chain rules with DependsOnRuleId so downstream rules access upstream results via RuleContext.
var authId = Guid.NewGuid();
var authRule = new Rule(authId)
{
Description = "Authenticated?",
Expression = "user.IsAuthenticated",
IsActive = true
};
var adminId = Guid.NewGuid();
var adminRule = new Rule(adminId)
{
Description = "Admin?",
Expression = "context.GetValue<bool>(authId) && user.Role == \"Admin\"",
DependsOnRuleId = authId,
IsActive = true
};
var discountId = Guid.NewGuid();
var discountRule = new Rule(discountId)
{
Description = "VIP discount",
Expression = "context.GetValue<bool>(adminId) && user.IsVip",
DependsOnRuleId = adminId,
IsActive = true
};
var workflow = new Workflow();
workflow.Rules.Add(authRule);
workflow.Rules.Add(adminRule);
workflow.Rules.Add(discountRule);
workflow.Compile(new[] { new RuleParameter("user", typeof(User)) });
// Auth → Admin → Discount (dependency order)
var results = workflow.Execute(new[]
{
new RuleParameter("user", typeof(User), currentUser)
});
Key points:
DependsOnRuleIdensures execution orderRuleContext.GetValue<T>(ruleId)retrieves upstream results- Cyclic dependencies throw
CircularReferenceExceptiononValidate()
Logging with Serilog
using Serilog;
using Microsoft.Extensions.Logging;
// Create a Serilog logger
var serilog = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
var logger = new SerilogLoggerFactory(serilog).CreateLogger<Rule>();
// Attach to any rule
var rule = new Rule
{
Description = "Check adult",
Expression = "customer.Age >= 18",
Logger = logger
};
// Execute — output appears in console:
// [15:42:10.123] [PASS] Check adult (Id: ...) — 0.031ms
ExpandoObject (Dynamic)
Use when the data shape is not known at compile time. Slower but flexible.
using System.Dynamic;
dynamic customer = new ExpandoObject();
customer.Name = "Alice";
customer.Age = 25;
var parameters = new[]
{
new RuleParameter("customer", typeof(object), customer)
};
var rule = new Rule
{
Description = "Check adult",
Expression = "((dynamic)customer).Age >= 18",
IsActive = true
};
var compiler = new ExpressionCompiler();
rule.Compile(compiler, parameters, new[] { "System.Dynamic" });
var result = rule.Execute(parameters);
// result.Success = true
Caveat: Missing properties return null, not throw. Test for existence if needed:
Expression = "((dynamic)customer).Age != null && ((dynamic)customer).Age >= 18"
JSON Configuration
Store rules in JSON files for configuration-driven setups:
using RoslynRules.Extensions;
// Load from JSON
var workflow = JsonRuleLoader.LoadWorkflowFromFile("customer-rules.json");
// Validate, compile, execute as normal
workflow.Validate();
workflow.Compile(parameters);
var results = workflow.Execute(parameters);
customer-rules.json:
{
"description": "Customer validation",
"rules": [
{
"description": "Adult check",
"expression": "customer.Age >= 18",
"isActive": true
},
{
"description": "Name required",
"expression": "!string.IsNullOrEmpty(customer.Name)",
"isActive": true
}
]
}
Loading Rules into a Batch
Combine rules from multiple sources into a single batch:
using RoslynRules.Batch;
using RoslynRules.Extensions;
var batch = new RuleBatch();
// From code
batch.AddRule(new Rule
{
Description = "Adult check",
Expression = "customer.Age >= 18"
});
// From JSON file
var jsonWorkflow = JsonRuleLoader.LoadFromFile("compliance-rules.json");
batch.AddRules(jsonWorkflow.Rules);
// From database (EF Core)
var dbRules = dbContext.Rules
.Where(r => r.WorkflowId == workflowId && r.IsActive)
.ToList();
batch.AddRules(dbRules);
// From a service
var apiRules = await ruleService.GetRulesForTenantAsync(tenantId);
batch.AddRules(apiRules);
// Single compile, parallel evaluation
batch.Compile(parameters);
var results = batch.EvaluateParallel(parameters);
foreach (var result in results.Where(r => !r.Success))
{
Console.WriteLine($"Failed: {result.RuleDescription}");
}
Batch Evaluation (10+ Rules)
Evaluate many rules together with shared compilation:
var batch = new RuleBatch();
// Add 10 validation rules
for (int i = 0; i < 10; i++)
{
batch.AddRule(new Rule
{
Description = $"Check {i + 1}",
Expression = $"customer.Field{i} > 0"
});
}
// Compile once
batch.Compile(parameters);
// Evaluate all at once — parallel mode for speed
var results = batch.EvaluateParallel(parameters);
// Summary
var passCount = results.Count(r => r.Success);
Console.WriteLine($"Passed: {passCount}/{results.Length}");
Rule Priority
Control execution order when some checks must run before others:
var workflow = new Workflow
{
Rules =
{
// Critical checks run first
new Rule
{
Description = "Check authentication",
Expression = "customer.IsAuthenticated",
Priority = 100
},
new Rule
{
Description = "Check authorization",
Expression = "customer.HasPermission",
Priority = 90
},
// Standard validation
new Rule
{
Description = "Validate email",
Expression = "customer.Email.Contains(\"@\")",
Priority = 0 // Default
},
// Cleanup / logging runs last
new Rule
{
Description = "Audit log",
Expression = "true",
Action = "Log(customer.Id)",
Priority = -10
}
}
};
workflow.Compile(parameters);
// Order: Auth (100) → AuthZ (90) → Email (0) → Audit (-10)
var results = workflow.Execute(parameters);
Priority rules:
- Higher number = earlier execution
0is default- Negative numbers = after defaults
- Same priority = original order preserved
- Immutable after
Compile()
Per-Rule Timeout
Prevent infinite loops or blocking operations from hanging execution:
var rule = new Rule
{
Description = "External API call",
Expression = "await CheckApiAsync(customer.Id)",
Timeout = TimeSpan.FromSeconds(10), // Fail after 10 seconds
IsActive = true
};
// If execution exceeds 10s, RuleTimeoutException is thrown
Validation Before Compile
try
{
workflow.Validate();
workflow.Compile(parameters);
}
catch (InvalidOperationException ex)
{
// Catches:
// - Empty rules
// - Syntax errors in expressions
// - Circular child references
// - Duplicate rule IDs
Console.WriteLine($"Validation failed: {ex.Message}");
}
Semantic Validation (No Rule Instance)
Validate an expression string without creating a Rule — useful for validating user input or API parameters before storing them.
// Using a Type
Rule.ValidateSemantics("param > 0", typeof(int));
// Using a type alias
Rule.ValidateSemantics("name.Length > 0", "string", "name");
// Using full type name
Rule.ValidateSemantics("date.Year >= 2000", "System.DateTime", "date");
Supported aliases: bool, byte, char, decimal, double, float, int, long, short, string, object, and any full type name (e.g., System.DateTime).
Throws RuleCompilationException if the expression has undefined variables, missing types, or incorrect method signatures.
Non-Throwing Validation
Collect all validation errors without throwing:
ValidationError[] errors = workflow.ValidateAll();
foreach (var error in errors)
{
Console.WriteLine($"[{error.ErrorType}] {error.Message}");
}
// ErrorType values: NoActiveRules, EmptyRule, SyntaxError, CircularReference, DuplicateRuleId, MissingDependency