Я, чесно, пока не понимаю зачем вы так усложняете себе жизнь. За час на коленке я написал простенькое решение, которое умеет комбинировать фильтры. Все просто, вы пишете IQueryable расширения и комбинируете как угодно.
К примеру используя одни и те же методы:
class Program
{
static void Main(string[] args)
{
var query = new[]
{
new SomeClass { City = "NY", Years = 20 },
new SomeClass { City = "Seattle", Years = 16 },
}.AsQueryable();
var semi = query.CombineOr(q => q.IsReadyToDrink(), q => q.LiveIn("NY", "Seattle"))
.ToArray();
var strict = query
.IsReadyToDrink()
.LiveIn("NY", "Seattle")
.ToArray();
var semi2 = query.CombineOr(q => q.CityAndDrinkability("NY", "Seattle"), q => q.IsReadyToDrink())
.ToArray();
var strict2 = query
.CityAndDrinkability("NY", "Seattle")
.ToArray();
}
}
Здесь помогают интерфейсы, которые вы зададите своим сущностям:
public interface IAge
{
int Years { get; }
}
public interface ICity
{
string City { get; }
}
public class SomeClass : IAge, ICity
{
public string SomeValue { get; set; }
public int Years { get; set; }
public string City { get; set; }
}
public static class BusinessRules
{
public static IQueryable<T> IsReadyToDrink<T>(this IQueryable<T> source)
where T: IAge
{
return source.Where(s => s.Years >= 18).Where(s => s.Years <= 70);
}
public static IQueryable<T> LiveIn<T>(this IQueryable<T> source,
params string[] cities)
where T: ICity
{
return source.Where(s => cities.Contains(s.City));
}
public static IQueryable<T> CityAndDrinkability<T>(this IQueryable<T> source, params string[] cities)
where T: ICity, IAge
{
return source.LiveIn(cities).IsReadyToDrink();
}
}
Ну и сама реализация (использован метод Transform из билиотеки CodeJam)
public static class SpecsExtensions
{
private static Expression Unwrap(Expression expr)
{
if (expr.NodeType == ExpressionType.Quote)
return Unwrap(((UnaryExpression) expr).Operand);
return expr;
}
private static IEnumerable<Expression> CollectCondition(Expression query, ParameterExpression obj)
{
if (query.NodeType == ExpressionType.Call)
{
var mc = (MethodCallExpression) query;
if (mc.Method.IsGenericMethod && mc.Method.GetGenericMethodDefinition() == _whereMethodInfo)
{
var unwrapped = (LambdaExpression)Unwrap(mc.Arguments[1]);
foreach (var cond in CollectCondition(mc.Arguments[0], obj))
{
yield return cond;
}
var corrected = unwrapped.Body.Transform(e => e == unwrapped.Parameters[0] ? obj : e);
yield return corrected;
}
else
{
var canProcess = mc.Method.DeclaringType != typeof(Queryable) && mc.Arguments.Count > 0;
if (canProcess)
{
canProcess = mc.Arguments[0].Type.IsGenericType;
if (canProcess)
{
canProcess = mc.Arguments[0].Type.GetGenericTypeDefinition() == typeof(IQueryable<>);
}
}
if (!canProcess)
{
throw new NotImplementedException();
}
// processing user defined functions, so filters can be reused in other filters
var innerExpression = ((IQueryable) Expression.Lambda(mc).Compile().DynamicInvoke()).Expression;
foreach (var cond in CollectCondition(innerExpression, obj))
{
yield return cond;
}
}
}
}
public static MethodInfo GetMethodInfo<T>(Expression<Action<T>> expression)
{
var member = expression.Body as MethodCallExpression;
if (member != null)
return member.Method;
throw new ArgumentException("Expression is not a method", "expression");
}
private static readonly MethodInfo _whereMethodInfo = GetMethodInfo<IQueryable<int>>(q => q.Where((Expression<Func<int, bool>>)null)).GetGenericMethodDefinition();
public static IQueryable<T> CombineOr<T>(this IQueryable<T> source, params Func<IQueryable<T>, IQueryable<T>>[] queries)
{
Expression condition = null;
var param = Expression.Parameter(typeof(T), "q");
var fake = Enumerable.Empty<T>().AsQueryable();
foreach (var query in queries)
{
var filter = query(fake);
var strict = CollectCondition(filter.Expression, param)
.Aggregate(Expression.AndAlso);
condition = condition == null ? strict : Expression.OrElse(condition, strict);
}
if (condition == null)
return source;
var filterBody = Expression.Lambda(condition, param);
var result = (IQueryable<T>) _whereMethodInfo.MakeGenericMethod(typeof(T))
.Invoke(null, new object[] { source, filterBody });
return result;
}
}
Как на меня просто, наглядно и легко понимаемо. Да тут шаманство с деревьями выражений, но я это сделал за вас.
Все проверяется компилятором и никаких дополнительных абстракций.
Сейчас принято писать асинхронные методы, да и хорошая практика для сервера
Ваша абстракция понятна, только, я не помню в своем случае когда коня/ORM меняли на переправе. Будут сидеть на EF до конца. Учитывая что абстракции резко ухудшают производительность как самой программы так и программистов, я бы задумался нужны ли они в таком виде. DbContext уже сам по себе неслабый репозиторий. Пряча от разработчиков LINQ, вы просто усложняете себе жизнь. IMHO, абстракции лучше строить на уровне бизнес задач/сервисов и в крайнем случае над отдельными сущностями.
Простой пример, что вы сделаете когда надо достать Id первых 100 пользователей, у которых установлен язык английский? Что вам дадут два репозитория которые возвращают целые таблицы в память, а вам нужно достать всего сотню целых чисел из данных связанных двумя таблицами?
Тут приходит на помощь LINQ, репозитории могут возвращать IQueryable. Только зачем эти репозитории тогда, если они просто враппер над DbContext? Я, например, не вижу в них смысла, это ухудшает читабельность, нагружает контроллер дополнительными зависимостями. Если контроллер большой, в него ижектят с 20-к репозиториев, несмотря на то что в основном нужны только несколько на один запрос.
Слегка сумбурно, но это я сейчас наблюдаю во всех ASP.NET проектах в которых просят помочь разобраться с производительностью. Написано по патернам, которые я бы уже и сам назвал антипатернами.
Это было познавательно с IIListProvider. Будем знать.
А тест надо переписать чтобы забрать зависимость от этой фичи. Умом понимаешь, что ToList будет быстрее чем ToArray, а тут как обухом.
Если чесно, я думаю они правильно поступили. EF 6 настолько уныло строит SQL запросы, что слеза наворачивается и одна надежда на мозг SQL оптимизатора.
Но, также замечу, что подход с Lazy Loading, на том же проекте привнес значительные тормоза у реальных пользователей системы с большими обьемами данных.
Сам поучавствовал в этом сраче 2015 года. Яркий пример как подход к базе данных как к тупому хранилищу объектов заканчиватся плачевно.
А вы знаете отличие IQueryable от IEnumerable или просто бездумно скопировали дефиницию из стандартной библиотеки? Так вам C++ ников долго придется об стенку до полного прозрения или наоборот?
Потому EF не может. Хранимки это и есть костыли. Шаг влево, шаг вправо — FromSql или хранимки. Выигрыш использования LINQ слегка пропал. Если в инструменте сплошные ограничения, я чувствую себя как на минном поле. Но ниче, потом так и рождаются сертифицированные специалисты — они знали, они плавали.
Может еще и оконные функции вспомним? И, думаю, никогда или не очень скоро будет поддерживаться. Разрабатывают EF Core очень квалифицированные специалисты, но к реальной разработке высоконагруженных систем, имеющие далекое отношение. По этому не удивляйтесь поставленным приоритетам.
Как это не завезли??? Видать «конкуренты» до сих пор абстрактной «фигней» над абстрактным хранилищем страдают.
Для жаждущих пока только для 2.1, на след неделе докручу к 3.0, а то немножко там нарефакторили и надо легкий тюнинг — linq2db.EntityFrameworkCore
Редко меня можно удивить SQL запросом который я не могу на LINQ написать.
Уже лучше. EF Core 3.0 имеет еще более допиленный Change Tracker, Code First, и чуток лучший парсер Expression Tree, но это не значит лучший SQL. Использовать ли это в продакшине, хотя нет, использовать ли это для высоких нагрузок — решать вам.
Мой пример высосан из пальца :) Я не работал с Jsonb в Postgres, просто почитал документацию и решил что можно показать как это гибко.
Если найдете то, что мы не можем превратить в SQL — вперед, создали issue в linq2db репозитории и я лично пошаманю.
С документацией проблема, только начинаешь ее писать — бац реквест на фичу и, к сожалению, плохие мы писатели документации. Спасаеют ссылки на тесты, где человек может увидеть как это работает.
Про типы не переживайте, есть механизм перехвата создания такого экспрешина, крутим как хотим без изменения основного кода. На этом, кстати, создана поддержка оконных функций — работает как часы.
Есть тестовый проект с бенчмарками, так и не оформился в статью, возможно будет вторая реинкарнация на BenchmarkDotNet. Итог очевиден, мы быстрее EF Core, да и Dapper, несмотря на их синтетические тесты — так вот же.
Весь цирк этих тестов выбрать тривиальную вещь и замапать, и никто не ганял это в потоках, с асинками.
Библиотека заточена на самый что ни на есть перформанс, в полоть до того что инлайнить в SQL, а что параметрами прокидывать. И часто бывает, что лучше параметры выкинуть. Гонка за производительностью заставляет библиотеку быть аскетичной но предсказуемой.
Вот и можете, кстати, стать одним из тестеров зверушки, так как мы не работам с EF и поддержали только то что увидели. Но то что у нас SQL лучше и эффективней, я вам гарантирую, хотя бы по тому что библиотека развивается уже второе десятилетие людьми которые знают как готовить базы данных.
Ну что же. Добавьте в свой велосипед еще скорости FastExpressionCompiler .
И все-таки странно что Automapper слил, есть им еще куда стремиться, хотя думал по той тропинке столько зверей потопталось, что дальше уже некуда.
Скажем так, в linq2db такие экстеншины пишутся с полпинка
public static class Jsonb
{
[Sql.Expression("{0}->>{1}", ServerSideOnly = true, InlinePrameters = true)]
public static T Value<T>(object field, string propName)
{
throw new NotImplementedException();
}
}
А зверушка позволяет перенаправлять дерево выражений на наш Linq парсер. Да и по ссылке просто почитайте BulkCopy, Delete From, Update From, Insert From.
То что EF от вас спрятал и заставил писать на хранимках или Dapper, вполне себе легко пишется на linq.
Вот вполне себе может понадобиться апдейтнуть одно поле в Jsonb:
ctx.SomeItems
.Where(e => Jsonb.Value<string>(e.Field, "SomeValue") == "Old Value")
.Set(e => e.Field, e => Jsonb.Set(e.Field, "SomeValue", "New Value"))
.Update();
Для этого конечно же надо добавить экстеншин
public static class Jsonb
{
[Sql.Expression("jsonb_set({0}, {1}, {2})", ServerSideOnly = true, InlinePrameters = true)]
public static T Set<T>(T field, string path, string newValue)
{
throw new NotImplementedException();
}
}
Вот так, в обход ChangeTracker, мы изменили поле в нескольких записях не потянув ни одной записи.
А есть, те кому удалось переслать свою игрушку для MK-61 в «Техника молодежи» с публикацией?
Эх были времена, я парочку послал, но так и не дошло и я расстроился кажется, совсем маленький был ;)
Огромный челенж был вписаться в 105, когда на листочке получалось 160 шагов программы.
Думаю вам надо было сразу смотреть в исходники Npgsql.EntityFrameworkCore, где они добавляли поддержку Range.
Очень надеюсь что это все не поломается в 3.0, кстати проверьте.
Если станет совсем не в моготу запустить нужную выборку — попробуйте нашу зверушку (ну так как-то называем) linq2db.EntityFrameworkCore. Легким тюнингом, можно Json и в linq ганять, например при использовании LATERAL и jsonb_to_recordset.
Опять же, вы написали типичный CROSS JOIN. from + from ничего другог не родит, если у ORM нету дополнительных мозгов по анализу запроса.
И да вы правы, то что сейчас предлагает LINQ, немножко путает. Я краем глаза видел что у Microsoft есть планы по его расширению, но когда это будет — мне не известно.
Offtop
Я тоже как то задумался над этой неоднозначностью и пришла идея просто добавить еще парочку функций и унифицировать синтаксис. Предупреждаю, что сие работает только для linq2db
from t in mainTable
from st in subTable.InerJoin(st => st.Id == t.Id)
from lt in subTable.LeftJoin(lt => lt.Id == t.Id)
from rt in subTable.RightJoin(rt => rt.Id == t.Id)
from ft in subTable.FullJoin(ft => ft.Id == t.Id)
select new
{
t,
st,
lt,
rt,
ft
}
Как оказалось, это намного удобней и пересталять джоины местами, и менять LEFT на INNER и наоборот намного проще.
К примеру используя одни и те же методы:
Здесь помогают интерфейсы, которые вы зададите своим сущностям:
Ну и сама реализация (использован метод Transform из билиотеки CodeJam)
Как на меня просто, наглядно и легко понимаемо. Да тут шаманство с деревьями выражений, но я это сделал за вас.
Все проверяется компилятором и никаких дополнительных абстракций.
Ваша абстракция понятна, только, я не помню в своем случае когда коня/ORM меняли на переправе. Будут сидеть на EF до конца. Учитывая что абстракции резко ухудшают производительность как самой программы так и программистов, я бы задумался нужны ли они в таком виде. DbContext уже сам по себе неслабый репозиторий. Пряча от разработчиков LINQ, вы просто усложняете себе жизнь. IMHO, абстракции лучше строить на уровне бизнес задач/сервисов и в крайнем случае над отдельными сущностями.
Простой пример, что вы сделаете когда надо достать Id первых 100 пользователей, у которых установлен язык английский? Что вам дадут два репозитория которые возвращают целые таблицы в память, а вам нужно достать всего сотню целых чисел из данных связанных двумя таблицами?
Тут приходит на помощь LINQ, репозитории могут возвращать IQueryable. Только зачем эти репозитории тогда, если они просто враппер над DbContext? Я, например, не вижу в них смысла, это ухудшает читабельность, нагружает контроллер дополнительными зависимостями. Если контроллер большой, в него ижектят с 20-к репозиториев, несмотря на то что в основном нужны только несколько на один запрос.
Слегка сумбурно, но это я сейчас наблюдаю во всех ASP.NET проектах в которых просят помочь разобраться с производительностью. Написано по патернам, которые я бы уже и сам назвал антипатернами.
А тест надо переписать чтобы забрать зависимость от этой фичи. Умом понимаешь, что ToList будет быстрее чем ToArray, а тут как обухом.
Но, также замечу, что подход с Lazy Loading, на том же проекте привнес значительные тормоза у реальных пользователей системы с большими обьемами данных.
Сам поучавствовал в этом сраче 2015 года. Яркий пример как подход к базе данных как к тупому хранилищу объектов заканчиватся плачевно.
FastReport прекрасно заменяется этим DevExpress XtraReports
Для жаждущих пока только для 2.1, на след неделе докручу к 3.0, а то немножко там нарефакторили и надо легкий тюнинг — linq2db.EntityFrameworkCore
Редко меня можно удивить SQL запросом который я не могу на LINQ написать.
www.smashcompany.com/technology/docker-is-a-dangerous-gamble-which-we-will-regret
www.nuget.org/packages/System.Text.Json
Если найдете то, что мы не можем превратить в SQL — вперед, создали issue в linq2db репозитории и я лично пошаманю.
С документацией проблема, только начинаешь ее писать — бац реквест на фичу и, к сожалению, плохие мы писатели документации. Спасаеют ссылки на тесты, где человек может увидеть как это работает.
Про типы не переживайте, есть механизм перехвата создания такого экспрешина, крутим как хотим без изменения основного кода. На этом, кстати, создана поддержка оконных функций — работает как часы.
Есть тестовый проект с бенчмарками, так и не оформился в статью, возможно будет вторая реинкарнация на BenchmarkDotNet. Итог очевиден, мы быстрее EF Core, да и Dapper, несмотря на их синтетические тесты — так вот же.
Весь цирк этих тестов выбрать тривиальную вещь и замапать, и никто не ганял это в потоках, с асинками.
Библиотека заточена на самый что ни на есть перформанс, в полоть до того что инлайнить в SQL, а что параметрами прокидывать. И часто бывает, что лучше параметры выкинуть. Гонка за производительностью заставляет библиотеку быть аскетичной но предсказуемой.
Вот и можете, кстати, стать одним из тестеров зверушки, так как мы не работам с EF и поддержали только то что увидели. Но то что у нас SQL лучше и эффективней, я вам гарантирую, хотя бы по тому что библиотека развивается уже второе десятилетие людьми которые знают как готовить базы данных.
И все-таки странно что Automapper слил, есть им еще куда стремиться, хотя думал по той тропинке столько зверей потопталось, что дальше уже некуда.
А зверушка позволяет перенаправлять дерево выражений на наш Linq парсер. Да и по ссылке просто почитайте BulkCopy, Delete From, Update From, Insert From.
То что EF от вас спрятал и заставил писать на хранимках или Dapper, вполне себе легко пишется на linq.
Вот вполне себе может понадобиться апдейтнуть одно поле в Jsonb:
Для этого конечно же надо добавить экстеншин
Вот так, в обход ChangeTracker, мы изменили поле в нескольких записях не потянув ни одной записи.
Эх были времена, я парочку послал, но так и не дошло и я расстроился кажется, совсем маленький был ;)
Огромный челенж был вписаться в 105, когда на листочке получалось 160 шагов программы.
Очень надеюсь что это все не поломается в 3.0, кстати проверьте.
Если станет совсем не в моготу запустить нужную выборку — попробуйте нашу зверушку (ну так как-то называем) linq2db.EntityFrameworkCore. Легким тюнингом, можно Json и в linq ганять, например при использовании LATERAL и
jsonb_to_recordset.И да вы правы, то что сейчас предлагает LINQ, немножко путает. Я краем глаза видел что у Microsoft есть планы по его расширению, но когда это будет — мне не известно.
Offtop
Я тоже как то задумался над этой неоднозначностью и пришла идея просто добавить еще парочку функций и унифицировать синтаксис. Предупреждаю, что сие работает только для linq2db
Как оказалось, это намного удобней и пересталять джоины местами, и менять LEFT на INNER и наоборот намного проще.