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
    2. Linq в NHibernate
    3. Linq-запросы без объекта ISession
    4. Linq запрос к БД через NHibernate из внешнего процесса
    5. Пишем тестовое приложение
    Заключение
    Ссылки

    1. IEnumerable vs IQueryable


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


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


    Как происходит исполнение 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.
    Преобразование Linq в Sql


    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 выглядит следующим образом
    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-объект.
    Определим его:
    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, мы получим следующее дерево выражений:

    Выше я писал, что дерево выражений можно отредактировать в runtime, следовательно мы можем обойти полученное дерево и заменить MockDataSource на NhQueryable. NhQueryable можно получить, вызвав session.Query().
    Для того, чтобы выполнить обход дерева используем паттерн Visitor. Чтобы не писать визитор дерева выражений «с нуля», воспользуемся этой базовой реализацией. В наследнике переопределим методы 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.

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


    Плюсы 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
    Поделиться публикацией

    Комментарии 14

      0
      Полезная тема, особенно с реализацией.

      Вопрос — к хиберу куча дополнительных библиотек разрабатывается как самой командой хибера так и просто сторонними разработчиками. Не нашли готового решения?
        0
        К сожалению не нашел.
        Был проект на WCF, в котором имелся список методов сервиса с запросом в БД аля:
        IEnumerable<Message> GetMessagesOnLaskWeek();
        IEnumerable<Message> GetMessagesOnCurrentDay();
        IEnumerable<User> GetActiveUsers();
        

        Решил избавиться от такого бардака, разработав свой велосипед, позволяющий писать эффективные запросы в сочетании с удобством разработки.
        0
        В момент вызова метода GetEnumerator() построенное дерево выражений будет не скомпилировано, а транслировано в sql-запрос.

        При каждом запросе разбор происходит заново? Или есть некоторый кэш?
          +1
          Есть некий кэш. Если углубляться в NHibernate, то Linq сначала преобразуется в Hql запрос, и только после этого преобразуется в SQL. Когда ковырял исходники, видел кэш Hql-запросов.
          0
          Извините, но чем не подошёл WCF Data Services, реализация OData для NHibernate? Я вижу как преимущества хорошо описанный протокол, контроль над тем, какие запросы допускается использовать клиенту, возможность простого вызова через любой http client, нет протекающих абстракций вида PostQuery.
            +1
            Не подошел тем, что:
            1) я не всегда использую http, бывает нужен и tcp.
            2) WCF Data Services — это REST, а WCF Services — это SOAP. WCF Data Services — на мой взгляд, достаточно нишевая штука.
            На счет PostQuery согласен — эта фигня мне тоже не особо нравиться, возможно в будущем доделаю код для динамического определения незамапленных свойств объекта на сервере.

            PS: забыл добавить, что я не занимаюсь вебом, мой фронтенд — desktop и mobile.
            0
            > Однако, если возникнет потребность в формировании запроса во внешнем процессе, например, в клиентской части сервиса, то в таком случае Reflection уже не спасет, клиентская часть, как правило, не знает (и не должна ничего знать) про серверный ORM.

            ORM — это очень непростая штука, и она будет течь. Как минимум, далеко не каждый Expression сможет выполниться в NH. И не каждый ORM вообще поддерживает Linq.
            Что бы абстракция работала — она должна быть максимально простой, чего тут нет.

            Если вы просто выставляете БД наружу — зачем вам серверная часть? NH и mobile не дружат?
              0
              ORM — это очень непростая штука, и она будет течь.

              Не совсем понимаю, к чему это написано.
              Как минимум, далеко не каждый Expression сможет выполниться в NH.

              Мне и не нужен любой Expression, мне нужны expressions, которые наворачивают Linq-методы. В NH поддерживается большая часть методов linq.
              И не каждый ORM вообще поддерживает Linq. Что бы абстракция работала — она должна быть максимально простой, чего тут нет

              Я писал статью под названием linq2nhibernate, а не linq2AnyORM, когда встанет вопрос абстрагирования от конкретной ORM, тогда буду делать абстракцию.Пока что, over 9999 сценариев в моей работе закрывает NHibernate.
              Если вы просто выставляете БД наружу — зачем вам серверная часть? NH и mobile не дружат?

              1. Я хочу иметь удобный API, с помощью которого смогу писать селекты на клиенте, а выполнять их на сервере, чтобы не плодить кучу методов в WCF-сервисе
              2 Я стараюсь придерживаться парадигмы CQRS, в которой Linq-запрос является (Q)uery, он не модифицирует БД, клиент вообще ничего не знает про БД, он знает, что есть удаленный репозиторий, из которого можно получать сущности.
              3 Я писал в заключении — что можно докрутить expression с учетом своей системы авторизации, которая бы делала запрос с учетом разрешений пользовательского контекста.
                0
                > В NH поддерживается большая часть методов linq.

                То есть у вас на клиенте «LINQ to NH» by desing? То есть у вас клиент знает про ОРМ и БД (жирным, 2 раза).
                Тогда к чему написано процитированное мной «клиентская часть, как правило, не знает (и не должна ничего знать) про серверный ORM»?

                > 1. Я хочу иметь удобный API, с помощью которого смогу писать селекты на клиенте, а выполнять их на сервере, чтобы не плодить кучу методов в WCF-сервисе

                Не понял зачем вам WCF, и «выполнять» что-то на сервере (там же невозможно ничего выполнить, кроме оверхеда на ORM/WCF). Перенесите NH на клиент и сделайте для запросов обычную двухзвенку, будет удобный API и никаких методов для (Q)uery в WCF.
                На вопрос «зачем вам серверная часть» ответ «хочу выполнять на сервере» несколько странный.

                2, 3) В любой приличной БД авторизация для запросов уже есть. Хотя признаю, у неё, конечно, есть фатальный недостаток, как и у WCF с WCF DS.

                Но применению CQRS и авторизации двухзвенка не противоречит, а весь код выше можно выкинуть.

                Я просто не понимаю проблемы. Я могу представить, что хочется что-то выполнять на сервере(вычисления по сырым/секретным/сложным данным), но тогда это будут точно не linq запросы с клиента.

                А без авторизации это просто бэкдор.
                  0
                  То есть у вас на клиенте «LINQ to NH» by desing? То есть у вас клиент знает про ОРМ и БД (жирным, 2 раза).

                  Вы выдергиваете слова из контекста, сначала написали про непереваривания NH любого expression, я вам ответил, что то, что нужно, он переварит. А про то, что он не может переварить знает валидатор клиентских expression'ов.
                  На вопрос «зачем вам серверная часть» ответ «хочу выполнять на сервере» несколько странный

                  это вопрос по области применения, опять-таки возвращаюсь к своему примеру, в проекте, который меня побудил сделать это, часть бизнес логики, критичной к безопасности и масштабированию, выполнялась на сервере. Разумеется, в простом случае, описанном вами, нет никакого смысла применять такое решение. WCF используем, как стандартный транспорт.
                  А без авторизации это просто бэкдор.

                  Авторизация запроса — тема, выходящая за рамки данной статьи (которая и так получилась тяжеловатой для восприятия)
                    0
                    > А про то, что он не может переварить знает валидатор клиентских expression'ов.

                    То есть у вас есть валидатор, заточенный на NH.

                    > в проекте, который меня побудил сделать это, часть бизнес логики, критичной к безопасности и масштабированию, выполнялась на сервере.

                    Как Query? То есть у вас 2 механизма выполнения запросов (которые не команды)?
              0
              А как быть например с полнотекстовым поиском?
                0
                Ну, если вы делаете Contains по текстовому свойству например, то для него придётся написать Visitor, если я ничего не путаю.
                Собсна, пишите код на клиенте, смотрите что упало, чините =)

                ПС: нормальный полнотекстовый поиск всё равно надо делать иначе, с индексами, с кучей заморочек. Имхо, не тема статьи.
                0
                Профит тонет в абстракциях и сериализациях. Я хочу сказать, что выглядит это круто, особенно с Mock запросом, но на практике это только увеличит сроки разработки и ничем не поможет поддержке. Учитывая подход «Я использую 100% только NHibernate и вряд ли уйду на что-то другое» — смысла городить такую абстракцию не вижу. Но решение крутое. Жаль, что решение ради решения, которое ничего в общем-то не решает.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое