Динамический 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