IQueryable в BL&UI
От: artelk  
Дата: 22.04.13 14:55
Оценка:
Снова поднимаю периодически возникающий тут вопрос об использовании IQueryable в BL&UI (т.е. чтобы DAL отдавал в вышестоящие слои данные по этому интерфейсу).
Преимущества понятны — клиенский код лучше знает, что ему нужно, может накладывать свои предикаты и делать проекции, вместо того, чтобы на каждый чих (для каждого запроса) добавлять методы в DAL.
С другой стороны, проблемы тоже есть. Вот тут товарищ Mark Seemann хорошо расписал к чему это может привести.
Лично я склоняюсь к тому, чтобы оставить клиенту возможность делать свои запросы и\или их кастомизации (paging, sorting...) — т.е. чтобы явно или неявно отдавался IQueryable.
И вот возникла у меня идейка, которую хочу расшарить и обсудить. Суть в том, что мы сделаем "свой IQueryable" — с блэкджеком и....
Свой IQueryable нам нужен, чтобы ограничить набор возможных методов (в классе Queryable их миллион) только теми, которые мы можем реализовать. Плюс сделать так, чтобы для разных провайдеров можно было делать свои реализации того или иного метода (именно для этого я эти методы засовываю в интерфейс).
Короче, код:
  Скрытый текст
    public interface IQuery<T> : IEnumerable<T>
    {
        IQuery<T> Where(Expression<Func<T, bool>> predicate);
        IQuery<TResult> Select<TResult>(Expression<Func<T, TResult>> selector);

        //IQuery<TResult> SelectMany<TResult>(Expression<Func<T, IEnumerable<TResult>>> selector);
        //IQuery<TResult> SelectMany<TCollection, TResult>(Expression<Func<T, IEnumerable<TCollection>>> collectionSelector,
        //                                                 Expression<Func<T, TCollection, TResult>> resultSelector);
        //IQuery<T> Take(int count);
        //IQuery<T> Skip(int count);
        //IQuery<T> Distinct();
        //T First();
        //T FirstOrDefault();
        //T Single();
        //T SingleDefault();
        //...

        //Добавил до кучи полезный метод, который могу реализовать
        IQuery<T> IsIn<TProp>(Expression<Func<T, TProp>> propertyExpression, params TProp[] collection);
    }

    //Реализация "по умолчанию"
    public abstract class QueryBase<T> : IQuery<T>
    {
        protected readonly IQueryable<T> Items;

        protected QueryBase(IQueryable<T> items)
        {
            if (items == null) throw new ArgumentNullException("items");
            Items = items;
        }

        protected abstract IQuery<TResult> New<TResult>(IQueryable<TResult> items);

        public IEnumerator<T> GetEnumerator()
        {
            return Items.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return Items.GetEnumerator();
        }

        public virtual IQuery<T> Where(Expression<Func<T, bool>> predicate)
        {
            return New(Items.Where(predicate));
        }

        public virtual IQuery<TResult> Select<TResult>(Expression<Func<T, TResult>> selector)
        {
            return New(Items.Select(selector));
        }

        public virtual IQuery<T> IsIn<TProp>(Expression<Func<T, TProp>> propertyExpression, params TProp[] collection)
        {
            //Что-то типа такого (как в Linq to Sql):
            //return New(items.Where(v => collection.Contains(propertyExpression(v))));

            return null;
        }
    }

    //Для Linq to Object и, возможно, для провайдеров, для который реализация в QueryBase работает нормально
    public class DefaultQuery<T> : QueryBase<T>
    {
        public DefaultQuery(IQueryable<T> items) : base(items)
        {
        }

        protected override IQuery<TResult> New<TResult>(IQueryable<TResult> items)
        {
            return new DefaultQuery<TResult>(items);
        }
    }

    //Для EF
    public class EFQuery<T> : QueryBase<T>
    {
        public EFQuery(IQueryable<T> items) : base(items)
        {
        }

        protected override IQuery<TResult> New<TResult>(IQueryable<TResult> items)
        {
            return new EFQuery<TResult>(items);
        }

        //Украл отсюда: http://blogs.msdn.com/b/phaniraj/archive/2008/07/17/set-based-operations-in-ado-net-data-services.aspx
        public override IQuery<T> IsIn<TProp>(Expression<Func<T, TProp>> propertyExpression, params TProp[] collection)
        {
            var param = propertyExpression.Parameters[0];
            var left = propertyExpression.Body;
            Expression filterPredicate = Expression.Constant(false);

            foreach (var item in collection)
            {
                var comparison = Expression.Equal(left, Expression.Constant(item));
                filterPredicate = Expression.Or(filterPredicate, comparison);
            }

            var filterLambdaExpression = Expression.Lambda<Func<T, bool>>(filterPredicate, param);
            return New(Items.Where(filterLambdaExpression));
        }
    }

Вобщем, все это нужно, чтобы 1) было LSP для возвращаемого IQueryable (который пришлось завернуть в свой IQuery) 2) таки оставить клиентскому коду возможность решать, что ему нужно (сохранить возможность делать Linq-запросы).
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.