Introduction: Why Your Code Should Speak the Language of Business
\n\nImagine: you are writing a configuration for a complex trading bot. Instead of hundreds of lines of if-else and magic numbers, you simply write: BuyWhen(Price.CrossesAbove(MA(50))). Your manager, who doesn't know C#, looks at this code and understands the logic. This is not magic — this is a DSL in C#.
Domain-Specific Languages (DSL) are not just a trendy fad. They are a tool that turns the chaos of technical details into elegant, readable code. In this article, we will not just talk about theory — we will create a real DSL in C# from scratch, break down typical mistakes, and show how to implement this practice in your project today.
\n\nWhat is a DSL and Why Does a Developer Need It?
\n\nA DSL is a programming language created to solve a specific problem. Unlike a general-purpose language (C#, Java, Python), a DSL speaks the language of the domain. In C#, there are two main approaches:
\n\n- \n
- Internal DSLs — built on top of C# using Fluent API and Expression Trees. You stay within your native environment. \n
- External DSLs — require parsing (often via Roslyn) and generate C# code. \n
The real benefits of DSL: reduced cognitive load, fewer bugs (business logic is separated from technical implementation), and faster onboarding for new developers.
\n\nCreating an Internal DSL in C#: A Step-by-Step Guide
\n\nStep 1: Define the Domain and Syntax
\n\nLet's say we are creating a DSL for data validation. Instead of:
\n\nif (user.Age < 18 || user.Name == null || user.Email == null)\n throw new ValidationException();\n\nWe want to write it like this:
\n\nValidate.User(user)\n .MustBeAdult()\n .MustHaveName()\n .MustHaveEmail();\n\nStep 2: Fluent Interface — The Foundation of DSL in C#
\n\nThe key element is returning this from each method. Let's create a validator class:
public class UserValidator\n{\n private readonly User _user;\n private readonly List<string> _errors = new();\n\n public UserValidator(User user) => _user = user;\n\n public UserValidator MustBeAdult()\n {\n if (_user.Age < 18)\n _errors.Add("User is underage");\n return this;\n }\n\n public UserValidator MustHaveName()\n {\n if (string.IsNullOrWhiteSpace(_user.Name))\n _errors.Add("Name is required");\n return this;\n }\n\n public void Validate()\n {\n if (_errors.Any())\n throw new ValidationException(string.Join("; ", _errors));\n }\n}\n\nAnd a factory method for an elegant entry point:
\n\npublic static class Validate\n{\n public static UserValidator User(User user) => new UserValidator(user);\n}\n\nStep 3: Adding Conditional Constructs and Context
\n\nA real DSL in C# must support branching. Let's implement When:
public UserValidator When(bool condition, Func<UserValidator, UserValidator> validatorFunc)\n{\n if (condition)\n return validatorFunc(this);\n return this;\n}\n\nUsage:
\n\nValidate.User(user)\n .MustHaveName()\n .When(user.Age > 18, v => v.MustBeAdult());\n\nAdvanced Level: DSL with Expression Trees in C#
\n\nFor building dynamic queries (ORM, rules), we use Expression<Func<T, bool>>. Let's create a rule builder:
public class RuleBuilder<T>\n{\n private Expression<Func<T, bool>> _expression;\n\n public RuleBuilder(Expression<Func<T, bool>> expression) => _expression = expression;\n\n public RuleBuilder<T> And(Expression<Func<T, bool>> other)\n {\n var andExpression = Ex