Прямой SQL в EntityFramework. Теперь со строгой типизацией

    Привет!


    Сегодня мы немного поговорим про EntityFramework. Совсем чуть-чуть. Да, я знаю что к нему можно относиться по-разному, многие от него плюются, но за неимением лучшей альтернативы — продолжают использовать.


    Так вот. Часто ли вы используете в своём C#-проекте с настроенным ORM-ом прямые SQL-запросы в базу? Ой, да бросьте, не отнекивайтесь. Используете. Иначе как бы вы реализовывали удаление/обновление сущностей пачками и оставались живы


    Что мы больше всего любим в прямом SQL? Скорость и простоту. Там, где "в лучших традициях ORM" надо выгрузить в память вагончик объектов и всем сделать context.Remove (ну или поманипулировать Attach-ем), можнo обойтись одним мааааленьким SQL-запросом.
    Что мы больше всего не любим в прямом SQL? Правильно. Отсутствие типизации и взрывоопасность. Прямой SQL обычно делается через DbContext.Database.ExecuteSqlCommand, а оно на вход принимает только строку. Следовательно, Find Usages в студии никогда не покажет вам какие поля каких сущностей ваш прямой SQL затронул, ну и помимо прочего вам приходится полагаться на свою память в вопросе точных имён всех таблиц/колонок которые вы щупаете. А ещё молиться, что никакой лоботряс не покопается в вашей модели и не переименует всё в ходе рефакторинга или средствами EntityFramework, пока вы будете спать.


    Так ликуйте же, адепты маленьких raw SQL-запросов! В этой статье я покажу вам как совместить их с EF, не потерять в майнтайнабильности и не наплодить детонаторов. Ныряйте же под кат скорее!


    А чего конкретно хотим достичь?


    Итак, в этой статье я покажу вам отличный подход, который раз и навсегда избавит вас от беспокойства о проблемах, которые обычно вызывает прямой SQL в тандеме с EntityFramework. Ваши запросы приобретут человеческий облик, будут находиться через Find Usages и станут устойчивы к рефакторингу (удалению/переименованию полей в сущностях), а ваши ноги потеплеют, язвы рассосутся, карма очистится.


    Нам понадобится: C# 6.0 (ну, тот, в котором интерполяция строк реализована), лямбда-выражения и немножко прямых рук. Я назвал эту технику "SQL Stroke". В конечном счете мы напишем несколько extension-методов для DbContext, позволяющих отправлять в базу SQL со строго типизированными вставками. Для этого нам понадобится пообщаться с метаданными EntityFramework, попарсить лямбда-выражения и починить все возникающие по ходу баги и corner case-ы.


    Вот как будет выглядеть ваш прямой SQL после прочтения этой статьи:


    using (var dc = new MyDbContext())
    {
        //----------
        dc.Stroke<Order>(x => $"DELETE FROM {x} WHERE {x.Subtotal} = 0");
        //                                              ^ IntelliSense!
    
        //----------
        var old = DateTime.Today.AddDays(-30);
        dc.Stroke<Customer>(x => $"UPDATE {x} SET {x.IsActive} = 0 WHERE {x.RegisterDate} < {old}");
    
        //----------
        dc.Stroke<Item, Order>((i, o) => $@"
    UPDATE {i} SET {i.Name} = '[FREE] ' + {i.Name} 
    FROM {i}
    INNER JOIN {o} ON {i.OrderId} = {o.Id}
    WHERE {o.Subtotal} = 0"
    , true);
    
    }
    

    TL;DR: короче вот оно на гитхабе, там нагляднее


    Здесь мы видим, что при вызове .Stroke тип-параметрами мы указываем сущности (замапленные на таблицы), с которыми будем работать. Они же становятся аргументами в последующем лямбда-выражении. Если кратко, то Stroke пропускает переданную ему лямбду через парсер, превращая {x} в таблицы, а {x.Property} в соответствующее имя колонки.


    Как-то так. Теперь давайте просмакуем подробности.


    Сопоставление классов и свойств с таблицами и колонками


    Давайте освежим ваши знания Reflection-а: представьте что у вас есть класс (точнее Type) и у вас есть строка с именем проперти из этого класса. Так же имеется наследник EF-ного DbContext-а. Имея оные две вилки и тапок вам надобно добыть имя таблицы, на которую мапится ваш класс и имя колонки в БД, на которую мапится ваша проперть. Сразу же оговорюсь: решение этой задачи будет отличаться в EF Core, однако же на основную идею статьи это никак не влияет. Так что я предлагаю читателю самостоятельно реализовать/нагуглить решение этой задачи.


    Итак, EF 6. Требуемое можно достать через весьма популярную магию приведения EF-ного контекста к IObjectContextAdapter:


    public static void GetTableName(this DbContext context, Type t)
    {
        // кастуем наш контекст к ObjectContext-у
        var objectContext = ((IObjectContextAdapter)context).ObjectContext;
    
        // достаем метаданные
        var metadata = objectContext.MetadataWorkspace;
    
        // из них извлекаем нашу коллекцию объектов из CLR-пространства
        var objectItemCollection = ((ObjectItemCollection)metadata.GetItemCollection(DataSpace.OSpace));
    
        // и в оных ищем наш тип. Получаем EF-ный дескриптор нашего типа
        var entityType = metadata.GetItems<EntityType>(DataSpace.OSpace)
                    .FirstOrDefault(x => objectItemCollection.GetClrType(x) == t);
    
        // ищем в метадате контейнер из концептуальной модели
        var container = metadata
                    .GetItems<EntityContainer>(DataSpace.CSpace)
                    .Single()
                    .EntitySets
                    .Single(s => s.ElementType.Name == entityType.Name);
    
        // вытаскиваем маппинги этого контейнера на свет б-жий
        var mapping = metadata.GetItems<EntityContainerMapping>(DataSpace.CSSpace)
                    .Single()
                    .EntitySetMappings
                    .Single(s => s.EntitySet == container);          
    
        // уплощаем, вытаскиваем данные об источнике данных (таблица)
        var tableEntitySet = mapping
                    .EntityTypeMappings.Single()
                    .Fragments.Single()
                    .StoreEntitySet;
    
        // берем имя оной
        var tableName = tableEntitySet.MetadataProperties["Table"].Value ?? tableEntitySet.Name;
    
        // можно покурить
        return tableName;     
    }
    

    И, пожалуйста, не спрашивайте меня что же разработчики EntityFramework курили имели в виду, создавая такие лабиринты абстракций и что в нём означает каждый закоулочек. Честно признаюсь — я сам в этом лабиринте могу заблудиться и кусок выше я, не писал, а просто нашел и распотрошил.


    Так, с таблицей вроде разобрались. Теперь имя колонки. Благо, оно лежит рядом, в маппингах контейнера сущности:


    public static void GetTableName(this DbContext context, Type t, string propertyName)
    {
        // вот ровно тот же самый код, до var mappings = ...
    
        // только вытаскиваем мы из них проперть
        var columnName = mapping
                .EntityTypeMappings.Single()
                .Fragments.Single()
                .PropertyMappings
                .OfType<ScalarPropertyMapping>()
                .Single(m => m.Property.Name == propertyName)
                .Column
                .Name;
    
        // быстро, не так ли?
        return columnName;
    }

    Так, и вот тут я сразу и крупными буквами предупреждаю читателя: копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться. Но все равно держите в голове: реальные концептуальные модели EF — стозёвные чудища, хранящие в себе взводы и дивизии различных объектов. Если вам нужно только соотношение тип-имя таблицы и тип/свойство — имя колонки, то лучше один раз достаньте и закэшируйте (только не напоритесь там на утечку памяти — не храните ничего от DbContext-а). В EF Core, говорят, с этим по-лучше.


    Выражения


    Самое скучное позади. Теперь — лямбда-выражения. Положим, мы хотим иметь метод Stroke, чтобы вызывать его можно было вот таким макаром:


    context.Stroke<MyEntity>(x => $"UPDATE {x} WHERE {x.Age} > 10")

    Сам метод Stroke простой:


    public static void Stroke<T>(this DbContext s, Expression<Func<T, string>> stroke)
    {
        object[] pars = null;
        var sql = Parse(context, stroke, out pars);
        context.Database.ExecuteSqlCommand(sql, pars);
    }

    В его основе лежит метод Parse, который и делает всю основную работу. Как нетрудно догадаться, этот метод должен разбирать лямбда-выражение, полученное от интерполяции строки. Ни для кого не секрет, что шарповая интерполяция строк является синтаксическим сахаром для String.Format. Следовательно, когда вы пишете $"String containing {varA} and {varB}", то компилятор преобразует эту конструкцию в вызов String.Format("String containing {0} and {1}", varA, varB). Первым параметром у этого метода идёт строка формата. В ней мы невооруженным глазом наблюдаем плейсхолдеры — {0}, {1} и так далее. Format просто заменяет эти плейсхолдеры на то, что идет после строки формата, в порядке, обозначенном цифрами в плейсхолдерах. Если плейсхолдеров больше, чем 4 — то интерполированная строка компилируется в вызов перегрузки String.Format от двух параметров: самой строки формата и массива, в который пакуются все, страждущие попасть в результирующую строку параметры.


    Таким образом, что мы сейчас сделаем в методе Parse? Мы клещами вытянем оригинальную строку формата, а аргументы форматирования пересчитаем, заменяя где надо на имена таблиц и колонок. После чего сами вызовем Format, чем и соберем оригинальную строку формата и обработанные аргументы в результирующую SQL-строку. Честное слово, это гораздо проще закодить чем объяснить :)


    Итак, начнем:


    public static string Parse(DbContext context, LambdaExpression query, out object[] parameters){
    
        // для начала отсечём совсем уж трешак
        const string err = "Плохая, негодная лямбда!";
        var bdy = query.Body as MethodCallExpression;
    
        // у нас точно вызов метода?
        if (bdy == null) throw new Exception(err);
    
        // и этот метод - точно String.Format?
        if (bdy.Method.DeclaringType != typeof(String) || bdy.Method.Name != "Format")
        {
            throw new Exception(err);
        }

    Как вы знаете, лямбда-выражения в C# — в прямом смысле выражения. То есть всё, что идет после => должно быть одним и только одним выражением. В делегаты можно запихивать операторы и разделять их точкой с запятой. Но когда вы пишете Expression<> — всё. Отныне вы ограничиваете входные данные одним и только одним выражением. Так происходит в нашем методе Stroke. LambdaExpression же — это предок Expression<>, только без ненужных нам generic-ов. Следовательно, надо бы удостоверится, что единственное выражение, которое содержится в нашем query — это вызов string.Format и ничто иное, что мы и сделали. Теперь будем смотреть с какими аргументами его вызвали. Ну с первым аргументом всё ясно — это строка формата. Извлекаем её на радость всему честному народу:


        // берем самый первый аргумент
        var fmtExpr = bdy.Arguments[0] as ConstantExpression;
        if (fmtExpr == null) throw new Exception(err);
        // ...и достаём строку формата
        var format = fmtExpr.Value.ToString();

    Дальше надо сделать небольшой финт ушами: как было сказано выше, если у интерполированной строки больше 4х плейсхолдеров, то она транслируется в вызов string.Format-а с двумя параметрами, второй из которых — массив (в форме new [] { ... }). Давайте же обработаем эту ситуацию:


        // стартовый индекс, с которого мы позже будем перебирать аргументы
        // 1 - потому что первый аргумент - строка формата
        int startingIndex = 1;
    
        // коллекция с аргументами
        var arguments = bdy.Arguments;
        bool longFormat = false;
    
        // если у нас всего два аргумента
        if (bdy.Arguments.Count == 2)
        {
            var secondArg = bdy.Arguments[1];
            // ...и второй из них - new[] {...}
            if (secondArg.NodeType == ExpressionType.NewArrayInit)
            {
                var array = secondArg as NewArrayExpression;
                // то подменяем нашу коллекцию с аргументами на этот массив
                arguments = array.Expressions;
                // сбрасываем индекс
                startingIndex = 0;
                // проставляем флаг, чтобы ниже по коду понять что происходит
                longFormat = true;
            }
        }

    Теперь давайте пройдемся по образовавшейся коллекции arguments и, наконец, преобразуем каждый аргумент, который связан с параметрами нашей лямбды в имя таблицы/колонки, а всё, что не является отсылками к таблицам и колонкам — вычислим и закинем в список параметров запроса, оставив в параметрах формата {i}, где i — индекс соответствующего параметра. Ничего нового для опытных пользователей ExecuteSqlCommand.


        // сюда мы будем складывать преобразованные аргументы для
        // последующего вызова string.Format
        List<string> formatArgs = new List<string>();
    
        // а сюда - параметры запроса
        List<object> sqlParams = new List<object>();
    

    Первое, что надо сделать — маленькая техническая особенность C#-повых лямбд: в виду строгой типиазции, когда вы пишете, например x => "a" + 10, компилятор оборачивает вашу десятку в Convert — приведение типа (очевидно, к строке). По существу всё правильно, но в ходе парсеринга лямбд это обстоятельство дюже мешается. Поэтому, тут мы сделаем маленький метод Unconvert, который проверит наш аргумент на предмет обёрнутости в Convert и при необходимости развернет:


    private static Expression Unconvert(Expression ex)
    {
        if (ex.NodeType == ExpressionType.Convert)
        {
            var cex = ex as UnaryExpression;
            ex = cex.Operand;
        }
        return ex;
    }

    Чудно. Далее нам потребуется понять имеет ли очередной аргумент отношение к параметрам выражения. Ну то есть имеет форму p.Field1.Field2..., где p — параметр нашего выражения (то, что ставится перед лямбда-оператором =>). Потому как если не имеет — то надобно этот аргумент просто вычислить, а результат запомнить как параметр SQL-запроса, для последующего скармливания EF-у. Самый простой и топорный способ определить обращаемся ли мы к полю какого-либо из параметров — это следующие два метода:


    В первом мы просто перебираем цепочку обращений к членам, пока не дойдем до корня (я назвал его GetRootMember):


    private static Expression GetRootMember(MemberExpression expr)
    {
        var accessee = expr.Expression as MemberExpression;
        var current = expr.Expression;
        while (accessee != null)
        {
            accessee = accessee.Expression as MemberExpression;
            if (accessee != null) current = accessee.Expression;
        }
        return current;
    }

    Во втором — собственно проверяем требуемые нам условия:


    private static bool IsScopedParameterAccess(Expression expr)
    {
        // если это просто параметр - ну то есть {x}, то да, надо переводить
        if (expr.NodeType == ExpressionType.Parameter) return true;
        var ex = expr as MemberExpression;
    
        // если это не обращение к члену вообще - надо вычислять
        if (ex == null) return false;
    
        // достаем корень цепочки обращений
        var root = GetRootMember(ex);
    
        // да, такое тоже бывает
        if (root == null) return false;
    
        // если это не параметр - вычислим
        if (root.NodeType != ExpressionType.Parameter) return false;
    
        // ну и тут немного вариантов остаётся
        return true;
    }

    Готово. Возвращаемся к перебору аргументов:


        // поехали
        for (int i = startingIndex; i < arguments.Count; i++)
        {
            // убираем возможный Convert
            var cArg = Unconvert(arguments[i]);
    
            // если это НЕ доступ к параметру/полю 
            if (!IsScopedParameterAccess(cArg))
            {
                // собираем бесконтекстное лямбда-выражение
                var lex = Expression.Lambda(cArg);
                // компилим
                var compiled = lex.Compile();
                // вычисляем
                var result = compiled.DynamicInvoke();
                // в результирующей строке оставляем {i}, где i - номер параметра
                formatArgs.Add(string.Format("{{{0}}}", sqlParams.Count));
                // сохраняем полученный объект как SQL-параметр
                sqlParams.Add(result);
                // идем к следующему аргументу
                continue;
            }

    Отлично. Мы отсекли все параметры, которые гарантированно не являются ссылками на наши таблицы/колонки. Список sqlParams потом вернётся через out-параметр — мы его наряду со строкой-результатом скормим context.Database.ExecuteSqlCommand вторым аргументом. Пока же обработаем ссылки на таблицы:


            // если встречаем {x}, то 
            if (cArg.NodeType == ExpressionType.Parameter)
            {
                // заменяем его на имя таблицы, из нашего контекста
                formatArgs.Add(string.Format("[{0}]", context.GetTableName(cArg.Type)))
                // и переходим к следующему аргументу
                continue;
            }

    Тут нам придется отрезать возможность обращаться к агрегатам, ибо как это приведет к необходимости переколбашивать запрос JOIN-ами, чего мы технически сделать не можем. Так что — увы и ах. Если наш аргумент — это обращение к члену, но не к члену непосредственно параметра выражения — то звиняйте, ничем не можем помочь:


            var argProp = cArg as MemberExpression;
    
            if (argProp.Expression.NodeType != ExpressionType.Parameter)
            {
                var root = GetRootMember(argProp);
                throw new Exception(string.Format("Пожалуйста, не лезьте в душу {0}", root.Type));
            }

    И вот, наконец, мы можем добыть наше имя колонки и добавить его в переработанный список формат-аргументов.


            var colId = string.Format("[{0}]", context.GetColumnName(argProp.Member.DeclaringType, argProp.Member.Name));        
            formatArgs.Add(colId);
            // и поехали к следующему формат-аргументу
        }

    Теперь, когда все аргументы перебраны, мы можем наконец-таки сделать string.Format самостоятельно и получить SQL-строку и массив параметров, готовые к скармливанию ExecuteSqlCommand.


        var sqlString = string.Format(format, formatArgs.ToArray());
        parameters = sqlParams.ToArray();
        return sqlString;
    }

    Готово


    Вот как-то так. Для статьи я намеренно упростил код. В частности, полная версия автоматически подставляет алиасы таблиц, нормально кэширует имена таблиц и колонок, а так же содержит перегрузки .Stroke до 8 параметров. С полным исходным кодом вы можете ознакомитья в моем github. За сим прощаюсь и желаю всяческих удач в разработке.


    А, ну и опросик напоследок:

    Как часто вы используете прямой SQL, даже если подключен ORM?

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

    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 53
    • +1

      Наколько я понял по исходникам, есть поддержка TPH и TPC, но не TPT.
      Попробуйте ещё его релизовать.

      • 0

        Попробовал. Оказалось чуть сложнее, чем ожидалось.

        • 0
          // Mapped to Table A
          public class A {
            public string MappingName {get; set;}
          }
          
          //Mapped to Table B
          public class B: A {
            public long OrderIndex {get; set;}
          }
          

          работаем с «B»

          Delete from {x} where {x.MappingName} = 'Custom'


          И тут выходит, что потребуется фактически парсить Sql, т.к. от конкретного оператора
          зависит преобразование.

          в этот delete придётся инжектить еще операторы:

          delete t1 
          from B t1 
          inner join A t2 
          on A.Id = B.Id 
          where t2.MappingName = 'Custom'
          
          delete
          from A
          where MappingName = 'Custom'


          Для общего случая с TPT, имхо, не применимо.
          • 0

            В моей реализации кинет ошибку, мол, "такой колонки нет в этой таблице". К этому решению приходишь весьма естественным путем.

            • 0
              ИМХО, имеет смысл упомянуть в статье об этом ограничении. TPT не такая уж редко используемая конфигурация.
              • 0

                Да я в гитхаб лучше докоммичу решение и там в readme упомяну


                К слову: TPT как раз встретился в том проекте, в рамках которого я изобрел этот подход. Добрался до него в ходе тестирования.

      • 0
        Есть такая замечательная штука, как FormattableString. Код заполнения параметров команды упрощается примерно до такого:
        private static void FillParameters(this DbCommand cmd, FormattableString sql)
        {
            var substitutions = new object[sql.ArgumentCount];
        
            for (var i = 0; i < sql.ArgumentCount; i++)
            {
                var name = string.Concat("p", i.ToString());
                var parameter = cmd.CreateParameter();
                parameter.ParameterName = name;
                parameter.Value = sql.GetArgument(i);
                cmd.Parameters.Add(parameter);
        
                substitutions[i] = string.Concat("@", name);
            }
        
            cmd.CommandText = sql.ArgumentCount > 0 ? string.Format(sql.Format, substitutions) : sql.Format;
        }
        • 0

          До такого он не упростится никак — потому что параметры там "виртуальные", в их роли могут выступать имена таблиц и колонок.


          Более того, его даже в форме Expression<Func<T, FormattableString>> использовать не получится — потому что компилятор создает наследника для этого типа с заранее неизвестным конструктором, из которого непонятно как вытаскивать аргументы.

          • 0
            Конструктор известен, это FormattableStringFactory.Create(string, params object[]).
            Попробовал все же реализовать. Пусть не весь функционал, но справляется с:
            dc.FormatSql<Order>(x => $"DELETE FROM {x} WHERE {x.Subtotal} = 0");

            листинг
            public static string FormatSql<T>(this DbContext context, Expression<Func<T, FormattableString>> expression)
                    {
                        var body = (MethodCallExpression) expression.Body;
            
                        var sql = (string) ((ConstantExpression) body.Arguments[0]).Value;
            
                        var args = ((NewArrayExpression) body.Arguments[1]).Expressions;
                        
                        var parameters = new object[args.Count];
            
                        for (var i = 0; i < args.Count; i++)
                        {
                            var arg = args[i];
                            
                            if (arg.NodeType == ExpressionType.Parameter) // table
                            {
                                var tableName = context.GetTableName(arg.Type);
                                parameters[i] = $"[{tableName}]";
                            }
                            else
                            {
                                var operand = ((UnaryExpression) arg).Operand;
            
                                if (operand is MemberExpression me) // column
                                {
                                    var tableName = context.GetTableName(me.Expression.Type);
                                    var columnName = context.GetColumnName(me.Expression.Type, me.Member.Name);
            
                                    parameters[i] = $"[{tableName}].{columnName}";
                                }
                                else // parameters
                                {
                                    // TODO:
                                }
                                
                            }
                        }
            
                        return string.Format(sql, parameters);
                    }

            • 0

              Вы какбы учитывайте, что некоторые параметры надо передать именно объектами, дабы EF сам обернул их в SqlParameter чтобы избежать, например SQL-инъекций и передачи дат в неправильном формате.

              • 0
                Как раз для этого там и стоит TODO :)
                • 0

                  Тогда мне неочевидно в чем профит использования FormattableString :( Возможно я глупенький.

                  • +1
                    Как минимум обходится проверка на число параметров у string.Format(...)
                    • 0

                      А как максимум? :)

                      • +1
                        Вам этого мало? :)
                        Но вообще да, больше плюсов от использования FormattableString нет, остальной код копирует ваш один в один.
        • 0

          Мне вот эта часть жутко не нравится:


                      // собираем бесконтекстное лямбда-выражение
                      var lex = Expression.Lambda(cArg);
                      // компилим
                      var compiled = lex.Compile();
                      // вычисляем
                      var result = compiled.DynamicInvoke();

          Неужели никто не знает способа ускорить это дело?

          • 0
            Компиляция выражения в FastExpressionCompiler декларируется в 15 раз быстрее, чем в .NET
            (я не проверял)
            • 0

              Мне она тоже не нравится. Написал из того, что было под рукой. Там вон ниже человек предложил FastExpressionCompiler, но сдается мне, что если вы самостоятельно вычислите параметры в замыканиях и будете подставлять в запрос готовые переменные, то существенного прироста в скорости от изменения способа компиляции не будет (сугубо ИМХО).
              Ну и да. По сравнению с проходом метаданных EF, .Compile/.DynamicInvoke — это быстро :)

              • 0

                Лямбду можно скомпилировать в конкретный делегат. По скорости будет как если бы вы написали эту лямбду в коде. Замыкания стоит вытащить в аргументы, что-бы не плодить объекты.


                Тут есть примеры


                Взгляните на GenerateGetHashCode, он по-понятней будет.

                • 0

                  Там человек так же применяет .Compile и, скорее всего, последующий .DynamicInvoke, что полностью эквивалентно решению, изложенному в статье, которым не доволен mayorovp.

                  • –2

                    Этот человек я =) Нет, вы получаете настоящую взаправдовскую лямбду.
                    Вызов выглядит так


                    var x = getHashCodeLambda(obj);
                    • +1

                      Эм… Вы точно статью читали? Мне не нужна лямбда.

                      • –2

                        Вы исползуете DynamicInvoke, сначит вы используете лямбду. Но ваша композиция не позволяет эффективно использовать кодогенерацию.


                        К примеру, вы вызываете Compile для каждого параметра, что 1, очень медленно (Сначала вы компилите в IL, потом в машинный код), 2, при длительном использовании программа свалится, когда не будет места для выделения памяти для нового куска кода.


                        Я бы создал лямбду для каждой связки строка + аргументы и дергал бы их.

                        • 0

                          У нас задача: вычислить каждый аргумент и сложить их в массив. Тут максимум что можно сделать — собрать лямбду а-ля NewArrayInit и единожды скомпилировать/посчитать. Никак не возьму в толк что вы предлагаете.

                          • 0
                            Набросал на коленке
                            class Program
                                {
                                    class MyClass
                                    {
                                        public string Data { get; set; }
                            
                                        private static Dictionary<Expression, Dictionary<Expression, Func<object, string>>> Cache = new Dictionary<Expression, Dictionary<Expression, Func<object, string>>>();
                            
                                        public string ParseInterpolation(Expression<Func<MyClass, string>> expression)
                                        {
                                            var extrapolationCallExpr = (MethodCallExpression)expression.Body;
                            
                                            var args = extrapolationCallExpr
                                                    .Arguments
                                                    .Skip(1)
                                                    .Select(x => x is UnaryExpression e ? e.Operand : x)
                                                    .ToArray();
                            
                                            if (Cache.ContainsKey(expression) == false)
                                            {
                                                var cache = new Dictionary<Expression, Func<object, string>>();
                                                Cache.Add(expression, cache);
                            
                                                var frmtStr = (string)((ConstantExpression)extrapolationCallExpr.Arguments.First()).Value;
                            
                                                var converters = new List<Func<object, string>>();
                            
                                                for (int i = 0; i < args.Count(); i += 1)
                                                {
                                                    switch (unwrapClosure(args[i]))
                                                    {
                                                        case ConstantExpression e:
                                                            {
                            
                                                                var param = Expression.Parameter(typeof(object));
                            
                                                                var access = Expression.MakeMemberAccess(
                                                                    Expression.Convert(
                                                                        param,
                                                                        ((MemberExpression)args[i]).Expression.Type),
                                                                    ((MemberExpression)args[i]).Member);
                            
                                                                cache.Add(
                                                                    e,
                                                                    Expression.Lambda<Func<object, string>>(
                                                                        Expression.Call(
                                                                            access,
                                                                            getMemberType(((MemberExpression)args[i]).Member).GetMethod("ToString", new Type[0])
                                                                        ),
                                                                        param).Compile()
                                                                    );
                                                            }
                            
                                                            break;
                            
                                                        case MemberExpression e:
                                                            {
                                                                if (e.Expression.Type == typeof(MyClass))
                                                                {
                                                                    var memberType = getMemberType(e.Member);
                                                                    var param = Expression.Parameter(typeof(object), e.Member.Name + "_param");
                            
                                                                    var access = Expression.MakeMemberAccess(
                                                                        Expression.Convert(
                                                                            param,
                                                                            typeof(MyClass)),
                                                                        e.Member);
                            
                                                                    var call =
                                                                        Expression.Call(
                                                                            ((Func<string, string, string>)convertMember).Method,
                                                                            Expression.Constant(e.Member.Name, typeof(string)),
                                                                            Expression.Call(
                                                                                access,
                                                                                memberType.GetMethod("ToString", new Type[0])
                                                                            ));
                            
                                                                    cache.Add(e, Expression.Lambda<Func<object, string>>(call, param).Compile());
                                                                }
                                                            }
                            
                                                            break;
                            
                                                        default:
                                                            throw new NotImplementedException();
                                                    }
                                                }
                            
                                            }
                            
                                            var arr =
                                                args
                                                .Select(arg =>
                                                {
                                                    var cache = Cache[expression];
                            
                                                    switch (unwrapClosure(arg))
                                                    {
                                                        case ConstantExpression e:
                                                            return cache[e](e.Value);
                            
                                                        case MemberExpression e:
                            
                                                            {
                                                                if (e.Expression.Type == typeof(MyClass))
                                                                {
                                                                    if (e.Expression is ParameterExpression)
                                                                        return cache[e](this);
                                                                }
                            
                                                                else
                                                                {
                            
                                                                }
                                                            }
                            
                                                            break;
                            
                                                        default:
                                                            throw new NotImplementedException();
                                                    }
                                                    throw new NotImplementedException();
                                                })
                                                .ToArray(); ;
                            
                                            return string.Format((string)((ConstantExpression)extrapolationCallExpr.Arguments[0]).Value, arr);
                            
                                            Expression unwrapClosure(Expression e)
                                            {
                                                if (e is MemberExpression me)
                                                    if (me.Expression is ConstantExpression ce)
                                                        if (ce.Type.Name.StartsWith("<>"))
                                                            return me.Expression;
                            
                                                return e;
                                            }
                            
                                            Type getMemberType(MemberInfo nfo)
                                            {
                                                switch (nfo)
                                                {
                                                    case FieldInfo i:
                                                        return i.FieldType;
                            
                                                    case PropertyInfo i:
                                                        return i.PropertyType;
                            
                                                    default:
                                                        throw new Exception("Wrong member type.");
                                                }
                                            }
                                        }
                            
                                        [MethodImpl(MethodImplOptions.AggressiveInlining)]
                                        private static string convertMember(string memberName, string value)
                                        {
                                            return $"member name: {memberName}, member value: {value}.";
                                        }
                                    }
                            
                                    static void Main(string[] args)
                                    {
                                        Console.WriteLine("Hello World!");
                            
                                        var obj = new MyClass { Data = "MyClassData" };
                                        var i = 1;
                            
                                        Console.WriteLine(obj.ParseInterpolation(x => $"Static data: {i}, Dynamic data: {x.Data}."));
                                        Console.WriteLine(obj.ParseInterpolation(x => $"Static data: {i}, Dynamic data: {x.Data}."));
                            
                                        var obj2 = new MyClass { Data = "some other data" };
                                        var j = 3;
                            
                                        Console.WriteLine(obj2.ParseInterpolation(x => $"Static data: {j}, Dynamic data: {x.Data}."));
                            
                                        Console.ReadLine();
                                    }
                                }

                            Поскольку данные замыкания меняются, закешировать все выражение будет сложно. Можно передавать в нее само дерево, но это реально много работы. Второй вариант закешировать конвертеры. Для каждого аргумента, для каждого схожего случая вызова. Замкнутые переменные можно выдернуть из дерева. Самый большой оверхед тут, это сама генерация дерева.


                            Однако есть небольшая проблема. Поскольку для каждого вызова метода генерируется новое выражение, для кеширования нужно писать свой IEqualityComparer. Без кеша, как я сказал выше, программа вылетит когда кончится память (выражения компилируются как часть AppDomain'а, а .net не умеет выгружать его частями.)


                            Теоретически, возможно получится кешировать конвертеры для общего случая — сравнивать маленькие деревья проще и быстрее.

                            • 0
                              Выражения компилируются как Dynamic Method, а их .net выгружать умеет.
                            • +1

                              Для проблемы, которой передо мной не стоит вы предоставили решение, о котором вас не просили.

                              • 0

                                Вдобавок ещё и неправильное. Cache.ContainsKey(expression) всегда будет давать false, как вы верно заметили. EqualityComparer для лямбда-выражения, пожалуйста, в студию.
                                Поймите, наконец, что мне не нужно кэшировать аргументы. Мне их надо высчитывать каждый раз, ибо как оные могут быть разные.
                                Ознакомьтесь, пожалуйста, со спецификой задачи ещё раз.

                          • –1

                            Если бы вы еще предложили эффективный способ сравнения элементов — все было бы вообще замечательно!


                            Вы понимаете что перед каждым вызовом Stoke внешний код генерирует новое AST, не содержащее ничего от старого?


                            Вот вам пример. Допустим, у нас есть вот такой запрос:


                            var baz = ...;
                            db.Stroke<Foo>(x => $"UPDATE {x} SET {x.Bar}={x.Bar+1} WHERE {x.Baz} = {baz}");

                            Компилятор константу baz передает примерно вот так:


                            var scope = new { baz = ... };
                            Expression.Field( Expression.Constant(scope), "baz" );

                            Что из этого вы будете кешировать? И как вы вообще поймете что тут можно хоть что-то закешировать?


                            Ах да, еще требуется чтобы этот способ не развалился на новых версиях компилятора или в нестандартной ситуации (например, когда переменная baz захвачена в замыкание или является полем какого-нибудь класса)

                            • 0

                              .

                              • +1

                                Э… вы уверены что это именно я не понял? Или это вы о том что я случайно {x.Bar+1} вместо {x.Bar}+1 написал? :-)

                                • 0

                                  Пардон, невнимателен. Конечно же не вы. Прошу прощения :)

              • 0
                Есть еще такая вещь, как linq2db. И одни и те же классы для маппинга таблиц базы можно размечать двумя типами атрибутов сразу — и EF, и linq2db.
                • 0
                  Прощу обучить EF понимать атрибуты linq2db.
                • 0

                  Может лучше так сделать?


                  var emptyOrders = from o in db.Orders where o.Subtotal == 0 select o;
                  emptyOrders.Delete();
                  
                  var allOrders = from o in db.Orders  select o;
                  allOrders.Delete();
                  
                  var old = DateTime.Today.AddDays(-30);
                  var oldCustomers = from c in db.Customers where c.RegisterDate < old  select с;
                  oldCustomers.Update(c => new { IsActive = 0});
                  
                  var items = from i in db.Items where i.Order.Subtotal == 0 select i;
                  items.Update(i => new { Name =  '[FREE] ' + i.Name });
                  • 0

                    Ну вот попробуйте и сделайте :)


                    Спойлер: не вы первый додумались до такого крутого интерфейса использования. Проблема в том, что для него требуется писать свой конструктор запросов, что равноценно переписыванию EF с нуля.

                    • 0

                      Возможно уже все сделано за нас: http://entityframework-plus.net/. Использовать не пробовал.

                      • 0

                        Зато я пробовал. Z.EntityFramework.Extensions. 800 баксов стоит эта радость.

                        • 0

                          А это не их исходники? https://github.com/zzzprojects/EntityFramework-Plus

                          • 0

                            Их, но там далеко не всё, что нужно.

                            • 0

                              Там полные исходники насколько я вижу. Правда там есть только реализация удаления и обновления но нет вставки, что странно. Реализация вставки должна быть чуть проще чем обновления.

                  • +1
                        // и этот метод - точно String.Format?
                        if (bdy.Method.DeclaringType != typeof(String) && bdy.Method.Name != "Format")
                        {
                            throw new Exception(err);
                        }

                    Наверное здесь все-таки задумывалась более жесткая проверка условий через «ИЛИ» (||)?
                    • 0

                      Да. Опечатался. Спасибо :)

                    • 0
                      Хотелось бы увидеть зависимости от версий, сейчас MS предлагает NET Standart 2.0 и для него EF 2.0, вот только код из старых версий плохо переносится (особенно касается части EF Core, аналогично под SQlite).
                      копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться.

                      Совсем недавно столкнулся с использованием службы SQL как прокси-доступа к базе SQL для среды NET с утечками памяти (кто-то посоветовал кешировать метадату), и ужасает, что подобные статьи не содержат примеров правильного использования IDisposable контекстов в using (для наглядности новичкам), зато технологично.
                      • 0

                        Эм… в нормальных системах контексты запихиваются в IoC-контейнер и диспозятся сами, используя настройки лайфтайма контейнера. У меня, кстати, контекст обернут в юзинг. Так что нече.


                        Про EF Core — сказал же — упражнение читателю

                      • 0
                        Если нужен sql raw то почему не использовать хранимку?
                        • +1

                          Сомневаюсь, что вы горите желанием писать хранимку для каждого однострочного запроса.
                          Если бы я это сделал на текущем рабочем месте — у нас бы из воздуха появилось ~200 хранимок.
                          Плюс хранимки не решают проблемы устойчивости к переименованию и рефакторингу.

                          • 0
                            По большому счету каждый такой запрос это способ использовать EF не как ORM а как базу данных. И вы говорите что у вас таких случаев около 200. Возможно проблема не EF? Если не секрет, о чем проект и откуда возникла нужда в таком количестве разных запросов?
                            • 0

                              Одинаковых запросов.
                              Нужда возникла из необходимости удалять тысячи сущностей из БД, не создавая аналогичное число объектов в памяти, когда можно просто указать критерий удаления. Проект об управлении складом.

                        • 0
                          Для того, чтобы избежать переименования параметров, можно использовать nameof.
                          • +1
                            Нельзя: имя свойства в классе и атрибута в базе могут отличаться.

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

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