public class Category : HasIdBase<int>
{
public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50;
//...
}
var niceCategories = db.Query<Category>.Where(Category.NiceRating);
К сожалению, этот номер не пройдет, если вы хотите выбрать продукты из соответствующих категорий, потому что NiceRating имеет тип
Expression<Func<Category, bool>>
, а в случае с Product нам потребуется Expression<Func<Product, bool>>
. То есть, необходимо осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>
. public class Product: HasIdBase<int>
{
public virtual Category Category { get; set; }
//...
}
var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating); // так нельзя!
К счастью, осуществить это довольно просто!
// Фактически мы реализуем композицию выражений,
// которая даст нам выражение, соответствующее композиции целевых функций
public static Expression<Func<TIn, TOut>> Compose<TIn, TInOut, TOut>(
this Expression<Func<TIn, TInOut>> input
, Expression<Func<TInOut, TOut>> inOutOut)
{
// это параметр x => blah-blah. Для лямбды нам нужен null
var param = Expression.Parameter(typeof(TIn), null);
// получаем объект, к которому применяется выражение
var invoke = Expression.Invoke(input, param);
// и выполняем "получи объект и примени к нему его выражение"
var res = Expression.Invoke(inOutOut, invoke);
// возвращаем лямбду нужного типа
return Expression.Lambda<Func<TIn, TOut>>(res, param);
}
// Добавляем "продвинутый" вариант Where
public static IQueryable<T> Where<T, TParam>(this IQueryable<T> queryable
, Expression<Func<T, TParam>> prop
, Expression<Func<TParam, bool>> where)
{
return queryable.Where(prop.Compose(where));
}
// Проверяем
[Fact]
public void AdvancedWhere_Works()
{
var product = new Product(new Category() {Rating = 700}, "Some Product", 100500);
var q = new[] {product}.AsQueryable();
var values = q.Where(x => x.Category, Category.NiceRating).ToArray();
Assert.Equal(700, values[0].Category.Rating);
}
UPD
Razaz добавил еще несколько удобных Extension-методов, позволяющих комбинировать Expression. Вместе с Compose можно создавать цепочки AND и OR. Публикую их в статье as is.
public static class PredicateBuilder
{
public static Expression<Func<T, bool>> True<T>()
{
return f => true;
}
public static Expression<Func<T, bool>> False<T>()
{
return f => false;
}
public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
return Expression.Lambda<Func<T, bool>>
(Expression.OrElse(expr1.Body, invokedExpr), expr1.Parameters);
}
public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> expr1,
Expression<Func<T, bool>> expr2)
{
var invokedExpr = Expression.Invoke(expr2, expr1.Parameters);
return Expression.Lambda<Func<T, bool>>
(Expression.AndAlso(expr1.Body, invokedExpr), expr1.Parameters);
}
public static Expression<Func<TIn, bool>> And<TIn, TInOut>(this Expression<Func<TIn, bool>> target,
Expression<Func<TIn, TInOut>> input,
Expression<Func<TInOut, bool>> inOutOut)
{
var invoke = Expression.Invoke(input, target.Parameters);
var invokedExpr = Expression.Invoke(inOutOut, invoke);
return Expression.Lambda<Func<TIn, bool>>(Expression.AndAlso(target.Body, invokedExpr), target.Parameters);
}
public static Expression<Func<TIn, bool>> Or<TIn, TInOut>(this Expression<Func<TIn, bool>> target,
Expression<Func<TIn, TInOut>> input,
Expression<Func<TInOut, bool>> inOutOut)
{
var invoke = Expression.Invoke(input, target.Parameters);
var invokedExpr = Expression.Invoke(inOutOut, invoke);
return Expression.Lambda<Func<TIn, bool>>(Expression.OrElse(target.Body, invokedExpr), target.Parameters);
}
}
Для полноты картины ссылка на LinqKit, в котором все это за нас уже сделано. Однако, LinqKit предполагает использование метода AsExpandable и ориентирован, в первую очередь на Entity Framework. Те, кому функциональность LinqKit, как и мне, кажется избыточной могут ограничиться приведенными в статье методами расширений.
Комментарии (81)
xRay
23.10.2016 23:14-1Может проще использовать SQL-запрос?
marshinov
23.10.2016 23:33+1Как это решит задачу реиспользуемости бизнес-правил фильтрации? Будете SQL-строки собирать?
Razaz
24.10.2016 00:46+1Вам никто не мешает написать Visitor, который сконвертирует это в запрос к любому хранилищу — у меня в LDAP запросы так генерируются. Правда ушли от дефолтных Expression, что бы ограничить возможные варианты запросов типа MethodCallExpression и тд.
marshinov
24.10.2016 00:49А чем заменили Expression? Свой API?
Razaz
24.10.2016 01:02+1Угу. Простенько и обрезано под бизнес область. Соответственно свой аналог IQueryable + IQueryProvider с минимальным функционалом. Но на LDAP или SQL легко раскладывать. И можно свои подтипы для IQueryable рисовать и ограничивать набор выражений используемых в запросах и добавлять специфику.
Ноги отсюда растут — SCIM parser Это была отправная точка ;)
chumakov-ilya
25.10.2016 13:21+1что бы ограничить возможные варианты запросов типа MethodCallExpression
А зачем? Можно пример? Неужели отбиваются трудозатраты на реализацию IQueryProvider?
mayorovp
25.10.2016 14:00-1Во-первых, реализация IQueryProvider — не самый трудный этап. Во-вторых, если они ушли от Expression — значит, они не стали реализовывать IQueryProvider!
chumakov-ilya
25.10.2016 16:33читаем внимательно
аналог IQueryable + IQueryProvider
если вам так принципиально, можете читать мой предыдущий комментарий как трудозатраты на реализацию аналога IQueryProvider
Razaz
25.10.2016 17:38+2Трудозатраты — 2 рабочих дня + вечер ковыряния из дома :P Результат — специфичный для доменной области механизм построения запросов, который генерит заметно меньше объектов и жестко ограничивает возможные варианты композиции, что делает написание разных потребителей заметно проще и большую часть ошибок можно отсечь на этапе компиляции.
Например IPermissionQuery имеет свой набор доступных выражений.
Плюс упростилось само дерево выражений и транслировать его стало проще :)
В принципе написать IQueryProvider не так сложно, если ковыряться в этой теме, но у нас есть композиция сторов и это автоматом делает IQueryProvider не очень удобным, так как хочется собрать запрос и пихнуть в 10 сторов, которые могут быть удаленными, локальными, реляционными и нет, включая файловую систему, а рисовать композицию внутри него не шибко хочется. Плюс логика материализации зависит от стора.
У нас специфичное решение и, в общем случае, я бы нарисовал обычных выражений :)
KIVan
24.10.2016 04:09+1https://github.com/scottksmith95/LINQKit? (Создал библиотеку автор LINQPad)
С ней можно писать
.Where(p => NiceRating.Invoke(p.Categoory)
и ещё много полезных дополнений.Bronx
25.10.2016 20:30+2Когда я глядел на LinqKit последний раз, у него были проблемы с .Include(), поэтому я использовал PredicateBuilder от Pete Montgomery.
Hydro
24.10.2016 07:20+1var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating); // так нельзя!
Либо утро понедельника на меня так влияет, либо я действительно чего-то не понимаю. В чем собственно проблема?AndreyRubankov
24.10.2016 08:27+1Дык, параметр дженерика не тот. Нельзя в Where для Product закидывать дженерик параметрезированный Category.
marshinov
24.10.2016 10:48AndreyRyabov правильно ответил. Я дописал, чтобы было понятнее:
К сожалению, этот номер не пройдет, если вы хотите выбрать продукты из соответствующих категорий, потому что NiceRating имеет тип Expression<Func<Category, bool>>, а в случае с Product нам потребуется Expression<Func<Product, bool>>. То есть, необходимо осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>
Gebb
24.10.2016 10:18+1Есть ещё библиотека LinqKit, тоже позволяющая комбинировать выражения. Было бы что-то вроде
q.Where(p => Category.IsNice.Invoke(p.Category).Expand())
marshinov
24.10.2016 10:27+1Есть, в ней еще куча обвесов для EF, которые не нужны, если у вас, например NHibernate. Читаемость этого примера хуже, не находите?
SergeyEgorov
24.10.2016 10:22+1Если я правильно помню, сигнатура
Expression.Parameter
подразумевает следующий набор аргументов(Type type, string name)
. Соответственно приведенный вами код, компилироваться не должен, из-за невозможности преобразованияTIn
вstring
в строке 9. Или я чего-то неправильно прочитал?
gandjustas
24.10.2016 11:05Надо использовать интерфейсы для таких вещей
public interface IRatingable { int Rating { get; set; } } public static class IRatingableExtensions { public static IQueryable<T> NiceRating<T>(this IQueryable<T> q) where T : IRatingable, class { return q.Where(x => x.Rating > 50); } }
Я на эту тему пост писал 6 лет назад :)
marshinov
24.10.2016 11:09Не прокатит
var products = db.Query<Product>().Where(x => x.Category.NiceRating()); // не пойдет var products = db.Query<Product>().NiceRating(); // не пойдет
Вот так можно обойти, но я не уверен, как разные провайдеры такое реализуют (не тестировал). И такой вариант не подойдет, если нужно обрабатывать 2 связанных класса.
dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products)
gandjustas
24.10.2016 11:31Куда не пойдет?
Вот прмиер
public interface IWithRating { int Rating { get; set; } } public class Product:IWithRating { public int Id { get; set; } public string Name { get; set; } public int Rating { get; set; } } public class StoreContext : DbContext { public DbSet<Product> Products { get; set; } } public static class RatingExtensions { public static IQueryable<T> NiceRating<T>(this IQueryable<T> q) where T : class, IWithRating { return q.Where(x => x.Rating > 50); } } class Program { static void Main(string[] args) { using (var ctx = new StoreContext()) { ctx.Database.Log = Console.WriteLine; var q = from p in ctx.Products.NiceRating() where p.Name.StartsWith("a") select new { p.Id, p.Name }; q.ToArray(); } } }
В консоли внезапно:
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name] FROM [dbo].[Products] AS [Extent1] WHERE ([Extent1].[Rating] > 50) AND ([Extent1].[Name] LIKE N'a%')
Вот ссылка на Gist https://gist.github.com/gandjustas/e65c8602b59c86966616fa29a69fe9a6
marshinov
24.10.2016 11:35Только это не соответствует моей цели. Я хочу получить продукты у которых рейтинг категории > 50. У самого продукта рейтинга нет.
gandjustas
24.10.2016 11:41from c in ctx.Category.NiceRating() from p in c.Products where p.Name.StartsWith("a") select new { p.Id, p.Name };
?
marshinov
24.10.2016 12:00+1Да, так сработает. Видимо, вам важнее написать, чем внимательно прочитать. Я выше написал такой-же пример, но с использованием extension:
dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products);
Это один из способов обойти ограничение. Я привел другой. Мне обычно удобнее делать запрос к целевой сущности. Давайте закончим это обсуждение.gandjustas
24.10.2016 12:06Мой вариант:
dbContext.Categories.NiceRating().SelectMany(x => x.Products);
Ваш:
dbContext.Products.Where(x => x.Category, Category.NiceRating).
Ваш вариант длиннее, discoverability хуже, какие-то странные манипуляции с expression делает.
Мой вариант гораздо гибче, так как в интерфейсе может быть несколько полей, метод-расширение может работать с несколькими интерфейсами.
marshinov
24.10.2016 12:15+1Это не странные манипуляции.Попробуйте записать лябмду x => x.Category.Rating > 50 и посмотрите какой получится Expression Tree. Это полезно для понимания, что «под капотом» у LINQ. И это не ортогональные вещи. Можно комбинировать интерфейсы и экстеншны, там где нужно, а где не нужно — не использовать.
Я же не агитирую. Вам не нравится — вы не будете использовать, ну и не используйте.gandjustas
24.10.2016 13:57Я привел решение конкретной задачи, которое не уступает вашему. Хачить деревья выражений для такой задачи вовсе необязательно.
Про хаки с деревьями выражений я также писал 6 лет назад http://blog.gandjustas.ru/2010/06/13/expression-tree/
Razaz
24.10.2016 15:37+1Ваш вариант менее информативен. Что за метод NiceRating и что он делает непонятно. И непонятно почему из него можно сделать SelectMany, а не Select.
Второй вариант уже понятнее с первого взгялда.
Ваш вариант был бы понятнее в виде: dbContext.Categories.WithNiceRating();
Но это сугубо ИМХО.
Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.gandjustas
24.10.2016 15:44Докопаться и до столба можно. Мы тут не названия методов обсуждаем, а подход к декомпозиции Linq запросов.
Можно устраивать игрища к с деревьями выражений, но в большинстве случаев достаточно набора методов-расширений.
Razaz
24.10.2016 16:221. Декомпозиция должна быть читаемой и выразительной. Ваш вариант выглядит странно хотя бы уже за SelectMany(x => x.Products) — это не интуитивно понятно.
2. PredicateBuilder можно нагуглить на пару секунд или прочитав C# in Nutshell.
Что-то отличное от этого имеет смысл делать если есть специфические требования, но в обычных бизнес приложениях это редкость.
chumakov-ilya
24.10.2016 19:28+2И непонятно почему из него можно сделать SelectMany, а не Select.
Непонятно будет только тем, кто не понимает SelectMany, как вы считаете? С моей точки зрения, оба варианта читаются одинаково, но вариант с extention method проще технологически. Впрочем, было бы интереснее узнать, генерит ли EF эквивалентный SQL в обоих случаях.
Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.
К сожалению, со своими ограничениями (часть из списка уже пофиксили) и относительно медленным компилятором (люди пишут свои).
Razaz
24.10.2016 20:42+1Странный аргумент. Для меня SelectMany по категориям выглядит чужеродно.
Логично делать выборку по множеству продуктов, с фильтром по категориям, а не наоборот.
Ограничения есть всегда. Тут вопрос в том, критичны ли они. Я выше уже отписался, что отказался от встроенных выражений по определенным причинам.
Скомпилированные выражения можно кешировать. Но тут все зависит от вашего кейса. В случае с этим вопросом мне кажется будет эффективнее просто сделать And через PredicateBuilder.
А если уж опираться в производительность, то LINQ на hot paths лучше избегать вообще :)
Вот что будет внутри Where — Queryable.Where
Через билдер предикатов пример в ссылке, указанной в ответе выше.
Пример из него:
public static Expression<Func<Product, bool>> HasNiceRating() { return prod => prod.Rating > 50; }
Это еще сэкономит вызов к QueryProvider при комбинации.
Тут уж каждому свое :) Я за вариант автора :)
Razaz
24.10.2016 21:22Еще один минус решения через метод расширения — наличие IQueryable. EF далеко не всегда присутствует и QueryProvider может отсутствовать в принципе.
Те же выражения можно собрать и конвертнуть в фильтр для удаленного ресурса.gandjustas
25.10.2016 00:53-1Если нет IQueryable, то о чем разговор вообще? Как без него декомпозировать запросы?
Razaz
25.10.2016 02:08Просто оставлю это здесь — Практическое руководство. Реализация обхода дерева выражения И еще тут: The Query Translator
Для трансляции дерева выражений IQueryable не нужен.gandjustas
25.10.2016 02:55Ты предлагаешь IQueryable самому реализовать? Это минимум два человеко-года по оценке Microsoft.
Razaz
25.10.2016 02:59Просто Visitor не катит? Это пара человеко дней. Почитайте внимательно для начала. Там дан пример простейшего генератора SQL запросов через обход дерева выражений.
gandjustas
25.10.2016 03:55В том и прикол, что "простейшего". Генератор запросов уровня linq2sql\ef — минимум два человеко-года.
Razaz
25.10.2016 09:49Все зависит от задачи. Простейший генератор удовлетворяет в большинстве случаев. Все доп кейсы по необходимости легко реализуются. Но, как выше уже не раз писал, а вы игнорировали — выражения используются и без QueryProvider.
gandjustas
25.10.2016 11:16Не видел в живой природе использования таких генераторов? И в чем смысл когда есть orm?
Razaz
25.10.2016 11:29Конвертация в LDAP, конвертация в query string, единый набор выражений при композиции хранилищь с разным способом доступа(даже если и есть QueryProvider — они разные), парсинг выражений и их эвалюейшен относительно объекта(включая rewrite например).
EF, как ОРМ, слишком жирная абстракция. Для проектов с низкими требованиями к слою хранения данных вполне подходит. Как только вы выходите за рамки SqlServer — он превращается в тыкву на костылях.gandjustas
25.10.2016 12:09Даже для Odata (конвертация в querystring) написан очень даже провайдер. Linq2LDAP (https://linqtoldap.codeplex.com/) тоже не самая простая штука.
Я не понимаю о каких "простых" случаях идет речь.
Razaz
25.10.2016 13:49+1Если нет OData, что делать? Если у вас другой протокол? Проблема в том что надо независящий от провайдера механизм. Самому написать Visitor не проблема.
А вы попробуйте Linq2LDAP для начала, а потом приводите это чудо в пример. Я вот использовал его в проде — выкинул нафиг и написал свой транслятор запросов, так как количество аллокейшенов там просто зашкаливало.
Вот такой запрос чем собрать? Это в query string:
"((userName lk \"*Jacob*\") and (title gt \«Intern\» or title eq \«Employee\») or lastModified ge \«2011-05-13T04:42:34Z\»)"
Все ваши рассуждения строятся на том что всегда есть добрый дядя, который напишет провайдер. Провайдер это ничто иное как создание IQueryable + набор трансляторов. Сделать набор трансляторов можно самому под конкретные требования. Создать точку входа можно через PredicateBuilder и им же комбинировать, не плодя новый IQueryable и не завязываясь на конкретного провайдера.
marshinov
25.10.2016 11:36В том, что во многих проектах ORM «не работает», потому что тормозит. Razaz уже написал выше, где он видел такие генераторы и в чем их смысл.
ggrnd0
24.10.2016 22:55И все же ваш вариант уступает в гибкости.
Что вы будете делать если у поиска продукта будет у фильтр по 2 полям-ссылкам?
class Product{ Category; Seller; } class Category{ List<Product> Products; Expression<Func<Category, bool>> NiceRating = c => c.Rating >= 50; } class Seller{ List<Product> Products; Expression<Func<Seller, bool>> GoodSeller = s => s.Rating >= 4; }
Следуя приведенный автором метод расширения можно написать так:
dbContext.Products .Where(x => x.Category, Category.NiceRating) .Where(x => x.Seller, Seller.GoodSeller)
gandjustas
25.10.2016 01:06И тогда бизнес-логика прекрасно выглядит:
dbContext.Products .HasGoodCategory() .HasTopSeller()
Те же предикаты уезжают внутрь экстеншенов.
При этом остается возможность в экстеншенах дополнительную логику написать, вычисления какие-нибудь например. С деревьями так не получится
Но гораздо интереснее становится, когда у многих сущностей появляются одинаковые свойства. Например флажки IsActive\IsDeleted, разные рейтинги, даже Id и Title поля, которые и так почти всегда есть. В этом случае мы не просто декомпозируем запрос, но и повторно используем логику.
ggrnd0
25.10.2016 02:33Отличие
dbContext.Products .Where(x => x.Category, Category.NiceRating) .Where(x => x.Seller, Seller.GoodSeller)
от
dbContext.Products .HasGoodCategory() .HasTopSeller()
в переиспользовании Category.NiceRating и Seller.GoodSeller.
Их не надо дублировать в виде Product.NiceRating и Product.GoodSeller:
class Product{ Category; Seller; Expression<Func<Product, bool>> NiceRating = p => p.Category.Rating >= 50; Expression<Func<Product, bool>> GoodSeller = p => p.Seller.Rating >= 4; } class Category{ List<Product> Products; Expression<Func<Category, bool>> NiceRating = c => c.Rating >= 50; } class Seller{ List<Product> Products; Expression<Func<Seller, bool>> GoodSeller = s => s.Rating >= 4; }
сами условия пишутся один раз.
Так же можно добавить методы расширения And/Or и состряпать порядочный такой predicate-buildergandjustas
25.10.2016 02:54Без конкретного сценария непонятно что обсуждаем.
Я говорю про одинаковое поведение для разных типов. Ты говоришь о похожем поведении для разных типов. При этом в твоем случае также присутствует дублирование кода.ggrnd0
25.10.2016 10:17Где дублирование? Третий листинг — пример дублирования от которого избавляет метод расширения автора.
Ориентироваться надо на первый листингgandjustas
25.10.2016 11:20Это ты о чем сейчас? Ты предлягаешь написать две лямбды, я предлагаю два метода-расширения. И в том, и в другом случае используются похожие поля, но простого способа свести их к одной лямбде\одному методу нет.
Меня интересует другой случай. На практике чаще приходится сталкиваться с одинаковым поведением полей в разных сущностях. Тогда интерфейсы и расширения удобнее и гибче.
marshinov
25.10.2016 11:33И тогда это другой случай, но чукча — писатель, а не читатель ;)
gandjustas
25.10.2016 13:00Это тот самый случай, описанный в посте автора. А про композицию — уже твои фантазии, которые от реальности ушли.
Razaz
25.10.2016 11:41-1And, Or как будете делать?
Внутри в вашем Where будет похожий код, только он еще сходит в QueryProvider и создаст новый IQueryable.
Вот вам с интерфейсами :)gandjustas
25.10.2016 13:01And, Or как будете делать?
Какую проблему решаем?
Я and и or делал много раз без predicate builder.
Я что-то не так делал?
ggrnd0
25.10.2016 15:05+1Я предлагаю 2 ляьбды вместо 4 методов расширений.
Так как критерии NiceRating и GoodSeller должны быть применимы не только в Продукту, но и к Категории/Продавец соответственно.
Условия предикатов могут быть сколько угодно сложными.
А их использование может не ограничиваться только 2мя сущностями.
Если каждый критерий копипастить для нескольких сущностей, изменения какого-либо критерия будет приводить к его изменению в каждой из этих сущностей.
ggrnd0
25.10.2016 15:09Ваш случай не настолько другой.
Если вместо привязки предиката к сущности, привязывать его к интерфейсу — предложенный автором подход так же применим!
Просто делается замена
Category -> IHaveRating
Seller -> IHaveStars
И если продукт или его поля реализуют указанные интерфейсы к ним можно применить указанные предикаты.
Другое дело, что эта задача тривиальна, совсем. Это просто декомпозиция и выделение интерфейса.
Razaz
25.10.2016 02:15+1Этот вариант сразу отпадает если захочется сделать Or. Приехали. Не говоря опять о том, что без реализации полноценного QueryProvider это нерабочий вариант. Прибивать правила бизнес логики к слою данных — не самое мудрое решение.
Как вариант накидал предикат билдер с тем, что у автора в статье. В принципе можно красивее сделать, но думаю для базы хватит.
Gist
mayorovp
24.10.2016 16:32+1Нельзя не упомянуть еще один инструмент: DelegateDecompiler от alexanderzaytsev — библиотека, которая преобразует IL в Expressions. Пост про нее на Хабре: https://habrahabr.ru/post/155437/
И мой форк, где решена проблема с Include, которую не видит автор: DelegateDecompiler
Serginio1
26.10.2016 12:48+1Спасибо за статью. Решил свой пробел в Expression восполнить. Поэто му по мотивам
Динамическое построение Linq запроса
По аналогии
public static IQueryable<T> Beetwen<T>(this IQueryable<T> src, Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to) { Expression<Func<DateTime, bool>> func = d => d >= from && d <= to; return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First())); }
сделать
public static IQueryable<T> NiceRating<T>(this IQueryable<T> q,Expression<Func<T, Category>> propertyExpression) { Expression<Func<Category, bool>> func = x => x.Rating > 50; return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First())); }
Прошу прощения, за некомпетентность. Решил наверстать упущенноеSerginio1
26.10.2016 16:24+1И соответственно вызов
dbContext.Products .NiceRating(x => x.Category)
ggrnd0
26.10.2016 16:42+1Лучше воспользоваться методом Compose
return src.Where(propertyExpression.Compose(func));
Но тогда будет не очень удобно использовать с IQueriable
Serginio1
26.10.2016 16:59+1Ну тут уж на любителя. Написать один раз
С таким же успехом можнгно И отдельную Функцию Написать
public static IQueryable<T> Compose<T,Y>(this IQueryable<T> src,Expression<Func<T, Y>> propertyExpression,Expression<Func<Y, bool>> func ) { return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First())); }
Сейчас проверю
Это не принципиально.
Главное использование
dbContext.Products .NiceRating(x => x.Category)
А можно ссылочку на ComposeSerginio1
26.10.2016 17:10+1Проверил на IEnumerable
public class TestExpression { public DateTime Created { get; set; } public TestExpression(DateTime Created) { this.Created = Created; } } public static class РасширениеLinq { public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func) { return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile()); } public static IEnumerable<T> Beetwen<T>(this IEnumerable<T> src, System.Linq.Expressions.Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to) { System.Linq.Expressions.Expression<Func<DateTime, bool>> func = d => d >= from && d <= to; return src.Where(System.Linq.Expressions.Expression.Lambda<Func<T, bool>>(System.Linq.Expressions.Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile()); } public static IEnumerable<T> Beetwen2<T>(this IEnumerable<T> src, System.Linq.Expressions.Expression<Func<T, DateTime>> propertyExpression, DateTime from, DateTime to) { System.Linq.Expressions.Expression<Func<DateTime, bool>> func = d => d >= from && d <= to; return src.Compose(propertyExpression, func); } }
И использование
var Дата = DateTime.Now; var d = new List<TestExpression>() { new TestExpression(DateTime.Now) }; var res = d.Beetwen2(_ => _.Created, Дата.AddDays(-1), Дата.AddDays(1)).FirstOrDefault(); res = d.Beetwen2(_ => _.Created, Дата.AddDays(1), Дата.AddDays(1)).FirstOrDefault();
ggrnd0
26.10.2016 17:10Compose представлен автором в статье. Сразу после
К счастью, осуществить это довольно просто!
Habr съел тег, приведенный фильтр будет неудобно использовать с IQueryable<Category>.
А именно
dbContext.Categories.NiceRating(x => x)
Вариант автора удобнее.
Serginio1
26.10.2016 17:15А чем это удобнее
public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func) { return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters.First()).Compile()); }
То же самое, только кода меньше.Serginio1
27.10.2016 10:04Даже First не нужен
public static IEnumerable<T> Compose<T, Y>(this IEnumerable<T> src, Expression<Func<T, Y>> propertyExpression, Expression<Func<Y, bool>> func) { return src.Where(Expression.Lambda<Func<T, bool>>(Expression.Invoke(func, propertyExpression.Body), propertyExpression.Parameters).Compile()); }
Я к тому, что когда начал разбираться с примером, то решил восполнить свои пробелы в Expression и для меня пример Динамическое построение Linq запроса
Показался более понятным. А автору большой респект за Expression/ Кармы не хватает, а так бы плюсик поставил.
sentyaev
А могли бы вы привести примеры «бизнес-процессов», а то само решение понятно, а вот почему именно так — нет.
marshinov
Синтетический пример
Сначала мы считали, что активные аккаунты, это те, что IsActive, потом ввели soft-delte, потом стали учитывать баланс, потом дату последнего посещения и пошло-поехало. Если эти правила не группировать, а копипастить, то рано или поздно где-то забудем поменять. Значит условия нужно группировать.
Из реальных кейсов бизнес-процессов, однажды клиент попросил формировать URL для товаров, добавленных до определенной даты одним способом, а после — другим.
Xandrmoro
Ваш ситетический — почти один в один мой реальный :)
marshinov
Да, он много у кого есть такой. Вы совсем не одиноки.
Bronx
Критерий NiceRating — это бизнес-правило, к.м.к ему не место в определении сущности БД, лучше определять его где-то извне. Я, например, выносил повторяющиеся выражения в extension methods, где можно делать что хочешь без особой Expression-магии (не отрицая полезность и изящность оной). Например:
Используем:
marshinov
У нас тоже такое есть.
У вас пример shared-правил. Их логично выносить. Правила, которые относятся только к сущности я группирую в сущности и считаю, что там самое логичное место, потому что без этой сущности нет и правила.
Кроме этого мы не используем анемичные модели на стороне ORM. Если нужна легковесная модель, то делаем проекцию в DTO.
SergeyEgorov
Похоже я чего-то недопонял, так что если не возражаете, тоже задам вопрос на тему места размещения экземпляров деревьев выражений.
А если завтра появится требование применять отдельные бизнес-правила к категориям, с рейтингом ниже 20, назовем их к примеру
PoorRating
? Что будем делать?marshinov
Сразу оговорюсь, что этот пример подходит для каких-то приложений и не подходит для других. Зависит от сложности приложения и договоренностей внутри команды.
Где именно находится условие: в классе сущности или отдельном месте — вопрос, который решает команда. Например, можно вынести все в спецификации (мы поступаем часто именно так).
В статье рассматривается простой пример, как осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>, не более. Вопросы организации бизнес-логики я затрагиваю в других постах, крайний — вот этот.