- PVSM.RU - https://www.pvsm.ru -

Client-side Linq to NHibernate

Практически любой .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]

1. IEnumerable vs IQueryable

Для начала, следует вкратце вспомнить об интерфейсах IEnumerable и IQueryable. Про них писали здесь [8] и здесь [9]. А также полезно почитать про деревья выражений (expression tree) [10]и как они работают [11].

IEnumerable

Client-side Linq to NHibernate - 1

Как происходит исполнение 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

Client-side Linq to NHibernate - 2

Как происходит исполнение 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 создаст замыкание [13] на локальную переменную ageLimit. При разборе этого лямбда-выражения, дерево выражений вида m.AgeLimit >= ageLimitInCurrentState будет вычислено в выражение m.AgeLimit >= 16 и уже в таком виде выражение будет отдано транслятору Linq2Sql.

Преобразование Linq в Sql

Client-side Linq to NHibernate - 3

IQueryable и IQueryProvider


Поподробнее рассмотрим интерфейсы IQueryable [14]и IQueryProvider [15].
В интерфейсе 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 выглядит следующим образом

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.
Client-side Linq to NHibernate - 4

3. Linq-запросы без объекта ISession

Отвязывание запроса от сессии предполагает избавление от вызова Session.Query() и NhQueryable в корне дерева выражений соответственно. Для того, чтобы было возможно использовать linq, необходим источник-заглушка, возвращающий IQueryable-объект.
Определим его:

RemoteQueryable

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);

Для удобства использования обернем создание запроса в класс-репозиторий:

RemoteRepository

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, мы получим следующее дерево выражений:
Client-side Linq to NHibernate - 5
Выше я писал, что дерево выражений можно отредактировать в runtime, следовательно мы можем обойти полученное дерево и заменить MockDataSource на NhQueryable. NhQueryable можно получить, вызвав session.Query().
Для того, чтобы выполнить обход дерева используем паттерн Visitor. Чтобы не писать визитор дерева выражений «с нуля», воспользуемся этой базовой реализацией [16]. В наследнике переопределим методы VisitConstant и VisitMethodCall, а также переопределим метод Visit, который будет являться точкой доступа редактирования выражения:

NhibernateExpressionVisitor

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.
Client-side Linq to NHibernate - 6
Сами по себе такие телодвижения не разумны, если код, собирающий такое выражение, динамически находится в одном процессе с 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) для передачи запроса на сервер:

RemoteQueryProvider

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;
  }
}

QueryDto

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, который будет модифицировать выражение

ClientExpressionVisitor

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

Методы RemoteQueryableProvider

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-выборки.

PostQueryable

  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); }
  }

и расширение для удобного оборачивания запроса

Ex

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.

NHibernateTypesHelper

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, восстанавливать его и исполнять:

RemoteQueryExecutor

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.

Рисунки запросов

Client-side Linq to NHibernate - 7

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-коллекции данных. За основу возьмем пример из этой статьи [19] и модифицируем пример для пейджинг-загрузки данных из БД на клиент.
Реализуем свой IItemsProvider и заменим реализацию из примера на наш класс DemoWorkItemProvider. В методах FetchCount() и FetchRange() будем использовать Linq-запросы с помощью RemoteQueryable API. В методе FetchRange мы указываем запрос только того диапазона данных, который потребуется для отображения.

DemoWorkItemProvider

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

Демонстрация работы

Client-side Linq to NHibernate - 8

Плюсы RemoteQueryable API:

  • 1. Чистый и понятный код в методах FetchRange и FetchCount
  • 2. Возможность строить динамические запросы на клиенте с помощью Dynamic.Linq (например для фильтрации данных в таблицах)
  • 3. Один и единственный метод WCF сервиса для обработки запроса.
  • 4. Оптимизация получения данных — из БД будут выбраны только те записи, которые действительно были запрошены клиентом.

Заключение

На этом, пожалуй, можно остановиться. Следующим шагом может стать модификация запроса на серверной стороне с учетом авторизации пользователя или оптимизация кода, работающего с 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