- PVSM.RU - https://www.pvsm.ru -
Практически любой .NET разработчик так или иначе использует в своей практике технологию Linq. Linq позволяет писать красивый и лаконичный код для получения объектов из источника данных с возможностью определения критериев получения и/или трансформации запрошенных объектов «на лету». Поддержка Linq присутствует практически во всех популярных ORM-фреймворках, в том числе и в NHibernate. NHibernate предоставляет Linq-провайдер, с помощью которого мы можем написать запрос на этапе разработки (Design-Time), но для того, чтобы составить запрос в runtime, придется повозиться с Reflection. Однако, если возникнет потребность в формировании запроса во внешнем процессе, например, в клиентской части сервиса, то в таком случае Reflection уже не спасет, клиентская часть, как правило, не знает (и не должна ничего знать) про серверный ORM.
Ниже мы разберем как создать API для написания Linq запросов к NHibernate в ситуации, когда запрос пишется в одном процессе, а выполняется в другом. Также, реализуем собственный IQueryProvider, который будет транслировать запросы из приложения-источника в исполняющее приложение.
1. IEnumerable vs IQueryable [1]
2. Linq в NHibernate [2]
3. Linq-запросы без объекта ISession [3]
4. Linq запрос к БД через NHibernate из внешнего процесса [4]
5. Пишем тестовое приложение [5]
Заключение [6]
Ссылки [7]
Для начала, следует вкратце вспомнить об интерфейсах IEnumerable и IQueryable. Про них писали здесь [8] и здесь [9]. А также полезно почитать про деревья выражений (expression tree) [10]и как они работают [11].
Как происходит исполнение IEnumerable запроса:
1. Источник данных представляется как IEnumerable (перечислимый), в случае с коллекциями это необязательное действие.
2. Перечислимый источник данных оборачивается (декорируется) итератором WhereListIterator
3. Первый итератор WhereListIterator декорируется следующим итератором WhereListIterator
4. В конструктор List, передается WhereListIterator «верхнего уровня». В грубом приближении, можно сказать, что заполнение внутреннего контейнера List происходит через обход полученного WhereListIterator циклом foreach [12]. При запросе следующего элемента, декоратор «верхнего уровня» вызывает всю цепочку декораторов, каждый элемент которой, определяет какой элемент можно вытолкнуть наверх, а какой должен быть пропущен
// 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
Преимущество деревьев выражений заключается в том, что это по своей сути высокоуровневый API для генерации IL-кода, который мы можем построить/отредактировать в runtime, скомпилировать в делегат и вызвать.
Типичный сценарий работы с 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 создаст замыкание [13] на локальную переменную ageLimit. При разборе этого лямбда-выражения, дерево выражений вида m.AgeLimit >= ageLimitInCurrentState будет вычислено в выражение m.AgeLimit >= 16 и уже в таком виде выражение будет отдано транслятору Linq2Sql.
Поподробнее рассмотрим интерфейсы IQueryable [14]и IQueryProvider [15].
В интерфейсе IQueryable есть свойство Expression, которое предоставляет текущее выражение-декоратор (декоратор источника данных, либо декоратор другого выражения), а также свойство Provider с типом IQueryProvider, через которое можно получить текущий провайдер запроса.
Интерфейс IQueryProvider предоставляет методы CreateQuery(Expression expression) — для декорирования выражения нижнего уровня и Execute(Expression expression) для выполнения запроса к источнику данных.
Все методы Linq можно разделить на две группы:
Вызов метода 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.
Отвязывание запроса от сессии предполагает избавление от вызова 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. Чтобы не писать визитор дерева выражений «с нуля», воспользуемся этой базовой реализацией [16]. В наследнике переопределим методы 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 запрос с фейковым источником во внешний процесс.
Процесс, из которого приходят запросы, условно назовем «клиент», а процесс, который запросы исполняет — «сервер»
Для полноценной реализации 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 [17]. Класс QueryDto также содержит свойства RequestedTypeName и RequestedTypeAssemblyName, для идентификации типа запрашиваемой сущности и правильного восстановления expression на сервере. Сам же объект класса QueryDto сериализуется в строку с помощью библиотеки Json.NET [18]. Сериализованный запрос передается на сервер через прокси-объект IChannelProvider.
1. Обработка замыканий в expression.
В разделе Linq в Nhibernate [2] я неслучайно упомянул о процессе вычисления замыканий в выражениях. Поскольку, формирование запроса теперь находится во внешнем процессе, перед отправкой запроса необходимо вычислить значения замыканий в построенном 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 [17]. Затем модифицируем expression с помощью визитора NhibernateExpressionVisitor — подменяем фейковый корень на NhQueryable, как объяснялось выше.
Полученный expression делим на два запроса:
Запрос к БД отработает по уже известной схеме, без компиляции. Запрос к результатам выборки компилируется и обрабатывает объекты указанными в выражениях методами. Запрос к результатам предыдущего запроса компилируется и выполняется как обычный EnumerableQuery.
Реализуем тестовое клиент-серверное приложение. Для клиентской части используем технологию 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-коллекции данных. За основу возьмем пример из этой статьи [19] и модифицируем пример для пейджинг-загрузки данных из БД на клиент.
Реализуем свой 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:
На этом, пожалуй, можно остановиться. Следующим шагом может стать модификация запроса на серверной стороне с учетом авторизации пользователя или оптимизация кода, работающего с Reflection, либо подключение Dynamic.Linq, но это уже тема отдельной статьи.
1. Репозитории с кодом и примерами из статьи на GitHub [20]
2. IEnumerable и IQueryable, в чем разница?
3. Принципы работы IQueryable и LINQ-провайдеров данных [21]
4. Замыкания в языке программирования C# [13]
5. Исходники Serialize.Linq [17]
6. Исходники NHibernate [22]
7. Remotion.Linq на codeplex [23]
8. IQueryProvider на MSDN [15]
9. How to: Implement an Expression Tree Visitor [16]
Автор: Hydro
Источник [24]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/129150
Ссылки в тексте:
[1] 1. IEnumerable vs IQueryable: #IEnumerableVsIQueryable
[2] 2. Linq в NHibernate: #Linq2NHibernate
[3] 3. Linq-запросы без объекта ISession: #LinqWithoutSession
[4] 4. Linq запрос к БД через NHibernate из внешнего процесса: #LinqFromExternalProcess
[5] 5. Пишем тестовое приложение: #HowItUse
[6] Заключение: #Resume
[7] Ссылки: #Links
[8] здесь : https://habrahabr.ru/post/108407/
[9] здесь: https://habrahabr.ru/post/256821/
[10] деревья выражений (expression tree) : https://msdn.microsoft.com/en-us/library/mt654263.aspx
[11] работают: https://habrahabr.ru/post/83169/
[12] foreach: https://msdn.microsoft.com/ru-ru/library/aa288257(v=vs.71).aspx
[13] замыкание: http://sergeyteplyakov.blogspot.ru/2010/04/c.html
[14] IQueryable : https://msdn.microsoft.com/ru-ru/library/system.linq.iqueryable(v=vs.110).aspx
[15] IQueryProvider: https://msdn.microsoft.com/ru-ru/library/system.linq.iqueryprovider(v=vs.110).aspx
[16] этой базовой реализацией: https://msdn.microsoft.com/en-us/library/bb882521(v=vs.90).aspx
[17] Serialize.Linq: https://github.com/esskar/Serialize.Linq
[18] Json.NET: http://www.newtonsoft.com/json
[19] этой статьи: https://habrahabr.ru/post/208792/
[20] Репозитории с кодом и примерами из статьи на GitHub : https://github.com/MaximVanyushkin/Sharp.RemoteQueryable
[21] Принципы работы IQueryable и LINQ-провайдеров данных: https://habrahabr.ru/post/256821
[22] Исходники NHibernate: https://github.com/nhibernate/nhibernate-core
[23] Remotion.Linq на codeplex: https://relinq.codeplex.com/
[24] Источник: https://habrahabr.ru/post/302594/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.