Практически любой .NET разработчик так или иначе использует в своей практике технологию Linq. Linq позволяет писать красивый и лаконичный код для получения объектов из источника данных с возможностью определения критериев получения и/или трансформации запрошенных объектов «на лету». Поддержка Linq присутствует практически во всех популярных ORM-фреймворках, в том числе и в NHibernate. NHibernate предоставляет Linq-провайдер, с помощью которого мы можем написать запрос на этапе разработки (Design-Time), но для того, чтобы составить запрос в runtime, придется повозиться с Reflection. Однако, если возникнет потребность в формировании запроса во внешнем процессе, например, в клиентской части сервиса, то в таком случае Reflection уже не спасет, клиентская часть, как правило, не знает (и не должна ничего знать) про серверный ORM.
Ниже мы разберем как создать API для написания Linq запросов к NHibernate в ситуации, когда запрос пишется в одном процессе, а выполняется в другом. Также, реализуем собственный IQueryProvider, который будет транслировать запросы из приложения-источника в исполняющее приложение.
Содержание
1. IEnumerable vs IQueryable
2. Linq в NHibernate
3. Linq-запросы без объекта ISession
4. Linq запрос к БД через NHibernate из внешнего процесса
5. Пишем тестовое приложение
Заключение
Ссылки
1. IEnumerable vs IQueryable
Для начала, следует вкратце вспомнить об интерфейсах IEnumerable и IQueryable. Про них писали здесь и здесь. А также полезно почитать про деревья выражений (expression tree) и как они работают.
Как происходит исполнение IEnumerable запроса:
1. Источник данных представляется как IEnumerable (перечислимый), в случае с коллекциями это необязательное действие.
2. Перечислимый источник данных оборачивается (декорируется) итератором WhereListIterator
3. Первый итератор WhereListIterator декорируется следующим итератором WhereListIterator
4. В конструктор List, передается WhereListIterator «верхнего уровня». В грубом приближении, можно сказать, что заполнение внутреннего контейнера List происходит через обход полученного WhereListIterator циклом foreach. При запросе следующего элемента, декоратор «верхнего уровня» вызывает всю цепочку декораторов, каждый элемент которой, определяет какой элемент можно вытолкнуть наверх, а какой должен быть пропущен
// List ctor
public List<T>(IEnumerable<T> source)
{
// получаем IEnumerator последнего WhereListIterator.
var enumerator = source.GetEnumerator();
// в этот момент происходит вызов всей цепочки WhereListIterator'ов для получения следующего элемента с учетом фильтра
while(enumerator.MoveNext())
{
items.Add(enumerator.Current)
}
}
Как происходит исполнение IQueryable запроса на примере коллекции:
1. Источник данных оборачивается объектом EnumerableQueryable (назовем его “А”), внутри которого создается выражение ConstantExpression с замыканием ссылки на объект-источник (также создается IQueryProvider, который в случае с IEnumerable, будет смотреть на исходную коллекцию).
2. Объект “А” декорируется новым объектом EnumerableQueryable (назовем его “B”), из объекта А берется свойство Expression, которое декорируется выражением MethodCallExpression, где в качестве вызываемого метода указывается Queryable.Where; провайдером запроса в новом объекте устанавливается провайдер запроса из объекта А. Объект А больше не нужен.
3. Полученный на предыдущем шаге объект B с выражением MemberExpression декорируется новым объектом EnumerableQueryable (назовем его “C”), из объекта B берется свойство с типом Expression, которое декорируется выражением MethodCallExpression, где в качестве вызываемого метода указывается Queryable.Where; провайдером запроса в новом объекте устанавливается провайдер запроса из объекта B. Объект B больше не нужен.
4. Основное действо происходит на этапе обращения к результатам запроса:
Интерфейс IQueryable является наследником IEnumerable, следовательно, объект типа IQueryable также может быть передан в конструктор List, перед выполнением цикла foreach (снова грубое приближение), у переданного IQueryable-объекта будет вызван метод GetEnumerator. Во время вызова GetEnumerator() провайдер запроса скомпилирует результирующий MethodCallExpression и вернет, как и в случае с IEnumerable, цепочку методов-декораторов, которая будет возвращать по запросу следующий элемент.
public List<T>(IQueryable<T> source)
{
// получаем IEnumerator последнего WhereListIterator.
var enumerator = source.GetEnumerator();
// в этот момент происходит вызов всей цепочки WhereListIterator'ов для получения следующего элемента с учетом фильтра
while(enumerator.MoveNext())
{
items.Add(enumerator.Current)
}
}
public class EnumerableQueryable<T> : IQueryable<T>
{
private Expression expression;
private IEnumerable enumerableResult;
public IEnumerator GetEnumerator()
{
if (enumerableResult == null)
enumerableResult = expression.Compile().Invoke();
return enumerableResult.GetEnumerator();
}
}
Здесь мы подходим к тому, чем же все-таки отличаются IEnumerable и IQueryable
- При работе с IEnumerable происходит декорирование источника данных с помощью объектов-итераторов.
- При работе с IQueryable происходит декорирование источника данных с помощью деревьев выражений.
Преимущество деревьев выражений заключается в том, что это по своей сути высокоуровневый API для генерации IL-кода, который мы можем построить/отредактировать в runtime, скомпилировать в делегат и вызвать.
2. Linq в NHibernate
Типичный сценарий работы с Linq в NHibernate выглядит так:
var activeMasterEntities = session
// вернет NhQueryable<T>, с ConstantExpression внутри, замкнутым на самого себя в качестве источника данных.
.Query<Entity>()
// вернет IQueryable, с MethodCallExpression внутри, который декорирует ConstantExpression.
.Where(e => e.IsMaster == true)
// вернет IQueryable, с MethodCallExpression внутри, который декорирует первый MethodCallExpression.
.Where(e => e.IsActive == true)
// запустит выполнение запроса
.ToList()
Вызов session.Query() вернет объект типа NhQueryable. Дальнейшее наворачивание условий Where будет декорировать expression из исходного объекта NhQueryable. Оборачивание исходного запроса происходит точно также, как и в случае с запросом к коллекции. Отличия начинаются с момента вызова ToList().
В момент вызова метода GetEnumerator() построенное дерево выражений будет не скомпилировано, а транслировано в sql-запрос. За механизм трансляции Linq-запроса в SQL отвечает библиотека Remotion.Linq, она разбирает полученное от NHibernate expression tree. На этапе разбора происходит вычислений замыканий в узлах дерева, например:
int stateCoefficient = 0.9;
int ageLimitInCurrentState = 18 * stateCoefficient;
var availableMovies = session
.Query<Movie>()
.Where(m => m.AgeLimit >= ageLimitInCurrentState)
.ToList()
Лямбда-выражение m => m.AgeLimit >= ageLimit создаст замыкание на локальную переменную ageLimit. При разборе этого лямбда-выражения, дерево выражений вида m.AgeLimit >= ageLimitInCurrentState будет вычислено в выражение m.AgeLimit >= 16 и уже в таком виде выражение будет отдано транслятору Linq2Sql.
IQueryable и IQueryProvider
Поподробнее рассмотрим интерфейсы IQueryable и IQueryProvider.
В интерфейсе IQueryable есть свойство Expression, которое предоставляет текущее выражение-декоратор (декоратор источника данных, либо декоратор другого выражения), а также свойство Provider с типом IQueryProvider, через которое можно получить текущий провайдер запроса.
Интерфейс IQueryProvider предоставляет методы CreateQuery(Expression expression) — для декорирования выражения нижнего уровня и Execute(Expression expression) для выполнения запроса к источнику данных.
Все методы Linq можно разделить на две группы:
- Методы-декораторы, которые оборачивают выражение предыдущего объекта IQueryable (Where(), Select(), OrderBy() и т.д). Методы-декораторы используют метод CreateQuery() объекта IQueryProvider и возвращают IQueryable.
- Методы-процессоры, которые приводят запрос в исполнение (Count(), First(), Last(), Sum() и т.д.).
Методы-процессоры используют метод Execute() объекта IQueryProvider и возвращают результат.
Вызов метода session.Query() возвращает объект типа NhQueryable, свойство Provider которого означено объектом типа INhQueryProvider.
Сильно упрощенная реализация NhQueryable выглядит следующим образом
public class NhQueryable<T> : QueryableBase<T>
{
public NhQueryable(ISessionImplementor session)
{
// Создаем провайдер с типом INhQueryProvider
Provider = QueryProviderFactory.CreateQueryProvider(session);
// источником данных устанавливаем текущий объект.
Expression = Expression.Constant(this);
}
}
Дальнейшее оборачивание объекта NhQueryable методами из класса System.Linq.Queryable(такими как Where, Select, Skip, Take и т.д.) будет использовать один и тот же провайдер данных, который был создан в объекте NhQueryable.
3. Linq-запросы без объекта ISession
Отвязывание запроса от сессии предполагает избавление от вызова Session.Query() и NhQueryable в корне дерева выражений соответственно. Для того, чтобы было возможно использовать linq, необходим источник-заглушка, возвращающий IQueryable-объект.
Определим его:
public class RemoteQueryable<T> : IQueryable<T>
{
public Expression Expression { get; set; }
public Type ElementType { get; set; }
public IQueryProvider Provider { get; set; }
public RemoteQueryable()
{
Expression = Expression.Constant(this);
}
}
Теперь мы можем написать что-то вроде:
var query = new RemoteQueryable<Entity>().Where(e => e.IsMaster);
Для удобства использования обернем создание запроса в класс-репозиторий:
public static class RemoteRepository
{
public static IQueryable<TResult> CreateQuery<TResult>(IChannelProvider provider)
{
return new RemoteQueryable<TResult>(provider);
}
}
Теперь написав вот примерно такой код
var query = RemoteRepository.CreateQuery<Entity>()
.Where(e => e.IsMaster)
.Where(e => e.IsActive);
и обратившись к свойству Expression объекта query, мы получим следующее дерево выражений:
Выше я писал, что дерево выражений можно отредактировать в runtime, следовательно мы можем обойти полученное дерево и заменить MockDataSource на NhQueryable. NhQueryable можно получить, вызвав session.Query().
Для того, чтобы выполнить обход дерева используем паттерн Visitor. Чтобы не писать визитор дерева выражений «с нуля», воспользуемся этой базовой реализацией. В наследнике переопределим методы VisitConstant и VisitMethodCall, а также переопределим метод Visit, который будет являться точкой доступа редактирования выражения:
public class NhibernateExpressionVisitor : ExpressionVisitor
{
protected IQueryable queryableRoot;
public new Expression Visit(Expression sourceExpression, IQueryable queryableRoot)
{
this.queryableRoot= queryableRoot;
return Visit(sourceExpression);
}
protected override Expression VisitMethodCall(MethodCallExpression m)
{
var query = m;
var constantArgument = query.Arguments.FirstOrDefault(e => e is ConstantExpression && e.Type.IsGenericType && e.Type.GetGenericTypeDefinition() == typeof(EnumerableQuery<>));
if (constantArgument != null)
{
var constantArgumentPosition = query.Arguments.IndexOf(constantArgument);
var newArguments = new Expression[query.Arguments.Count];
for (int index = 0; index < newArguments.Length; index++)
{
if (index != constantArgumentPosition)
newArguments[index] = query.Arguments[index];
else
newArguments[index] = queryableRoot.Expression;
}
return Expression.Call(query.Object, query.Method, newArguments);
}
return base.VisitMethodCall(query);
}
protected override Expression VisitConstant(ConstantExpression c)
{
if (c.Type.IsGenericType && typeof(RemoteQueryable<>).IsAssignableFrom(c.Type.GetGenericTypeDefinition()))
return queryableRoot.Expression;
return c;
}
}
Как этим пользоваться:
var query = RemoteRepository.CreateQuery<Entity>()
.Where(e => e.IsMaster)
.Where(e => e.IsActive);
using (var session = CreateSession())
{
var nhQueryable = session.Query<Entity>();
var nhQueryableWithExternalQuery = new NhibernateExpressionVisitor().Visit(query.Expression, nhQueryable);
var result = nhQueryable.Provider.Execute(nhQueryableWithExternalQuery);
}
На строчке new NhibernateExpressionVisitor().Visit(query.Expression, nhQueryable) произошла подмена заглушки RemoteQueryable на NhQueryable.
Сами по себе такие телодвижения не разумны, если код, собирающий такое выражение, динамически находится в одном процессе с NHibernate, далее мы рассмотрим, как вынести код генерирующий linq запрос с фейковым источником во внешний процесс.
4. Linq запрос к БД через NHibernate из внешнего процесса
Процесс, из которого приходят запросы, условно назовем «клиент», а процесс, который запросы исполняет — «сервер»
Клиентская часть
Для полноценной реализации linq необходимо определить клиентский провайдер запросов, который будет предоставлять фейковая заглушка. Но прежде определим интерфейс, через который можно обращаться к серверному процессу:
public interface IChannelProvider
{
T SendRequest<T>(string request);
}
Ответственность за процесс сериализации результатов запроса (объектов или коллекции объектов) возложим на транспортный уровень, а точнее на реализацию IChannelProvider. Далее необходимо доработать конструктор RemoteQueryable и класс RemoteRepository, чтобы в эти классы можно было передать провайдер данных серверного процесса (IChannelProvider)
public RemoteQueryable(IChannelProvider channelProvider)
{
Expression = Expression.Constant(this);
Provider = new RemoteQueryableProvider<T>(channelProvider);
}
public static IQueryable<TResult> CreateQuery<TResult>(IChannelProvider provider)
{
return new RemoteQueryable<TResult>(provider);
}
Для упрощения процесса передачи запроса будем отдавать его транспортному уровню в виде строки. Далее определим клиентский провайдер запросов (RemoteQueryableProvider) с интерфейсом IQueryProvider, а также DTO класс (QueryDto) для передачи запроса на сервер:
public class RemoteQueryProvider : IQueryProvider
{
public IQueryable CreateQuery(Expression expression)
{
var enumerableQuery = new EnumerableQuery<T>(expression);
var resultQueryable = ((IQueryProvider)enumerableQuery).CreateQuery(expression);
return new RemoteQueryable<T>(this, resultQueryable.Expression);
}
public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
{
var enumerableQuery = new EnumerableQuery<TElement>(expression);
var resultQueryable = ((IQueryProvider)enumerableQuery).CreateQuery<TElement>(expression);
return new RemoteQueryable<TElement>(this, resultQueryable.Expression);
}
public object Execute(Expression expression)
{
var serializedQuery = SerializeQuery(expression);
return channelProvider.SendRequest<object>(serializedQuery);
}
public TResult Execute<TResult>(Expression expression)
{
var serializedQuery = SerializeQuery(expression);
return this.channelProvider.SendRequest<TResult>(serializedQuery);
}
public RemoteQueryableProvider(IChannelProvider channelProvider)
{
this.channelProvider = channelProvider;
}
private static string SerializeQuery(Expression expression)
{
var newQueryDto = QueryDto.CreateMessage(expression, typeof(T));
var serializedQuery = JsonConvert.SerializeObject(newQueryDto, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.All
});
return serializedQuery;
}
}
public class QueryDto
{
public ExpressionNode SerializedExpression { get; set; }
public string RequestedTypeName { get; set; }
public string RequestedTypeAssemblyName { get; set; }
public static QueryDtoCreateMessage(Expression expression, Type type)
{
var serializedExpression = expression.ToExpressionNode();
return new QueryDto(serializedExpression, type.FullName, type.Assembly.FullName);
}
private QueryDto(ExpressionNode serializedExpression, string requestedTypeName, string requestedTypeAssemblyName)
{
this.SerializedExpression = serializedExpression;
this.RequestedTypeName = requestedTypeName;
this.RequestedTypeAssemblyName = requestedTypeAssemblyName;
}
protected QueryDto() { }
}
Фабричный метод QueryDto.CreateMessage() сериализует полученный expression с помощью библиотеки Serialize.Linq. Класс QueryDto также содержит свойства RequestedTypeName и RequestedTypeAssemblyName, для идентификации типа запрашиваемой сущности и правильного восстановления expression на сервере. Сам же объект класса QueryDto сериализуется в строку с помощью библиотеки Json.NET. Сериализованный запрос передается на сервер через прокси-объект IChannelProvider.
Дьявол в деталях
1. Обработка замыканий в expression.
В разделе Linq в Nhibernate я неслучайно упомянул о процессе вычисления замыканий в выражениях. Поскольку, формирование запроса теперь находится во внешнем процессе, перед отправкой запроса необходимо вычислить значения замыканий в построенном expression. Нас интересуют (пока) только замыкания в предикатах например
RemoteRepository.Query<WorkItem>()
.Where(w => w.Priority == EnvironmentSettings.MaxPriority)) // ссылка на EnvironmentSettings
Для вычисления значения ссылок и конвертирования значений в ConstantExpression напишем клиентский ExpressionVisitor, который будет модифицировать выражение
internal class ClientExpressionVisitor : ExpressionVisitor
{
public Expression Evaluate(Expression expression)
{
return base.Visit(expression);
}
private Expression EvaluateIfNeed(Expression expression)
{
var memberExpression = expression as MemberExpression;
if (memberExpression != null)
{
if (memberExpression.Expression is ParameterExpression)
return expression;
var rightValue = GetValue(memberExpression);
return Expression.Constant(rightValue);
}
var methodCallExpression = expression as MethodCallExpression;
if (methodCallExpression != null)
{
var obj = ((ConstantExpression)methodCallExpression.Object).Value;
var result = methodCallExpression.Method.Invoke(obj,
methodCallExpression.Arguments.Select(ResolveArgument).ToArray());
return Expression.Constant(result);
}
return expression;
}
protected override Expression VisitBinary(BinaryExpression b)
{
Expression left = this.EvaluateIfNeed(this.Visit(b.Left));
Expression right = this.EvaluateIfNeed(this.Visit(b.Right));
Expression conversion = this.Visit(b.Conversion);
if (left != b.Left || right != b.Right || conversion != b.Conversion)
{
if (b.NodeType == ExpressionType.Coalesce && b.Conversion != null)
return Expression.Coalesce(left, right, conversion as LambdaExpression);
else
return Expression.MakeBinary(b.NodeType, left, right, b.IsLiftedToNull, b.Method);
}
return b;
}
private static object ResolveArgument(Expression exp)
{
var constantExp = exp as ConstantExpression;
if (constantExp != null)
return constantExp.Value;
var memberExp = exp as MemberExpression;
if (memberExp != null)
return GetValue(memberExp);
return null;
}
private static object GetValue(MemberExpression exp)
{
var constantExpression = exp.Expression as ConstantExpression;
if (constantExpression != null)
{
var member = constantExpression.Value
.GetType()
.GetMember(exp.Member.Name)
.First();
var fieldInfo = member as FieldInfo;
if (fieldInfo != null)
return fieldInfo.GetValue(constantExpression.Value);
var propertyInfo = member as PropertyInfo;
if (propertyInfo != null)
return propertyInfo.GetValue(constantExpression.Value);
}
var expression = exp.Expression as MemberExpression;
if (expression != null)
return GetValue(expression);
return null;
}
}
и доработаем методы Execute класса RemoteQueryableProvider с учетом функциональности ClientExpressionVisitor
public object Execute(Expression expression)
{
var partialEvaluatedExpression = this.expressionEvaluator.Evaluate(expression);
var serializedQuery = SerializeQuery(partialEvaluatedExpression);
return channelProvider.SendRequest<object>(serializedQuery);
}
public TResult Execute<TResult>(Expression expression)
{
var partialEvaluatedExpression = this.expressionEvaluator.Evaluate(expression);
var serializedQuery = SerializeQuery(partialEvaluatedExpression);
return this.channelProvider.SendRequest<TResult>(serializedQuery);
}
2. Использование в запросах свойств сущности, не указанных в маппинге.
Если в NHibernate написать условие вида Where(x => x.UnmappedProperty == 4)), то валидатор запроса NHibernate не пропустит такое выражение. Для решения этой проблемы введем API пост запросов, т.е. запросов к тем данным, которые уже были получены в результате sql-выборки.
internal class PostQueryable<T> : BaseQueryable<T>
{
public PostQueryable(IChannelProvider channelProvider) : base(channelProvider) { }
public PostQueryable(AbstractQueryProvider provider, Expression expression) : base(provider, expression) { }
public PostQueryable() { Expression = Expression.Constant(this); }
}
и расширение для удобного оборачивания запроса
public static class Ex
{
public static IQueryable<T> PostQuery<T>(this IQueryable<T> sourceQuery)
{
var query = Expression
.Call(null, typeof (PostQueryable<T>).GetMethod(nameof(PostQueryable<T>.WrapQuery)), new [] {sourceQuery.Expression});
return sourceQuery.Provider.CreateQuery<T>(query);
}
}
Теперь можно написать запроса вида:
int stateCoefficient = 0.9;
int ageLimitInCurrentState = 18 * stateCoefficient;
var availableMovies = session
.Query<Movie>()
.Where(m => m.AgeLimit >= ageLimitInCurrentState)
.PostQuery()
.Where(m => m.RatingInCurrentState > 8) // unmapped-свойство RatingInCurrentState
.ToList()
и отправить его на сервер.
Серверная часть
И серверный и клиентский код должен находиться в одной сборке, но в коде имеются ссылки на типы из сборки NHibernate. Необходимо отвязать лишнюю зависимость для клиента. Напишем хелпер для работы с типами Nhibernate, чтобы убрать жесткую ссылку на сборку Nhibernate.dll. Сама же сборка NHibernate.dll на серверной части будет загружаться через Reflection.
internal static class NHibernateTypesHelper
{
private static readonly Assembly nhibernateAssembly;
public static Type SessionType { get; private set; }
public static Type LinqExtensionType { get; private set; }
public static bool IsSessionObject(object inspectedObject)
{
return SessionType.IsInstanceOfType(inspectedObject);
}
static NHibernateTypesHelper()
{
nhibernateAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName.Contains("NHibernate")) ?? Assembly.Load("NHibernate");
if (nhibernateAssembly == null)
throw new InvalidOperationException("Caller invoking server-side types, but the NHibernate.dll not found in current application domain");
SessionType = nhibernateAssembly.GetTypes()
.Single(p => p.FullName.Equals("NHibernate.ISession", StringComparison.OrdinalIgnoreCase));
LinqExtensionType = nhibernateAssembly.GetTypes()
Single(p => p.FullName.Equals("NHibernate.Linq.LinqExtensionMethods", StringComparison.OrdinalIgnoreCase));
}
}
На серверной стороне определяем класс RemoteQueryExecutor, который будет принимать сериализованный QueryDto, восстанавливать его и исполнять:
public static class RemoteQueryExecutor
{
public static object Do(string serializedQueryDto, object sessionObject)
{
var internalRemoteQuery = DeserializeQueryDto(serializedQueryDto);
var deserializedQuery = DeserializedQueryExpressionAndValidate(internalRemoteQuery);
var targetType = ResolveType(internalRemoteQuery);
return Execute(deserializedQuery, targetType, sessionObject);
}
private static TypeInfo ResolveType(QueryDto internalRemoteQuery)
{
var targetAssemblyName = internalRemoteQuery.RequestedTypeAssemblyName;
var targetAssembly = GetAssemblyOrThrownEx(internalRemoteQuery, targetAssemblyName);
var targetType = GetTypeFromAssemblyOrThrownEx(targetAssembly, internalRemoteQuery.RequestedTypeName,
targetAssemblyName);
return targetType;
}
private static Expression DeserializedQueryExpression(QueryDto internalRemoteQuery)
{
var deserializedQuery = internalRemoteQuery.SerializedExpression.ToExpression();
return deserializedQuery;
}
private static TypeInfo GetTypeFromAssemblyOrThrownEx(Assembly targetAssembly, string requestedTypeName, string targetAssemblyName)
{
var targetType = targetAssembly.DefinedTypes
.FirstOrDefault(type => type.FullName.Equals(requestedTypeName, StringComparison.OrdinalIgnoreCase));
if (targetType == null)
throw new InvalidOperationException(string.Format("Type with name '{0}' not found in assembly '{1}'", requestedTypeName, targetAssemblyName));
return targetType;
}
private static Assembly GetAssemblyOrThrownEx(QueryDto internalRemoteQuery, string targetAssemblyName)
{
var targetAssembly = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(asm => asm.FullName.Equals(internalRemoteQuery.RequestedTypeAssemblyName, StringComparison.OrdinalIgnoreCase));
if (targetAssembly == null)
throw new InvalidOperationException(string.Format("Assembly with name '{0}' not found in server app domain", targetAssemblyName));
return targetAssembly;
}
private static QueryDto DeserializeQueryDto(string serializedQueryDto)
{
var internalRemoteQuery = JsonConvert
.DeserializeObject<QueryDto>(serializedQueryDto, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
TypeNameHandling = TypeNameHandling.All
});
return internalRemoteQuery;
}
private static object Execute(Expression expression, Type targetType, object sessionObject)
{
var queryable = GetNhQueryableFromSession(targetType, sessionObject);
var nhibernatePartialExpression = ExpressionModifier.GetNhibernatePartialExpression(expression, queryable);
var resultFromStorage = queryable.Provider.Execute(nhibernatePartialExpression);
var requestedCollection = resultFromStorage as IEnumerable<object>;
if (requestedCollection == null)
return resultFromStorage;
var resultCollectionType = requestedCollection.GetType();
if (resultCollectionType.IsGenericType)
targetType = resultCollectionType.GetGenericArguments().Single();
var enumerableQueryable = (IQueryable)Activator
.CreateInstance(typeof(EnumerableQuery<>).MakeGenericType(targetType), new[] { requestedCollection });
var postQueryPartialExpression = ExpressionModifier
.GetPostQueryPartialExpression(expression, enumerableQueryable);
if (postQueryPartialExpression == null)
return resultFromStorage;
return enumerableQueryable.Provider.Execute(postQueryPartialExpression);
}
private static IQueryable GetNhQueryableFromSession(Type targetType, object sessionObject)
{
var finalQueryMethod = ResolveQueryMethod(targetType);
var queryable = (IQueryable) finalQueryMethod.Invoke(null, new object[] {sessionObject});
return queryable;
}
private static MethodInfo ResolveQueryMethod(Type targetType)
{
var queryMethod = typeof(LinqExtensionMethods).GetMethods(BindingFlags.Public | BindingFlags.Static)
.Where(m => m.IsGenericMethod)
.Where(m => m.Name.Equals("Query"))
.Single(m => m.GetParameters().Length == 1 && NHibernateTypesHelper.SessionType.IsAssignableFrom(m.GetParameters().First().ParameterType));
var finalQueryMethod = queryMethod.MakeGenericMethod(targetType);
return finalQueryMethod;
}
}
Получив запрос, на сервере восстанавливаем QueryDto десериализатором Json.NET. Сериализованный expression внутри DTO-объекта восстанавливаем с помощью библиотеки Serialize.Linq. Затем модифицируем expression с помощью визитора NhibernateExpressionVisitor — подменяем фейковый корень на NhQueryable, как объяснялось выше.
Полученный expression делим на два запроса:
- Непосредственно запрос к БД
- Запрос к результатам выборки из БД
Запрос к БД отработает по уже известной схеме, без компиляции. Запрос к результатам выборки компилируется и обрабатывает объекты указанными в выражениях методами. Запрос к результатам предыдущего запроса компилируется и выполняется как обычный EnumerableQuery.
5. Пишем тестовое приложение
Реализуем тестовое клиент-серверное приложение. Для клиентской части используем технологию WPF, на серверной стороне в качестве БД будем использовать SQLite, для коммуникации между процессами будем использовать WCF с HTTP-привязками. В качестве объектной модели используем класс WorkItem
[DataContract]
public class WorkItem : BaseEntity
{
[DataMember]
public virtual string Text { get; set; }
[DataMember]
public virtual int Priority { get; set; }
}
За кадром оставим настройку маппинга NHibernate и конфигурацию nhibernate.cfg.xml, а также настройку WCF для передачи данных и установим цель — отображать 200 объектов WorkItem в ListView, подгружая по мере необходимости данные из БД.
WPF для списков предоставляет механизм виртуализации данных на слое UI, который можно доработать и для виртуализации на уровне ViewModel-коллекции данных. За основу возьмем пример из этой статьи и модифицируем пример для пейджинг-загрузки данных из БД на клиент.
Реализуем свой IItemsProvider и заменим реализацию из примера на наш класс DemoWorkItemProvider. В методах FetchCount() и FetchRange() будем использовать Linq-запросы с помощью RemoteQueryable API. В методе FetchRange мы указываем запрос только того диапазона данных, который потребуется для отображения.
public class DemoWorkItemProvider : IItemsProvider<WorkItem>
{
public int FetchCount()
{
return RemoteRepository.CreateQuery<WorkItem>(new DemoChannelProvider())
.Count();
}
public IList<WorkItem> FetchRange(int startIndex, int count)
{
return RemoteRepository.CreateQuery<WorkItem>(new DemoChannelProvider())
.Skip(startIndex)
.Take(count)
.ToList();
}
}
Немного правим UI и стиль ListView для отображения WorkItem. Запускаем сервер и клиент, при нажатии на кнопку Refresh клиент отправляет на сервер Linq-запрос на количество элементов и два последующих запроса на получение первой и второй страницы списка. При прокрутке списка вниз следующая страница подгружается из БД через цепочку
RemoteQueryablyProvider -> WCF -> HTTP -> WCF -> RemoteQueryExecutor -> NHibernate -> SQLite
Плюсы RemoteQueryable API:
- 1. Чистый и понятный код в методах FetchRange и FetchCount
- 2. Возможность строить динамические запросы на клиенте с помощью Dynamic.Linq (например для фильтрации данных в таблицах)
- 3. Один и единственный метод WCF сервиса для обработки запроса.
- 4. Оптимизация получения данных — из БД будут выбраны только те записи, которые действительно были запрошены клиентом.
Заключение
На этом, пожалуй, можно остановиться. Следующим шагом может стать модификация запроса на серверной стороне с учетом авторизации пользователя или оптимизация кода, работающего с Reflection, либо подключение Dynamic.Linq, но это уже тема отдельной статьи.
Ссылки
1. Репозитории с кодом и примерами из статьи на GitHub
2. IEnumerable и IQueryable, в чем разница?
3. Принципы работы IQueryable и LINQ-провайдеров данных
4. Замыкания в языке программирования C#
5. Исходники Serialize.Linq
6. Исходники NHibernate
7. Remotion.Linq на codeplex
8. IQueryProvider на MSDN
9. How to: Implement an Expression Tree Visitor
Автор: Hydro