FeelSharp User Guide v1.3.0

Comprehensive guide for C# developers using FeelSharp - the complete .NET FEEL (Friendly Enough Expression Language) implementation with integrated compilation engine.

Table of Contents

Getting Started

Installation

dotnet add package FeelSharp --version 1.3.0

Basic Setup

using FeelSharp;
    
    var engine = FeelEngine.Create();

Your First Expression

var result = engine.EvaluateExpression("2 + 3 * 4");
    Console.WriteLine(result.Value); // NumberValue(14)

API Reference

IFeelEngine Interface

The main interface for evaluating FEEL expressions.

Methods

EvaluateExpression

FeelResult<FeelValue> EvaluateExpression(string expression, object? context = null)

Evaluates a FEEL expression and returns the result.

EvaluateUnaryTests

FeelResult<bool> EvaluateUnaryTests(string tests, object input)
    FeelResult<bool> EvaluateUnaryTests(string tests, object input, object? context) // NEW in v1.1.0

Evaluates unary tests (for DMN decision tables). The context overload allows referencing variables in unary tests.

ParseExpression

FeelResult<ParsedExpression> ParseExpression(string expression)

Parses an expression without evaluating it (useful for validation).

FeelResult

Represents the result of a FEEL operation with success/failure handling.

Properties

bool IsSuccess      // True if operation succeeded
    bool IsFailure      // True if operation failed
    T? Value           // The result value (null if failed)
    string? Error      // Error message (null if succeeded)

Methods

T GetValueOrThrow()                    // Throws exception if failed
    T GetValueOrDefault(T defaultValue)    // Returns default if failed
    U Map<U>(Func<T, U> mapper)           // Transform successful result

Usage Patterns

// Pattern 1: Check IsSuccess
    var result = engine.EvaluateExpression("2 + 3");
    if (result.IsSuccess)
    {
        Console.WriteLine($"Result: {result.Value}");
    }
    else
    {
        Console.WriteLine($"Error: {result.Error}");
    }
    
    // Pattern 2: Exception handling
    try
    {
        var value = result.GetValueOrThrow();
        Console.WriteLine($"Result: {value}");
    }
    catch (FeelEvaluationException ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
    
    // Pattern 3: Default values
    var value = result.GetValueOrDefault(NumberValue.Zero);

FeelValue Types

FEEL values are represented by these types:

NumberValue      // decimal numbers
    StringValue      // strings
    BooleanValue     // true/false
    NullValue        // null
    ListValue        // lists/arrays
    ContextValue     // objects/maps
    DateValue        // dates
    TimeValue        // times
    DateTimeValue    // date-times
    DurationValue    // durations

Creating Values

// From .NET types (automatic conversion)
    var result = engine.EvaluateExpression("age", new { age = 25 });
    
    // From constructors
    var number = new NumberValue(42);
    var text = new StringValue("Hello");
    var list = new ListValue(new FeelValue[] { new NumberValue(1), new NumberValue(2), new NumberValue(3) });
    
    // From factory methods
    var context = FeelValue.FromObject(new { name = "John", age = 25 });
    
    // Using implicit conversions
    FeelValue numberValue = 42;        // Implicit NumberValue
    FeelValue stringValue = "Hello";   // Implicit StringValue  
    FeelValue boolValue = true;        // Implicit BooleanValue

FEEL Language Guide

Basic Expressions

// Arithmetic
    engine.EvaluateExpression("2 + 3 * 4");        // 14
    engine.EvaluateExpression("10 / 3");           // 3.333...
    engine.EvaluateExpression("2 ** 3");           // 8 (power)
    
    // Comparison
    engine.EvaluateExpression("5 > 3");            // true
    engine.EvaluateExpression("\"abc\" = \"abc\""); // true
    
    // Logical
    engine.EvaluateExpression("true and false");   // false
    engine.EvaluateExpression("not false");        // true

Working with Context

var person = new { 
        name = "John", 
        age = 30, 
        address = new { city = "Berlin", country = "Germany" }
    };
    
    // Simple property access
    var result1 = engine.EvaluateExpression("name", person); // "John"
    var result2 = engine.EvaluateExpression("age > 18", person); // true
    
    // Nested property access
    var result3 = engine.EvaluateExpression("address.city", person); // "Berlin"
    
    // Complex expressions
    var result4 = engine.EvaluateExpression(
        "if age >= 18 then \"adult\" else \"minor\"", person); // "adult"

Lists and Collections

var data = new { numbers = new[] { 1, 2, 3, 4, 5 } };
    
    // List operations
    engine.EvaluateExpression("count(numbers)", data);        // 5
    engine.EvaluateExpression("sum(numbers)", data);          // 15
    engine.EvaluateExpression("numbers[1]", data);            // 1 (1-based indexing)
    
    // List filtering
    var employees = new { 
        employees = new[] {
            new { name = "John", salary = 80000 },
            new { name = "Jane", salary = 90000 },
            new { name = "Bob", salary = 60000 }
        }
    };
    
    engine.EvaluateExpression("employees[salary > 70000]", employees);
    // Returns employees with salary > 70000

Control Flow

// If-then-else
    engine.EvaluateExpression("if age >= 18 then \"adult\" else \"minor\"", 
        new { age = 25 });
    
    // For expressions
    engine.EvaluateExpression("for x in [1,2,3] return x * 2");
    // Returns [2,4,6]
    
    // Quantified expressions
    engine.EvaluateExpression("some x in [1,2,3] satisfies x > 2");  // true
    engine.EvaluateExpression("every x in [1,2,3] satisfies x > 0"); // true

Built-in Functions

String Functions

engine.EvaluateExpression("upper case(\"hello\")");           // "HELLO"
    engine.EvaluateExpression("substring(\"hello\", 2, 3)");      // "ell"
    engine.EvaluateExpression("contains(\"hello\", \"ell\")");    // true
    engine.EvaluateExpression("starts with(\"hello\", \"he\")");  // true

Numeric Functions

engine.EvaluateExpression("abs(-5)");           // 5
    engine.EvaluateExpression("round(3.14159, 2)"); // 3.14
    engine.EvaluateExpression("min([1,2,3])");      // 1
    engine.EvaluateExpression("max([1,2,3])");      // 3

List Functions

engine.EvaluateExpression("count([1,2,3])");              // 3
    engine.EvaluateExpression("append([1,2], 3)");            // [1,2,3]
    engine.EvaluateExpression("distinct values([1,2,2,3])");  // [1,2,3]
    engine.EvaluateExpression("reverse([1,2,3])");            // [3,2,1]

Date/Time Functions

engine.EvaluateExpression("date(\"2023-06-15\")");
    engine.EvaluateExpression("time(\"14:30:00\")");
    engine.EvaluateExpression("now()");                    // Current date-time
    engine.EvaluateExpression("today()");                  // Current date

Range Operations

// Range literals
    engine.EvaluateExpression("[1..5]");           // Range from 1 to 5
    engine.EvaluateExpression("3 in [1..10]");     // true
    
    // Date ranges
    engine.EvaluateExpression("date(\"2023-06-15\") in [date(\"2023-01-01\")..date(\"2023-12-31\")]");

Advanced Usage

FeelEngineBuilder (v1.1.0)

The FeelEngineBuilder provides a fluent API for creating configured FEEL engines with custom providers.

Basic Engine Creation

// Create a basic engine (equivalent to FeelEngine.Create())
    var basicEngine = FeelEngine.Builder().Build();
    
    var result = basicEngine.EvaluateExpression("2 + 3");
    Console.WriteLine(result.Value); // NumberValue(5)

Single Custom Function Provider

var engine = FeelEngine.Builder()
        .WithCustomFunctionProvider(new MathFunctionProvider())
        .Build();
    
    // Now you can use custom functions
    var result = engine.EvaluateExpression("factorial(5)"); // 120
    var doubled = engine.EvaluateExpression("double(21)");  // 42

Multiple Providers (Enterprise Scenario)

var enterpriseEngine = FeelEngine.Builder()
        .WithCustomFunctionProvider(new MathFunctionProvider())
        .WithCustomFunctionProvider(new StringUtilsProvider())  
        .WithCustomFunctionProvider(new BusinessRulesProvider())
        .WithCustomValueMapper(new CustomObjectMapper())
        .WithAutoDiscovery(true)
        .Build();
    
    // Complex business logic
    var context = new { 
        customer = new { age = 30, tier = "premium" },
        order = new { value = 1500, items = 5 } 
    };
    
    var discountResult = enterpriseEngine.EvaluateExpression(
        "calculateTieredDiscount(customer.age, customer.tier, order.value)", context);

Auto-Discovery for Plugin Architecture

// Automatically discover and load providers from assemblies
    var pluginEngine = FeelEngine.Builder()
        .WithAutoDiscovery(true)  // Scans assemblies for ICustomFunctionProvider implementations
        .Build();
    
    // All discovered providers are automatically available
    var result = pluginEngine.EvaluateExpression("customPluginFunction(42)");

DMN Decision Tables with Builder

public class DMNDecisionEngine
    {
        private readonly IFeelEngine _engine;
    
        public DMNDecisionEngine()
        {
            _engine = FeelEngine.Builder()
                .WithCustomFunctionProvider(new BusinessRulesProvider())
                .WithCustomValueMapper(new EntityMapper())
                .Build();
        }
    
        public string DetermineDiscount(Customer customer, Order order)
        {
            var businessContext = new { 
                customerAge = customer.Age,
                customerTier = customer.Tier,
                orderValue = order.Value,
                loyaltyYears = customer.LoyaltyYears 
            };
    
            // Use unary tests with context for DMN rules
            if (_engine.EvaluateUnaryTests(">= seniorAge", customer.Age, businessContext).Value)
                return "senior_discount";
    
            if (_engine.EvaluateUnaryTests("premiumTier", customer.Tier, businessContext).Value && 
                _engine.EvaluateUnaryTests("> premiumThreshold", order.Value, businessContext).Value)
                return "premium_discount";
    
            return "standard_pricing";
        }
    }

Configuration Patterns

// Development vs Production configuration
    public static class FeelEngineFactory
    {
        public static IFeelEngine CreateForDevelopment()
        {
            return FeelEngine.Builder()
                .WithCustomFunctionProvider(new DevMathFunctionProvider())
                .WithAutoDiscovery(false)  // Explicit control in dev
                .Build();
        }
    
        public static IFeelEngine CreateForProduction()
        {
            return FeelEngine.Builder()
                .WithCustomFunctionProvider(new ProdBusinessRulesProvider())
                .WithCustomValueMapper(new OptimizedEntityMapper())
                .WithAutoDiscovery(true)   // Plugin support in prod
                .Build();
        }
    }
    
    // Usage in different environments
    var engine = Environment.IsDevelopment() 
        ? FeelEngineFactory.CreateForDevelopment()
        : FeelEngineFactory.CreateForProduction();

πŸš€ Revolutionary v1.1.0 Features

Method-to-Property Access

FeelSharp automatically exposes parameterless .NET methods as properties for seamless integration:

public class Person {
        public string Name { get; set; }
        public string GetDisplayName() => $"Person: {Name}";  // Accessible as property
        public int GetAge(DateTime at) => /* ... */;          // NOT accessible (has parameters)
    }
    
    // In FEEL expressions:
    var person = new Person { Name = "John" };
    var result = engine.EvaluateExpression("person.GetDisplayName", new { person });
    // Result: "Person: John" (method automatically invoked)

Features: - Automatic Discovery: Reflection-based parameterless method detection - Smart Filtering: Excludes Object methods, property getters/setters, special methods - Exception Safety: All method calls wrapped in try-catch with graceful fallback - Priority System: Properties take precedence over methods with same names - Performance Optimized: Efficient single-pass reflection with optimal filtering

Enhanced Dual-Mode Error Handling

FeelSharp provides world’s first dual-mode FEEL implementation:

// FEEL Compliance Mode (standard behavior)
    var result = engine.EvaluateExpression("10 / 0");
    Console.WriteLine(result.Value); // null (per FEEL specification)
    
    // Dual-Mode with Diagnostics  
    var resultWithFailures = engine.EvaluateExpressionWithFailures("10 / 0");
    Console.WriteLine(resultWithFailures.Value);              // null (FEEL compliant)
    Console.WriteLine(resultWithFailures.SuppressedFailures); // ["ArithmeticError: Division by zero"]

Benefits: - FEEL Specification Compliance: Operations return null per standard - Diagnostic Reporting: Simultaneous collection of detailed error information - Enhanced Error Detection: Specific property name extraction and context analysis - Production Ready: Both compliance and debugging capabilities

Case-Insensitive Property Access

Enhanced context member access with intelligent fallback:

var context = new { Name = "John", AGE = 25 };
    
    // Both work seamlessly:
    engine.EvaluateExpression("Name", context);   // Direct match
    engine.EvaluateExpression("name", context);   // Case-insensitive fallback
    engine.EvaluateExpression("age", context);    // Finds "AGE"

Features: - Exact Match Priority: Direct case matches preferred - Fallback Lookup: Automatic case-insensitive search - .NET Integration: Works with all .NET object properties - Performance Optimized: Efficient lookup with minimal overhead

Custom Functions

FeelSharp supports custom functions through the ICustomFunctionProvider interface:

// Define a custom function provider
    public class MathFunctionProvider : ICustomFunctionProvider
    {
        public FeelFunction? GetFunction(string name) => name switch
        {
            "factorial" => new FeelFunction("factorial", new[] { "n" }, args =>
            {
                if (args[0] is NumberValue num)
                    return new NumberValue(Factorial((int)num.Value));
                return NullValue.Instance;
            }),
            "double" => new FeelFunction("double", new[] { "x" }, args =>
            {
                if (args[0] is NumberValue num)
                    return new NumberValue(num.Value * 2);
                return NullValue.Instance;
            }),
            _ => null
        };
        
        public IEnumerable<FeelFunction> GetFunctions(string name) =>
            GetFunction(name) is { } func ? new[] { func } : Enumerable.Empty<FeelFunction>();
        
        public IEnumerable<string> FunctionNames => new[] { "factorial", "double" };
        
        private static decimal Factorial(int n) => n <= 1 ? 1 : n * Factorial(n - 1);
    }
    
    // C# FeelEngineBuilder integration (v1.1.0) - PRODUCTION READY
    var engine = FeelEngine.Builder()
        .WithCustomFunctionProvider(new MathFunctionProvider())
        .Build();
    
    // Use custom functions in expressions with the configured engine
    var factorialResult = engine.EvaluateExpression("factorial(5)"); // 120
    var doubleResult = engine.EvaluateExpression("double(21)");   // 42
    
    // Multiple providers can be chained
    var advancedEngine = FeelEngine.Builder()
        .WithCustomFunctionProvider(new MathFunctionProvider())
        .WithCustomFunctionProvider(new StringUtilsProvider())
        .WithCustomValueMapper(new CustomObjectMapper())
        .WithAutoDiscovery(true)
        .Build();
    
    // Enterprise scenarios with business rules
    var businessEngine = FeelEngine.Builder()
        .WithCustomFunctionProvider(new BusinessRulesProvider())
        .Build();
    
    var customerContext = new { age = 30, tier = "premium", orderValue = 1500 };
    var discountRule = businessEngine.EvaluateExpression(
        "calculateDiscount(age, tier, orderValue)", customerContext);

Custom Value Mappers

FeelSharp supports custom object mapping through the ICustomValueMapper interface:

// Define a custom value mapper for GUID objects
    public class GuidValueMapper : ICustomValueMapper
    {
        public int Priority => 10; // Higher priority = evaluated first
        
        public FeelValue? ToFeelValue(object value, Func<object, FeelValue> innerMapper)
        {
            return value is Guid guid ? new StringValue(guid.ToString()) : null;
        }
        
        public object? FromFeelValue(FeelValue feelValue, Func<FeelValue, object> innerMapper)
        {
            return feelValue is StringValue str && Guid.TryParse(str.Value, out var guid) 
                ? guid : null;
        }
    }
    
    // Use custom mappers with value mapping context
    var customMappers = new[] { new GuidValueMapper() };
    var context = new ValueMappingContext(customMappers);
    
    // Convert custom objects to FEEL values
    var guid = Guid.NewGuid();
    var feelValue = context.ToFeelValue(guid); // StringValue("guid-string")
    
    // Automatic discovery (providers are auto-discovered from assemblies)
    // Requires: using FeelSharp.Core;
    var functionProviders = ServiceLoader.loadFunctionProviders();
    var valueMappers = ServiceLoader.loadValueMappers();

Performance Optimization

// Reuse engine instance
    private static readonly IFeelEngine Engine = FeelEngine.Create();
    
    // Parse once, evaluate multiple times
    var parsed = engine.ParseExpression("age > threshold");
    if (parsed.IsSuccess)
    {
        // Use parsed expression for validation or metadata
        Console.WriteLine($"Expression parsed successfully: {parsed.Value.Expression}");
        
        // Evaluate with different contexts
        var result1 = engine.EvaluateExpression("age > threshold", new { age = 25, threshold = 18 });
        var result2 = engine.EvaluateExpression("age > threshold", new { age = 16, threshold = 18 });
    }

DMN Integration

Unary Tests for Decision Tables

// Simple comparisons
    engine.EvaluateUnaryTests("> 18", 25);        // true
    engine.EvaluateUnaryTests("< 100", 50);       // true
    engine.EvaluateUnaryTests("= \"premium\"", "premium"); // true
    
    // Range tests
    engine.EvaluateUnaryTests("[18..65]", 30);    // true (age between 18 and 65)
    engine.EvaluateUnaryTests("> 18, < 65", 30);  // true (multiple conditions)
    
    // List membership
    engine.EvaluateUnaryTests("\"gold\", \"silver\", \"bronze\"", "silver"); // true

Unary Tests with Context Variables (NEW in v1.1.0)

Dynamic decision tables with context variables enable flexible business rules:

// Define business rule context
    var businessRules = new {
        minAge = 21,
        seniorAge = 65,
        premiumThreshold = 1000,
        discountTiers = new[] { "gold", "platinum" }
    };
    
    // Use context variables in unary tests
    engine.EvaluateUnaryTests("> minAge", 25, businessRules);           // true (25 > 21)
    engine.EvaluateUnaryTests(">= seniorAge", 70, businessRules);       // true (70 >= 65)
    engine.EvaluateUnaryTests("> premiumThreshold", 1500, businessRules); // true (1500 > 1000)
    
    // Context variables take precedence - input value accessible as "?" if needed
    engine.EvaluateUnaryTests("? > minAge", 30, businessRules);         // true (30 > 21)

Enhanced Decision Table Implementation

public class DecisionTable
    {
        private readonly IFeelEngine engine = FeelEngine.Create();
        
        // Business rules as configurable context
        private readonly object businessRules = new {
            seniorAge = 65,
            premiumCustomerType = "premium",
            premiumThreshold = 100,
            bulkThreshold = 500
        };
        
        public string DetermineDiscount(int age, string customerType, decimal orderValue)
        {
            // Rule 1: Senior discount (using context variables)
            if (engine.EvaluateUnaryTests(">= seniorAge", age, businessRules).Value)
                return "senior_discount";
                
            // Rule 2: Premium customer (context-aware)
            if (engine.EvaluateUnaryTests("premiumCustomerType", customerType, businessRules).Value && 
                engine.EvaluateUnaryTests("> premiumThreshold", orderValue, businessRules).Value)
                return "premium_discount";
                
            // Rule 3: Large order (context-aware)
            if (engine.EvaluateUnaryTests("> bulkThreshold", orderValue, businessRules).Value)
                return "bulk_discount";
                
            return "no_discount";
        }
        
        // Legacy implementation for comparison
        public string DetermineDiscountLegacy(int age, string customerType, decimal orderValue)
        {
            // Original hardcoded approach
            if (engine.EvaluateUnaryTests(">= 65", age).Value)
                return "senior_discount";
                
            if (engine.EvaluateUnaryTests("\"premium\"", customerType).Value && 
                engine.EvaluateUnaryTests("> 100", orderValue).Value)
                return "premium_discount";
                
            if (engine.EvaluateUnaryTests("> 500", orderValue).Value)
                return "bulk_discount";
                
            return "no_discount";
        }
    }

Complex Decision Logic

public class LoanApproval
    {
        private readonly IFeelEngine engine = FeelEngine.Create();
        
        public bool ApproveLoan(object applicant)
        {
            var expression = @"
                creditScore >= 650 and 
                income >= 50000 and 
                (employmentYears >= 2 or assets >= 100000) and
                debtToIncome <= 0.4
            ";
            
            var result = engine.EvaluateExpression(expression, applicant);
            return result.IsSuccess && result.Value is BooleanValue bv && bv.Value;
        }
    }

Performance & Best Practices

Engine Reuse

// βœ… Good: Reuse engine instance
    public class FeelService
    {
        private static readonly IFeelEngine Engine = FeelEngine.Create();
        
        public FeelResult<FeelValue> Evaluate(string expression, object context)
        {
            return Engine.EvaluateExpression(expression, context);
        }
    }
    
    // ❌ Bad: Create engine for each evaluation
    public FeelResult<FeelValue> Evaluate(string expression, object context)
    {
        var engine = FeelEngine.Create(); // Expensive!
        return engine.EvaluateExpression(expression, context);
    }

Context Optimization

// βœ… Good: Use strongly typed objects
    var context = new { age = 25, name = "John" };
    var result = engine.EvaluateExpression("age > 18", context);
    
    // βœ… Good: Reuse context objects
    var contextTemplate = new { age = 0, name = "" };
    // ... modify properties as needed
    
    // ❌ Avoid: Large dictionaries with unused keys
    var context = new Dictionary<string, object>();
    // ... with 100+ keys when only 2 are used

Expression Optimization

// βœ… Good: Simple expressions
    "age >= 18"
    "status = \"active\""
    
    // βœ… Good: Efficient list operations
    "count(items) > 0"
    
    // ⚠️ Consider: Complex nested expressions
    "for x in items return (for y in x.details return y.value * multiplier)"

Memory Management

// Engine is thread-safe and can be shared
    public static class FeelEngineFactory 
    {
        private static readonly Lazy<IFeelEngine> LazyEngine = 
            new(() => FeelEngine.Create());
        
        public static IFeelEngine Instance => LazyEngine.Value;
    }

Troubleshooting

Common Errors

Parse Errors

// ❌ Invalid syntax
    var result = engine.EvaluateExpression("2 + + 3");
    // Error: Parse error at position 4: unexpected '+'
    
    // βœ… Fix: Correct syntax
    var result = engine.EvaluateExpression("2 + 3");

Variable Not Found

// ❌ Variable doesn't exist in context
    var result = engine.EvaluateExpression("unknownVar", new { age = 25 });
    // Error: NO_VARIABLE_FOUND: unknownVar
    
    // βœ… Fix: Use correct variable name
    var result = engine.EvaluateExpression("age", new { age = 25 });

Type Mismatches

// ❌ Wrong type operation
    var result = engine.EvaluateExpression("\"hello\" + 5");
    // Returns null (FEEL handles type mismatches gracefully)
    
    // βœ… Better: Consistent types
    var result = engine.EvaluateExpression("\"hello\" + string(5)");

Debugging Tips

Enable Detailed Error Information

var result = engine.EvaluateExpression("complex expression");
    if (result.IsFailure)
    {
        Console.WriteLine($"Error: {result.Error}");
        // Provides detailed error information
    }

Test Expressions Incrementally

// Start simple
    var result1 = engine.EvaluateExpression("age", context);
    Console.WriteLine($"age = {result1.Value}");
    
    // Add complexity gradually  
    var result2 = engine.EvaluateExpression("age > 18", context);
    Console.WriteLine($"age > 18 = {result2.Value}");
    
    // Full expression
    var result3 = engine.EvaluateExpression("age > 18 and status = \"active\"", context);

Validate Context Data

public static void ValidateContext(object context)
    {
        var json = System.Text.Json.JsonSerializer.Serialize(context, 
            new JsonSerializerOptions { WriteIndented = true });
        Console.WriteLine($"Context: {json}");
    }

Performance Issues

Slow Expression Evaluation

  1. Check expression complexity - Avoid deeply nested expressions
  2. Optimize context size - Only include needed properties
  3. Reuse engine instance - Don’t create new engines repeatedly
  4. Profile your expressions - Time critical evaluations

Memory Usage

  1. Monitor context object size - Large contexts use more memory
  2. Dispose of results - Don’t hold references to large result sets
  3. Use appropriate data types - Prefer primitive types when possible

Getting Help

Examples Repository

For complete working examples, see the CLI sample application:

samples/FeelSharp.Cli/Program.cs

This demonstrates: - Basic expression evaluation - Context handling - Error management - Interactive FEEL evaluation


This guide covers FeelSharp v1.1.0 with complete custom function and value mapper support. For the latest updates, check the NuGet page or visit our documentation site.