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.0using FeelSharp;
var engine = FeelEngine.Create();var result = engine.EvaluateExpression("2 + 3 * 4");
Console.WriteLine(result.Value); // NumberValue(14)The main interface for evaluating FEEL expressions.
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.0Evaluates 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).
Represents the result of a FEEL operation with success/failure handling.
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)T GetValueOrThrow() // Throws exception if failed
T GetValueOrDefault(T defaultValue) // Returns default if failed
U Map<U>(Func<T, U> mapper) // Transform successful result// 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);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// 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// 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"); // truevar 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"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// 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"); // trueengine.EvaluateExpression("upper case(\"hello\")"); // "HELLO"
engine.EvaluateExpression("substring(\"hello\", 2, 3)"); // "ell"
engine.EvaluateExpression("contains(\"hello\", \"ell\")"); // true
engine.EvaluateExpression("starts with(\"hello\", \"he\")"); // trueengine.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])"); // 3engine.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// 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\")]");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");
Console.WriteLine(result.Value); // NumberValue(5)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)"); // 42var 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);// 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()
{
_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";
}
}// 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();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");
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
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
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)
? 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();// 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 });
}// 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"); // trueDynamic 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)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";
}
}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;
}
}// β
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)
{
Console.WriteLine($"Error: {result.Error}");
// Provides detailed error information
}// 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);public static void ValidateContext(object context)
{
var json = System.Text.Json.JsonSerializer.Serialize(context,
new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine($"Context: {json}");
}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.