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

от автора

В комментариях к первой части мне справедливо сделали замечание, что я обещал унификацию IEnumerable и IQueriable, а сам спрятал их за самописным интерфейсом типа репозитория. В этой статье я постараюсь исправится и дать пример что же делать, если мы хотим работать с 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 QueriableExtensions     {         public static IQueryable<TElement> AsMaybeQueriable<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 AsMaybeQueriable(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.AsMaybeQueriable(); } ... var names = GetBooks.Select(c=>c.Author.Name).ToArray(); 

Важное замечание: Операция по переписыванию дерева выражений — не бесплатная. Она быстрая по сравнению с забором данных из бд, однако я бы не рекомендовал это решение использовать для работы чисто с IEnumerable.

ссылка на оригинал статьи http://habrahabr.ru/post/256403/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *