Вступление: Почему ваш код должен говорить на языке бизнеса
Представьте: вы пишете конфигурацию для сложного торгового робота. Вместо сотен строк if-else и магических чисел вы просто пишете: BuyWhen(Price.CrossesAbove(MA(50))). Ваш менеджер, не знающий C#, смотрит на этот код и понимает логику. Это не магия — это DSL на C#.
Предметно-ориентированные языки (Domain-Specific Languages, DSL) — это не просто модный тренд. Это инструмент, который превращает хаос технических деталей в элегантный, читаемый код. В этой статье мы не просто поговорим о теории — мы создадим реальный DSL на C# с нуля, разберём типичные ошибки и покажем, как внедрить эту практику в ваш проект уже сегодня.
Что такое DSL и зачем он нужен разработчику?
DSL — это язык программирования, созданный для решения конкретной задачи. В отличие от общего языка (C#, Java, Python), DSL говорит на языке предметной области. В C# есть два основных подхода:
- Внутренние DSL — строятся поверх C# с помощью Fluent API и Expression Trees. Вы остаётесь в родной среде.
- Внешние DSL — требуют парсинга (часто через Roslyn) и генерируют C# код.
Реальная польза DSL: снижение когнитивной нагрузки, уменьшение количества багов (бизнес-логика отделена от технической реализации) и ускорение онбординга новых разработчиков.
Создаём внутренний DSL на C#: пошаговый гайд
Шаг 1: Определяем домен и синтаксис
Допустим, мы создаём DSL для валидации данных. Вместо:
if (user.Age < 18 || user.Name == null || user.Email == null)
throw new ValidationException();
Хотим писать так:
Validate.User(user)
.MustBeAdult()
.MustHaveName()
.MustHaveEmail();
Шаг 2: Fluent Interface — база DSL на C#
Ключевой элемент — возврат this из каждого метода. Создаём класс-валидатор:
public class UserValidator
{
private readonly User _user;
private readonly List<string> _errors = new();
public UserValidator(User user) => _user = user;
public UserValidator MustBeAdult()
{
if (_user.Age < 18)
_errors.Add("Пользователь несовершеннолетний");
return this;
}
public UserValidator MustHaveName()
{
if (string.IsNullOrWhiteSpace(_user.Name))
_errors.Add("Имя обязательно");
return this;
}
public void Validate()
{
if (_errors.Any())
throw new ValidationException(string.Join("; ", _errors));
}
}
И фабричный метод для элегантного входа:
public static class Validate
{
public static UserValidator User(User user) => new UserValidator(user);
}
Шаг 3: Добавляем условные конструкции и контекст
Настоящий DSL на C# должен поддерживать ветвления. Реализуем When:
public UserValidator When(bool condition, Func<UserValidator, UserValidator> validatorFunc)
{
if (condition)
return validatorFunc(this);
return this;
}
Использование:
Validate.User(user)
.MustHaveName()
.When(user.Age > 18, v => v.MustBeAdult());
Продвинутый уровень: DSL с Expression Trees на C#
Для построения динамических запросов (ORM, правила) используем Expression<Func<T, bool>>. Создаём построитель правил:
public class RuleBuilder<T>
{
private Expression<Func<T, bool>> _expression;
public RuleBuilder(Expression<Func<T, bool>> expression) => _expression = expression;
public RuleBuilder<T> And(Expression<Func<T, bool>> other)
{
var andExpression = Expression.AndAlso(_expression.Body, other.Body);
_expression = Expression.Lambda<Func<T, bool>>(andExpression, _expression.Parameters);
return this;
}
public Expression<Func<T, bool>> Build() => _expression;
}
Теперь можно писать правила интуитивно:
var rule = new RuleBuilder<User>(u => u.Age > 18)
.And(u => u.IsActive)
.Build();
Этот подход лежит в основе Entity Framework и многих ORM — вы создаёте предметно-ориентированный язык запросов, не покидая C#.
Внешние DSL на C#: когда нужна полная свобода
Если ваш DSL должен использоваться не-программистами или требовать собственного синтаксиса, пора выходить за рамки C#. Варианты:
- Roslyn — парсинг и генерация C# кода на лету.
- ANTLR — классический генератор парсеров.
- Regex + Reflection — для простых скриптов.
Пример минимального внешнего DSL на C# с Regex:
public class SimpleDslParser
{
private static readonly Regex CommandRegex = new(@"(\w+)\s*:\s*""(.*?)""", RegexOptions.Compiled);
public List<DslCommand> Parse(string input)
{
return CommandRegex.Matches(input)
.Select(m => new DslCommand(m.Groups[1].Value, m.Groups[2].Value))
.ToList();
}
}
public record DslCommand(string Action, string Argument);
Запускаем:
var parser = new SimpleDslParser();
var commands = parser.Parse("print: \"Hello DSL\"\n wait: \"5s\"");
foreach (var cmd in commands)
Console.WriteLine($"{cmd.Action}: {cmd.Argument}");
Топ-3 ошибки при создании DSL на C#
- Переусложнение. Не пытайтесь сделать DSL, который решает все проблемы. Сфокусируйтесь на одной задаче.
- Игнорирование производительности. Expression Trees — мощь, но они медленнее прямого кода. Кэшируйте скомпилированные делегаты.
- Плохая диагностика ошибок. Если ваш DSL выдаёт Stack Trace вместо понятного сообщения — это провал. Добавьте кастомные исключения с контекстом.
Как внедрить DSL в существующий проект C#
План действий без боли:
- Найдите повторяющийся паттерн. Например, 50 раз конфигурируете объекты через свойства.
- Создайте Fluent Builder — это 80% пользы при 20% усилий.
- Покройте тестами. DSL должен быть надёжнее обычного кода.
- Документируйте синтаксис. Сделайте шпаргалку для команды.