Pull to refresh

Унифицируем поведение LINQ to IEnumerable и LINQ to IQueriable в части работы с null значениями. Часть вторая. Своя реализация IQueryProvider

Reading time4 min
Views9.1K
В комментариях к первой части мне справедливо сделали замечание, что я обещал унификацию IEnumerable и IQueryable, а сам спрятал их за самописным интерфейсом типа репозитория. В этой статье я постараюсь исправится и дать пример что же делать, если мы хотим работать с LINQ напрямую. Для этого я предложу собственную реализацию интерфейса IQueryProvider.

GitHub
Nuget


Итак, первая часть закончилась на том, что мы написали ExpressionVisitor, который добавлял в каждый узел дерева выражений, который является обращением к свойству обертку, проверяющую на null:
    public class AddMaybeVisitor : ExpressionVisitor
    {
        //Этот метод мы будем вызывать над выражением, которое нужно преобразовать.
        public Expression<Func<T1, T2>> Modify<T1, T2>(Expression<Func<T1, T2>> expression)
        {
            return (Expression<Func<T1, T2>>)Visit(expression);
        }

        // Этот метод вызывается в том случае, если узлом дерева выражений является обращение к свойству или полю, как раз то, что нам и требуется.
        protected override Expression VisitMember(MemberExpression node)
        {
            var expression = Visit(node.Expression);

            var expressionType = expression.Type;
            var memberType = node.Type;

            var withMethodinfo = typeof(AddMaybeVisitor)
                .GetMethod("With")
                .MakeGenericMethod(expressionType, memberType);

            var p = Expression.Parameter(expressionType);
            var l = Expression.Lambda(Expression.MakeMemberAccess(p, node.Member), p);

            return Expression.Call(withMethodinfo,
                expression,
                Expression.Constant(l.Compile(), typeof(Func<,>).MakeGenericType(expressionType, memberType))
                );
        }
        public static TResult With<TSource, TResult>(TSource source, Func<TSource, TResult> action) where TSource : class
        {
            if (source != default(TSource))
                return action(source);
            return default(TResult);
        }
    }


Пусть у нас снова есть пара источников данных, предоставляющих доступ к коллекции книг. При этом у каждой книги может быть указан автор, представленный в таблице авторов. Один из источников данных — бд, и соответственно из него возвращается четный IQueryable, второй — in-memory cache возвращающий List. Как и в первый раз спрячем это за интерфейс, однако теперь он будет возвращать IQueryable, а все остальные преобразования мы будем делать стандартными LINQ методами.
public interface IBookSource
{
    IQueryable<Book> GetBooks();
}


Стандартный подход как получить из IEnumerable (и, в частности из List) IQueryable — это вызвать метод расширение AsQueryable(). Однако нам этот вариант не подходит, потому что нам нужно с каждым применяемым выражением производить собственные манипуляции, а именно — обернуть property getter в проверку на null.
Поэтому мы напишем свой собственный метод расширения:
    public static class QueryableExtensions
    {
        public static IQueryable<TElement> AsMaybeQueryable<TElement>(this IEnumerable<TElement> source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var elements = source as IQueryable<TElement>;
//здесь отличие от стандартной реализации метода AsQueryable()
            return elements ?? new MaybeEnumerableQuery<TElement>(source);
        }

        public static IQueryable AsMaybeQueryable(this IEnumerable source)
        {
            if (source == null)
                throw new ArgumentNullException("source");
            var queryable = source as IQueryable;
            if (queryable != null)
                return queryable;
            var enumType = FindGenericType(typeof(IEnumerable<>), source.GetType());
            if (enumType == null)
                throw new ArgumentException("Source is not generic","source");
//здесь отличие от стандартной реализации метода AsQueryable()
            return MaybeEnumerableQuery.Create(enumType.GetGenericArguments()[0], source);
        }

        private static Type FindGenericType(Type definition, Type type)
        {
            while (type != null && type != typeof(object))
            {
                if (type.IsGenericType && type.GetGenericTypeDefinition() == definition)
                    return type;
                if (definition.IsInterface)
                {
                    foreach (Type itype in type.GetInterfaces())
                    {
                        Type found = FindGenericType(definition, itype);
                        if (found != null)
                            return found;
                    }
                }
                type = type.BaseType;
            }
            return null;
        }
    }


Все отличие заключается в том, что вместо стандартного класса EnumerableQuery мы возвращаем свою реализацию MaybeEnumerableQuery, которая является оберткой вокруг EnumerableQuery:
    public class MaybeEnumerableQuery<T>: MaybeEnumerableQuery, IQueryProvider, IOrderedQueryable<T>, IQueryable<T>, IOrderedQueryable, IQueryable, IEnumerable<T>, IEnumerable
    {
        private EnumerableQuery<T> _innerQuery;

        public MaybeEnumerableQuery(IEnumerable<T> enumerable)
        {
            _innerQuery = new EnumerableQuery<T>(enumerable);
        }

        public MaybeEnumerableQuery(Expression expression)
        {
            _innerQuery = new EnumerableQuery<T>(RewriteExpression(expression));
        }

        private Expression RewriteExpression(Expression expression)
        {
            var rewriter = new AddMaybeVisitor();
            return rewriter.Visit(expression);
        }

        public IQueryable CreateQuery(Expression expression)
        {
            return ((IQueryProvider)_innerQuery).CreateQuery(RewriteExpression(expression));
        }

        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return ((IQueryProvider)_innerQuery).CreateQuery<TElement>(RewriteExpression(expression));
        }

        public object Execute(Expression expression)
        {
            return ((IQueryProvider)_innerQuery).Execute(RewriteExpression(expression));
        }

        public TResult Execute<TResult>(Expression expression)
        {
            return ((IQueryProvider)_innerQuery).Execute<TResult>(RewriteExpression(expression));
        }
        ...
        //я пропустил методы реализации интерфейсов, которые являются чистыми прокси с возвращением значения из _innerQuery
    }

При этом каждое получаемое выражение мы обрабатываем с помощью нашего ExpressionVisitor.

Пример использования:
IQueryable<Book> GetBooks()
{
    List<Book> books = GetDataFromCache();
    return books.AsMaybeQueryable();
}
...
var names = GetBooks.Select(c=>c.Author.Name).ToArray();


Важное замечание: Операция по переписыванию дерева выражений — не бесплатная. Она быстрая по сравнению с забором данных из бд, однако я бы не рекомендовал это решение использовать для работы чисто с IEnumerable.
Tags:
Hubs:
Total votes 12: ↑11 and ↓1+10
Comments3

Articles