IQueryable LINQ vs. Builder pattern

6yrop

Как-то херово с архитектурной точки зрения выглядит LINQ для IQueryable. Проявляется эта архитектурная лажа вот на таких задачах.
http://tomasp.net/articles/dynamic-linq-queries.aspx
http://www.albahari.com/nutshell/predicatebuilder.aspx
В статьях приведены воркэраунды, но это всего лишь воркэраунды. Такие вещи должны быть доступны по умолчанию и в красивом виде.
Майкросовтовцы сделали много красивых шагов, но все же слажали. Почему нельзя было немного подумать, посмотреть на других, и сделать решение на основе паттерна Builder?
2: ты как-то писал, что LINQ это и есть Builder, так вот, нифига это не Builder. Классический Builder гораздо гибче в применении, чем Expression-ы.

Dasar

2: ты как-то писал, что LINQ это и есть Builder, так вот, нифига это не Builder. Классический Builder гораздо гибче в применении, чем Expression-ы.
в каком контексте я это говорил?

6yrop

в каком контексте я это говорил?

Dasar

http://forumbgz.ru/ashowthreaded.php?Cat=&Board=prog&...
спасибо, вспомнил.
имелось ввиду, что linq хороший builder для обработки коллекций в "длину", а в "ширину"(по полям) - он да, плохо билдится.

6yrop

имелось ввиду, что linq хороший builder для обработки коллекций в "длину", а в "ширину"(по полям) - он да, плохо билдится.
Я с большой вероятностью предполагаю, что синтаксического сахара, приведенного ниже, хватило бы для реализации нормального Builder-а. Причем синтаксис запросов в точности совпадал бы с текущим LINQ.
И еще, можно было бы делать свой билдер прям в SQL, а не выстраивать какой-то универсальный язык запросов. Это, на мой взгляд, плюс, ибо с SQL Server-ом приятнее работать на его родном языке SQL.
1. C# индексер может иметь generic параметр.
2. C# индексер может быть оформлен как extension метод.
3. Выражение _ => _.LaborSubtotal может быть записано в краткой форме @LaborSubtotal
4. Перегрузка оператора может быть оформлена как extension метод.
5. Если у типа MyType есть индексер, принимающий параметр Expression<Func<TEntity, TProperty>> propertyExpression, и у типа TEntity есть свойство Property1, то выражение myType[@Property1] можно записать в краткой форме myType.Property1. В этой записи свойство Property1 можно рассматривать как extension свойство, аналогично extention методам.
От Expression-ов можно полносттью отказаться. Упомянутые Expression-ы в параметре индексера можно (и даже нужно) заменить на тип, который предоставлял бы доступ к имени свойства, типу, геттору и сеттору.

timefim

Почему нельзя было немного подумать, посмотреть на других, и сделать решение на основе паттерна Builder?
Можно код?

6yrop

Можно код?
для кода требуется 1-2 месяца разработки. Но концептуально он будет во много похож на Reactive Relation. Последняя версия находится в ветке \ReactiveRelation\DevBranches\MethodsWrappedEntityProperties

timefim

Хочется увидить кусок псевдо кода по которому можно понять суть выделенного утверждения.

6yrop

этого достаточно?

void Test
{
db.Customer.Where(c => c.FirstName == "Test1" || SecondCriterion(c
}

IValue<bool> SecondCriterion(IValue<Customer> c)
{
return c.LastName == "Test2";
}

timefim

Единственную проблему которую я здесь вижу это получение Expression по MethodInfo, об этом речь?

6yrop

подробнее распиши код, который ты имеешь ввиду (в моем примере експрешенов нет).

timefim

Where(c => c.FirstName == "Test1" || SecondCriterion(c
Expression -> IQueryable
Анализатор смотрит на ast и видет в нем вызов функции SecondCriterion, единственно что он может, это получить MethodInfo этой функции, соответственно если бы у него была возможность получить Expression, он дальше смог бы производить разбор, а так приходится кидать ексепшен.

6yrop

Анализатор смотрит на ast и видет в нем вызов функции SecondCriterion, единственно что он может, это получить MethodInfo этой функции, соответственно если бы у него была возможность получить Expression, он дальше смог бы производить разбор, а так приходится кидать ексепшен.
в этом то и проблема. В методе SecondCriterion может быть что угодно — условные операторы, циклы, присвоение/изменение переменных и т.д. Имхо, в языке с состоянием анализатору слабо это все обработать. А если опираться не на анализатор AST, а на билдер, то проблемы вообще нет.

timefim

Что можем то обрабатываем, иначе кидаем ексепшен.
Как выглядит код который решает данную проблему при помощи билдера?

6yrop

Как выглядит код который решает данную проблему при помощи билдера?
Код использования:
 

public class Customer
{
public string FirstName { get; set; }
public string LastName { get; set; }
}

public class MainTest
{
private IRelation<Customer> Customers
{
get { throw new NotImplementedException; }
}

[Test]
public void Test
{
var query = Customers.Where(
c => c._(_ => _.FirstName).Equal("Test1".Const.Or(SecondCriterion(c
);
}

private IValue<bool> SecondCriterion(ITuple<Customer> c)
{
return c._(_ => _.LastName).Equal("Test1".Const;
}
}

Скелет реализации билдера:
 

public static class RelationExtensions
{
public static IQuery<TEntity> Where<TEntity>(
this IRelation<TEntity> relation,
Func<ITuple<TEntity>, IValue<bool>> predicate)
{
throw new NotImplementedException;
}
}

public static class ValueExtensions
{
public static IValue<T> Const<T>(this T arg)
{
throw new NotImplementedException;
}
}

public static class StringValue
{
public static IValue<bool> Equal(
this IValue<string> value1, IValue<string> value2)
{
throw new NotImplementedException;
}
}

public static class BoolValue
{
public static IValue<bool> Or(
this IValue<bool> value1, IValue<bool> value2)
{
throw new NotImplementedException;
}
}

public interface IRelation<TEntity>
{

}

public interface ITuple<TEntity>
{
IValue<TProperty> _<TProperty>(
Expression<Func<TEntity, TProperty>> propertyExpression);
}

public interface IValue<T>
{

}

public interface IQuery<TEntity>
{

}

Код удобнее смотреть под Visual Studio. Код можно скачать тут . Файл солюшета в папке \TypedSqlBuilder\Trunk\Source\TypedSqlBuilder.sln

Dasar

но в Linq одно из интересных фич, что можно добавить вспомогательные поля при запросе, а потом с ними прозрачно работать в полученном результате
в reactive такого не будет.

6yrop

но в Linq одно из интересных фич, что можно добавить вспомогательные поля при запросе, а потом с ними прозрачно работать в полученном результате
ты про анонимные классы? Они, естественно, будут работать также.

Dasar

ты про анонимные классы? Они, естественно, будут работать также.
совместно с builder-ом?

6yrop

да

Dasar

да
покажи пример использования

timefim

А какие еще проблемы решаются кроме or?

6yrop

покажи пример использования
Да, с проекциями оказалось сложнее, чем я предполагал. Существующий синтаксис анонимных классов не очень подходит для билдера. Для не анонимного типа синтаксис вот такой:

var query = Customers
.Where(
c => c._(_ => _.FirstName).Equal("Test1".Const.Or(SecondCriterion(c
)
.Select(
c => Tuple.New<CustomerProjection>
.Bind(_ => _.FirstName, c._(_ => _.FirstName
.Bind(_ => _.FullName, c._(_ => _.FirstName).Add(c._(_ => _.LastName
);

Но ведь у разработчиков языка, всегда есть возможность добавить синтаксис :). Хотя да увлекаться расширением синтаксиса не стоит...

6yrop

А какие еще проблемы решаются кроме or?
в первой ссылке из первого поста рассмотрены три проблемы.
У нас еще была проблема локализации запросов, т.е. выводить, вместо английского столбца, столбец на выбранном языке.
Вообще, я начинаю собирать коллекцию хороших примеров, где LINQ плохо строит запросы. Буду рад, если вы поделитесь своими примерами :).

timefim

Имелось в виду что все эти проблеммы решины в рамках or.
А билдер может наружу выдавать Expression? Если его еще научить кроме статика принимать стринги, возможно получилась бы неплахая альтернатива dynamic linq.

6yrop

А билдер может наружу выдавать Expression?
Думаю, да. Билдеру все равно чего строить, а API для построения Expression-ов во фрейворке есть. Только вот, я думаю, нафига конкурировать с LINQ, примеров, демонстрирующих явное преимущество билдера пока нет. Хотя при большом количестве запросов в приложении преимущества билдера проявят себя. Я думаю, лучше позиционировать билдер, как инструмент для построения напрямую TSQL. С нормальной поддержкой LEFT OUTER JOIN и т.п. Можно даже такие вещи как ROW_NUMBER поддержать.
ROW_NUMBER OVER(ORDER BY SalesYTD DESC) AS 'Row Number'  

Если его еще научить кроме статика принимать стринги

Так ведь, весь этот огород ради того, чтобы избежать стрингов. SQL в виде стринга всегда можно написать :).

timefim

SQL в виде стринга всегда можно написать
Для этого как мининмум нужно написать код который будет этот sql собирать на основе мапинга, тоесть написать то что уже реализовано, зачем?

6yrop

Для этого как мининмум нужно написать код который будет этот sql собирать на основе мапинга, тоесть написать то что уже реализовано, зачем?
какой нафиг мепинг? LINQ to SQL исповедует правильную идеологию — база данных главней. Поэтому не понимаю о чем ты, пишем просто SQL на основе табличек.

6yrop

или ты про про убожество под названием EF? Которое уже годами в таком состоянии что вот еще немного и ее можно будет использовать :grin:

timefim

Допустим метаданные описывают классы а не таблицы, например Data Annotation. Следовательно о таблицах я узнаю только через мапинг.

timefim

или ты про про убожество под названием EF?
Из того что я читал, к следующей версии они провели неплохую работу над ошибками.

6yrop

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

var query = Customers
.Where(
c => c._(_ => _.FirstName).Equal("Test1".Const.Or(SecondCriterion(c
)
.Select(
c =>
{
var tuple = _.New<CustomerProjection>
tuple._(_ => _.FirstName, c._(_ => _.FirstName;
tuple._(_ => _.FullName, c._(_ => _.FirstName).Add(c._(_ => _.LastName;
return tuple;
}
);

Если бы эти сетторы собирались бы в краткую запись так же как в Object Initializer, то запись бы отличалась только "_.New<CustomerProjection>" вместо "new CustomerProjection". А для этого уже можно и синтаксический сахар сделать. Анонимные классы через эту запись тоже вводятся.

6yrop

Допустим метаданные описывают классы а не таблицы, например Data Annotation. Следовательно о таблицах я узнаю только через мапинг.
Тк тебе SQL в итоге на таблицах надо выполнить. Посмотри в базу имена таблиц и пиши свой SQL. Единственная проблема это проинстанцировать коллекцию объектов по резал сету, но для этого в LINQ to SQL есть незатейливое правило — имена полей объектов должны совпадать с именами полей в резал сете.

timefim

Есть DisplayColumnAttribute по которому я могу понять какая свойство отвечает за название сущности, и есть Type, для которого нужно получить все пары вида ключ, название. Все данные завязаны на объекте, следовательно sql специфику нужно вытягивать через мапинг.

6yrop

Есть DisplayColumnAttribute по которому я могу понять какая свойство отвечает за название сущности
а нафига мне дисплейное название сущности для составления SQL запроса?
..и есть Type, для которого нужно получить все пары вида ключ, название
не понял о чем и к чему это ты

timefim

class NamedID {object ID; string Name;}
class Product {int ID; [DisplayColumnAttribute ] string Name; int Price;}
List<NamedID> GetAllEntityNamedIDs(Type type)
var result = GetAllEntityNamedIDs(typeof(Product;
Оставить комментарий
Имя или ник:
Комментарий: