← Back to Documentation Index

Examples

Quick-reference code snippets for common scenarios.

On This Page

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 workflow = new Workflow { Rules = { rule } };
workflow.Compile(parameters);

var result = workflow.Execute(parameters).First();
// 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)
};

var workflow = new Workflow { Rules = { rule } };
workflow.Compile(parameters);
workflow.Execute(parameters); // first result 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.Value 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 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.

var authId = Guid.NewGuid();
var authRule = new Rule(authId)
{
    Description = "Authenticated?",
    Expression = "user.IsAuthenticated",
    Action = "user.IsAuthenticated = true",
    IsActive = true
};

var adminId = Guid.NewGuid();
var adminRule = new Rule(adminId)
{
    Description = "Admin?",
    Expression = "user.IsAuthenticated && user.Role == \"Admin\"",
    Action = "user.IsAdmin = user.IsAuthenticated && user.Role == \"Admin\"",
    DependsOnRuleId = authId,
    IsActive = true
};

var discountId = Guid.NewGuid();
var discountRule = new Rule(discountId)
{
    Description = "VIP discount",
    Expression = "user.IsAdmin && 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:

  • DependsOnRuleId ensures execution order
  • 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<T>(ruleId) API can inspect results from C# code, but is not available inside expression strings
  • Cyclic dependencies throw CircularReferenceException on Validate()

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 workflow = new Workflow { Rules = { rule } };
workflow.Compile(parameters, new[] { "System.Dynamic" });

var result = workflow.Execute(parameters).First();
// 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.Json;

// 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.Json;

var batch = new RuleBatch();

// From code
batch.AddRule(new Rule
{
    Description = "Adult check",
    Expression = "customer.Age >= 18"
});

// From JSON file
var jsonWorkflow = JsonRuleLoader.LoadWorkflowFromFile("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
  • 0 is 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, CircularReference, SyntaxError, DuplicateRuleId, MissingDependency, General

Table of contents


Back to top

MIT License. Built with Roslyn + Typed Delegates.