[Из песочницы] Принципы работы IQueryable и LINQ-провайдеров данных28.04.2015 13:32
Средства LINQ позволяют .Net-разработчикам единообразно работать как с коллекциями объектов в памяти, так и с объектами, хранящимися в базе данных или ином удаленном источнике. Например, для запроса десяти красных яблок из списка в памяти и из БД средствами Entity Framework мы можем использовать абсолютно идентичный код:
List appleList;
DbSet appleDbSet;
var applesFromList = appleList.Where (apple => apple.Color == «red»).Take (10);
var applesFromDb = appleDbSet.Where (apple => apple.Color == «red»).Take (10);
Однако, выполняются эти запросы по-разному. В первом случае при перечислении результата с помощью foreach яблоки будут отфильтрованы с помощью заданного предиката, после чего будут взяты первые 10 из них. Во втором случае синтаксическое дерево с выражением запроса будет передано специальному LINQ-провайдеру, который транслирует его в SQL-запрос к базе данных и выполнит, после чего сформирует для 10 найденных записей объекты С# и вернет их. Обеспечить такое поведение позволяет интерфейс IQueryable, предназначенный для создания LINQ-провайдеров к внешним источникам данных. Ниже мы попробуем разобраться с принципами организации и использования этого интерфейса.Интерфейсы IEnumerable и IQueryable
На первый взгляд может показаться, что в основе LINQ лежит набор методов-расширений вроде Where (), Select (), First (), Count () и т.д. к интерфейсу IEnumerable, что в итоге дает разработчику возможность единообразно писать запросы как к объектам в памяти (LINQ to Objects), так и к базам данных (например, LINQ to SQL, LINQ to Entities) и удаленным сервисам (например, LINQ to OData Services). Но это не так. Дело в том, что внутри методов-расширений к IEnumerable уже реализованы соответствующие операции с последовательностями. Так, например, метод First (Func predicate) реализован в .Net Framework 4.5.2, исходники которого нам доступны здесь, следующим образом:
public static TSource First(this IEnumerable source, Func predicate) {
if (source == null) throw Error.ArgumentNull («source»);
if (predicate == null) throw Error.ArgumentNull («predicate»);
foreach (TSource element in source) {
if (predicate (element)) return element;
}
throw Error.NoMatch ();
}
Понятно, что в общем случае такой метод не может быть выполнен над данными, расположенными в БД или сервисе. Для его выполнения мы можем лишь предварительно загрузить весь набор данных непосредственно в приложение, что по понятным причинам неприемлемо.Для реализации LINQ-провайдеров к внешним по отношению к приложению данным используется интерфейс IQueryable (наследник от IEnumerable) вместе с набором методов-расширений, почти полностью идентичных тем, что написаны для IEnumerable. Именно потому, что List реализует IEnumerable, а DbSet из Entity Framework — IQueryable, приведенные в начале статьи запросы с яблоками выполняются по-разному.
Особенность методов-расширений к IQueryable состоит в том, что они не содержат логики обработки данных. Вместо этого они просто формируют синтаксическую структуру с описанием запроса, «наращивая» ее при каждом новом вызове метода в цепочке. При вызове же агрегатных методов (Count () и т.п.) или при перечислении с помощью foreach описание запроса передается на выполнение провайдеру, инкапсулированному внутри конкретной реализации IQueryable, а тот уже преобразует запрос в язык источника данных, с которым работает, и выполняет его. В случае с Entity Framework таким языком является SQL, в случае с .Net-драйвером для MongoDb — это поисковый json-объект и т.д.
Кстати говоря, из этой особенности вытекают некоторые «интересные» характеристики LINQ-провайдеров:
запрос, который успешно выполняется одним провайдером, может не поддерживаться другим; более того, узнаем мы об этом даже не на этапе конструирования запроса, а только на этапе его выполнения провайдером;
перед выполнением запроса провайдер может предварительно его модифицировать; например, ко всем запросам может добавляться ограничение на количество возвращаемых объектов, дополнительные фильтры и т.д. Делаем LINQ своими руками: ISimpleQueryable
Прежде чем описывать устройство интерфейса IQueryable, попробуем самостоятельно написать его простой аналог — интерфейс ISimpleQueryable, а также пару методов-расширений к нему в стиле LINQ. Это позволит наглядно продемонстрировать основные принципы работы с IQueryable, не вдаваясь пока в нюансы его реализации.
public interface ISimpleQueryable: IEnumerable {
string QueryDescription { get; }
ISimpleQueryable CreateNewQueryable (string queryDescription);
TResult Execute();
}
В интерфейсе мы видим свойство QueryDescription, которое содержит описание запроса, а также метод Execute (), который должен этот запрос при необходимости выполнить. Это generic-метод, поскольку результат выполнения может быть как перечислением, так и значением агрегатной функции, такой как Count (). Кроме того, в интерфейсе есть метод CreateNewQueryable (), который позволяет при добавлении нового LINQ-метода сформировать новый экземпляр ISimpleQueryable, но уже с новым описанием запроса. Заметим, что описание запроса здесь представлено в виде строки, а в LINQ для этого используются деревья выражений (Expression Trees), о которых можно почитать здесь или здесь.Теперь перейдем к методам-расширениям:
public static class SimpleQueryableExtentions
{
public static ISimpleQueryable Where(this ISimpleQueryable queryable,
Expression> predicate) {
string newQueryDescription = queryable.QueryDescription + ».Where (» + predicate.ToString () + »)»;
return queryable.CreateNewQueryable (newQueryDescription);
}
public static int Count(this ISimpleQueryable queryable) {
string newQueryDescription = queryable.QueryDescription + ».Count ()»;
ISimpleQueryable newQueryable = queryable.CreateNewQueryable (newQueryDescription);
return newQueryable.Execute();
}
}
Как мы видим, эти методы просто дописывают информацию о себе в описание запроса и создают новый экземпляр ISimpleQueryable. Кроме того, метод Where (), в отличие от своего аналога для IEnumerable, принимает не сам предикат Func, а упомянутое ранее дерево выражения (expression tree) с его описанием Expression>. В данном примере это просто дает нам возможность получить строку с кодом предиката, а в случае с реальным LINQ — возможность сохранить все детали запроса в виде дерева выражений.Наконец, создадим простую реализацию нашего ISimpleQueryable, которая будет содержать все необходимое для написания LINQ-запросов, за исключением метода их выполнения. Для придания реалистичности добавим туда ссылку на источник данных (_dataSource), которая должна использоваться при выполнении запроса методом Execute ().
public class FakeSimpleQueryable: ISimpleQueryable
{
private readonly object _dataSource;
public string QueryDescription { get; private set; }
public FakeSimpleQueryable (string queryDescription, object dataSource) {
_dataSource = dataSource;
QueryDescription = queryDescription;
}
public ISimpleQueryable CreateNewQueryable (string queryDescription) {
return new FakeSimpleQueryable(queryDescription, _dataSource);
}
public TResult Execute() {
//Здесь должна быть обработка QueryDescription и применение запроса к dataSource
throw new NotImplementedException ();
}
public IEnumerator GetEnumerator () {
return Execute>();
}
IEnumerator IEnumerable.GetEnumerator () {
return GetEnumerator ();
}
}
Теперь рассмотрим простой запрос к FakeSimpleQueryable:
var provider = new FakeSimpleQueryable(», null);
int result = provider.Where (s => s.Contains («substring»)).Where (s => s!= «some string»).Count ();
Попробуем разобраться, что будет происходить, при выполнении приведенного выше кода (см. также рисунок ниже): сначала первый вызов метода Where () возьмет у созданного с помощью конструктора экземпляра FakeSimpleQueryable пустое описание запроса, добавит к нему ».Where (s => s.Contains («substring»))» и сформирует второй экземпляр FakeSimpleQueryable с новым описанием;
затем второй вызов Where () возьмет у созданного ранее FakeSimpleQueryable описание запроса, добавит к нему ».Where (s => s!= «some string»)», после чего опять сформирует новый, третий по счету, экземпляр FakeSimpleQueryable с описанием запроса ».Where (s => s.Contains («substring»)).Where (s => s!= «some string»)»;
наконец, вызов Count () возьмет у созданного на предыдущем шаге экземпляра FakeSimpleQueryable описание запроса, добавит к нему » .Count ()» и сформирует четвертый экземпляр FakeSimpleQueryable, после чего вызовет у него метод Execute, поскольку дальше построение запроса невозможно;
в результате внутри метода Execute () мы будет иметь значение QueryDescription, равное ».Where (s => s.Contains («substring»)).Where (s => s!= «some string»).Count ()», которое и нужно обрабатывать дальше.
Настоящий IQueryable… и IQueryProvider
Рассмотрим теперь, что собой представляет интерфейс IQueryable, реализованный в .Net:
public interface IQueryable: IEnumerable {
Expression Expression { get; }
Type ElementType { get; }
IQueryProvider Provider { get; }
}
public interface IQueryable: IEnumerable, IQueryable {}
public interface IQueryProvider {
IQueryable CreateQuery (Expression expression);
IQueryable CreateQuery(Expression expression);
object Execute (Expression expression);
TResult Execute(Expression expression);
}
Отметим, что: в .Net есть generic- и обычная версия IQueryable;
для хранения дерева с описанием LINQ-запроса используется свойство Expression (в нашей реализации мы использовали строчное QueryDescription);
свойство ElementType содержит информацию о типе возвращаемых запросом элементов и используется в реализациях LINQ-провайдеров для проверки соответствия типов;
пара методов по созданию новых экземпляров IQueryable (CreateQuery () и CreateQuery()), а также пара методов по выполнению запроса (Execute () и Execute()) вынесены в отдельный интерфейс IQueryProvider; можно предположить, что такое разделение понадобилось для того, чтобы отделить сам запрос, который пересоздается при каждом новом вызове метода-расширения, от того объекта, который реально имеет доступ к источнику данных, делает всю основную работу и может быть достаточно «тяжеловесным» для постоянного пересоздания;
свойство IQueryable.Provider указывает на связанный экземпляр IQueryProvider.
Теперь рассмотрим работу методов-расширений к IQueryable на примере метода Where ():
public static IQueryable Where(this IQueryable source, Expression> predicate) {
if (source == null) throw Error.ArgumentNull («source»);
if (predicate == null) throw Error.ArgumentNull («predicate»);
return source.Provider.CreateQuery(
Expression.Call (
null,
((MethodInfo)MethodBase.GetCurrentMethod ()).MakeGenericMethod (typeof (TSource)),
new Expression[] { source.Expression, Expression.Quote (predicate) }
));
}
Мы видим, что метод конструирует новый экземпляр IQueryable, передавая в CreateQuery () выражение, в котором к исходному выражению из source.Expression добавлен вызов собственно метода Where () с переданным предикатом в качестве аргумента.Таким образом, несмотря на некоторые отличия интерфейсов IQueryable и IQueryProvider от созданного нами ранее ISimpleQueryable, принципы их использования в LINQ те же: каждый метод-расширение, добавленный к запросу, дополняет дерево выражений информацией о себе, после чего создает новый экземпляр IQueryable с помощью метода CreateQuery(), а агрегатные методы, кроме того, инициируют выполнение запроса, вызывая метод Execute().
Пара слов о разработке LINQ-провайдеров
Поскольку механизм конструирования LINQ-запроса уже реализован в .Net за нас, то разработка LINQ-провайдера в большинстве своем сводится к реализации методов Execute () и Execute (). Именно здесь требуется разобрать пришедшее на выполнение expression tree, сконвертировать его в язык источника данных, выполнить запрос, обернуть результаты в C#-объекты и вернуть их. К сожалению, эта процедура включает в себя обработку немалого количества различных нюансов. Более того, доступной информации по разработке LINQ-провайдеров довольно мало. Ниже приведены наиболее информативные, по мнению автора, статьи на эту тему: Надеюсь, что материал этой статьи будет полезен всем, кто хотел разобраться с организацией работы LINQ-провайдеров к удаленным источникам данных или подступиться к созданию такого провайдера, но пока не решался.
© Habrahabr.ru