Pull to refresh

Comments 52

Вот зря на эволюцию видов сослался (что якобы интеллект побеждает). Можно холивар раздуть)

Боюсь, если бы написал, что гибкость - это божественно, то было бы тоже самое. :)

а также дает возможность недорого заменить одну технологию доступа к БД на другую. Postgres можно заменить на MongoDb и спецификации в домене менять не придётся

Всё это замечательно выглядит, но ровно до тех пор, пока игрушечные примеры не поменяем на бизнес-логику чуть посложнее. Что, например, делать, если наша задача хорошо ложится на оконные функции или на какой-нибудь lateral join?

Если хорошо ложится, то не нужно никого никуда заменять :), но бывают случаи когда это необходимо сделать. Например после февраля 2022 некоторым фирмам приходится менять технологии доступа к данным по понятным причинам, и в этом случае проще будет тому, кто таки вынес всю бизнес-логику в домен.

Cогласен. И до сих пор непонятно, почему такой нужный/гибкий функционал не затащили в сам EF.Core, имею ввиду не про сам Specification паттерн, а про все эти Expression обвязки. Приходится писать самомум или пользоваться сторонними библиотеками.

IQueryable уже сам по себе является ни чем иным, как спецификацией. Но людям нравится городить ещё 25 абстракций поверх. Никак не угомонятся :)

Нельзя создать IQueryable в доменном слое и передать его в репозиторий, как спецификацию. Так, чтобы один и тот же IQueryable подходил к разным репозиториям.

Блин, IQueyrable это спецификация. Это чистая абстракция, об этом даже говорит тот факт, что это интерфейс. DbContext это репозиторий + unit of work. В чистом виде. Когда я вижу проекты, где творят какие-то generic IRepository, мне хочется плакать. Зачем нужны эти "разные репозитории"? Это для чего?

Да, я согласен, что IRepository не нужен, если в проекте уже есть ORM.

Но, ради терминологической строгости, IQueyrable - не спецификация. Спецификация - это абстракция, определяющая критерии выборки данных, которую можно применить к разным источникам данных. Созданный экземпляр IQueyrable пришит к какому-то одному источнику, поэтому не может выполнять эту роль.

Вы говорите про экземпляр чего-то, что реализует IQueryable. Провайдер. Ну так одно другому не мешает. Вы можете сделать так: Array.Empty().AsQueryable(), затем наполнить полученный экземпляр предикатами, затем можно извлечь Expression и поместить в необходимую реализацию IQueryable.

Обычно, реализации спецификаций хранят в себе Expression. Так IQueryable тоже хранит :) Т.е. он перекрывает спецификацию и Query Object. Ну и плюс, можно делать не только предикаты, но и больше.

Но возможности LINQ просто даже близко не дотягивают до возможностей SQL. Поэтому многие разработчики даже не пользуются LINQ.

Вы можете сделать [IQueryable с пустым источником данных] затем наполнить полученный экземпляр предикатами, затем можно извлечь Expression и поместить в необходимую реализацию IQueryable

И как же можно "извлечь Expression из IQueryable"? Я в этом интерфейсе не вижу подходящих методов.

Суть IQueryable понятна, речь о повторяющихся фильтрах, которые при разных запросах (с учётом BL) самим IQueryable никак не решаются.

Чего это не решаются? Очень даже решаются.

public static IQueryable<Contract> ByDealer(this IQueryable<Contract> linq, int dealerId)
{
   return linq.Where(p => p.Dealer.Id == dealerId);
}

Это слишком простейший пример. А теперь попробуйте соедините 2 разных предиката через Or в одном запросе. Причем эти же два предикаты используются в другом запросе но уже в nested select-e.

Если очень хочется, из IQueryable можно извлекать выражения и встраивать их в "подзапросы", чтобы можно было делать даже так:

public static IQueryable<Contract> ByDealer(this IQueryable<Contract> linq, int dealerId)
{
   return linq.Where(p => p.Dealer.Id == dealerId);
}

public static IQueryable<Contract> ByFilifal(this IQueryable<Contract> linq, int filialId)
{
   return linq.Where(p => p.Filial.Id == filialId);
}

// затем, вуаля:

var query = dbContext.Contracts.ByDealer(123).Or().ByFilial(filialId);

Такие финты можно делать, и сильно сложнее. Другое дело, что в реальности игры с LINQ, спецификациями и прочими игрушками приносят в основном вред, и совсем не приносят пользы.

Возможностей LINQ достаточно, чтобы делать простые выборки и получить до 90% типизированных запросов. Но если выйти за рамки совсем примитивных приложений, окажется, что очень нужны и часто, оконные функции, кубы, предварительные промежуточные выборки, в общем для многих задач нужен SQL. Без плясок, приколов, попытками это запихать в LINQ. И нужно оба этих мира, чтоб программист не тратил время на проектирование спецификаций, которые как окажется потом весьма слабо и плохо комбинируются, но нужны конкретные запросы и возможность без плясок использовать SQL.

Это если конечно говорить о боевых приложениях с какой-никакой нагрузкой и хоть с какой-то сложностью в логике.

Проектирование, тема холиварная, да и отклоняемся от первоначальной, что IQueryable всемогущий - умеет всё! По примеру выше, надеюсь не с какого-нибудь ChatGPT (он любит иногда выдавать желаемое, за действительное). Поскольку выше описанного Or(), в природе не существует https://github.com/dotnet/efcore/issues/13915 и список доступных методов https://learn.microsoft.com/ru-ru/dotnet/api/system.linq.queryable?view=net-8.0. И его надо делать на Expression-х. И чем дальше, например попробовать затащить в nested запросы данные предикаты, будет ещё интереснее.

var query = dbContext.Contracts.ByDealer(123).Or().ByFilial(filialId);

Вы привели тут какой-то нерабочий пример. Для любой произвольной IQueryable, невозможно написать Or() из вашего примера.

Так дело в трансляторе в sql, а не в спецификациях.

C# не позволяет определять методы с анонимными типами в качестве параметров

dynamic?

Кажется что да, на самом деле непосредственно поля с анонимным типом там не будет: ссылка на SharpLab

dynamic не дружит с Expression Trees, автору статьи нужна функция типа такой:

var FilterByName(var sourceQuery, string name)
{
    return sourceQuery.Where(x => x.Name == name);
}

которая вызывалась бы так:

var query1 = DbContext.Users.Select(x => new { x.Id, x.Name });
var query2 = FilterByName(query1, "Ivan");
return query2.ToList();

Я чот типа такого делал для эксп три, уже не помню правда - как именно. На интерфейсах это тоже вроде решалось, с динамиком тоже чот тыкал.

Но чисто тезис:

C# не позволяет определять методы с анонимными типами в качестве параметров

Некорректен.

Тут про это сказано:

You cannot declare a field, a property, an event, or the return type of a method as having an anonymous type. Similarly, you cannot declare a formal parameter of a method, property, constructor, or indexer as having an anonymous type.

C# не позволяет определять методы с анонимными типами в качестве параметров

Позволяет. В качестве параметра анонимный тип? Да.

var anon = new { id = 5, name = "anon" };
Show(anon);
void Show(dynamic anon)
{
    Console.WriteLine(anon.id);
}

Просто то, что написано у микрософта это про то, что такое не имеет смысла. Типа такого:

void Show(anon=new{ id=5, name="anon" }){
  Console.WriteLine(anon.id);
}

Если речь о втором варианте - то да, ибо это не имеет смысла, для этого есть интерфейсы. В js нет интерфейсов - там такое сплошь. "Анонимные" типы в шарпах - это просто типы с автогенерацией кода, не более.

Но и в этом случае можно сделать метод, который будет проверять "анонимность" через рефлекшн. Но в спецификациях вроде такого нету, ибо эт не нужно. Вообще с работой ExpressionTree (на Sql например) там больше закос на получение строки - имени свойства, чем работа с реальным объектом. Типа:

var query = expr.Property<int>(q=>q.Id).Where(id=>Id<15);
var query2 = expr.Entity<IAnon>().Where(a=>a.Id<15);
interface IAnon { int Id {get;set;} }

Позволяет. В качестве параметра анонимный тип? Да.

Нет. Мы же видели в декомпиляторе, что тип параметра функции Show - object, а не анонимный тип.

"анонимные" типы в шарпах - нефига не анонимные на самом деле, имя есть, но оно прекомпилированно. Использовать их как переменные - можно, я показал как. Использовать их "анонимные типы" как типы методов - нельзя по определению. Анонимные типы приводимы к object.

В общем, если имелось ввиду это:

void Show(var anon=new{ id=5, name="anon" }){ Console.WriteLine(anon.id); }

То да, нельзя, ибо это бред какой-то. А еще нельзя открыть баклажан цифрой восемь. И попробовать цвет единорога. И если вы и впрямь про этот вариант - то ок, вы правы, бесмыслица бессмысленна. Имхо это очевидно.

Берем код:

static void Main(string[] args)
{
    var t = new { Property1 = 123 };
    Some(t);
}

static void Some(dynamic argument)
{
    Console.WriteLine(argument.Property1);

На выходе видим это:

private static void Main(string[] args)
{
    Some(new <>f__AnonymousType0<int>(123));
}

private static void Some([Dynamic] object argument)
{
    //Содержимое не принципиально
}

Анонимный тип как аргумент функции это когда вот так:

private static void Some(<>f__AnonymousType0<int> argument)
{
    //Содержимое не принципиально
}

Такое нельзя сделать в C#, хотя-бы потому что нельзя использовать <> в названии типа.

По вашей логике и dynamic особо не нужен, можно и так

 static void Main(string[] args)
    {
        var t = new { SomeProperty = 12345 };
        WriteSomePropertyOfAnything(t);
    }
    
    static void WriteSomePropertyOfAnything(object anything)
    {
        var someProperty = anything.GetType().GetProperty("SomeProperty");
        if (someProperty is not null)
        {
            Console.WriteLine(someProperty.GetValue(anything));
        }
        else
        {
            Console.WriteLine($"У {anything.GetType()} нет свойства SomeProperty");
        }
    }

Да успокойтесь вы уже) Понятен красен псевдо код jit компилятора для машины состояний нельзя писать в спецификациях языка (Intermediate Language). Зачем это мусолить? Очевидно же это все (по крайней мере - для меня). Давайте ща еще асм код для arm64 начнем мусолить...

Использовать их "анонимные типы" как типы методов - нельзя по определению. Анонимные типы приводимы к object

Гипотетически, компилятор и стандарт языка можно докрутить. В плюсовых шаблонах закомментированная строчка работает

void Show<T>(T t) where T: class
{
    Console.WriteLine(t.GetType());
    // Console.WriteLine(t.Id);
}

var t = new { Id = 1 };
Show(t);

В .NET, если правильно помню, рантайм такое не позволяет делать, в T явно должно быть свойство Id

Как выше писал ildarin, в .net наличие свойства декларируется интерфейсом. Но описание анонимного класса генерирует компилятор, поэтому интерфейс мы на него повесить не можем.

Выходом было бы наличие концептов, как в c++. Код выглядел бы как

void Show<T>(T t) where T: HasProperty(T.Id)
{
    Console.WriteLine(t.Id);
}

и рантайм доволен.

Жаль нельзя интерфейсы "навешивать" на анонимы:

var x = new {Id = 42, Name = "Ivan"} : INamed;

Как говорилось уже выше, главный плюс спецификаций в том, что логика получения данных теперь находится там, где она должна быть - на уровне домена. 

А что в этом хорошего? На какие потребительские характеристики это влияет? Как влияет на совокупные трудозатраты и стоимость поддержки кода?
Желательно в примерах.

ваше приложение будет более гибким

Как расположение кода в проекте влияет на гибкость?

"А что в этом хорошего?" - "хорошесть" вынесения всей бизнес-логики в домен - величина переменная. Например, если Ваш продукт рассчитан на 3 года работы и не более, то можно обойтись монолитом или например, если Вы задумали уволится с этой галеры, и что с ней будет через 5-7 лет вам фиолетово, тогда, конечно, можно и на уровне инфраструктуры логику оставить. В противном случае, лучше потратить время и привести продукт к тому состоянию, когда смена любых инфраструктур не будет приводить к переписыванию логики - самой дорогой части Вашего решения. За 10 лет может и смена движка БД случиться. Примеры у Фаулера, Дяди Боба, Вона и других (да я ушёл от вопроса про примеры ибо лень). Надеюсь ответил на Ваш вопрос, в любом случае спасибо за него.:)

Вместо зоопарка типов, у нас теперь трёхэтажные шаблоны.

Посвятил UPD этой проблеме. Спасибо за коммент!

Есть и динамический Sql. Но тут ЕF уже не в тему. Я вообще сторонник минимализма в разработке, но достаточного, чтобы не переусложнять простые вещи и который работает.

А что мешает вот тут:

select new // анонимный тип

{

Field1,

Field30

};

вместо анонимного типа использовать реальный?

Ответ есть в статье:

Казалось бы - успешный успех, но такую промежуточную модель придётся добавлять для каждого подобного метода. Количество классов вырастет как попкорн во время нагрева, а оно нам надо? - нет!

Ответ есть в статье:

Это ответ про другое. Если делать реальный класс, то на один select с анонимным типом, будет один класс с его реальным представлением. Писанины будет только чуть больше. Этот класс придется предварительно описать.

Плюсую, отличный вопрос! Непонятно зачем делать селект анонимного типа, а потом конвертировать этот анонимный тип в кукую-то еще реальную модель. Может лучше сразу использовать реальный тип? Избавимся от конвертации.
И кстати здесь анонимный тип передается в extension-метод, а дальше написано что этого делать нельзя )

return anonimousModels.ToКакиетоТоМодели():

Гораздо лучше будут читаться выражения состоящие из спецификаций.

Тут надо добавить, что выражения должны состоять только из спецификаций, текущая реализация спецификации не позволяет комбинировать её с произвольным Linq выражением. Нельзя написать что-то вроде x => IdAbove(100) && x.Id > 100, такое выражение не скомпилируется

И при реализации AndSpecification можно обойтись без Expression.Invoke при помощи подмены параметра. EF для обоих вариантов строит запрос, но для подмены параметра запрос получается более читаемым, это удобнее при отладке

В спецификации оба оператора должны возвращать false

 public static bool operator false (Specification<T> specification) => false;
 public static bool operator true(Specification<T> specification) => false;

Как видим, SQL без спецификаций более эффективен

Стоило сравнивать не только косты, которые являются оценкой, но и реальное время выполнения запроса.
Hash Left Join (cost=26608.37..36174.56 rows=5 width=20) (actual time=1815.390..2405.787 rows=3 loops=1) - без спецификаций
Nested Loop Left Join (cost=0.87..136740.12 rows=5 width=48) (actual time=559.034..1088.576 rows=3 loops=1) - со спецификациями
Запрос со спецификацией работает в 2,2 раза быстрее )

А с оптимизированными запросами для спецификаций (где добавлены дополнительные джойны) что-то не то. В запросе есть фильтр по passenger.update_ts (p.update_ts < @__dateTo_2 OR p.update_ts IS NULL), но в плане при сканировании таблицы passenger нет фильтра по update_ts, такого не может быть (Index Scan using passenger_pkey on passenger p). Этот план от какого-то другого запроса.

Хорошо бы аккуратно перепроверить все зпросы и планы из статьи, а также их реальное время выполнения.

Денис (мне кажется я общаюсь с Денисом, а не Андреем) добрый день! Да пример не совершенен как и его автор, но как мне кажется мысль о том, что ЕF может генерировать неэффективный SQL, если строит соединения только на основе навигационных полей, эта мысль кмк верная, я предложил путь как этого избежать и всё ещё пользоваться теми преимуществами, которые дарит "Спецификация". В любом случае спасибо за комментарий!

Спасибо за интересную стать. Единственная ремарка что мне кажется некорректным сравнивать Паттерн спецификация и результаты конкретной имплементации. В этом и идея спецификации что это описание того, какие данные нужны. Это примерно как тебе дали бумажку с надписью "купи хлеб" а ты пошел в магазин в соседнем городе пешком, устал, а потом рассказываешь что что надписи на бумажках не эффективны.

Спасибо за комментарий! Всегда пожалуйста! Данная статья - критика варианта использования, который дан в абзаце, "Паттерн "Спецификация". Блеск", такой подход выдуман не мной, а позаимствован из статей наших зарубежных коллег. В целом со спецификациями выбор не велик: либо пишется nuget с спецификациями, описывающими наиболее сложные и часто используемые условия выборки данных на основе Expression, с возможностью их комбинации spec4 = spec1 & spec2 | !spec3, это сложно и оправдано только в сложных доменах. Либо второй вариант спецификации на основе IQueryable и выборками не намного сложнее, чем x => x.Id == someId . Спецификации будут эффективны только на очень сложных доменах.

Sign up to leave a comment.