В комментариях к первой части мне справедливо сделали замечание, что я обещал унификацию IEnumerable и IQueryable, а сам спрятал их за самописным интерфейсом типа репозитория. В этой статье я постараюсь исправится и дать пример что же делать, если мы хотим работать с LINQ напрямую. Для этого я предложу собственную реализацию интерфейса IQueryProvider.
GitHub
Nuget
Итак, первая часть закончилась на том, что мы написали ExpressionVisitor, который добавлял в каждый узел дерева выражений, который является обращением к свойству обертку, проверяющую на null:
Пусть у нас снова есть пара источников данных, предоставляющих доступ к коллекции книг. При этом у каждой книги может быть указан автор, представленный в таблице авторов. Один из источников данных — бд, и соответственно из него возвращается четный IQueryable, второй — in-memory cache возвращающий List. Как и в первый раз спрячем это за интерфейс, однако теперь он будет возвращать IQueryable, а все остальные преобразования мы будем делать стандартными LINQ методами.
Стандартный подход как получить из IEnumerable (и, в частности из List) IQueryable — это вызвать метод расширение AsQueryable(). Однако нам этот вариант не подходит, потому что нам нужно с каждым применяемым выражением производить собственные манипуляции, а именно — обернуть property getter в проверку на null.
Поэтому мы напишем свой собственный метод расширения:
Все отличие заключается в том, что вместо стандартного класса EnumerableQuery мы возвращаем свою реализацию MaybeEnumerableQuery, которая является оберткой вокруг EnumerableQuery:
При этом каждое получаемое выражение мы обрабатываем с помощью нашего ExpressionVisitor.
Пример использования:
Важное замечание: Операция по переписыванию дерева выражений — не бесплатная. Она быстрая по сравнению с забором данных из бд, однако я бы не рекомендовал это решение использовать для работы чисто с IEnumerable.
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.
alexstz
Выглядит здорово. По большому счёту, в простых случаях можно написать и
но, если выражение посложнее, и их много, то решение имеет смысл.
mird Автор
вы в вашем коде получите тот же самый нулреф. Чтобы не получить, нужно вызывать так:
GetBooks().Select(c => c.Author).Where(a=>a!=null).Select(a => a.Name).ToArray(); а это слишком многословно.
alexstz
Да, согласен. Что-то написал не подумав.