TL;DR
Используй JSON для создания LINQ-выражений!
Содержание
Решаемая проблема
Почему не GraphQL
Решение проблемы – JsonToLinq
Преимущества JsonToLinq
Как использовать JsonToLinq
Операторы
Конфигурация JsonToLinq
Тестирование JsonToLinq
Демонстрации и эксперименты
Ограничения JsonToLinq
Потенциал JsonToLinq
Поддержать проект
Решаемая проблема
Существует категория проектов, UI которых на 90-100% состоит из гридов. Со временем пользователи предъявляют все новые требования к колонкам, связям между столбцами, ограничению доступа и поиску данных. Раньше, когда в моде был ASP.NET MVC, мы использовали Telerik for MVC, в котором из коробки доступен отличный функционал для гридов. После перехода к раздельной разработке API + SPA (Angular/React/Vue) для подобных сайтов появилась проблема управления данными с клиента: функционал обработки фильтров стал занимать большую часть инфраструктуры, а с увеличением UX все больше времени стало уходить на его поддержку.
Мы стали разрабатывать UI на Angular, используя Telerik for Angular, который имеет мощный и гибкий функционал для работы с гридами. Фильтры колонок автоматически синхронизируются и связываются в единый объект на TS. Однако управление через RxJS/NgRx требовало их дополнительной модификации перед отправкой на сервер. Универсального метода на C# для их разбора не было, из-за чего каждый фильтр проходил собственную цепочку преобразований и валидации перед применением в EF. Трудоемкость по сравнению с ASP.NET MVC выросла в разы, а серверная часть фильтрации оказалась жестко связана со спецификой фильтров Telerik. После того как наша компания запретила использование Telerik, мы были вынуждены перейти на Material Design. Эта библиотека содержит лишь базовые визуальные компоненты и не предоставляет инфраструктуры уровня Telerik, Prime, AG Grid или Syncfusion. Реализация сложных гридов на ее основе потребует разработки собственного механизма фильтрации с нуля. Что же делать с нашими тяжелыми гридами?..
Есть уже готовые библиотеки парсинга текстового представления Expression, но составлять это текстовое выражение вручную не так просто, как может показаться на первый взгляд:
Объем текста ухудшает читаемость, даже относительно простые выражения превращаются в громоздкие строки.
Чтобы текстовые выражения оставались читаемыми и не оформлялись каждым разработчиком по-своему, потребуется разработать правила форматирования. Но даже с выравниванием такой текст сложно воспринимать без подсветки синтаксиса. Его накопление в системе будет усложнять рефакторинг, поддержку и снижать привлекательность проекта для новых членов команды.
Составление и разбор подобных строк потребуют отдельного функционала для валидации, экранирования значений и обработки ошибок.
Front-End приложения не должны зависеть от конкретных серверных технологий вроде LINQ, иначе это нарушает разделение ответственности между клиентом и сервером. Ожидать от Front-End разработчиков знания LINQ и требовать от них реализации функционала по созданию таких текстовых выражений на JS/TS нецелесообразно и выходит за рамки их ответственности.
По этим же причинам не подходит и OData.
Зато все знают JSON!
Если черный ящик поддерживает входные сигналы в форме JSON, то им могут пользоваться самые разные клиенты!
Вот так и зародилась идея абстрагироваться и бэкам, и фронтам от этой рутины, вынеся ее в отдельный сервис, который будет автоматически транслировать любой валидный JSON в Expression tree.
Почему не GraphQL
GraphQL имеет готовый функционал фильтрации, но не является универсальным решением:
GraphQL не использует стандартный JSON, требуя знания дополнительного синтаксиса. Это не только осложняет разработку, но и ухудшает интерфейс системы, повышая порог интеграции для клиентов – аналогично текстовым LINQ-выражениям и OData.
GraphQL обеспечивает типобезопасность и IntelliSense запросов через схему (
.graphql), которая зависит от серверной модели. Любое изменение модели требует обновления схемы, усложняя поддержку и усиливая связанность клиента с сервером.
Решение проблемы – JsonToLinq
Для решения этой проблемы была создана .NET-библиотека JsonToLinq, которая конвертирует фильтры из JSON в LINQ-выражения напрямую без внешних зависимостей.
Преимущества JsonToLinq
-
Дружелюбность
JSON – простой и широко известный формат, легко воспринимаемый не только разработчиками.
-
Широта применения
Фильтры в формате JSON можно использовать в тестах, описывать в ТЗ, планах тестирования и др. технической документации, понижая порог входа читателей.
-
Универсальность
Чтобы получить данные из .NET-приложения с EF, нужно лишь запросить их в формате JSON. Это делает его доступным для любого клиента, который знает названия и типы полей.
-
Независимость клиента и сервера
Для создания фильтров клиенту достаточно знать лишь названия и типы полей данных, что обычно уже есть в DTO.
-
Гибкость
Из коробки доступно добавление своих операторов, а сам функционал может послужить ядром для других проектов – например, для создания аналога HotChocolate со стандартным JSON и другими особенностями.
-
Простота
Установи NuGet-пакет и вставляй JSON-фильтры в
Where()и другие методы.
Это подключает не минимальный, а полный функционал фильтрации!Не нужно:
Регистрировать что-либо в
Program.cs.Создавать схему, отдельные DTO, фильтры, резолверы и пр.
Генерировать схему для клиента.
Все необходимое для фильтрации уже есть в DTO!
Как использовать JsonToLinq
Установить NuGet-пакет.
Передавать текст JSON внутрь
Where()или других фильтрующих методов.Для использования с
IQueryableсначала создать предикат черезJsonLinq.ParseFilterExpression().
Быстрый старт
using Neomaster.JsonToLinq;
var users = source.Where(
"""
{
"Logic": "&&",
"Rules": [
{ "Field": "balance", "Operator": "=", "Value": 0 },
{ "Field": "status", "Operator": "in", "Value": [ 1, 3 ] },
{ "Field": "country", "Operator": "as lower contains", "Value": "islands" },
{
"Logic": "||",
"Rules": [
{ "Field": "lastVisitAt", "Operator": "=", "Value": null },
{ "Field": "lastVisitAt", "Operator": "<=", "Value": "2026-01-01T00:00:00Z" }
]
}
]
}
""");
Аналогичный запрос на LINQ:
var users = source.Where(u =>
(u.Balance == 0
&& new[] { 1, 3 }.Contains(u.Status)
&& u.Country.ToLower().Contains("islands"))
&&
(u.LastVisitAt == null
|| u.LastVisitAt <= JsonSerializer.Deserialize<DateTime?>("\"2026-01-01T00:00:00Z\"")));
Операторы
Встроенные операторы и логика их обработки инкапсулированы в классе
ExpressionOperatorMapper.Таблица соответствия операторов методам обработки доступна через свойство
ExpressionOperatorMapper.Pairs.Ключи операторов чувствительны к регистру.
Встроенные операторы
Оператор |
Описание |
|---|---|
|
Побитовое И |
|
Побитовое ИЛИ |
|
Логическое И |
|
Логическое ИЛИ |
|
Равно |
|
Не равно |
|
Больше |
|
Больше или равно |
|
Меньше |
|
Меньше или равно |
|
В коллекции |
|
Элемент в нижнем регистре в коллекции |
|
Элемент в верхнем регистре в коллекции |
|
Содержит подстроку |
|
Элемент в нижнем регистре содержит подстроку |
|
Элемент в верхнем регистре содержит подстроку |
|
Начинается с |
|
Элемент в нижнем регистре начинается с |
|
Элемент в верхнем регистре начинается с |
|
Заканчивается на |
|
Элемент в нижнем регистре заканчивается на |
|
Элемент в верхнем регистре заканчивается на |
Отрицательные операторы
!in/not in! as lower in! as upper in!contains! as lower contains! as upper contains! starts with! as lower starts with! as upper starts with! ends with! as lower ends with! as upper ends with
Слово not
Слово not – синоним !.
Оно облегчает читаемость, но подходит не для всех операторов:
Операторы со словами будут еще длиннее.
-
Употребление с
notне всегда будет грамматически верным.❌ not contains
✅ does not contain
Унарные операторы
Встроенных унарных операторов (!, !&&, is null, is not empty и пр.) пока нет. При необходимости их можно добавить самостоятельно. Возможно, они появятся в будущих версиях.
Транслируемость в SQL
Все встроенные операторы спроектированы так, чтобы их можно было напрямую передавать LINQ-провайдерам (например, EF) без промежуточной обработки.
Регистр и нормализация строк
Сравнение строк чувствительно к регистру и зависит от collation в БД, а не от операторов.
Регистр строковых значений в фильтре никогда не меняется автоматически.
Нормализация строк – ответственность клиента.
Операторы с
as lower/as upperизменяют регистр проверяемого элемента перед вычислением, не трогая значения в фильтре. Значения в фильтре всегда используются так, как указаны в JSON.
Коллекции в фильтре
Коллекция в фильтре может быть пустой, но не
null.Коллекция в фильтре и ее элементы никогда не изменяются автоматически.
Операторы с
as lower/as upperизменяют регистр каждого элемента исходной коллекции перед вычислением, не трогая коллекцию в фильтре.
Добавить свои операторы
Только свои
JsonLinq.Configure(options =>
{
options.OperatorMapper = new ExpressionOperatorMapper()
.Add("=", Expression.Equal)
.WithAliases("eq", "EQ")
.AddNot("!=", "=")
.WithAliases("neq", "NEQ")
.AddAlias("==", "=")
.WithNot("<>");
});
Расширить набор встроенных
JsonLinq.Configure(options =>
{
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.Add(...
});
Отрицательные операторы
Отрицательные (инверсные) операторы можно задавать без явного указания ключа.
В этом случае ключ генерируется автоматически через NegatedKeyProvider.
Этот провайдер можно задать через метод SetNegatedKeyProvider().
new ExpressionOperatorMapper()
// Провайдер по умолчанию
.Add("a", ...).WithNot() // "!a"
.Add("b c", ...).WithNot() // "! b c"
// Свой провайдер
.SetNegatedKeyProvider(key => (key.Contains(' ') ? "~ " : "~") + key)
.Add("x", ...).WithNot() // "~x"
.Add("y z", ...).WithNot() // "~ y z"
SQL-операторы
Библиотека реализована на netstandard2.1 и не зависит от EF или других ORM.
Для сравнения строк без учета регистра используйте встроенные операторы с as lower / as upper. Они отличаются только предпочтительным регистром значений в фильтре.
JsonLinq.Configure(options =>
{
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.Add("like", (element, pattern) =>
Expression.Call(
typeof(DbFunctionsExtensions).GetMethod(
nameof(DbFunctionsExtensions.Like),
[typeof(DbFunctions), typeof(string), typeof(string)]),
Expression.Constant(EF.Functions),
element,
pattern))
.Add("ilike", (element, pattern) =>
Expression.Call(
typeof(NpgsqlDbFunctionsExtensions).GetMethod(
nameof(NpgsqlDbFunctionsExtensions.ILike),
[typeof(DbFunctions), typeof(string), typeof(string)]),
Expression.Constant(EF.Functions),
element,
pattern));
});
Конфигурация JsonToLinq
Вы можете задавать свои настройки через JsonLinq.Configure().
Для сброса вызывается JsonLinq.ResetConfiguration(), что необходимо при тестировании.
JsonLinq.Configure(options =>
{
// Имена свойств в JSON-фильтре
options.LogicOperatorPropertyName = "?";
options.RulesPropertyName = "⚖️";
options.OperatorPropertyName = "⚡";
options.FieldPropertyName = "?";
options.ValuePropertyName = "?";
// Определения и синонимы операторов
options.OperatorMapper = ExpressionOperatorMapper.OnDefault()
.AddAlias("does not contain", "!contains");
// Логика обработки выражений с null
options.BindBuilder = ExpressionBindBuilders.NullAsFalse;
// Как имена свойств C# будут выглядеть в JSON
options.ConvertPropertyNameForJson = JsonNamingPolicy.SnakeCaseUpper.ConvertName;
});
Актуальными кажутся настройки структуры JSON-фильтра.
Например, добавлять сахар:
"Logic": "&&", "Rules": [...] -> "&&": [...].
Или фиксировать порядок свойств для использования TONL-фильтров.
Возможно, это будет реализовано в будущих версиях.
Тестирование JsonToLinq
Юнит-тесты покрывают:
Все, что задействовано в разборе JSON-фильтров
Операторы с кастомными выражениями (
in,containsи др.)Методы конфигурации
Методы расширения
IEnumerable
Полное покрытие юнит-тестами будет актуально после обкатки библиотеки в реальных проектах.
Демонстрации и эксперименты
Репозиторий содержит проект JsonToLinq.Demo с рабочими примерами. Это удобно для обратной связи и взаимодействия -
вы можете сделать PR со своими демонстрациями интересных примеров, багов или новых возможностей.
Для обозначения фильтров используется специальная нотация.
Нотация фильтра
Синтаксис
expr = logic[expr(, expr)*]
expr- одно условие или комбинация условийlogic- оператор для комбинирования условий, например,&&,&,||,|, или свой собственный
Примеры
&&[x = null]&&[a < 0, b > 0]&&[x = null, ||[a < 0, b > 0]]
Демонстрационный проект
Проект содержит примеры работы библиотеки с EF и PostgreSQL.
Реальная БД используется вместо in-memory хранилища для чистоты и реалистичности экспериментов.
Вы можете добавить свои примеры с другими БД и ORM через PR.
Перед запуском примеров нужно выбрать пункт меню Prepare Data, чтобы выполнить миграции и заполнить таблицы.

Ограничения JsonToLinq
-
IDE не подсвечивает синтаксис аргумента с JSON-фильтром в методах LINQ
Библиотека реализована на
netstandard2.1, который не поддерживаетStringSyntaxAttribute.
Нужно быть внимательным при написании фильтров вручную.
На практике это не критично, т.к. фильтры будут прилетать из клиентских приложений. -
Нет IntelliSense для написания JSON-фильтров
Без подсказок труднее контролировать правильность фильтров.
-
Не поддерживаются поля агрегатов
Пока рекомендуется использовать плоские DTO или DB view.
{ "Rules": [ { "Field": "✅ department_id", "Operator": "=", "Value": 123 }, { "Field": "❌ department.id", "Operator": "=", "Value": 123 } ] }
Возможно, все это будет реализовано в следующих версиях...
Потенциал JsonToLinq
Фильтрация – только первая стадия развития инфраструктуры JSON-LINQ.
Что может быть дальше:
Выборка данных – JSON для
Select()Группировка данных – JSON для
GroupBy()Движок GraphQL со стандартным JSON (в отличие от HotChocolate)
Серверные аналоги RxJS/NgRx для реактивной обработки данных
Интерактивные студии построения запросов – визуальные конструкторы с canvas, drag-n-drop, flowcharts, node-based UI
Семантический поиск и NLP
Новый стандарт обмена данными между сервисами
Поддержать проект
Вы можете поддержать развитие проекта одним из способов:
⭐ Поставить звезду на GitHub
Поделиться статьей с коллегами и друзьями
Сделать PR в JsonToLinq.Demo с демонстрацией примера, бага или новой фичи
Комментарии (2)

dprav
01.01.2026 17:36Даже если вы создали велосипед, можно продолжать улучшать свой проект и однажды ты поймешь, что ты создашь конкурента. Этот путь тернист и для сильных людей. Нужно приложить немало усилий и идти до конца.
Удачи вам.
Evgeniy_Van
Мне кажется вы изобрели велосипед, есть проект ucast - UCAST - Universal Conditions AST
- GitHub - stalniy/ucast: Conditions query translator for everything
- GitHub - open-policy-agent/ucast-linq: UCAST integration for LINQ. Uses LINQ Expression ASTs for efficient queries.