В комментариях к первой части мне справедливо сделали замечание, что я обещал унификацию IEnumerable и IQueriable, а сам спрятал их за самописным интерфейсом типа репозитория. В этой статье я постараюсь исправится и дать пример что же делать, если мы хотим работать с LINQ напрямую. Для этого я предложу собственную реализацию интерфейса IQueryProvider.
Итак, первая часть закончилась на том, что мы написали 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/
Добавить комментарий