Как стать автором
Обновить

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

Браво, изобрели генерацию запросов на лету :)

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

У ORM может быть такая фишка как предопределённые части запросов, рассматривали такую возможность?
Что то типа того что автоматом (через вызов специально написанного метода совместимого с ORM) добавлять к запросу «is_hidden = 0» или «user.active = 1», и при формировании SQL ORM дубли отсечёт и нужные альясы подставит.
пока не понял о чем речь, можно пример для любой 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'ы и т.д
А зачем что-то отделять от ORM?
а для чего мы вообще вводим абстракции, разбиваем приложение на слои?
ну и еще в качестве примера одна плюшка — в одном приложении у нас использовалась nosql (mongo) и sql (psql) субд. Было приятно писать код в едином стиле
а для чего мы вообще… разбиваем приложение на слои?
Я не знаю, для чего вы разбиваете приложения на слои? Я не разбиваю. Я разбиваю приложения на куски (как торт — slices). Каждый такой кусок может иметь какой ему вздумается доступ к данным.

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

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

Ужас какой. А что не так с, извините, FSM? Устарело?

хотелось бы более развернутого комменатрия. Причем тут FSM?

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


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

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

Но есть же LinqSpecs, которые работают вообще с чем угодно…
Это отличный проект, поддерживающий много фич, которые мне нравятся, и которые я пока не реализовал (сериализация и сравнения — две первоочередные). Но в тоже время есть несколько НО:
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 или что-то вроде того

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