День 2462. #ЗаметкиНаПолях
Динамический LINQ. Начало
Вы реализуете простую конечную точку поиска… а затем приходит таска: «А можно отфильтровать по статусу? По последнему входу в систему? Сортировать по любому столбцу?» и т.п. И вот ваш красивый LINQ-запрос превращается в лес if, а каждое изменение означает повторное развёртывание. Знакомо? Рассмотрим динамические предикаты, которые выполняются как настоящий LINQ (поэтому EF Core транслирует их в SQL): как они работают, когда использовать и несколько полезных советов.
Зачем?
- Неограниченные фильтры: интерфейсы администратора, конструкторы отчётов, сохранённые поисковые запросы — пользователи сами выбирают поля/операции.
- Разные тенанты: каждому клиенту нужны немного разные правила.
- Сохранение производительности EF: библиотека преобразует строку в лямбда-выражение и вызывает реальный метод LINQ (Where, OrderBy, …) для IQueryable; EF по-прежнему перекладывает работу на SQL.
Как работает?
С помощью C# Eval Expression вы можете предоставить выражение в виде строки и вызвать динамическое расширение: WhereDynamic, OrderByDynamic, SelectDynamic, FirstOrDefaultDynamic и т.д. Внутри библиотеки оно преобразуется в дерево выражений и вызывается фактический оператор LINQ. Это работает для IEnumerable и IQueryable (включая EF Core).
Мы рассмотрим некоторые самые распространённые варианты использования:
- WhereDynamic — динамическая фильтрация,
- OrderByDynamic/ThenByDynamic — динамическая сортировка,
- SelectDynamic — динамическая проекция,
- FirstOrDefaultDynamic — динамическое извлечение единичных значений.
1. WhereDynamic
До: куча условных блоков
После: один динамический предикат
Почему это лучше: один конвейер, отсутствие дублирующихся запросов, и вы можете добавлять необязательные критерии, не изменяя форму запроса.
Кстати, не обязательно вставлять литералы в строку. Передайте объект контекста (анонимный тип/словарь/expando/класс) и ссылайтесь на его свойства по имени внутри выражения:
Окончание следует…
Источник: https://thecodeman.net/posts/dynamic-linq-in-dotnet
Динамический LINQ. Начало
Вы реализуете простую конечную точку поиска… а затем приходит таска: «А можно отфильтровать по статусу? По последнему входу в систему? Сортировать по любому столбцу?» и т.п. И вот ваш красивый LINQ-запрос превращается в лес if, а каждое изменение означает повторное развёртывание. Знакомо? Рассмотрим динамические предикаты, которые выполняются как настоящий LINQ (поэтому EF Core транслирует их в SQL): как они работают, когда использовать и несколько полезных советов.
Зачем?
- Неограниченные фильтры: интерфейсы администратора, конструкторы отчётов, сохранённые поисковые запросы — пользователи сами выбирают поля/операции.
- Разные тенанты: каждому клиенту нужны немного разные правила.
- Сохранение производительности EF: библиотека преобразует строку в лямбда-выражение и вызывает реальный метод LINQ (Where, OrderBy, …) для IQueryable; EF по-прежнему перекладывает работу на SQL.
Как работает?
С помощью C# Eval Expression вы можете предоставить выражение в виде строки и вызвать динамическое расширение: WhereDynamic, OrderByDynamic, SelectDynamic, FirstOrDefaultDynamic и т.д. Внутри библиотеки оно преобразуется в дерево выражений и вызывается фактический оператор LINQ. Это работает для IEnumerable и IQueryable (включая EF Core).
Мы рассмотрим некоторые самые распространённые варианты использования:
- WhereDynamic — динамическая фильтрация,
- OrderByDynamic/ThenByDynamic — динамическая сортировка,
- SelectDynamic — динамическая проекция,
- FirstOrDefaultDynamic — динамическое извлечение единичных значений.
1. WhereDynamic
До: куча условных блоков
var q = context.Customers.AsQueryable();
if (onlyActive)
q = q.Where(x => x.Status == CustomerStatus.IsActive);
if (since != null)
q = q.Where(x => x.LastLogon >= since);
if (!string.IsNullOrWhiteSpace(search))
q = q.Where(x => x.Name.Contains(search));
var list = await q.OrderBy(x => x.Name).ToListAsync();
После: один динамический предикат
// using System.Linq;
// using Z.Expressions;
string filter = "x => true";
if (onlyActive)
filter += " && x.Status == 0";
if (since != null)
filter += $@" && x.LastLogon >= DateTime.Parse(""{since:yyyy-MM-dd}"")";
if (!string.IsNullOrWhiteSpace(search))
filter += $@" && x.Name.Contains(""{search}"")";
var list = await context.Customers
.WhereDynamic(filter)
.OrderBy(x => x.Name)
.ToListAsync();
Почему это лучше: один конвейер, отсутствие дублирующихся запросов, и вы можете добавлять необязательные критерии, не изменяя форму запроса.
Кстати, не обязательно вставлять литералы в строку. Передайте объект контекста (анонимный тип/словарь/expando/класс) и ссылайтесь на его свойства по имени внутри выражения:
var env = new {
IsActive = CustomerStatus.IsActive,
LastMonth = DateTime.Now.AddMonths(-1)
};
var recentActive = await context.Customers
.WhereDynamic(x => "x.Status == IsActive
&& x.LastLogon >= LastMonth", env)
.ToListAsync();Окончание следует…
Источник: https://thecodeman.net/posts/dynamic-linq-in-dotnet
👎22👍5
День 2463. #ЗаметкиНаПолях
Динамический LINQ. Окончание
Начало
2. OrderByDynamic/ThenByDynamic
Даём пользователю возможность отсортировать результаты по любой колонке (из белого списка).
3. SelectDynamic
Для сценариев отчётов/экспорта, выдаём только то, что запросил клиент:
4. FirstOrDefaultDynamic
Идеален для поиска одного элемента или валидации на основе критерия, определяющегося во время выполнения:
Бонус: Цепочки динамических конвейеров
Если очень нужно выполнить несколько шагов LINQ в одной динамической строке (фильтрация → упорядочивание → выборка → список), есть API Execute:
Это мощный метод, но код получается не особо читаемый.
Варианты использования
1. Администрирование «Конструктор запросов»
- UI генерирует: поле + оператор + значение;
- Бэкенд выбирает разрешённые поля/операции → строит WhereDynamic (и опционально OrderByDynamic);
- Результат: один конвейер запросов, почти бесконечное количество комбинаций.
2. Маркетинг «Конструктор сегментов»
- Сегменты сохраняются как читаемые выражения (например, «Активные, последние 90 дней,>$500,не тестовые аккаунты»);
- Приложение загружает правило → WhereDynamic → сохраняет результаты;
- Результат: правила развиваются без изменения кода или повторного развёртывания.
3. Многопользовательские правила
- Каждый пользователь хранит несколько предикатов (или базовых фильтров разрешения/запрета);
- Они комбинируются во время запроса и применяются динамически;
- Результат: меньше ветвлений/флагов, более чистая модель.
Замечания
- Белый список полей/операторов: не раскрывайте всю модель; предоставьте в UI только разрешённый список.
- Проверка выражений: отклоняйте неизвестные токены/поля перед выполнением.
- IQueryable до конца: применяйте динамические операции перед ToList(), чтобы EF мог преобразовать их в SQL.
- Нормализуйте значения: используйте даты ISO или параметры (объект env) вместо парсинга текста.
- Сохраняйте читаемость: предпочитайте короткие, компонуемые строки; добавляйте вспомогательные методы для частых случаев.
Когда не использовать
Если у вас 2-3 фиксированных фильтра, которые редко меняются, статический LINQ остается самым простым (и вполне приемлемым). Динамический LINQ имеет смысл при росте изменчивости и опциональности.
Источник: https://thecodeman.net/posts/dynamic-linq-in-dotnet
Динамический LINQ. Окончание
Начало
2. OrderByDynamic/ThenByDynamic
Даём пользователю возможность отсортировать результаты по любой колонке (из белого списка).
var sort = sortCol switch
{
"LastLogon" => "x => x.LastLogon",
"TotalSpent" => "x => x.TotalSpent",
_ => "x => x.Name"
};
var ordered = await context.Customers
.OrderByDynamic(sort)
.ThenByDynamic("x => x.Id")
.ToListAsync();
3. SelectDynamic
Для сценариев отчётов/экспорта, выдаём только то, что запросил клиент:
// Клиент выбрал: "Id,Name,Country"
var cols = selectedCols.Split(',')
.Select(c => c.Trim())
.Where(c => allowedCols.Contains(c));
var selectExpr = "x => new { "
+ string.Join(", ", cols.Select(c => $"{c} = x.{c}"))
+ " }";
var result = await context.Customers
.WhereDynamic("x => x.Status == 0")
.SelectDynamic(selectExpr)
.ToListAsync();
4. FirstOrDefaultDynamic
Идеален для поиска одного элемента или валидации на основе критерия, определяющегося во время выполнения:
csharp
var result = await context.Customers
.FirstOrDefaultDynamic(
"x => x.Email == \\\"name@mail.com\\\" && x.Status == 0");
Бонус: Цепочки динамических конвейеров
Если очень нужно выполнить несколько шагов LINQ в одной динамической строке (фильтрация → упорядочивание → выборка → список), есть API Execute:
var env = new {
IsActive = CustomerStatus.IsActive,
LastMonth = DateTime.Now.AddMonths(-1) };
var result = context.Customers.Execute<IEnumerable>(
"Where(x => x.Status == IsActive && x.LastLogon >= LastMonth)" +
".Select(x => new { x.CustomerID, x.Name })" +
".OrderBy(x => x.CustomerID).ToList()", env);Это мощный метод, но код получается не особо читаемый.
Варианты использования
1. Администрирование «Конструктор запросов»
- UI генерирует: поле + оператор + значение;
- Бэкенд выбирает разрешённые поля/операции → строит WhereDynamic (и опционально OrderByDynamic);
- Результат: один конвейер запросов, почти бесконечное количество комбинаций.
2. Маркетинг «Конструктор сегментов»
- Сегменты сохраняются как читаемые выражения (например, «Активные, последние 90 дней,>$500,не тестовые аккаунты»);
- Приложение загружает правило → WhereDynamic → сохраняет результаты;
- Результат: правила развиваются без изменения кода или повторного развёртывания.
3. Многопользовательские правила
- Каждый пользователь хранит несколько предикатов (или базовых фильтров разрешения/запрета);
- Они комбинируются во время запроса и применяются динамически;
- Результат: меньше ветвлений/флагов, более чистая модель.
Замечания
- Белый список полей/операторов: не раскрывайте всю модель; предоставьте в UI только разрешённый список.
- Проверка выражений: отклоняйте неизвестные токены/поля перед выполнением.
- IQueryable до конца: применяйте динамические операции перед ToList(), чтобы EF мог преобразовать их в SQL.
- Нормализуйте значения: используйте даты ISO или параметры (объект env) вместо парсинга текста.
- Сохраняйте читаемость: предпочитайте короткие, компонуемые строки; добавляйте вспомогательные методы для частых случаев.
Когда не использовать
Если у вас 2-3 фиксированных фильтра, которые редко меняются, статический LINQ остается самым простым (и вполне приемлемым). Динамический LINQ имеет смысл при росте изменчивости и опциональности.
Источник: https://thecodeman.net/posts/dynamic-linq-in-dotnet
👍6👎1
День 2464. #ЗаметкиНаПолях
Потокобезопасная Инициализация с Помощью LazyInitializer
Хотя Lazy<T> является основным решением для ленивой инициализации в .NET, существует менее известная альтернатива, которая может быть более эффективной в некоторых сценариях: LazyInitializer.EnsureInitialized.
Этот статический метод обеспечивает потокобезопасную инициализацию с рядом ключевых характеристик:
- Только ссылочные типы: работает только с классами, но не со значениями.
- Обнаружение на основе NULL: использует значение NULL для определения необходимости инициализации.
- Эффективное использование памяти по сравнению с System.Lazy<T>: не требуется дополнительный объект-обёртка.
Метод предлагает несколько перегрузок для различных сценариев:
Замечания по потокобезопасности
По умолчанию LazyInitializer.EnsureInitialized ведёт себя как LazyThreadSafetyMode.PublicationOnly:
Несколько потоков могут создавать экземпляры одновременно. Сохраняется только первое успешное назначение. Остальные экземпляры отбрасываются.
Для строгой потокобезопасности используйте перегрузку с блокировкой синхронизации, чтобы гарантировать, что фабричный метод будет выполнен только один раз.
Внутренняя реализация этого аналогична следующему коду:
Преимущества
Главное преимущество перед Lazy<T> — вместо хранения объекта-обёртки Lazy<T> вы напрямую используете целевой экземпляр. Также это снижает затраты памяти, экономя одно поле на каждое лениво инициализированное значение.
Источник: https://www.meziantou.net/thread-safe-initialization-with-lazyinitializer.htm
Потокобезопасная Инициализация с Помощью LazyInitializer
Хотя Lazy<T> является основным решением для ленивой инициализации в .NET, существует менее известная альтернатива, которая может быть более эффективной в некоторых сценариях: LazyInitializer.EnsureInitialized.
Этот статический метод обеспечивает потокобезопасную инициализацию с рядом ключевых характеристик:
- Только ссылочные типы: работает только с классами, но не со значениями.
- Обнаружение на основе NULL: использует значение NULL для определения необходимости инициализации.
- Эффективное использование памяти по сравнению с System.Lazy<T>: не требуется дополнительный объект-обёртка.
Метод предлагает несколько перегрузок для различных сценариев:
Sample? _instance = null;
// Инициализация через конструктор по умолчанию
var instance =
LazyInitializer.EnsureInitialized(ref _instance);
// Фабричный метод
var instance =
LazyInitializer.EnsureInitialized(
ref _instance,
() => new Sample()
);
// Фабричный метод вызывается 1 раз, даже если нескольким потокам требуется инициализация
var syncLock = new object();
var instance =
LazyInitializer.EnsureInitialized(
ref _instance,
ref syncLock,
() => new Sample()
);
Замечания по потокобезопасности
По умолчанию LazyInitializer.EnsureInitialized ведёт себя как LazyThreadSafetyMode.PublicationOnly:
Несколько потоков могут создавать экземпляры одновременно. Сохраняется только первое успешное назначение. Остальные экземпляры отбрасываются.
Для строгой потокобезопасности используйте перегрузку с блокировкой синхронизации, чтобы гарантировать, что фабричный метод будет выполнен только один раз.
Внутренняя реализация этого аналогична следующему коду:
var instance = Volatile.Read(ref _instance) ??
Interlocked.CompareExchange(
ref _instance,
new Sample(),
null);
Преимущества
Главное преимущество перед Lazy<T> — вместо хранения объекта-обёртки Lazy<T> вы напрямую используете целевой экземпляр. Также это снижает затраты памяти, экономя одно поле на каждое лениво инициализированное значение.
Источник: https://www.meziantou.net/thread-safe-initialization-with-lazyinitializer.htm
👍9