🔥 Pragmatic .NET Code Rules Course is on Presale - 40% off!BUY NOW

Interpreter Pattern in .NET

Your Business Rules Are Buried in If-Else Chains

The discount engine started simple: 10% off orders over $100. One condition, one rule.

Then marketing wanted: "20% off if the customer is VIP AND the order is over $200." Then: "Free shipping if items include category 'Electronics' OR total exceeds $500." Then: "Apply loyalty discount if customer has been active for 2+ years AND hasn't used a coupon this month."

Now your discount method is 150 lines of nested conditionals that nobody wants to touch:

C#
public decimal CalculateDiscount(Order order, Customer customer){ if (customer.IsVip && order.Total > 200) return order.Total * 0.20m; else if (order.Total > 100) return order.Total * 0.10m; else if (order.Items.Any(i => i.Category == "Electronics") || order.Total > 500) return 0; // Free shipping instead else if (customer.ActiveYears >= 2 && !customer.UsedCouponThisMonth) return order.Total * 0.05m; // 20 more conditions... return 0;}

Every new rule requires a developer to modify code, redeploy, and test the entire chain. Marketing can't update rules without a development cycle. And the logic is completely opaque — nobody can read it and understand the full rule set.

The Problem: Rules Hard-Coded in Application Logic

The real pain is that these rules are business logic that changes frequently, but they're locked inside compiled code. You need a way to represent rules as data — something that can be parsed, composed, and evaluated dynamically.

Enter the Interpreter Pattern

The Interpreter pattern defines a grammar for a language and provides an interpreter that evaluates expressions in that language. Each rule in the grammar becomes a class. Complex rules compose from simple ones.

Building It in .NET

Define an expression interface and atomic expressions:

C#
// The context holds data the expressions evaluate againstpublic class RuleContext{ public Order Order { get; init; } = null!; public Customer Customer { get; init; } = null!;} // Abstract expressionpublic interface IExpression{ bool Interpret(RuleContext context);} // Terminal expressions: atomic conditionspublic class OrderTotalGreaterThan : IExpression{ private readonly decimal _threshold;  public OrderTotalGreaterThan(decimal threshold) => _threshold = threshold;  public bool Interpret(RuleContext context) => context.Order.Total > _threshold;} public class CustomerIsVip : IExpression{ public bool Interpret(RuleContext context) => context.Customer.IsVip;} public class CustomerActiveYears : IExpression{ private readonly int _minYears;  public CustomerActiveYears(int minYears) => _minYears = minYears;  public bool Interpret(RuleContext context) => context.Customer.ActiveYears >= _minYears;} public class OrderContainsCategory : IExpression{ private readonly string _category;  public OrderContainsCategory(string category) => _category = category;  public bool Interpret(RuleContext context) => context.Order.Items.Any(i => i.Category.Equals(_category, StringComparison.OrdinalIgnoreCase));}

Now the non-terminal expressions — logical combinators:

C#
// Non-terminal: ANDpublic class AndExpression : IExpression{ private readonly IExpression _left; private readonly IExpression _right;  public AndExpression(IExpression left, IExpression right) { _left = left; _right = right; }  public bool Interpret(RuleContext context) => _left.Interpret(context) && _right.Interpret(context);} // Non-terminal: ORpublic class OrExpression : IExpression{ private readonly IExpression _left; private readonly IExpression _right;  public OrExpression(IExpression left, IExpression right) { _left = left; _right = right; }  public bool Interpret(RuleContext context) => _left.Interpret(context) || _right.Interpret(context);} // Non-terminal: NOTpublic class NotExpression : IExpression{ private readonly IExpression _expression;  public NotExpression(IExpression expression) => _expression = expression;  public bool Interpret(RuleContext context) => !_expression.Interpret(context);}

Compose rules from expressions:

C#
// Rule: VIP customer AND order over $200var vipHighValueRule = new AndExpression( new CustomerIsVip(), new OrderTotalGreaterThan(200)); // Rule: Electronics in cart OR order over $500var freeShippingRule = new OrExpression( new OrderContainsCategory("Electronics"), new OrderTotalGreaterThan(500)); // Rule: Active 2+ years AND NOT VIP (loyal but not yet upgraded)var loyaltyRule = new AndExpression( new CustomerActiveYears(2), new NotExpression(new CustomerIsVip())); // Evaluatevar context = new RuleContext { Order = order, Customer = customer }; if (vipHighValueRule.Interpret(context)) discount = 0.20m;else if (loyaltyRule.Interpret(context)) discount = 0.05m;

Rules are composable data structures. You can store them, serialize them, and build them from configuration.

Why This Is Better

Rules are data, not code. You can store rule definitions in a database and build expression trees at runtime. No redeployment needed for new rules.

Composable. AND, OR, and NOT combine any expressions. Complex rules build from simple, tested components.

Testable. Each expression is a single class with one method. Test OrderTotalGreaterThan independently of everything else.

Advanced Usage: Building Rules From Configuration

Parse rules from JSON or a database:

C#
public class RuleEngine{ public IExpression BuildFromConfig(RuleDefinition definition) { return definition.Type switch { "OrderTotalGreaterThan" => new OrderTotalGreaterThan(definition.GetParam<decimal>("threshold")), "CustomerIsVip" => new CustomerIsVip(), "CustomerActiveYears" => new CustomerActiveYears(definition.GetParam<int>("years")), "OrderContainsCategory" => new OrderContainsCategory(definition.GetParam<string>("category")), "AND" => new AndExpression( BuildFromConfig(definition.Left!), BuildFromConfig(definition.Right!)), "OR" => new OrExpression( BuildFromConfig(definition.Left!), BuildFromConfig(definition.Right!)), "NOT" => new NotExpression(BuildFromConfig(definition.Left!)), _ => throw new InvalidOperationException($"Unknown rule type: {definition.Type}") }; }} // JSON rule definition// {// "type": "AND",// "left": { "type": "CustomerIsVip" },// "right": { "type": "OrderTotalGreaterThan", "params": { "threshold": 200 } }// }

Now marketing can define rules in an admin panel. The rule engine parses and evaluates them without code changes.

Advanced Usage: Expression Evaluator for Calculated Fields

Build a simple math expression evaluator:

C#
public interface IMathExpression{ decimal Evaluate(Dictionary<string, decimal> variables);} public class NumberLiteral : IMathExpression{ private readonly decimal _value; public NumberLiteral(decimal value) => _value = value; public decimal Evaluate(Dictionary<string, decimal> variables) => _value;} public class Variable : IMathExpression{ private readonly string _name; public Variable(string name) => _name = name; public decimal Evaluate(Dictionary<string, decimal> variables) => variables[_name];} public class Multiply : IMathExpression{ private readonly IMathExpression _left, _right; public Multiply(IMathExpression left, IMathExpression right) { _left = left; _right = right; } public decimal Evaluate(Dictionary<string, decimal> variables) => _left.Evaluate(variables) * _right.Evaluate(variables);} // Expression: price * quantity * (1 - discountRate)var totalExpr = new Multiply( new Multiply(new Variable("price"), new Variable("quantity")), new Subtract(new NumberLiteral(1), new Variable("discountRate"))); var vars = new Dictionary<string, decimal>{ ["price"] = 29.99m, ["quantity"] = 3, ["discountRate"] = 0.10m}; var total = totalExpr.Evaluate(vars); // 80.973

When NOT to Use It

For complex grammars. If your language has loops, functions, or complex syntax, use a proper parser generator (ANTLR) or an existing scripting engine (Roslyn, Lua). The Interpreter pattern doesn't scale well for complex languages.

When performance matters. Each Interpret() call traverses the expression tree. For hot paths with millions of evaluations, compiled expressions (Expression<T>.Compile()) are orders of magnitude faster.

When rules rarely change. If rules are stable and change once a year, the overhead of building an interpreter isn't worth it. Just write the conditions directly.

Key Takeaways

  • The Interpreter pattern maps grammar rules to classes, making business rules composable and dynamic
  • AND, OR, and NOT combinators let you build arbitrarily complex rules from simple ones
  • Rules can be stored in databases and built at runtime from configuration
  • Use it for rule engines, discount calculators, and simple DSLs
  • Don't use it for complex languages or performance-critical evaluation paths

FAQ

What is the Interpreter pattern in simple terms?

The Interpreter pattern defines a grammar as a set of classes, where each class represents a rule. You compose rules into trees and evaluate them against a context. Think of it as a mini programming language for your business rules.

When should I use the Interpreter pattern?

When business rules change frequently and non-developers need to modify them. Also for building simple expression evaluators, query builders, or domain-specific mini-languages where the grammar is small and stable.

Is the Interpreter pattern overkill?

For 3-4 fixed conditions, yes. Just use if-else. The pattern shines when you have 20+ rules that change regularly and need to be configured outside of code.

What are alternatives to the Interpreter pattern?

For complex grammars, use ANTLR or a parser combinator library. For runtime evaluation, C# Expression<T> trees compile to IL. For simple rule evaluation, a dictionary of delegates or the Strategy pattern can work.

Wrapping Up

The Interpreter pattern is niche but powerful. Most developers won't need it. But when you find yourself building a rule engine, a filter system, or a simple DSL — and the business team keeps asking for new rules every sprint — this pattern turns a code change into a configuration change.

Build the small grammar. Compose the expressions. Let the tree evaluate itself.

That's all from me today.

P.S. Follow me on YouTube.


If you made it this far, you're clearly serious about writing better .NET code. Here's a 20% discount code: DEEP20 for Design Patterns that Deliver. Consider it a thank-you for actually reading the whole thing.


Here are 2 ebooks I have about design patterns:

About the Author

Stefan Djokic is a Microsoft MVP and senior .NET engineer with extensive experience designing enterprise-grade systems and teaching architectural best practices.

There are 3 ways I can help you:

1

Pragmatic .NET Code Rules Course

Stop arguing about code style. In this course you get a production-proven setup with analyzers, CI quality gates, and architecture tests — the exact system I use in real projects. Join here.

Not sure yet? Grab the free Starter Kit — a drop-in setup with the essentials from Module 01.

2

Design Patterns Ebooks

Design Patterns that Deliver — Solve real problems with 5 battle-tested patterns (Builder, Decorator, Strategy, Adapter, Mediator) using practical, real-world examples. Trusted by 650+ developers.

Just getting started? Design Patterns Simplified covers 10 essential patterns in a beginner-friendly, 30-page guide for just $9.95.

3

Join 20,000+ subscribers

Every Monday morning, I share 1 actionable tip on C#, .NET & Architecture that you can use right away. Join here.

Join 20,000+ subscribers who mass-improve their .NET skills with actionable tips on C#, Software Architecture & Best Practices.

Subscribe to
TheCodeMan.net

Subscribe to the TheCodeMan.net and be among the 20,000+ subscribers gaining practical tips and resources to enhance your .NET expertise.