Комментарии 19

Несовсем понял ваш комментарий. Подход и библиотека не о генерации запросов на лету (в конечном счете все опирается на iqueryable), а про то как можно их огранизовать в коде

У ORM может быть такая фишка как предопределённые части запросов, рассматривали такую возможность?
Что то типа того что автоматом (через вызов специально написанного метода совместимого с ORM) добавлять к запросу «is_hidden = 0» или «user.active = 1», и при формировании SQL ORM дубли отсечёт и нужные альясы подставит.
www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/filters.html
Я думаю в самой Докритн ОРМ побогаче этот функционал, не знаю почему тут так скромно описано.

И есть же LINQ to SQL, делаете метод, который принимает формируемое условие, что то в это условие добавляет и выдаёт что получилось — прямо так и задумано использовать LINQ, потом из сфомированного выражения LINQ генериться SQL, при этом строиться дерево выражений и конечно оно упрощается, ещё в далеком 2013 оно это умело и умело хорошо (по мнению некоторых более чем я прошаренных товарищей).
ага, более-менее понял.
ну по сути да, спецификация — изначально это как раз что-то похожее на фильтры.
Мы определяем объекты, которые соотвествуют нашим бизнес правилам, можем в последующем их объединять в более сложные правила.
зачем нужен еще один слой абстракции — я писал в статье и в комменатриях — что бы отделить уровень домена от конкретной реализации orm. в конечном счете (по крайне мере сейчас) внутри executor'a все опирается на linq, который через провайдер для orm и строит запросы, отвечая за все alias'ы и т.д
а для чего мы вообще вводим абстракции, разбиваем приложение на слои?
ну и еще в качестве примера одна плюшка — в одном приложении у нас использовалась nosql (mongo) и sql (psql) субд. Было приятно писать код в едином стиле
а для чего мы вообще… разбиваем приложение на слои?
Я не знаю, для чего вы разбиваете приложения на слои? Я не разбиваю. Я разбиваю приложения на куски (как торт — slices). Каждый такой кусок может иметь какой ему вздумается доступ к данным.

«в одном приложении» у нас использовалось целых 34 базы данных:
— mysql — легаси кусок от CMS на PHP
— ms sql — для хранения реляционных данных
— cosomsdb (mongo) — для хранения данных, которые не совсем реляционные
— firestore — подготовленная копия данных для клиента

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

Ну просто вот этот «паттерн Спецификация» выглядит как прекрасная иллюстрация десятого правила Гринспуна.


is an ad-hoc, informally-specified, bug-ridden, slow implementation of half of [FSM].

Можно просто взять (или сделать) работоспособную имплементацию FSM и построить полнофункциональный query builder с в разы меньшими затратами. Возможно, я просто чего-то недопонимаю, впрочем.

Это отличный проект, поддерживающий много фич, которые мне нравятся, и которые я пока не реализовал (сериализация и сравнения — две первоочередные). Но в тоже время есть несколько НО:
1) для того, что бы использовать фичи ORM, придется добавить референс на соотвествующую сборку из слоя домена. что не всегда возможно, если у вас домен пошарен между несколькими проектами, под разные платформы
2) возможно я ошибаюсь, но она поддерживает только склейку условий, состоящих из условий фильтрации(where), в моей реализации есть возможнолсть склейки произвольных правил, соотвественно я могу определить, к примеру, правило на фильтрацию и правило для пагинации, а потмо получить правило на пагинацию и фильтарцию

1) Ничего не мешает портировать этот код прямо в домен (в любом случае, вашу сборку либо так же референсить, либо эмбеддить). Код этой сборки очень прост.
2) Там есть склейка через && и || плюс инвертирование через !spec.


Использовал данное решение для динамического построения предикатов в проектах, где использовался ORM. Было очень удобно.

1) вы меня не совсем поняли. предположим, я хочу использовать Fetch (Include в ef.core). Что бы использовтаь его с LinqSpec (если это вообще возможно, я не уверен) — мне придется сослаться на библиотеку orm из слоя домена.
2) операторы склейки — это удобно, не спорю, только, все таки через & и |. Но судя по коду библиотеки (который на самом деле очень простой, как вы и отметили) склеить правила, которые включают в себя сортировку не получится.

я не в коем случае не говорю о том, что мое решение — серебрянная пуля. одной из целей, которую я преследовал, написав эту статью и опубликовав исходники — получить фидбек, что бы понять, «а не фигню ли я делаю» (с).

определенно, из LinqSpec есть что почерпунть!
Я, чесно, пока не понимаю зачем вы так усложняете себе жизнь. За час на коленке я написал простенькое решение, которое умеет комбинировать фильтры. Все просто, вы пишете 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;
    }
}

Как на меня просто, наглядно и легко понимаемо. Да тут шаманство с деревьями выражений, но я это сделал за вас.
Все проверяется компилятором и никаких дополнительных абстракций.
1) это справедливо для базового понятия спецификации, но, как я рассказывал в статье, мы хотели получить расширенные правила, которые не ограничивались бы фильтрами
2) если остановиться на iqueryable, то возвращаемся к проблеме, о которой я уже не однократно упоминал и в статье и в комментариях (возможно, она специфична для конкретного проекта/решения) — предположим, у вас домен пошарен между бэкенд частью веб приложения и богатым мобильным приложением. на бэке вы используете полновесную орм, а на мобилке у вас просто in memory хранилище. что бы на основе iqueryable использовать правила, включающие в себя оптимизации для загрузки придется сослаться на сборки орм, которых на мобильной платформе может не оказаться. поэтому и появляется этот слой абстракции…
кроме того, иногда linq не хватает. в практике был пример, когда спецификации пришлось переписать на использование icriteria. но благодаря этому слою не пришлось перелапачивать весь код приложения, а все правки остались в слое доступа.

p.s. код лучше было оформить в виде ссылки на codebin или что-то вроде того
1) Возможно, специфика есть специфика
2) Так и делают. Тянут ORM на клиент, хотя в случае EF Core это толпа зависимостей. По этому часто используют Remote LINQ или что-то в этом роде.

О случае когда вам IQueryable не хватило можно поподробнее?

p.s. код лучше было оформить в виде ссылки на codebin или что-то вроде того

Возможно, но так как писано на коленке, тогда и не задумывался.
Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Информация

Дата основания
Местоположение
Россия
Сайт
www.singularis-lab.com
Численность
11–30 человек
Дата регистрации

Блог на Хабре