Комментарии 82
var activeAccount = db.Query<Account>().Where(x => x.IsActive && x.IsNotDeleted && x.Balance > 0 && x.LastVisited > new DateTime(2015, 01, 01) && x.SuperPuper > 100500 && x.Whatever)
Сначала мы считали, что активные аккаунты, это те, что IsActive, потом ввели soft-delte, потом стали учитывать баланс, потом дату последнего посещения и пошло-поехало. Если эти правила не группировать, а копипастить, то рано или поздно где-то забудем поменять. Значит условия нужно группировать.
Из реальных кейсов бизнес-процессов, однажды клиент попросил формировать URL для товаров, добавленных до определенной даты одним способом, а после — другим.
// filter "soft-deleted" entities
public static IQuerable<T> Active<T>(this IQueriable<T> entities)
where T: AuditedEntity
{
return entities.Where(entity => !entity.IsDeleted);
}
// filter low-rated products
public static IQuerable<T> WithMinRating<T>(this IQueriable<T> products, int minRating)
where T: Product
{
return products.Where(product => product.Category.Rating >= minRating);
}
// do server-side pagination
public static IQuerable<T> Paginate<T>(this IQueriable<T> items, int total, int skip, int take)
{
// preventing querying DB when there are no items
if (total == 0) return new List<T>().AsQueriable();
// using lambdas in Skip/Take to make the SQL query parameterized and its plan reusable.
return items.Skip(() => skip).Take(() => take);
}
...
Используем:
public PagedList<Product> Handle(QueryProducts request)
{
using (var db = new ProductDb(_connection)
{
var all = db.Products.AsNoTracking().Active().WithMinRating(request.MinRating);
var total = all.Count();
val result = all
.OrderBy(item => item.Name)
.Paginate(total, request.Skip, request.Take)
.ToList();
return new PagedList<Product>(result, total);
}
}
У вас пример shared-правил. Их логично выносить. Правила, которые относятся только к сущности я группирую в сущности и считаю, что там самое логичное место, потому что без этой сущности нет и правила.
Кроме этого мы не используем анемичные модели на стороне ORM. Если нужна легковесная модель, то делаем проекцию в DTO.
Похоже я чего-то недопонял, так что если не возражаете, тоже задам вопрос на тему места размещения экземпляров деревьев выражений.
А если завтра появится требование применять отдельные бизнес-правила к категориям, с рейтингом ниже 20, назовем их к примеру PoorRating
? Что будем делать?
public class Category : HasIdBase<int>
{
public static readonly Expression<Func<Category, bool>> NiceRating = x => x.Rating > 50;
public static readonly Expression<Func<Category, bool>> PoorRating= x => x.Rating < 50;
public static readonly Expression<Func<Category, bool>> NiceName = x =>
x.Name.StartsWith("Ктулху");
//...
}
Где именно находится условие: в классе сущности или отдельном месте — вопрос, который решает команда. Например, можно вынести все в спецификации (мы поступаем часто именно так).
В статье рассматривается простой пример, как осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>, не более. Вопросы организации бизнес-логики я затрагиваю в других постах, крайний — вот этот.
Ноги отсюда растут — SCIM parser Это была отправная точка ;)
что бы ограничить возможные варианты запросов типа MethodCallExpression
А зачем? Можно пример? Неужели отбиваются трудозатраты на реализацию IQueryProvider?
Во-первых, реализация IQueryProvider — не самый трудный этап. Во-вторых, если они ушли от Expression — значит, они не стали реализовывать IQueryProvider!
читаем внимательно
аналог IQueryable + IQueryProvider
если вам так принципиально, можете читать мой предыдущий комментарий как трудозатраты на реализацию аналога IQueryProvider
Например IPermissionQuery имеет свой набор доступных выражений.
Плюс упростилось само дерево выражений и транслировать его стало проще :)
В принципе написать IQueryProvider не так сложно, если ковыряться в этой теме, но у нас есть композиция сторов и это автоматом делает IQueryProvider не очень удобным, так как хочется собрать запрос и пихнуть в 10 сторов, которые могут быть удаленными, локальными, реляционными и нет, включая файловую систему, а рисовать композицию внутри него не шибко хочется. Плюс логика материализации зависит от стора.
У нас специфичное решение и, в общем случае, я бы нарисовал обычных выражений :)
С ней можно писать
.Where(p => NiceRating.Invoke(p.Categoory)
и ещё много полезных дополнений.
var niceProductsCompilationError = db.Query<Product>.Where(Category.NiceRating); // так нельзя!
Либо утро понедельника на меня так влияет, либо я действительно чего-то не понимаю. В чем собственно проблема?
К сожалению, этот номер не пройдет, если вы хотите выбрать продукты из соответствующих категорий, потому что NiceRating имеет тип Expression<Func<Category, bool>>, а в случае с Product нам потребуется Expression<Func<Product, bool>>. То есть, необходимо осуществить преобразование Expression<Func<Category, bool>> => Expression<Func<Product, bool>>
Есть ещё библиотека LinqKit, тоже позволяющая комбинировать выражения. Было бы что-то вроде
q.Where(p => Category.IsNice.Invoke(p.Category).Expand())
Если я правильно помню, сигнатура Expression.Parameter
подразумевает следующий набор аргументов (Type type, string name)
. Соответственно приведенный вами код, компилироваться не должен, из-за невозможности преобразования TIn
в string
в строке 9. Или я чего-то неправильно прочитал?
Надо использовать интерфейсы для таких вещей
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 лет назад :)
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)
Куда не пойдет?
Вот прмиер
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
from c in ctx.Category.NiceRating()
from p in c.Products
where p.Name.StartsWith("a")
select new { p.Id, p.Name };
?
dbContext.Categories.Where(Category.NiceRating).SelectMany(x => x.Products);
Это один из способов обойти ограничение. Я привел другой. Мне обычно удобнее делать запрос к целевой сущности. Давайте закончим это обсуждение.
Мой вариант:
dbContext.Categories.NiceRating().SelectMany(x => x.Products);
Ваш:
dbContext.Products.Where(x => x.Category, Category.NiceRating).
Ваш вариант длиннее, discoverability хуже, какие-то странные манипуляции с expression делает.
Мой вариант гораздо гибче, так как в интерфейсе может быть несколько полей, метод-расширение может работать с несколькими интерфейсами.
Я же не агитирую. Вам не нравится — вы не будете использовать, ну и не используйте.
Я привел решение конкретной задачи, которое не уступает вашему. Хачить деревья выражений для такой задачи вовсе необязательно.
Про хаки с деревьями выражений я также писал 6 лет назад http://blog.gandjustas.ru/2010/06/13/expression-tree/
Второй вариант уже понятнее с первого взгялда.
Ваш вариант был бы понятнее в виде: dbContext.Categories.WithNiceRating();
Но это сугубо ИМХО.
Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.
Докопаться и до столба можно. Мы тут не названия методов обсуждаем, а подход к декомпозиции Linq запросов.
Можно устраивать игрища к с деревьями выражений, но в большинстве случаев достаточно набора методов-расширений.
2. PredicateBuilder можно нагуглить на пару секунд или прочитав C# in Nutshell.
Что-то отличное от этого имеет смысл делать если есть специфические требования, но в обычных бизнес приложениях это редкость.
И непонятно почему из него можно сделать SelectMany, а не Select.
Непонятно будет только тем, кто не понимает SelectMany, как вы считаете? С моей точки зрения, оба варианта читаются одинаково, но вариант с extention method проще технологически. Впрочем, было бы интереснее узнать, генерит ли EF эквивалентный SQL в обоих случаях.
Ничего страшного в деревьях выражений нет — очень мощный и гибкий механизм.
К сожалению, со своими ограничениями (часть из списка уже пофиксили) и относительно медленным компилятором (люди пишут свои).
Логично делать выборку по множеству продуктов, с фильтром по категориям, а не наоборот.
Ограничения есть всегда. Тут вопрос в том, критичны ли они. Я выше уже отписался, что отказался от встроенных выражений по определенным причинам.
Скомпилированные выражения можно кешировать. Но тут все зависит от вашего кейса. В случае с этим вопросом мне кажется будет эффективнее просто сделать And через PredicateBuilder.
А если уж опираться в производительность, то LINQ на hot paths лучше избегать вообще :)
Вот что будет внутри Where — Queryable.Where
Через билдер предикатов пример в ссылке, указанной в ответе выше.
Пример из него:
public static Expression<Func<Product, bool>> HasNiceRating()
{
return prod => prod.Rating > 50;
}
Это еще сэкономит вызов к QueryProvider при комбинации.
Тут уж каждому свое :) Я за вариант автора :)
Те же выражения можно собрать и конвертнуть в фильтр для удаленного ресурса.
Если нет IQueryable, то о чем разговор вообще? Как без него декомпозировать запросы?
Для трансляции дерева выражений IQueryable не нужен.
Ты предлагаешь IQueryable самому реализовать? Это минимум два человеко-года по оценке Microsoft.
В том и прикол, что "простейшего". Генератор запросов уровня linq2sql\ef — минимум два человеко-года.
Не видел в живой природе использования таких генераторов? И в чем смысл когда есть orm?
EF, как ОРМ, слишком жирная абстракция. Для проектов с низкими требованиями к слою хранения данных вполне подходит. Как только вы выходите за рамки SqlServer — он превращается в тыкву на костылях.
Даже для Odata (конвертация в querystring) написан очень даже провайдер. Linq2LDAP (https://linqtoldap.codeplex.com/) тоже не самая простая штука.
Я не понимаю о каких "простых" случаях идет речь.
А вы попробуйте 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 и не завязываясь на конкретного провайдера.
И тогда бизнес-логика прекрасно выглядит:
dbContext.Products
.HasGoodCategory()
.HasTopSeller()
Те же предикаты уезжают внутрь экстеншенов.
При этом остается возможность в экстеншенах дополнительную логику написать, вычисления какие-нибудь например. С деревьями так не получится
Но гораздо интереснее становится, когда у многих сущностей появляются одинаковые свойства. Например флажки IsActive\IsDeleted, разные рейтинги, даже Id и Title поля, которые и так почти всегда есть. В этом случае мы не просто декомпозируем запрос, но и повторно используем логику.
Без конкретного сценария непонятно что обсуждаем.
Я говорю про одинаковое поведение для разных типов. Ты говоришь о похожем поведении для разных типов. При этом в твоем случае также присутствует дублирование кода.
Это ты о чем сейчас? Ты предлягаешь написать две лямбды, я предлагаю два метода-расширения. И в том, и в другом случае используются похожие поля, но простого способа свести их к одной лямбде\одному методу нет.
Меня интересует другой случай. На практике чаще приходится сталкиваться с одинаковым поведением полей в разных сущностях. Тогда интерфейсы и расширения удобнее и гибче.
Внутри в вашем Where будет похожий код, только он еще сходит в QueryProvider и создаст новый IQueryable.
Вот вам с интерфейсами :)
Как вариант накидал предикат билдер с тем, что у автора в статье. В принципе можно красивее сделать, но думаю для базы хватит.
Gist
Нельзя не упомянуть еще один инструмент: DelegateDecompiler от alexanderzaytsev — библиотека, которая преобразует IL в Expressions. Пост про нее на Хабре: https://habrahabr.ru/post/155437/
И мой форк, где решена проблема с Include, которую не видит автор: DelegateDecompiler
Динамическое построение 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()));
}
Прошу прощения, за некомпетентность. Решил наверстать упущенное
dbContext.Products
.NiceRating(x => x.Category)
С таким же успехом можнгно И отдельную Функцию Написать
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)
А можно ссылочку на Compose
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();
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> 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/ Кармы не хватает, а так бы плюсик поставил.
Устранение дублирования Where Expressions в приложении