.NET & C# Development · Lesson 45 of 229
Interpreter — Grammar as Code
Interpreter — Grammar as Code
The Interpreter pattern defines a representation for a grammar and provides an interpreter to process sentences in that grammar. It turns a domain language into an executable class hierarchy.
Mathematical Expression Evaluator
// Abstract expression — every node in the syntax tree
public interface IExpression
{
double Evaluate(Dictionary<string, double> context);
}
// Terminal: a literal number
public class NumberExpression(double value) : IExpression
{
public double Evaluate(Dictionary<string, double> _) => value;
}
// Terminal: a variable reference
public class VariableExpression(string name) : IExpression
{
public double Evaluate(Dictionary<string, double> ctx)
=> ctx.TryGetValue(name, out var val) ? val
: throw new KeyNotFoundException($"Variable '{name}' not defined");
}
// Non-terminal: binary operations
public class AddExpression(IExpression left, IExpression right) : IExpression
{
public double Evaluate(Dictionary<string, double> ctx)
=> left.Evaluate(ctx) + right.Evaluate(ctx);
}
public class MultiplyExpression(IExpression left, IExpression right) : IExpression
{
public double Evaluate(Dictionary<string, double> ctx)
=> left.Evaluate(ctx) * right.Evaluate(ctx);
}
public class SubtractExpression(IExpression left, IExpression right) : IExpression
{
public double Evaluate(Dictionary<string, double> ctx)
=> left.Evaluate(ctx) - right.Evaluate(ctx);
}
// Build expression tree for: (a + b) * (c - 2)
var context = new Dictionary<string, double> { ["a"] = 3, ["b"] = 7, ["c"] = 5 };
IExpression expr = new MultiplyExpression(
new AddExpression(
new VariableExpression("a"),
new VariableExpression("b")
),
new SubtractExpression(
new VariableExpression("c"),
new NumberExpression(2)
)
);
Console.WriteLine(expr.Evaluate(context)); // (3+7) * (5-2) = 30Business Rule Engine
// Boolean expression interpreter for eligibility rules
public interface IRule<T>
{
bool IsSatisfied(T subject);
string Description { get; }
}
public class MinimumAgeRule(int minAge) : IRule<Customer>
{
public string Description => $"Age >= {minAge}";
public bool IsSatisfied(Customer c) => c.Age >= minAge;
}
public class MinimumSpendRule(decimal threshold) : IRule<Customer>
{
public string Description => $"Total spend >= £{threshold}";
public bool IsSatisfied(Customer c) => c.TotalSpend >= threshold;
}
public class AndRule<T>(IRule<T> left, IRule<T> right) : IRule<T>
{
public string Description => $"({left.Description} AND {right.Description})";
public bool IsSatisfied(T s) => left.IsSatisfied(s) && right.IsSatisfied(s);
}
public class OrRule<T>(IRule<T> left, IRule<T> right) : IRule<T>
{
public string Description => $"({left.Description} OR {right.Description})";
public bool IsSatisfied(T s) => left.IsSatisfied(s) || right.IsSatisfied(s);
}
public class IsVipRule : IRule<Customer>
{
public string Description => "Is VIP";
public bool IsSatisfied(Customer c) => c.IsVip;
}
// (age >= 18 AND spend >= 500) OR is a VIP
IRule<Customer> rule = new OrRule<Customer>(
new AndRule<Customer>(
new MinimumAgeRule(18),
new MinimumSpendRule(500m)
),
new IsVipRule()
);
var customer = new Customer { Age = 25, TotalSpend = 600m, IsVip = false };
Console.WriteLine(rule.Description);
// ((Age >= 18 AND Total spend >= £500) OR Is VIP)
Console.WriteLine(rule.IsSatisfied(customer)); // TrueSimple Filter DSL
// DSL: "status:Paid AND total:>100"
public static class FilterParser
{
public static Func<Order, bool> Parse(string filter)
{
var parts = filter.Split(" AND ");
var predicates = parts.Select(ParseClause).ToList();
return order => predicates.All(p => p(order));
}
private static Func<Order, bool> ParseClause(string clause)
{
var parts = clause.Trim().Split(':', 2);
return (parts[0], parts[1]) switch
{
("status", var val) => o => o.Status == val,
("total", var val) when val.StartsWith(">")
=> o => o.Total > decimal.Parse(val[1..]),
("total", var val) when val.StartsWith("<")
=> o => o.Total < decimal.Parse(val[1..]),
(var field, _) => throw new ArgumentException($"Unknown filter: {field}")
};
}
}When to Use
Use Interpreter when:
✓ Defining a small DSL for business rules, queries, or formulas
✓ Rules need to be composed at runtime (AND, OR, NOT)
✓ Non-developers need to configure eligibility or pricing rules
Avoid when:
✗ Grammar is complex — use a parser library (Sprache, ANTLR)
✗ Simple one-off conditionals — just write the if statementInterview Answer
"The Interpreter pattern represents a grammar as a class hierarchy — terminal classes handle literals and variables, non-terminal classes handle operations (AND, OR, +, *). Sentences in the language are built as object trees (composite) and evaluated by calling a common method. Common uses: business rule engines (eligibility checks composed from individual rules), expression evaluators, and DSL parsers for filtering or querying. LINQ expression trees are the built-in .NET implementation — EF Core interprets them to generate SQL. The trade-off: works well for simple grammars; becomes unmanageable for complex languages — use a proper parser generator for those."