DSL на C#: Создаём свой язык программирования за вечер

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Вступление: Почему ваш код должен говорить на языке бизнеса

Представьте: вы пишете конфигурацию для сложного торгового робота. Вместо сотен строк 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#

  1. Переусложнение. Не пытайтесь сделать DSL, который решает все проблемы. Сфокусируйтесь на одной задаче.
  2. Игнорирование производительности. Expression Trees — мощь, но они медленнее прямого кода. Кэшируйте скомпилированные делегаты.
  3. Плохая диагностика ошибок. Если ваш DSL выдаёт Stack Trace вместо понятного сообщения — это провал. Добавьте кастомные исключения с контекстом.

Как внедрить DSL в существующий проект C#

План действий без боли:

  • Найдите повторяющийся паттерн. Например, 50 раз конфигурируете объекты через свойства.
  • Создайте Fluent Builder — это 80% пользы при 20% усилий.
  • Покройте тестами. DSL должен быть надёжнее обычного кода.
  • Документируйте синтаксис. Сделайте шпаргалку для команды.

Заключение: Ваш

Блоги

Рекомендаций по книгам