Comprehensive guide for C# developers using FeelSharp - the complete .NET FEEL (Friendly Enough Expression Language) implementation with integrated compilation engine.
dotnet add package FeelSharp --version 1.3.0
using FeelSharp;
var engine = FeelEngine.Create();
var result = engine.EvaluateExpression("2 + 3 * 4");
WriteLine(result.Value); // NumberValue(14) Console.
The main interface for evaluating FEEL expressions.
EvaluateExpression
EvaluateExpression(string expression, object? context = null) FeelResult<FeelValue>
Evaluates a FEEL expression and returns the result.
EvaluateUnaryTests
bool> EvaluateUnaryTests(string tests, object input)
FeelResult<bool> EvaluateUnaryTests(string tests, object input, object? context) // NEW in v1.1.0 FeelResult<
Evaluates unary tests (for DMN decision tables). The context overload allows referencing variables in unary tests.
ParseExpression
ParseExpression(string expression) FeelResult<ParsedExpression>
Parses an expression without evaluating it (useful for validation).
Represents the result of a FEEL operation with success/failure handling.
bool IsSuccess // True if operation succeeded
bool IsFailure // True if operation failed
// The result value (null if failed)
T? Value string? Error // Error message (null if succeeded)
GetValueOrThrow() // Throws exception if failed
T GetValueOrDefault(T defaultValue) // Returns default if failed
T // Transform successful result U Map<U>(Func<T, U> mapper)
// Pattern 1: Check IsSuccess
var result = engine.EvaluateExpression("2 + 3");
if (result.IsSuccess)
{WriteLine($"Result: {result.Value}");
Console.
}else
{WriteLine($"Error: {result.Error}");
Console.
}
// Pattern 2: Exception handling
try
{var value = result.GetValueOrThrow();
WriteLine($"Result: {value}");
Console.
}catch (FeelEvaluationException ex)
{WriteLine($"Error: {ex.Message}");
Console.
}
// Pattern 3: Default values
var value = result.GetValueOrDefault(NumberValue.Zero);
FEEL values are represented by these types:
// decimal numbers
NumberValue // strings
StringValue // true/false
BooleanValue // null
NullValue // lists/arrays
ListValue // objects/maps
ContextValue // dates
DateValue // times
TimeValue // date-times
DateTimeValue // durations DurationValue
// 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
42; // Implicit NumberValue
FeelValue numberValue = "Hello"; // Implicit StringValue
FeelValue stringValue = true; // Implicit BooleanValue FeelValue boolValue =
// Arithmetic
EvaluateExpression("2 + 3 * 4"); // 14
engine.EvaluateExpression("10 / 3"); // 3.333...
engine.EvaluateExpression("2 ** 3"); // 8 (power)
engine.
// Comparison
EvaluateExpression("5 > 3"); // true
engine.EvaluateExpression("\"abc\" = \"abc\""); // true
engine.
// Logical
EvaluateExpression("true and false"); // false
engine.EvaluateExpression("not false"); // true engine.
var person = new {
"John",
name = 30,
age = new { city = "Berlin", country = "Germany" }
address =
};
// 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"
var data = new { numbers = new[] { 1, 2, 3, 4, 5 } };
// List operations
EvaluateExpression("count(numbers)", data); // 5
engine.EvaluateExpression("sum(numbers)", data); // 15
engine.EvaluateExpression("numbers[1]", data); // 1 (1-based indexing)
engine.
// List filtering
var employees = new {
new[] {
employees = new { name = "John", salary = 80000 },
new { name = "Jane", salary = 90000 },
new { name = "Bob", salary = 60000 }
}
};
EvaluateExpression("employees[salary > 70000]", employees);
engine.// Returns employees with salary > 70000
// If-then-else
EvaluateExpression("if age >= 18 then \"adult\" else \"minor\"",
engine.new { age = 25 });
// For expressions
EvaluateExpression("for x in [1,2,3] return x * 2");
engine.// Returns [2,4,6]
// Quantified expressions
EvaluateExpression("some x in [1,2,3] satisfies x > 2"); // true
engine.EvaluateExpression("every x in [1,2,3] satisfies x > 0"); // true 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 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 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] engine.
EvaluateExpression("date(\"2023-06-15\")");
engine.EvaluateExpression("time(\"14:30:00\")");
engine.EvaluateExpression("now()"); // Current date-time
engine.EvaluateExpression("today()"); // Current date engine.
// Range literals
EvaluateExpression("[1..5]"); // Range from 1 to 5
engine.EvaluateExpression("3 in [1..10]"); // true
engine.
// Date ranges
EvaluateExpression("date(\"2023-06-15\") in [date(\"2023-01-01\")..date(\"2023-12-31\")]"); engine.
The FeelEngineBuilder provides a fluent API for creating configured FEEL engines with custom providers.
// Create a basic engine (equivalent to FeelEngine.Create())
var basicEngine = FeelEngine.Builder().Build();
var result = basicEngine.EvaluateExpression("2 + 3");
WriteLine(result.Value); // NumberValue(5) Console.
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
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 {
new { age = 30, tier = "premium" },
customer = new { value = 1500, items = 5 }
order =
};
var discountResult = enterpriseEngine.EvaluateExpression(
"calculateTieredDiscount(customer.age, customer.tier, order.value)", context);
// 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)");
public class DMNDecisionEngine
{private readonly IFeelEngine _engine;
public DMNDecisionEngine()
{Builder()
_engine = FeelEngine.WithCustomFunctionProvider(new BusinessRulesProvider())
.WithCustomValueMapper(new EntityMapper())
.Build();
.
}
public string DetermineDiscount(Customer customer, Order order)
{var businessContext = new {
Age,
customerAge = customer.Tier,
customerTier = customer.Value,
orderValue = order.LoyaltyYears
loyaltyYears = customer.
};
// 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 &&
EvaluateUnaryTests("> premiumThreshold", order.Value, businessContext).Value)
_engine.return "premium_discount";
return "standard_pricing";
} }
// 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()
CreateForDevelopment()
? FeelEngineFactory.CreateForProduction(); : FeelEngineFactory.
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
FeelSharp provides worldβs first dual-mode FEEL implementation:
// FEEL Compliance Mode (standard behavior)
var result = engine.EvaluateExpression("10 / 0");
WriteLine(result.Value); // null (per FEEL specification)
Console.
// Dual-Mode with Diagnostics
var resultWithFailures = engine.EvaluateExpressionWithFailures("10 / 0");
WriteLine(resultWithFailures.Value); // null (FEEL compliant)
Console.WriteLine(resultWithFailures.SuppressedFailures); // ["ArithmeticError: Division by zero"] Console.
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
Enhanced context member access with intelligent fallback:
var context = new { Name = "John", AGE = 25 };
// Both work seamlessly:
EvaluateExpression("Name", context); // Direct match
engine.EvaluateExpression("name", context); // Case-insensitive fallback
engine.EvaluateExpression("age", context); // Finds "AGE" engine.
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
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);
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)
null;
? guid :
}
}
// 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();
// 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
WriteLine($"Expression parsed successfully: {parsed.Value.Expression}");
Console.
// 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 });
}
// Simple comparisons
EvaluateUnaryTests("> 18", 25); // true
engine.EvaluateUnaryTests("< 100", 50); // true
engine.EvaluateUnaryTests("= \"premium\"", "premium"); // true
engine.
// Range tests
EvaluateUnaryTests("[18..65]", 30); // true (age between 18 and 65)
engine.EvaluateUnaryTests("> 18, < 65", 30); // true (multiple conditions)
engine.
// List membership
EvaluateUnaryTests("\"gold\", \"silver\", \"bronze\"", "silver"); // true engine.
Dynamic decision tables with context variables enable flexible business rules:
// Define business rule context
var businessRules = new {
21,
minAge = 65,
seniorAge = 1000,
premiumThreshold = new[] { "gold", "platinum" }
discountTiers =
};
// Use context variables in unary tests
EvaluateUnaryTests("> minAge", 25, businessRules); // true (25 > 21)
engine.EvaluateUnaryTests(">= seniorAge", 70, businessRules); // true (70 >= 65)
engine.EvaluateUnaryTests("> premiumThreshold", 1500, businessRules); // true (1500 > 1000)
engine.
// Context variables take precedence - input value accessible as "?" if needed
EvaluateUnaryTests("? > minAge", 30, businessRules); // true (30 > 21) engine.
public class DecisionTable
{private readonly IFeelEngine engine = FeelEngine.Create();
// Business rules as configurable context
private readonly object businessRules = new {
65,
seniorAge = "premium",
premiumCustomerType = 100,
premiumThreshold = 500
bulkThreshold =
};
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 &&
EvaluateUnaryTests("> premiumThreshold", orderValue, businessRules).Value)
engine.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 &&
EvaluateUnaryTests("> 100", orderValue).Value)
engine.return "premium_discount";
if (engine.EvaluateUnaryTests("> 500", orderValue).Value)
return "bulk_discount";
return "no_discount";
} }
public class LoanApproval
{private readonly IFeelEngine engine = FeelEngine.Create();
public bool ApproveLoan(object applicant)
{var expression = @"
650 and
creditScore >= 50000 and
income >= 2 or assets >= 100000) and
(employmentYears >= 0.4
debtToIncome <= ";
var result = engine.EvaluateExpression(expression, applicant);
return result.IsSuccess && result.Value is BooleanValue bv && bv.Value;
} }
// β
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);
}
// β
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
// β
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)"
// 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;
}
// β 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 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 });
// β 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)");
var result = engine.EvaluateExpression("complex expression");
if (result.IsFailure)
{WriteLine($"Error: {result.Error}");
Console.// Provides detailed error information
}
// Start simple
var result1 = engine.EvaluateExpression("age", context);
WriteLine($"age = {result1.Value}");
Console.
// Add complexity gradually
var result2 = engine.EvaluateExpression("age > 18", context);
WriteLine($"age > 18 = {result2.Value}");
Console.
// Full expression
var result3 = engine.EvaluateExpression("age > 18 and status = \"active\"", context);
public static void ValidateContext(object context)
{var json = System.Text.Json.JsonSerializer.Serialize(context,
new JsonSerializerOptions { WriteIndented = true });
WriteLine($"Context: {json}");
Console. }
samples/FeelSharp.Cli/
for working examplesFor 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.