Привет!


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


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

Комментарии (53)


  1. RyDmi
    30.01.2018 02:04

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


    1. pnovikov Автор
      30.01.2018 18:38

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


      1. RyDmi
        31.01.2018 12:41

        // 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, имхо, не применимо.


        1. pnovikov Автор
          31.01.2018 13:31

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


          1. RyDmi
            31.01.2018 14:31

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


            1. pnovikov Автор
              31.01.2018 14:45

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


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


  1. mntek
    30.01.2018 02:28

    Есть такая замечательная штука, как 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;
    }


    1. mayorovp
      30.01.2018 06:27

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


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


      1. mntek
        31.01.2018 00:42

        Конструктор известен, это 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);
                }


        1. pnovikov Автор
          31.01.2018 00:43

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


          1. mntek
            31.01.2018 00:46

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


            1. pnovikov Автор
              31.01.2018 00:53

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


              1. mntek
                31.01.2018 00:58

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


                1. pnovikov Автор
                  31.01.2018 01:01

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


                  1. mntek
                    31.01.2018 01:04

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


  1. mayorovp
    30.01.2018 06:34

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


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

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


    1. msin
      30.01.2018 09:51

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


    1. pnovikov Автор
      30.01.2018 10:39

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


      1. aikixd
        30.01.2018 11:33

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


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


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


        1. pnovikov Автор
          30.01.2018 11:45

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


          1. aikixd
            30.01.2018 11:48

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


            var x = getHashCodeLambda(obj);


            1. pnovikov Автор
              30.01.2018 11:50
              +1

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


              1. aikixd
                30.01.2018 12:01
                -1

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


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


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


                1. pnovikov Автор
                  30.01.2018 12:06

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


                  1. aikixd
                    30.01.2018 18:19

                    Набросал на коленке
                    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();
                            }
                        }


                    1. mayorovp
                      30.01.2018 18:31

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


                      1. aikixd
                        30.01.2018 18:40

                        В доках ничего не сказано о выгрузке кода. И метода для этого я не нашел.


                        1. mayorovp
                          30.01.2018 18:45
                          +1

                          А метод для этого и не нужен. Динамические методы собираются сборщиком мусора же.


                          Defines and represents a dynamic method that can be compiled, executed, and discarded. Discarded methods are available for garbage collection.
                          https://msdn.microsoft.com/en-us/library/system.reflection.emit.dynamicmethod(v=vs.110).aspx


                    1. pnovikov Автор
                      30.01.2018 19:58
                      +1

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


                      1. pnovikov Автор
                        30.01.2018 20:05

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


                1. mayorovp
                  30.01.2018 12:10
                  -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 захвачена в замыкание или является полем какого-нибудь класса)


                  1. pnovikov Автор
                    30.01.2018 12:25

                    .


                    1. mayorovp
                      30.01.2018 12:27
                      +1

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


                      1. pnovikov Автор
                        30.01.2018 12:28

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


  1. YetAnotherSlava
    30.01.2018 08:24
    -1

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


    1. mayorovp
      30.01.2018 08:34

      Прощу обучить EF понимать атрибуты linq2db.


  1. ARad
    30.01.2018 09:15

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


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


    1. pnovikov Автор
      30.01.2018 10:35

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


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


      1. ARad
        30.01.2018 13:27

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


        1. pnovikov Автор
          30.01.2018 13:38

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


          1. ARad
            30.01.2018 13:41

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


            1. pnovikov Автор
              30.01.2018 13:51

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


              1. ARad
                31.01.2018 05:46

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


  1. Funix
    30.01.2018 10:32
    +1

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

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


    1. pnovikov Автор
      30.01.2018 10:32

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


  1. Yerumaku
    30.01.2018 10:33

    Хотелось бы увидеть зависимости от версий, сейчас MS предлагает NET Standart 2.0 и для него EF 2.0, вот только код из старых версий плохо переносится (особенно касается части EF Core, аналогично под SQlite).

    копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться.

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


    1. pnovikov Автор
      30.01.2018 10:34
      +1

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


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


  1. Drag13
    30.01.2018 11:02

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


    1. pnovikov Автор
      30.01.2018 11:04

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


      1. Drag13
        30.01.2018 12:19

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


        1. pnovikov Автор
          30.01.2018 12:27

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


  1. MrDaedra
    31.01.2018 12:43

    Для того, чтобы избежать переименования параметров, можно использовать nameof.


    1. mayorovp
      31.01.2018 13:05
      +1

      Нельзя: имя свойства в классе и атрибута в базе могут отличаться.