How to Implement Interpreter Pattern in C#: Step-by-Step Guide
Understanding a design pattern conceptually is a solid start -- but the real value comes when you can build it yourself. If you want to implement interpreter pattern in C#, you need to understand expression interfaces, terminal and non-terminal nodes, abstract syntax trees, and how to wire a parser into the whole system. This guide covers every step from defining the core abstraction to evaluating complex expressions against a variable context.
The interpreter pattern provides a way to evaluate sentences in a language by representing grammar rules as a class hierarchy. Each rule becomes a class, and the tree of objects mirrors the structure of the expression being interpreted. It fits naturally into scenarios where you need to parse and evaluate domain-specific languages, rule engines, mathematical expressions, or configuration grammars. If you've worked with the composite pattern, you'll recognize a structural similarity -- the interpreter pattern uses a tree of expression objects where each node can be either a leaf (terminal) or a branch (non-terminal).
In this step-by-step guide, we'll define an expression interface, build terminal and non-terminal expression classes, create a variable context, write a string-to-expression-tree parser, wire everything together, and test it. By the end, you'll have a thorough understanding of how to implement the interpreter pattern in C# for real-world scenarios.
Step 1: Define the IExpression Interface
Every interpreter pattern implementation starts with a single abstraction -- the expression interface. This interface represents any node in the abstract syntax tree (AST) and declares a single method that evaluates the expression given some context.
public interface IExpression
{
double Interpret(InterpreterContext context);
}
That's deliberately minimal. The Interpret method takes an InterpreterContext (which we'll build in Step 4) and returns a double. Every expression in the language -- whether it's a raw number, a variable lookup, or a complex arithmetic operation -- implements this interface and provides its own evaluation logic.
This design is what makes the interpreter pattern powerful. Each class knows how to evaluate itself, and composite expressions delegate to their children. The calling code doesn't need to know whether it's dealing with a number literal or a deeply nested multiplication chain. It calls Interpret() and gets a result. If you've studied inversion of control, this should feel familiar -- the expression tree drives execution rather than a central evaluator pulling everything together.
Step 2: Create Terminal Expressions
Terminal expressions are the leaf nodes of the syntax tree. They don't contain other expressions -- they resolve directly to a value. For our arithmetic language, we need two terminal types: numbers and variables.
NumberExpression
NumberExpression wraps a constant numeric value. When interpreted, it simply returns that value regardless of context.
public sealed class NumberExpression : IExpression
{
private readonly double _value;
public NumberExpression(double value)
{
_value = value;
}
public double Interpret(InterpreterContext context)
{
return _value;
}
}
VariableExpression
VariableExpression holds a variable name and looks up its value from the context at interpretation time. This is what makes the interpreter pattern dynamic -- the same expression tree can produce different results with different variable bindings.
public sealed class VariableExpression : IExpression
{
private readonly string _name;
public VariableExpression(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"Variable name cannot be null or empty.",
nameof(name));
}
_name = name;
}
public double Interpret(InterpreterContext context)
{
return context.GetVariable(_name);
}
}
Both of these classes are simple. They implement IExpression, and their Interpret methods perform a single, focused operation. Terminal expressions form the foundation of the entire tree -- every branch eventually reaches one of these leaves.
Step 3: Create Non-Terminal Expressions
Non-terminal expressions represent operations that combine other expressions. Each one holds references to child expressions and evaluates them recursively. We'll build three: addition, subtraction, and multiplication.
AddExpression
public sealed class AddExpression : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public AddExpression(
IExpression left,
IExpression right)
{
_left = left
?? throw new ArgumentNullException(nameof(left));
_right = right
?? throw new ArgumentNullException(nameof(right));
}
public double Interpret(InterpreterContext context)
{
return _left.Interpret(context)
+ _right.Interpret(context);
}
}
SubtractExpression
public sealed class SubtractExpression : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public SubtractExpression(
IExpression left,
IExpression right)
{
_left = left
?? throw new ArgumentNullException(nameof(left));
_right = right
?? throw new ArgumentNullException(nameof(right));
}
public double Interpret(InterpreterContext context)
{
return _left.Interpret(context)
- _right.Interpret(context);
}
}
MultiplyExpression
public sealed class MultiplyExpression : IExpression
{
private readonly IExpression _left;
private readonly IExpression _right;
public MultiplyExpression(
IExpression left,
IExpression right)
{
_left = left
?? throw new ArgumentNullException(nameof(left));
_right = right
?? throw new ArgumentNullException(nameof(right));
}
public double Interpret(InterpreterContext context)
{
return _left.Interpret(context)
* _right.Interpret(context);
}
}
Notice the pattern. Every non-terminal expression takes two child IExpression references, validates them, and combines their results in Interpret(). The recursion is implicit -- when AddExpression.Interpret() calls _left.Interpret(), that left child might be another AddExpression, a MultiplyExpression, or a terminal NumberExpression. The tree evaluates itself from leaves to root.
This recursive structure mirrors the composite pattern almost exactly. Composites hold children and delegate operations to them. The key difference is intent -- the composite pattern focuses on treating individual objects and compositions uniformly, while the interpreter pattern focuses on evaluating a grammar.
If you wanted to add division, modulo, or exponentiation, you'd follow the same template. Each new operation is a new class implementing IExpression with the appropriate arithmetic in Interpret(). This extensibility is one of the interpreter pattern's core strengths -- adding new grammar rules doesn't require modifying existing code, which aligns well with the open-closed principle. You might also consider the strategy pattern as an alternative approach for swapping out evaluation logic at runtime.
Step 4: Build a Context Class for Variable Storage
The InterpreterContext class stores variable bindings and provides lookup functionality. It's the runtime environment that expressions evaluate against.
public sealed class InterpreterContext
{
private readonly Dictionary<string, double> _variables;
public InterpreterContext()
{
_variables = new Dictionary<string, double>(
StringComparer.OrdinalIgnoreCase);
}
public void SetVariable(string name, double value)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException(
"Variable name cannot be null or empty.",
nameof(name));
}
_variables[name] = value;
}
public double GetVariable(string name)
{
if (!_variables.TryGetValue(name, out double value))
{
throw new KeyNotFoundException(
$"Variable '{name}' is not defined " +
"in the current context.");
}
return value;
}
public bool HasVariable(string name)
{
return _variables.ContainsKey(name);
}
}
A few design decisions here. The dictionary uses StringComparer.OrdinalIgnoreCase so that variable names like x and X resolve to the same binding. The GetVariable method throws a clear exception when a variable isn't found rather than returning a default value -- this catches typos and missing assignments early. The HasVariable method allows parsers and validators to check for variable existence without triggering exceptions.
The context object is passed to every Interpret() call, which means you can evaluate the same expression tree against different contexts to get different results. Build the tree once, swap the context, and you get a different answer -- that's a powerful pattern for rule engines and templating systems. If you've used dependency injection with IServiceCollection, this approach of passing a context object feels similar to injecting dependencies into a service at runtime. You could even use the iterator pattern to walk through a collection of contexts and evaluate the same tree against each one.
Step 5: Create a Simple Parser
So far we've been building expression trees manually in code. That works for programmatic construction, but the interpreter pattern really shines when you can parse string input into an expression tree. Let's build a simple parser that converts infix arithmetic expressions like "x + 3 * y" into the correct AST.
This parser handles operator precedence (multiplication binds tighter than addition and subtraction) and supports numbers, variables, and parentheses.
public sealed class ExpressionParser
{
private readonly string _input;
private int _position;
public ExpressionParser(string input)
{
_input = input
?? throw new ArgumentNullException(nameof(input));
_position = 0;
}
public IExpression Parse()
{
IExpression result = ParseAddSubtract();
if (_position < _input.Length)
{
throw new FormatException(
$"Unexpected character '{_input[_position]}' " +
$"at position {_position}.");
}
return result;
}
private IExpression ParseAddSubtract()
{
IExpression left = ParseMultiply();
while (_position < _input.Length)
{
SkipWhitespace();
if (_position >= _input.Length)
{
break;
}
char op = _input[_position];
if (op != '+' && op != '-')
{
break;
}
_position++;
IExpression right = ParseMultiply();
left = op == '+'
? new AddExpression(left, right)
: new SubtractExpression(left, right);
}
return left;
}
private IExpression ParseMultiply()
{
IExpression left = ParsePrimary();
while (_position < _input.Length)
{
SkipWhitespace();
if (_position >= _input.Length
|| _input[_position] != '*')
{
break;
}
_position++;
IExpression right = ParsePrimary();
left = new MultiplyExpression(left, right);
}
return left;
}
private IExpression ParsePrimary()
{
SkipWhitespace();
if (_position >= _input.Length)
{
throw new FormatException(
"Unexpected end of expression.");
}
if (_input[_position] == '(')
{
_position++;
IExpression inner = ParseAddSubtract();
SkipWhitespace();
if (_position >= _input.Length
|| _input[_position] != ')')
{
throw new FormatException(
"Missing closing parenthesis.");
}
_position++;
return inner;
}
if (char.IsDigit(_input[_position])
|| _input[_position] == '.')
{
return ParseNumber();
}
if (char.IsLetter(_input[_position]))
{
return ParseVariable();
}
throw new FormatException(
$"Unexpected character '{_input[_position]}' " +
$"at position {_position}.");
}
private NumberExpression ParseNumber()
{
int start = _position;
while (_position < _input.Length
&& (char.IsDigit(_input[_position])
|| _input[_position] == '.'))
{
_position++;
}
string numberText = _input[start.._position];
if (!double.TryParse(
numberText,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double value))
{
throw new FormatException(
$"Invalid number '{numberText}' " +
$"at position {start}.");
}
return new NumberExpression(value);
}
private VariableExpression ParseVariable()
{
int start = _position;
while (_position < _input.Length
&& char.IsLetterOrDigit(_input[_position]))
{
_position++;
}
string name = _input[start.._position];
return new VariableExpression(name);
}
private void SkipWhitespace()
{
while (_position < _input.Length
&& char.IsWhiteSpace(_input[_position]))
{
_position++;
}
}
}
The parser uses a recursive descent approach with two levels of precedence. ParseAddSubtract() handles the lowest-precedence operators and calls ParseMultiply() for higher-precedence ones. ParsePrimary() handles the atomic units -- numbers, variables, and parenthesized sub-expressions. This layered approach ensures that 3 + 2 * x correctly parses as 3 + (2 * x) rather than (3 + 2) * x.
If you want to hide the parsing complexity behind a simpler interface, the facade pattern is a natural fit. You could wrap the parser, context setup, and evaluation into a single Evaluate(string expression, Dictionary<string, double> variables) method.
Step 6: Wire Everything Together
Let's put all the pieces together and evaluate some expressions:
// Build context with variable bindings
var context = new InterpreterContext();
context.SetVariable("x", 10);
context.SetVariable("y", 5);
context.SetVariable("z", 3);
// Parse and evaluate expressions
string[] expressions = new[]
{
"x + y",
"x - y * z",
"( x + y ) * z",
"x * y + z",
"42",
"x"
};
foreach (string expr in expressions)
{
var parser = new ExpressionParser(expr);
IExpression tree = parser.Parse();
double result = tree.Interpret(context);
Console.WriteLine($"{expr} = {result}");
}
Running this produces:
x + y = 15
x - y * z = -5
( x + y ) * z = 45
x * y + z = 53
42 = 42
x = 10
Notice that x - y * z evaluates to -5 because multiplication has higher precedence than subtraction, so the tree becomes x - (y * z) which is 10 - 15. The parenthesized expression ( x + y ) * z overrides the default precedence and evaluates to 45.
You can also build trees programmatically without the parser. This is useful when your expressions come from code rather than user input:
// Manual tree: (x + 10) * (y - 2)
IExpression manualTree = new MultiplyExpression(
new AddExpression(
new VariableExpression("x"),
new NumberExpression(10)),
new SubtractExpression(
new VariableExpression("y"),
new NumberExpression(2)));
double manualResult = manualTree.Interpret(context);
Console.WriteLine($"(x + 10) * (y - 2) = {manualResult}");
// Output: (x + 10) * (y - 2) = 60
The tree structure makes the evaluation order explicit. There's no ambiguity about precedence or associativity -- the AST encodes it structurally. This is one reason the interpreter pattern works well for command-like workflows where you want to build up operations as objects and execute them later.
Step 7: Unit Testing the Interpreter
Testing the interpreter pattern is straightforward because each expression class is small, focused, and deterministic. Let's write tests for the key scenarios using xUnit:
using Xunit;
public class InterpreterTests
{
[Fact]
public void NumberExpression_ReturnsConstantValue()
{
var expr = new NumberExpression(42);
var context = new InterpreterContext();
double result = expr.Interpret(context);
Assert.Equal(42, result);
}
[Fact]
public void VariableExpression_ReturnsContextValue()
{
var context = new InterpreterContext();
context.SetVariable("x", 7);
var expr = new VariableExpression("x");
double result = expr.Interpret(context);
Assert.Equal(7, result);
}
[Fact]
public void VariableExpression_ThrowsForUndefinedVariable()
{
var context = new InterpreterContext();
var expr = new VariableExpression("missing");
Assert.Throws<KeyNotFoundException>(
() => expr.Interpret(context));
}
[Fact]
public void AddExpression_SumsTwoExpressions()
{
var left = new NumberExpression(3);
var right = new NumberExpression(4);
var expr = new AddExpression(left, right);
var context = new InterpreterContext();
double result = expr.Interpret(context);
Assert.Equal(7, result);
}
[Fact]
public void SubtractExpression_SubtractsRightFromLeft()
{
var left = new NumberExpression(10);
var right = new NumberExpression(3);
var expr = new SubtractExpression(left, right);
var context = new InterpreterContext();
double result = expr.Interpret(context);
Assert.Equal(7, result);
}
[Fact]
public void MultiplyExpression_MultipliesExpressions()
{
var left = new NumberExpression(5);
var right = new NumberExpression(6);
var expr = new MultiplyExpression(left, right);
var context = new InterpreterContext();
double result = expr.Interpret(context);
Assert.Equal(30, result);
}
[Theory]
[InlineData("3 + 4", 7)]
[InlineData("10 - 3", 7)]
[InlineData("2 * 5", 10)]
[InlineData("2 + 3 * 4", 14)]
[InlineData("( 2 + 3 ) * 4", 20)]
public void Parser_EvaluatesCorrectly(
string input,
double expected)
{
var parser = new ExpressionParser(input);
IExpression tree = parser.Parse();
var context = new InterpreterContext();
double result = tree.Interpret(context);
Assert.Equal(expected, result);
}
[Fact]
public void Parser_WithVariables_EvaluatesCorrectly()
{
var context = new InterpreterContext();
context.SetVariable("x", 10);
context.SetVariable("y", 5);
var parser = new ExpressionParser("x + y * 2");
IExpression tree = parser.Parse();
double result = tree.Interpret(context);
Assert.Equal(20, result);
}
[Fact]
public void Parser_InvalidInput_ThrowsFormatException()
{
var parser = new ExpressionParser("3 + + 4");
Assert.Throws<FormatException>(() => parser.Parse());
}
}
The testing strategy covers several layers. Unit tests for individual expression classes verify that each node evaluates correctly in isolation. Parser integration tests verify that string input produces the right numeric result, including operator precedence. Error case tests confirm that invalid input and missing variables produce the expected exceptions.
Frequently Asked Questions
What types of problems is the interpreter pattern best suited for?
The interpreter pattern works well when you have a well-defined grammar that needs to be parsed and evaluated repeatedly. Common use cases include mathematical expression evaluators, rule engines, query language processors, template engines, and configuration file parsers. The pattern is less appropriate for complex grammars with many rules because the class hierarchy grows quickly -- for those cases, a dedicated parser generator like ANTLR is a better fit.
How does the interpreter pattern differ from the visitor pattern?
Both patterns work with tree structures, but they solve different problems. The interpreter pattern embeds evaluation logic inside each node class through the Interpret() method. The visitor pattern externalizes operations into separate visitor classes, making it easy to add new operations without modifying node classes. If you need many different operations on the same tree (evaluation, pretty-printing, optimization), the visitor pattern is more flexible. If you only need one or two operations, the interpreter pattern keeps things simpler.
Can the interpreter pattern handle operator precedence?
The interpreter pattern itself doesn't handle precedence -- that's the parser's job. The expression tree that the interpreter evaluates already encodes precedence through its structure. When you parse 3 + 2 * 4, the parser creates a tree where the multiplication node is a child of the addition node, ensuring multiplication happens first. Once the tree is built, the interpreter evaluates it correctly regardless of how the original string was written.
How do I add new operations to an existing interpreter?
Adding a new operation means creating a new class that implements IExpression. For example, to add division, create a DivideExpression class that holds left and right child expressions and divides their results in Interpret(). Then update the parser to recognize the division operator and create DivideExpression nodes. No existing expression classes need to change.
Is the interpreter pattern performant for large expressions?
For simple expressions and moderate tree depths, the interpreter pattern performs well. Each node evaluation involves a virtual method call and potential recursion, so deeply nested trees can suffer from call stack overhead. For performance-critical scenarios, consider compiling the expression tree into a delegate using System.Linq.Expressions or generating IL directly. Another optimization is caching -- if the same sub-expression appears multiple times and the context hasn't changed, memoize the result.
How does the interpreter pattern relate to abstract syntax trees?
An abstract syntax tree (AST) is the data structure that the interpreter pattern operates on. Each node in the AST corresponds to an IExpression implementation. Terminal nodes (like NumberExpression) are leaves, and non-terminal nodes (like AddExpression) are branches with children. The parser's job is to convert raw input into this tree, and the interpreter pattern's job is to evaluate it. The AST is the bridge between parsing and evaluation.
Can I use the interpreter pattern with dependency injection?
Yes. You can register the parser and context as services in your DI container. For example, register InterpreterContext as a scoped service so each request gets its own variable bindings, and register ExpressionParser as transient. Expression classes themselves don't need DI because the parser creates them during tree construction. If you want to make expression types swappable for testing, you could inject a factory that the parser uses to create expression nodes.

