Interpreter Pattern Real-World Examples
Production-ready examples demonstrating the Interpreter pattern in real-world scenarios.
Example 1: E-Commerce Pricing Engine
The Problem
An e-commerce platform needs to evaluate complex discount rules that vary by customer tier, cart total, promotional periods, and promo codes. Rules need to be:
- Defined by business analysts, not developers
- Composable (combine multiple discounts)
- Testable without code changes
- Executable in real-time during checkout
The Solution
Use the Interpreter pattern to create a pricing DSL that evaluates discount expressions against order context.
The Code
public sealed class PricingContext
{
public decimal CartTotal { get; set; }
public int ItemCount { get; set; }
public string CustomerTier { get; set; } = "Standard";
public bool IsHoliday { get; set; }
public string PromoCode { get; set; } = "";
public Dictionary<string, decimal> Variables { get; set; } = new();
}
public static Interpreter<PricingContext, decimal> CreatePricingInterpreter()
{
return Interpreter.Create<PricingContext, decimal>()
// Terminals: Read values from context
.Terminal("number", token => decimal.Parse(token))
.Terminal("percent", token => decimal.Parse(token.TrimEnd('%')) / 100m)
.Terminal("var", (token, ctx) => token switch
{
"cart_total" => ctx.CartTotal,
"item_count" => ctx.ItemCount,
"tier_discount" => ctx.CustomerTier switch
{
"Gold" => 0.10m,
"Platinum" => 0.15m,
"Diamond" => 0.20m,
_ => 0m
},
_ => ctx.Variables.GetValueOrDefault(token, 0m)
})
// Non-terminals: Arithmetic
.Binary("add", (l, r) => l + r)
.Binary("sub", (l, r) => l - r)
.Binary("mul", (l, r) => l * r)
.Binary("div", (l, r) => r != 0 ? l / r : 0m)
.Binary("min", Math.Min)
.Binary("max", Math.Max)
.Unary("round", v => Math.Round(v, 2))
// Non-terminals: Comparisons (return 1 for true, 0 for false)
.Binary("gt", (l, r) => l > r ? 1m : 0m)
.Binary("gte", (l, r) => l >= r ? 1m : 0m)
.Binary("lt", (l, r) => l < r ? 1m : 0m)
.Binary("eq", (l, r) => l == r ? 1m : 0m)
// Non-terminal: Conditional
.NonTerminal("if", (args, _) =>
args[0] > 0 ? args[1] : args[2])
.Build();
}
// Usage
var interpreter = CreatePricingInterpreter();
// Rule: 10% tier discount on cart total
var tierDiscountRule = NonTerminal("round",
NonTerminal("mul",
Terminal("var", "cart_total"),
Terminal("var", "tier_discount")));
// Rule: $5 off if cart > $50
var thresholdRule = NonTerminal("if",
NonTerminal("gt", Terminal("var", "cart_total"), Terminal("number", "50")),
Terminal("number", "5"),
Terminal("number", "0"));
var context = new PricingContext
{
CartTotal = 150m,
CustomerTier = "Gold"
};
var tierDiscount = interpreter.Interpret(tierDiscountRule, context); // 15.00
var thresholdDiscount = interpreter.Interpret(thresholdRule, context); // 5.00
Why This Pattern
- Business agility: Rules can be stored in a database and changed without deployment
- Testability: Each rule can be unit tested in isolation
- Composability: Complex discounts are built from simple, reusable expressions
- Type safety: The interpreter ensures expressions evaluate to the correct type
Alternative Approaches
| Approach | Trade-offs |
|---|---|
| Hardcoded rules | Faster but requires code changes for every rule update |
| Dynamic compilation | More powerful but security and complexity concerns |
| Expression trees | More .NET-native but steeper learning curve |
| Rule engine library | Feature-rich but external dependency |
Example 2: Query Filter Language
The Problem
A REST API needs to support complex filtering on list endpoints. Users should be able to filter with expressions like:
status = 'active' AND (priority > 5 OR assignee = 'admin')
The Solution
Create a boolean expression interpreter that evaluates filter conditions against entity properties.
The Code
public sealed class FilterContext
{
public Dictionary<string, object> Entity { get; set; } = new();
public object GetProperty(string name) =>
Entity.TryGetValue(name, out var value) ? value : null!;
}
public static Interpreter<FilterContext, bool> CreateFilterInterpreter()
{
return Interpreter.Create<FilterContext, bool>()
// Terminals
.Terminal("true", _ => true)
.Terminal("false", _ => false)
.Terminal("string", token => throw new InvalidOperationException("Use in comparison"))
// Property comparisons
.NonTerminal("eq", (args, ctx) => Compare(args, ctx, "eq"))
.NonTerminal("neq", (args, ctx) => Compare(args, ctx, "neq"))
.NonTerminal("gt", (args, ctx) => Compare(args, ctx, "gt"))
.NonTerminal("gte", (args, ctx) => Compare(args, ctx, "gte"))
.NonTerminal("lt", (args, ctx) => Compare(args, ctx, "lt"))
.NonTerminal("lte", (args, ctx) => Compare(args, ctx, "lte"))
.NonTerminal("contains", (args, ctx) => Compare(args, ctx, "contains"))
// Logical operators
.Binary("and", (l, r) => l && r)
.Binary("or", (l, r) => l || r)
.Unary("not", v => !v)
.Build();
}
// Helper for property access in comparisons
private static bool Compare(bool[] _, FilterContext ctx, string op)
{
// In practice, you'd use a different approach to pass property/value pairs
// This is simplified for demonstration
throw new NotImplementedException("Use typed comparison expressions");
}
// Alternative: Use a different result type for comparisons
public record ComparisonResult(string Property, string Op, object Value);
public static Interpreter<FilterContext, object> CreateFlexibleFilterInterpreter()
{
return Interpreter.Create<FilterContext, object>()
// Terminals
.Terminal("prop", (name, _) => name)
.Terminal("string", (value, _) => value)
.Terminal("number", (value, _) => double.Parse(value))
.Terminal("bool", (value, _) => bool.Parse(value))
// Comparisons - return bool
.Binary("eq", (prop, value, ctx) =>
{
var actual = ctx.GetProperty((string)prop);
return actual?.Equals(value) ?? false;
})
.Binary("gt", (prop, value, ctx) =>
{
var actual = ctx.GetProperty((string)prop);
if (actual is IComparable c && value is IComparable v)
return c.CompareTo(v) > 0;
return false;
})
// Logical - expect bools
.Binary("and", (l, r) => (bool)l && (bool)r)
.Binary("or", (l, r) => (bool)l || (bool)r)
.Unary("not", v => !(bool)v)
.Build();
}
Why This Pattern
- User-defined queries: End users can create their own filter logic
- Secure evaluation: No direct code execution, just expression interpretation
- Extensible operators: Easy to add new comparison or logical operators
Example 3: Mathematical Formula Evaluator
The Problem
A spreadsheet application needs to evaluate cell formulas like =SUM(A1:A10) * 1.08 with support for functions, cell references, and arithmetic.
The Solution
Build an interpreter for spreadsheet formulas with support for cell references, ranges, and functions.
The Code
public sealed class SpreadsheetContext
{
private readonly Dictionary<string, double> _cells = new();
public double GetCell(string reference) =>
_cells.TryGetValue(reference.ToUpper(), out var value) ? value : 0;
public void SetCell(string reference, double value) =>
_cells[reference.ToUpper()] = value;
public IEnumerable<double> GetRange(string start, string end)
{
// Simplified: assumes same column, sequential rows
// e.g., A1:A5 returns A1, A2, A3, A4, A5
var col = start[0];
var startRow = int.Parse(start[1..]);
var endRow = int.Parse(end[1..]);
for (var row = startRow; row <= endRow; row++)
yield return GetCell($"{col}{row}");
}
}
public static Interpreter<SpreadsheetContext, double> CreateFormulaInterpreter()
{
return Interpreter.Create<SpreadsheetContext, double>()
// Terminals
.Terminal("number", token => double.Parse(token))
.Terminal("cell", (ref_, ctx) => ctx.GetCell(ref_))
// Arithmetic
.Binary("add", (l, r) => l + r)
.Binary("sub", (l, r) => l - r)
.Binary("mul", (l, r) => l * r)
.Binary("div", (l, r) => r != 0 ? l / r : double.NaN)
.Unary("neg", v => -v)
.Binary("pow", Math.Pow)
// Functions (non-terminals with variable children)
.NonTerminal("sum", (args, _) => args.Sum())
.NonTerminal("avg", (args, _) => args.Average())
.NonTerminal("min", (args, _) => args.Min())
.NonTerminal("max", (args, _) => args.Max())
.NonTerminal("count", (args, _) => args.Length)
// Conditional
.NonTerminal("if", (args, _) =>
args[0] != 0 ? args[1] : args[2])
// Comparison
.Binary("gt", (l, r) => l > r ? 1 : 0)
.Binary("eq", (l, r) => l == r ? 1 : 0)
.Build();
}
// Usage
var interpreter = CreateFormulaInterpreter();
var ctx = new SpreadsheetContext();
ctx.SetCell("A1", 10);
ctx.SetCell("A2", 20);
ctx.SetCell("A3", 30);
// Formula: SUM(A1, A2, A3) * 1.08
var formula = NonTerminal("mul",
NonTerminal("sum",
Terminal("cell", "A1"),
Terminal("cell", "A2"),
Terminal("cell", "A3")),
Terminal("number", "1.08"));
var result = interpreter.Interpret(formula, ctx); // 64.8
Why This Pattern
- Dynamic formulas: Users create formulas that reference other cells
- Recalculation: When cells change, dependent formulas are re-evaluated
- Extensible functions: Easy to add new spreadsheet functions
Example 4: Access Control Policy Language
The Problem
An enterprise application needs fine-grained access control with policies like:
allow if (role = 'admin') or (role = 'manager' and department = resource.department)
The Solution
Create a policy interpreter that evaluates access decisions based on user and resource attributes.
The Code
public sealed class PolicyContext
{
public string UserId { get; set; } = "";
public string Role { get; set; } = "";
public string Department { get; set; } = "";
public Dictionary<string, string> ResourceAttributes { get; set; } = new();
public string GetUserAttr(string name) => name switch
{
"role" => Role,
"department" => Department,
"id" => UserId,
_ => ""
};
public string GetResourceAttr(string name) =>
ResourceAttributes.TryGetValue(name, out var v) ? v : "";
}
public static Interpreter<PolicyContext, bool> CreatePolicyInterpreter()
{
return Interpreter.Create<PolicyContext, bool>()
// Terminals
.Terminal("true", _ => true)
.Terminal("false", _ => false)
.Terminal("user", (attr, ctx) => ctx.GetUserAttr(attr))
.Terminal("resource", (attr, ctx) => ctx.GetResourceAttr(attr))
.Terminal("string", (value, _) => value)
// String comparison (returns bool)
.NonTerminal("eq", (args, _) =>
{
// args are actually objects - need type-aware handling
return args[0].Equals(args[1]);
})
// Logical
.Binary("and", (l, r) => l && r)
.Binary("or", (l, r) => l || r)
.Unary("not", v => !v)
.Build();
}
Why This Pattern
- Declarative policies: Security policies are expressed as data, not code
- Auditable: Policies can be logged and reviewed
- Externalized: Policies can be stored and updated without deployment
Key Takeaways
Choose the right abstraction: The Interpreter pattern works best for simple, well-defined grammars
Keep expressions simple: Complex logic is better handled by multiple simple interpreters
Use context for state: Pass environment through context, keep expressions pure
Consider persistence: Expressions are data and can be stored, versioned, and audited
Combine with other patterns: Use Factory for interpreter creation, Strategy for selecting interpreters
See Also
- Overview
- Comprehensive Guide
- API Reference
- InterpreterDemo.cs - Full working example