В нашем предыдущем уроке мы обсудили ключевые моменты деревьев выражений, их примеры использования и ограничения. Любая тема без практического примера, особенно если она связана с программированием, не имеет большого смысла. В этой статье мы рассмотрим вторую часть деревьев выражений в C# и покажем реальную мощь их использования на практике.
Что мы собираемся построить?
Наша основная цель — создать веб-API на Asp.NET Core с динамической функцией фильтрации, построенной с использованием минимального API, EF Core и, конечно же, деревьев выражений.
Мы планируем построить фильтрацию для базы данных продуктов и использовать деревья выражений, чтобы показать одну из реальных возможностей деревьев выражений при построении сложных и динамических запросов. Вот финальный пример с несколькими динамическими аргументами фильтрации:
Для более полных примеров обращайтесь к репозиторию на GitHub.
Начало работы
Сначала откройте Visual Studio и выберите шаблон веб-API Asp.NET Core с следующей конфигурацией:
Мы используем .NET 8.0, но сама тема не зависит от версии .NET. Вы даже можете использовать классический .NET Framework для работы с деревьями выражений. Название проекта — “ExpressionTreesInPractice”.
Вот сгенерированный шаблон из Visual Studio:
Чтобы иметь простое хранилище, мы будем использовать InMemory Ef Core
. Вы можете использовать любое другое подхранилище EF Core.
Теперь перейдите в Tool->Nuget Package Manager->Package Manager Console и введите следующую команду:
install-package microsoft.entityframeworkcore.inmemory
Теперь создадим нашу реализацию DbContext. Создайте папку под названием ‘Database
’ и добавьте в нее класс ProductDbContext
со следующим содержимым:
using ExpressionTreesInPractice.Models;
using Microsoft.EntityFrameworkCore;
namespace ExpressionTreesInPractice.Database
{
public class ProductDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
public ProductDbContext(DbContextOptions<ProductDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>().HasData(new List<Product>
{
new Product(){ Id = 1, Category = "TV", IsActive = true, Name = "LG", Price = 500},
new Product(){ Id = 2, Category = "Mobile", IsActive = false, Name = "Iphone", Price = 4500},
new Product(){ Id = 3, Category = "TV", IsActive = true, Name = "Samsung", Price = 2500}
});
base.OnModelCreating(modelBuilder);
}
}
}
Мы просто добавили базовые данные для инициализации при запуске приложения, и именно для этого нам нужно переопределить OnModelCreating
из DbContext
. Отличный пример использования паттерна "Шаблонный метод", не правда ли?
Нам нужна наша модель сущности под названием Product
, вы можете создать папку ‘Models
’ и добавить туда класс Product
со следующим содержимым:
namespace ExpressionTreesInPractice.Models
{
public class Product
{
public int Id { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public bool IsActive { get; set; }
public string Name { get; set; }
}
}
Теперь пришло время зарегистрировать нашу реализацию DbContext
в файле Program.cs
:
builder.Services.AddDbContext<ProductDbContext>(x => x.UseInMemoryDatabase("ProductDb"));
Кстати, в Program.cs есть множество ненужных кодовых фрагментов, которые нужно удалить. После всей очистки наш код должен выглядеть так:
using ExpressionTreesInPractice.Database;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// Добавляем сервисы в контейнер.
builder.Services.AddDbContext<ProductDbContext>(x => x.UseInMemoryDatabase("ProductDb"));
var app = builder.Build();
// Настраиваем конвейер HTTP-запросов.
app.UseHttpsRedirection();
app.Run();
Мы не хотим использовать контроллеры, так как они тяжеловесны и вызывают дополнительные проблемы. Поэтому мы выбираем минимальный API. Если вы не знакомы с минимальными API, пожалуйста, посмотрите наш видеоурок, чтобы узнать больше.
После того, как вы разберетесь, откройте Program.cs и добавьте следующий код:
app.MapGet("/products", async ([FromBody] ProductSearchCriteria productSearch, ProductDbContext dbContext) =>
{ }
Приведенный выше код определяет маршрут в минимальном API ASP.NET Core и создает конечную точку для HTTP-запроса GET
на путь /products
. Метод использует асинхронное программирование для обработки потенциально долгих операций без блокировки основного потока приложения.
ProductSearchCriteria
— это параметр, переданный в метод, который содержит критерии для фильтрации продуктов. Он помечен атрибутом [FromBody]
, что означает, что тело запроса будет привязано к этому параметру. Обычно GET
-запросы не используют тело запроса, но в этом случае оно разрешено, если нужно передать сложный объект.
ProductDbContext
— это контекст базы данных, который представляет сессию с базой данных. Он внедряется в метод, позволяя приложению выполнять такие операции, как запрос продуктов на основе критериев поиска.
Причина использования ProductSearchCriteria
вместо Product
заключается в том, что запрос должен быть динамическим. В этом случае пользователь может предоставить некоторые атрибуты продукта, но не все. Так как свойства Product
не допускают значения null
, пользователь был бы вынужден указывать все свойства, даже если не хочет фильтровать по всем.
Использование ProductSearchCriteria
дает больше гибкости. Это контейнер для необязательных и динамических параметров. Пользователь может указать только те атрибуты, по которым хочет искать, что делает его более подходящим для сценариев, когда не все свойства продукта необходимы в запросе.
Вот как выглядит наш класс ProductSearchCriteria
в папке ‘Models
’.
namespace ExpressionTreesInPractice.Models
{
public record PriceRange(decimal? Min, decimal? Max);
public record Category(string Name);
public record ProductName(string Name);
public class ProductSearchCriteria
{
public bool? IsActive { get; set; }
public PriceRange? Price { get; set; }
public Category[]? Categories { get; set; }
public ProductName[]? Names { get; set; }
}
}
Теперь давайте сосредоточимся на реализации минимального API. Обратите внимание, что целью данного урока не является показ лучших практик или написание чистого кода. Цель — продемонстрировать деревья выражений на практике, и после освоения материала вы легко сможете рефакторить код.
Вот первый фрагмент кода внутри функции MapGet
:
await dbContext.Database.EnsureCreatedAsync();
ParameterExpression parameterExp = Expression.Parameter(typeof(Product), "x");
Expression predicate = Expression.Constant(true);//x=>True && x.IsActive=true/false
if (productSearch.IsActive.HasValue)
{
MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.IsActive));
ConstantExpression constantExp = Expression.Constant(productSearch.IsActive.Value);
BinaryExpression binaryExp = Expression.Equal(memberExp, constantExp);
predicate = Expression.AndAlso(predicate, binaryExp);
}
var lambdaExp = Expression.Lambda<Func<Product, bool>>(predicate, parameterExp);
var data = await dbContext.Products.Where(lambdaExp).ToListAsync();
return Results.Ok(data);
Этот код использует классы выражений C# для динамического построения предиката для запроса к базе данных. Давайте разберем его по шагам.
await dbContext.Database.EnsureCreatedAsync();
Этот оператор асинхронно проверяет, создана ли база данных. Если она не существует, то будет создана. Это обычно используется в средах разработки или тестирования для обеспечения наличия схемы базы данных.
ParameterExpression parameterExp = Expression.Parameter(typeof(Product), "x");
Здесь создается параметр выражения, представляющий экземпляр класса Product
. Это будет действовать как входной параметр (x)
в дереве выражений, аналогично тому, как вы определяете лямбда-выражение вида x => ...
.
Expression predicate = Expression.Constant(true);
Изначально предикат создается как константное логическое выражение со значением true
. Это полезно для поэтапного построения динамического предиката, так как вы можете использовать его как базу для добавления других условий (например, true
AND
другие условия). Это служит отправной точкой для объединения дополнительных выражений.
if (productSearch.IsActive.HasValue)
Этот блок if
проверяет, что свойство IsActive
в productSearch
не равно null
, что означает, что пользователь задал фильтр для активности продукта.
Внутри блока if
:
MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.IsActive));
Это создает MemberExpression
, который получает доступ к свойству IsActive
экземпляра Product
, представленному parameterExp
(x.IsActive
). По сути, это представляет выражение x => x.IsActive
.
ConstantExpression constantExp = Expression.Constant(productSearch.IsActive.Value);
Создается ConstantExpression
со значением productSearch.IsActive
. Это значение, с которым будет производиться сравнение (true или false).
BinaryExpression binaryExp = Expression.Equal(memberExp, constantExp);
Создается BinaryExpression
, который сравнивает свойство IsActive
с заданным значением. Это представляет выражение x.IsActive == productSearch.IsActive
.
predicate = Expression.AndAlso(predicate, binaryExp);
Текущий предикат (который начался с true
) комбинируется с новым условием (x.IsActive == productSearch.IsActive
) с помощью логической операции AND
. Это приводит к выражению, которое можно использовать для фильтрации продуктов по их статусу активности.
В целом, приведенный выше код динамически строит дерево выражений, которое в конечном итоге будет использоваться для фильтрации продуктов в зависимости от того, активны они или нет. Изначальный предикат (true
) позволяет легко добавлять дополнительные условия без специальной обработки для первого условия. Если productSearch.IsActive
указано, добавляется условие, проверяющее, соответствует ли свойство IsActive
продукта заданному значению (true или false).
Затем переменной lambdaExp
присваивается лямбда-выражение, которое представляет функцию фильтрации для сущностей Product
. Это лямбда-выражение создается из предиката, построенного ранее, который может содержать такие условия, как проверка активности продукта (IsActive
). Вызов Expression.Lambda<Func<Product, bool>>
генерирует Func<Product, bool>
, то есть функцию, которая принимает продукт в качестве входного параметра и возвращает логическое значение, определяющее, соответствует ли продукт критериям фильтрации.
Далее это лямбда-выражение передается в метод Where
DbSet Products
в dbContext
. Метод Where
применяет этот фильтр к записям о продуктах в базе данных. Он создает запрос, который извлекает только те продукты, которые соответствуют условиям, определенным в лямбда-выражении.
Наконец, метод ToListAsync()
асинхронно выполняет запрос и извлекает соответствующие продукты в виде списка. Этот список затем возвращается в виде HTTP-ответа 200 OK с помощью Results.Ok(data)
. Результатом является отфильтрованный список продуктов, отправленный обратно в качестве ответа API.
Для тестирования просто запустите приложение и отправьте следующий GET
-запрос с телом через Postman:
Этот подход полезен при динамическом построении запросов, так как позволяет добавлять условия в зависимости от предоставленных фильтров.
Вот как будет выглядеть ваше выражение после компиляции дерева выражений:
{x => (True AndAlso (x.IsActive == True))}
Пока что мы реализовали самое простое свойство, которое имеет два значения: true
или false
. Но как насчет других свойств, таких как categories
, names
, price
и т. д.? Пользователи могут выбирать продукт не только по признаку активности, но, например, по его категории. Мы позволяем пользователям указывать несколько категорий одновременно, поэтому реализовали это как массив в нашем классе ProductSearchCategory
.
csharpCopy codeif (productSearch.Categories is not null && productSearch.Categories.Any())
{
//x.Category
MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Category));
Expression orExpression = Expression.Constant(false);
foreach (var category in productSearch.Categories)
{
var constExp = Expression.Constant(category.Name);
BinaryExpression binaryExp = Expression.Equal(memberExp, constExp);
orExpression = Expression.OrElse(orExpression, binaryExp);
}
predicate = Expression.AndAlso(predicate, orExpression);
}
Код добавляет динамическую фильтрацию по категориям продуктов. Сначала проверяется, не равны ли Categories
в объекте productSearch
null и содержат ли они элементы. Если да, то выполняется построение динамического выражения для фильтрации продуктов по категориям.
Начинается с доступа к свойству Category
класса Product
через выражение. Это выражение представляет x => x.Category
, где x
— это экземпляр Product
.
Изначальное выражение orExpression
установлено в false
. Это будет базой для динамического сравнения категорий. Используется цикл для перебора каждой категории в productSearch.Categories
. Для каждой категории создается константное выражение с именем категории и бинарное выражение, которое проверяет, равна ли категория продукта указанной категории.
Затем бинарные выражения объединяются с помощью OrElse
, что означает, что если продукт соответствует любой из предоставленных категорий, условие становится истинным. После обработки всех категорий, объединенное выражение orExpression
добавляется к основному предикату с помощью AndAlso
. Это означает, что основной предикат теперь будет проверять как предыдущие условия, так и то, соответствует ли категория продукта одной из категорий в критериях поиска.
Этот подход позволяет динамически фильтровать продукты по нескольким категориям и интегрирует фильтрацию категорий в существующий предикат.
В конце приведенного выше кода вы получите LINQ-выражение, которое представляет собой лямбда-функцию, используемую для фильтрации продуктов на основе динамических условий. Это выражение может быть преобразовано в предикат для использования в LINQ-запросе, который можно применить к вашему ProductDbContext
или любому IQueryable<Product>
.
LINQ-выражение в данном случае будет комбинацией логических операций (AND и OR), которые фильтруют продукты. В псевдокоде это будет выглядеть так:
products.Where(x => (x.Category == "Category1" || x.Category == "Category2" || ...) && другие условия)
Если пользователь указывает и isActive
, и категории, то мы получим следующее лямбда-выражение:
{x => ((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV"))))}
Для тестирования просто запустите приложение и отправьте следующий GET
-запрос с телом через Postman:
Мы используем тот же подход для поля Names
. Вот наш фрагмент кода:
if (productSearch.Names is not null && productSearch.Names.Any())
{
//x.Name
MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Name));
Expression orExpression = Expression.Constant(false);
foreach (var productName in productSearch.Names)
{
var constExp = Expression.Constant(productName.Name);
BinaryExpression binaryExp = Expression.Equal(memberExp, constExp);
orExpression = Expression.OrElse(orExpression, binaryExp);
}
predicate = Expression.AndAlso(predicate, orExpression);
}
Этот фрагмент кода динамически создает условие фильтрации для имен продуктов, используя деревья выражений. Сначала он проверяет, что свойство productSearch.Names
не равно null
и содержит элементы. Если имена продуктов для фильтрации присутствуют, то продолжается построение выражения для сравнения свойства Name
сущности Product
.
Выражение memberExp
ссылается на свойство Name
продукта (аналогично x.Name
в лямбда-выражении). Изначально создается выражение orExpression
, которое устанавливается как false
. Это выражение будет обновляться в цикле, чтобы накопить сравнения для каждого имени в коллекции productSearch.Names
.
Внутри цикла для каждого имени в коллекции productSearch.Names
создается константное выражение с именем продукта. Затем формируется бинарное выражение, которое проверяет, совпадает ли имя продукта с текущим именем из поиска. Цикл накапливает серию условий OR
, используя Expression.OrElse
, что создает логическую операцию OR между текущим orExpression
и новым сравнением.
После завершения цикла итоговое выражение orExpression
представляет собой цепочку условий OR, где имя продукта должно совпадать с одним из имен в коллекции productSearch.Names
. Это выражение объединяется с существующим предикатом с помощью Expression.AndAlso
, гарантируя, что фильтр по имени применяется наряду с любыми другими условиями, ранее определенными в предикате.
Проще говоря, наш блок кода динамически строит фильтр запроса, который сопоставляет продукты по их имени, позволяя использовать несколько возможных имен из коллекции productSearch.Names
.
Если пользователь указывает только имена(names)
в теле запроса, мы получим примерно следующее лямбда-выражение:
{x => (True AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung")))}
Если мы получим все параметры фильтрации, такие как isActive
, категории(categories
) и имена(names
) из тела запроса, то в итоге мы получим следующее лямбда-выражение:
{x => (((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV")) OrElse (x.Category == "Some Other")) OrElse (x.Category == "Mobile"))) AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung")))}
Вот как это будет выглядеть при запуске приложения и отправке запроса:
Последним аргументом для нашей динамической фильтрации является цена(price
). Это сложный объект, состоящий из минимального(min
) и максимального(max
) значений. Пользователь должен иметь возможность указать любое из них, оба или ни одного. Именно поэтому мы сделали эти параметры nullable
.
Вот как выглядит наша реализация кода:
if (productSearch.Price is not null)
{
//x.Price 400
MemberExpression memberExp = Expression.Property(parameterExp, nameof(Product.Price));
//x.Price >= min
if (productSearch.Price.Min is not null)
{
var constExp = Expression.Constant(productSearch.Price.Min);
var binaryExp = Expression.GreaterThanOrEqual(memberExp, constExp);
predicate = Expression.AndAlso(predicate, binaryExp);
}
//(x.Price >= min && x.Price <= max)
if (productSearch.Price.Max is not null)
{
var constExp = Expression.Constant(productSearch.Price.Max);
var binaryExp = Expression.LessThanOrEqual(memberExp, constExp);
predicate = Expression.AndAlso(predicate, binaryExp);
}
}
Этот код динамически создает предикат для фильтрации продуктов по диапазону цен с использованием деревьев выражений. Он начинает с проверки того, что объект productSearch.Price
не равен null
, что указывает на применение фильтра по цене.
Выражение memberExp
создается для представления свойства Price
продукта (x.Price
). Это выражение используется для сравнения цены продукта с минимальными и максимальными значениями, указанными в объекте productSearch.Price
.
Если указана минимальная цена (productSearch.Price.Min
не равен null
), создается выражение, проверяющее, больше ли цена продукта или равна минимальному значению. Это условие добавляется в общий предикат с использованием Expression.AndAlso
, что означает, что продукт должен удовлетворять этому условию, чтобы быть включенным в результаты.
Аналогично, если указана максимальная цена (productSearch.Price.Max
не равен null
), создается еще одно выражение, проверяющее, меньше ли цена продукта или равна максимальному значению. Это условие также добавляется к существующему предикату с использованием Expression.AndAlso
, гарантируя, что применяются оба условия — и минимальная, и максимальная цена.
Вкратце, код строит предикат, который фильтрует продукты по указанному диапазону цен, гарантируя, что продукты имеют цену, большую или равную минимальной (если указана), и меньшую или равную максимальной (если указана).
Если пользователь указывает только цену в теле запроса, мы получим примерно следующее лямбда-выражение:
{x => ((True AndAlso (x.Price >= 400)) AndAlso (x.Price <= 5000))}
Если мы получим все параметры фильтрации, такие как isActive
, categories
, names
и price
из тела запроса, то в итоге мы получим следующее лямбда-выражение:
{x => (((((True AndAlso (x.IsActive == True)) AndAlso (((False OrElse (x.Category == "TV")) OrElse (x.Category == "Some Other")) OrElse (x.Category == "Mobile"))) AndAlso (((False OrElse (x.Name == "LG")) OrElse (x.Name == "LG2")) OrElse (x.Name == "Samsung"))) AndAlso (x.Price >= 400)) AndAlso (x.Price <= 5000))}
Вот как это будет выглядеть при запуске приложения и отправке запроса:
Кстати, хотите увидеть всё на практике с подробным видео? Тогда вот моё видео, где я создаю всё с нуля и даю простое и понятное объяснение каждого шага
То же самый контент, где я всё объясняю на английском языкe.Кстати, эти видео — не переводы. Всё записано с нуля для каждого видео.
Изящное завершение
Эта статья служит практическим продолжением предыдущего урока по деревьям выражений в C#, с акцентом на их реальное использование в рамках веб-API на ASP.NET Core. Она исследует создание функционала динамической фильтрации с использованием минимального API, Entity Framework Core (EF Core) и деревьев выражений.
Проект включает создание базы данных продуктов с возможностью динамической фильтрации по таким атрибутам продукта, как IsActive
, Category
, Name
и Price
. В статье подчеркивается использование деревьев выражений для построения гибких и динамичных запросов без жесткого кодирования конкретных фильтров.
Настройка начинается с использования веб-API ASP.NET Core с базой данных в памяти для хранения, хотя можно использовать и другие базы данных, поддерживаемые EF Core. В статье делается акцент на использование минимального API вместо традиционных контроллеров для упрощения и повышения производительности, а также предоставляются инструкции для выполнения необходимых шагов, включая настройку контекста базы данных (DbContext
) и инициализацию данных.
Одной из основных функций, продемонстрированных в статье, является то, как деревья выражений используются для динамического построения предикатов. Например, при фильтрации по свойству IsActive
система проверяет, указал ли пользователь этот фильтр, и затем динамически создает условие, которое сравнивает статус активности продукта с предоставленным значением. Процесс расширяется, чтобы включить динамическую фильтрацию по другим свойствам, таким как Category
, Name
и Price
, каждое из которых позволяет гибко настраивать критерии для запросов.
Используя деревья выражений, статья показывает, как можно строить сложные и гибкие запросы без необходимости писать множество методов с жестко закодированными запросами. Пример фильтрации продуктов по Name
и Category
демонстрирует, как логические условия OR
могут быть динамически объединены в зависимости от ввода пользователя, что приводит к созданию лаконичной и многократно используемой логики запросов.
Кроме того, фильтрация по цене обрабатывается путем проверки как минимальных, так и максимальных значений и динамической настройки предиката, чтобы включить только те продукты, которые находятся в указанном диапазоне цен.
В заключение, эта статья демонстрирует мощь деревьев выражений в создании динамичных и гибких запросов в приложениях на C#. В ней приводятся практические примеры кода, использующие деревья выражений для построения запросов в веб-API на ASP.NET Core, предлагая практический способ управления сложными сценариями реального мира, такими как фильтрация баз данных продуктов на основе различного пользовательского ввода.
Хотите углубиться?
Я регулярно делюсь своим опытом на уровне senior на моих YouTube-каналах TuralSuleymaniTech на английском и TuralSuleymaniTechRu на русском, где разбираю сложные темы, такие как .NET, микросервисы, Apache Kafka, Javascript, проектирование программного обеспечения, Node.js и многое другое, делая их простыми для понимания. Присоединяйтесь к нам и повышайте свои навыки!
Комментарии (10)
jaanq
06.10.2024 00:34+1Мне кажется что сабж в статье - велосипед, и гораздо удобнее использовать для фильтров PredicateBuilder.
Он пока не входит в "коробку", но есть в изрядном количестве библиотек типа "linq on linq".
class MyAwesomeClass { public int Number { get; set; } public string Name { get; set; } = default!; public DateTime UpdateAt { get; set; } public string Description { get; set; } = default!; public override string ToString() { return $"Number = {Number}, Name = {Name}, UpdateAt = {UpdateAt.ToString("yyyy.MM.dd")}, Descr = {Description}"; } } internal class Program { static void Main(string[] args) { List<MyAwesomeClass> items = new List<MyAwesomeClass> { new MyAwesomeClass{ Name = "Dog", Number = 1, UpdateAt = new DateTime(2024,09,25) }, new MyAwesomeClass{ Name = "Cat", Number = 2, UpdateAt = new DateTime(2024,09,24) }, new MyAwesomeClass{ Name = "CatDog", Number = 3, UpdateAt = new DateTime(2024,09,20) }, new MyAwesomeClass{ Name = "DogCat", Number = 127, UpdateAt = new DateTime(2024,10,24) } }; var predicate = PredicateBuilder.True<MyAwesomeClass>(); predicate = predicate.And(x => x.Number > 1 ); predicate = predicate.And(x => x.Name.Contains("Cat")); predicate = predicate.And(x => x.UpdateAt < DateTime.UtcNow); var result = items.Where(predicate.Compile()); foreach (var item in result) Console.WriteLine(item.ToString()); } }
YegorP
06.10.2024 00:34+3Зачем использовать в C# деревья выражений так, будто их там нет? Весь смысл этой фичи в том, чтобы выражения на сишарпе транслировались в выражения на языке СУБД. Получается примерно как в Си написать всю программу ассемблерными вставками. Какой смысл тогда вообще отталкиваться от Си? Фигачьте всё на ассемблере.
Нормальный паттерн построения динамических фильтров уже показали в другом комменте. В явном виде использовать все эти "and expression" оправдано при разработке драйверов СУБД или ещё каких-нибудь трансляторов - как раз чтобы пользователь вашего драйвера мог писать на нормальном C#. Ну то есть если бы вы Entity Framework делали.
TerekhinSergey
06.10.2024 00:34+1Как показывает практика и болезненный опыт, нельзя давать пользователю прямую дыру в базу... Это неплохо работает при старте проекта, когда непонятно, что надо отдать пользователю, или в каких-то ограниченных ситуациях. В общем и целом - это должно быть запрещено законодательно и за такое желание надо бить по рукам.
Яркий пример реализации динамических фильтров - протокол OData. Почти что прямой sql. Сюда же можно и GraphQL приплести. Но есть и обратная сторона медали: ответственность за то, насколько запрос будет быстрым, ложится на вызывающий код со всеми вытекающими. Кроме того, тестировать это непонятно как - вроде как ответственность на вызывающем коде, но в то же время и внутренние ошибки не исключены. Получается миллион разных комбинаций "входные данные - запрос - результат", некоторые из которых могут быть некорректными, но возможности исправления тут весьма ограничены (запрос то от пользователя). Также любое отклонение от принятой канвы приводит к боли и страданиям: в OData например, не получится так просто прикрутить кэш или собрать данные из двух сервисов.
В общем, моё личное мнение, что для большинства сценариев (включая фильтры во всяких интернет-магазинах) достаточно фильтров наподобие упомянутых [здесь](https://habr.com/ru/articles/848446/#comment_27382348), а в остальных случаях стоит крепко подумать
Dimonogen
06.10.2024 00:34Статья выглядит странно. С одной стороны обозревается довольно мощный инструмент, но используется он для простой задачи, которая решается и более простыми и лаконичными инструментами типо той же linq функцией where , только тут мы конструируем выражение очень сложным и неудобным способом.
Я видел в коде реального проекта, где деревья выражений использовались чтобы построить фильтрацию по любым полям. То есть на входе некий класс фильтра, где есть строка, которая переводится в поле модели и в зависимости от типа поля строиться выражение. Показали бы такой код, вопросов бы к вам не было.
К тому же сложность туториала высокая, а разбираемые примеры не такие и сложные.
JustAskIfYouDontKnow
Местами как будто избыточно сложно все выглядит, что на счет такого подхода?
EgorovDenis
Сам использую подобный подход через задание Expression напрямую без излишнего велосипедостроения.
Только есть несколько моментов, которые надо учитывать:
1) ConstantExpression в примере автора приводит к тому, что запрос в EF Core не кешируется, поэтому лучше его не использовать
2) вместо операции "И" от EF Core можно написать собственные методы расширения Lambda Expression, которые смогут объединить операции "И" и "ИЛИ". Это может быть полезно, когда динамически строишь Expression без четкой модели запроса с именами параметров или в Entity-Attribute-Value. Тогда можно будет по частям строить предикаты и потом на отдельном этапе их объединить
SuleymaniTural Автор
Спасибо за ценные замечания! Давайте разберемся по пунктам.
По поводу ConstantExpression — вы абсолютно правы. Это может негативно сказаться на кешировании запросов в EF Core. В более сложных сценариях, где динамически генерируются выражения, лучше избегать использования ConstantExpression, чтобы не нарушать оптимизацию запросов. Можно использовать подходы, такие как кэширование динамически созданных выражений или их компиляция.
Что касается кастомных методов расширения для объединения операций "И" и "ИЛИ" — отличный совет! Этот подход действительно может быть полезен при динамическом построении выражений, особенно в случаях, когда модель запроса меняется или используется паттерн Entity-Attribute-Value (EAV). Создание собственных методов расширения для Lambda Expression помогает более гибко работать с предикатами и объединять их на более поздних этапах, что добавляет удобства в сложных сценариях фильтрации.
Однако, как я уже упоминал в статье, цель была продемонстрировать использование деревьев выражений (expression trees), а не показать лучшие практики. Тем не менее, ваши советы помогут тем, кто стремится оптимизировать подобные подходы в реальных проектах.
SuleymaniTural Автор
Спасибо большое за ваш комментарий! Вы абсолютно правы — такой подход с использованием LINQ-запросов отлично подходит для простых сценариев фильтрации, как тот, который вы предложили. Он лаконичен и понятен.
Однако, когда дело касается более сложных требований к фильтрации, особенно когда фильтры нужно создавать динамически во время выполнения программы на основе пользовательского ввода или других факторов, выражения деревьев (expression trees) становятся более мощным и гибким решением. Они позволяют создавать очень динамичные запросы, в которых условия не предопределены в коде, а генерируются программно. Это особенно полезно в приложениях, где есть много необязательных фильтров или структура запроса зависит от сложной логики.
По сути, деревья выражений обеспечивают большую масштабируемость и расширяемость при работе с более сложными требованиями фильтрации, при этом сохраняя производительность.
uhfath
Было бы интересно посмотреть на применение этого подхода в случае вычисляемых выражений.
Например, когда у продукта есть список покупателей и надо отфильтровать по его количеству.
AngryPinguin
Не по теме статьи, а по комментарию.
Со временем, это вот дело становится ОООЧ сложным. Например, на фронте хотят чтобы можно было реально выражения в духе "Телевизоры или мониторы, но не плоские, с ценой до 30000, выпущенные до 2009 года, в Туле, на заводе имени Петра Игнатова" . А потом еще - чтобы отдавалась не вся DTO'шка, а только превьюшки и краткое описание.
И вот такой подход перестает работать, либо превращается в неподдерживаемого монстра.