Вычисляемые поля для любого LINQ-провайдера

    Привет, Хабр!

    Сегодня я хочу рассказать, о маленькой библиотеке, которую я написал недавно на коленке всего за несколько часов. Эта библиотека может декомпилировать методы в их λ-представление.

    Зачем это может понадобиться — под катом.

    Intro


    В жизни случается, что в LINQ нужно использовать вычисляемое поле, к примеру у нас есть класс Employee с вычисляемым полем FullName

    class Employee
    {
        public string FullName
        {
            get { return FirstName + " " + LastName; }
        }
    
        public string LastName { get; set; }
    
        public string FirstName { get; set; }
    }
    

    И тут к вам приходит заказчик и говорит, что нам нужно добавить поиск по полному имени сотрудника. Вы недолго думаете берете и пишите следующий запрос:

    var employees = (from employee in db.Employees
                     where (employee.FirstName + " " + employee.LastName) == "Test User"
                     select employee).ToList();
    

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

    public class WayPoint 
    {
        // все остальное опущено в целях наглядности
        public virtual bool IsValid
        {
            get 
            {
                return (Account == null) ||
                   (Role == null || Account.Role == Role) &&
                   (StructuralUnit == null || Account.State.StructuralUnit == StructuralUnit);
            }
        }
    }
    

    С этим сложнее. Итак, приступим. Что же у нас есть для решения таких задач?

    <formula> в NHibernate


    Если вы используете NHibernate, то можете замапить данное поле как формулу, но этот путь не очень дружелюбен к рефакторингу, к тому же <formula> поддерживает только sql, и если вы пишете приложение, которое планируется использовать с разными базами данных, то здесь вам нужно быть особенно осторожными.

    Поддреживается только в NHibernate.

    Microsoft.Linq.Translations


    Для этого необходимо переписать наш класс и запрос следующим образом:

    class Employee 
    {
        private static readonly CompiledExpression<Employee,string> fullNameExpression
         = DefaultTranslationOf<Employee>.Property(e => e.FullName).Is(e => e.FirstName + " " + e.LastName);
    
        public string FullName 
        {
            get { return fullNameExpression.Evaluate(this); }
        }
    
        public string LastName { get; set; }
    
        public string FirstName { get; set; }
    }
    
    var employees = (from employee in db.Employees
                     where employee.FullName == "Test User"
                     select employee).WithTranslations().ToList()
    

    Все хорошо, запрос выглядит красиво, а вот объявление свойства — просто ужасно. К тому же Evaluate компилирует λ-выражение в момент исполнения, что, на мой взгляд не менее ужасно, чем задание вычисляемого поля.

    И, наконец, мы подошли к моему творениею — DelegateDecompiler

    DelegateDecompiler


    Все что нужно, это вычисляемое поля пометить атрибутом [Computed], а запрос преобразовать с помощью метода .Decompile()

    class Employee 
    {
        [Computed]
        public string FullName 
        {
            get { return FirstName + " " + LastName; }
        }
    
        public string LastName { get; set; }
    
        public string FirstName { get; set; }
    }
    
    var employees = (from employee in db.Employees
                     where employee.FullName == "Test User"
                     select employee).Decompile().ToList()
    

    По-моему изящно (сам не похвалишь — никто не похвалит)

    При вызове .Decompile() декомпилятор найдет все свойства и методы, помеченные атрибутом [Computed] и развернет их. Т.е. запрос будет преобразован к виду, из первоначального примера:

    var employees = (from employee in db.Employees
                     where (employee.FirstName + " " + employee.LastName) == "Test User"
                     select employee).ToList();
    

    Библиотечка в качестве декомпилятора использует Mono.Reflection (GitHub, NuGet) от Jean-Baptiste Evain — создателя Mono.Cecil. Сама Mono.Cecil не используется из-за ее громоздкости.

    PS: Естественно, то что внутри вычисляемого поля должно поддерживаться вашим LINQ-провайдером.
    PPS: Это альфа-версия очень далекая от релиза — используйте на свой страх и риск.

    Ссылки


    Исходный код на GitHub
    Пакет в NuGet
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 16
    • +1
      Хорошая идея, и сделано элегантно. Но, конечно, проще было бы не использовать вычисляемые поля в качестве поисковых. Или уже тогда заносить их в базу данных.
      • +2
        Да, проще не использовать, но иногда возникает необходимость, очень редко, но возникает. Не всегда можно занести в базу, к примеру если это не поле, а метод, принимающий какой-нибдуь параметр (такое так же поддерживается). Вообще библиотеку начал делать с прицелом на KnockoutMVC, где мне не нравились лямбды для вычисляемых полей во ViewModel, которые, если хотите использовать на сервере нужно предварительно скомпилировать. Самый первый пример

        public class HelloWorldModel
        {
          public string FirstName { get; set; }
          public string LastName { get; set; }
        
          public Expression<Func<string>> FullName() 
          {
            return () => FirstName + " " + LastName;
          }
        }
        
        После применения пример будет аналогичен тому, что в статье (нужно немного подпилисть KnockoutMVC)
      • 0
        Извините, не могу сейчас попробовать ваш код на практике, поэтому спрошу так. Насколько сложно отлавливать ошибки с таким подходом? То есть если я навешу [Compiled] на заведомо не вычисляемое на уровне БД выражение, что будет тогда — исключение в рантайме или проглотит?

        Ну и заодно, как производительность по сравнению с первым подходом?
        • +1
          Будет или нет исключение — зависит от провайдера. EF — с радостью плюется, если что-то не поддерживается.

          Производительность не проверял, но я думаю, что должно работать чуточку быстрее. Тесты на производительность запланировал на более позднюю версию, когда будет больше выражений поддерживаться.
          • 0
            Имея большой опыт работы с EF и трансформацией LINQ выражений могу сказать, что это пагубно повлияет на производительность, которая в EF и так не очень хорошая.

            Трансформация выражений и конвертация их в SQL запрос хорошо кушают процессор.

            Однако, для простых запросов это становится заметно только при нагрузке. На нашем проекте ничто не использует процессорное время так, как EF. Когда делаем нагрузочное тестирование, он оказывается основным боттлнеком. Приходится многое переводить на хранимки.

            Но если нагрузка не планируется, то могу предложить синтаксическое и архитектурное улучшение:
            можно реализовать обертки для IQueryProvider и IQueryable, позволяющие удобно подсовывать свои собственные трансформеры. Тогда клиентский код будет выглядеть как обычно, безо всяких .Decompile() и т.д., а вы сможете дать широкий простор фантазии в трансформации LINQ выражений.

            Например, у нас с помощью этого реализована система фильтров.

            PS Обещали, что в .NET 4.5 добавят кеширование сгенерированных SQL запросов и не будут повторно их генерировать при каждом вызове. Так что рекомендую перейти на 4.5.

            • 0
              Может у вас именно от того, что вы используете лишнюю абстракцию в вашем продукте и есть эти самые «боттлнеки»? Почему здесь нет QueryableWrapper и QueryProviderWrapper — именно для того, чтобы не заставлять пользователя совершать лишних телодвижений, а с этими дополнительными сущностями ему придется где-то сделать обворачивание. Тут же — где нужно, там вызвал .Decompile()

              Во-вторых в статье не идет речи про EF вообще (в комментарии выше, я привел EF как первый вспомнишийся провайдер, из тех, на которых я тестировал библиотеку).

              Произвводительность я субъективно оценил по отношению к Microsoft.Linq.Translations, т.к. там используется компиляция выражений в делегаты, что само по себе очень дорого, а здесь выражения просто конструируются, что очень дешево.

              За тем, что там обещали в EF не слежу, т.к. использую NHibernate (заодно улучшаю LINQ провайдер в нем), а там уже давно все закешировано.
              • 0
                Не, тормоза есть даже там, где обертки не используются. Дело в том, что нет других способов получить значение переменой из дерева выражений, кроме как скомпилировать выражение в делегат. Компиляция — медленно. И я подозреваю, что декомпиляция — тоже.
                • 0
                  >И я подозреваю, что декомпиляция — тоже.

                  Декомпиляция очень быстрый процесс (конечно зависит от количества инструкций). В .NET есть волшебный метод MethodBody.GetILAsByteArray который возвращает как понятно из названия IL код в виде массива байт, а Mono.Reflection просто приводит этот код к более удобному объектному виду github.com/jbevain/mono.reflection/blob/master/Mono.Reflection/MethodBodyReader.cs весь код «декомпилятора» 224 строки.

                  Дальше DelegateDecompiler в цикле интерпретирует инструкции — строит дерево выражений.
        • 0
          Немного полазил по коду, заинтересовало, а что обозначает использование @ перед именем параметра?)
          и кстати, класс Cache это же ConcurrentDictionary
          • 0
            >Cache это же ConcurrentDictionary

            Да, но не совсем. В ConcurentDictionary фабрика может быть вызвана больше 1 раза, у меня — нет. В этом главное различие.

            >что обозначает использование @ перед именем параметра?)

            Это экранирующий символ для зарезервированных слов.
          • +1
            Судя по частоте, с которой и я и другие с этим сталкиваемся — пора намекать на точно такую же фичу на уровне компилятора
            • 0
              Было бы хорошо, если б в языке не было разницы между делегатом и его лямбда представлением. Т.е., еслиб компилятор сам умел разбирать что хочет программист в каждом конкретном случае — дерево выражений или функцию.
            • 0
              Есть мнение, что дополнительной логики в сущностях быть не должно (FullName стерпеть можно, но что-то более сложное, типа вашего IsValid — нет, оно должно быть на уровне Specification).
            • 0
              Что-то уж очень мало вопросов :(
              • +2
                Ну статья не про Apple же, неудивительно.

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