Привет!
Сегодня мы немного поговорим про 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)
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; }
mayorovp
30.01.2018 06:27До такого он не упростится никак — потому что параметры там "виртуальные", в их роли могут выступать имена таблиц и колонок.
Более того, его даже в форме
Expression<Func<T, FormattableString>>
использовать не получится — потому что компилятор создает наследника для этого типа с заранее неизвестным конструктором, из которого непонятно как вытаскивать аргументы.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); }
pnovikov Автор
31.01.2018 00:43Вы какбы учитывайте, что некоторые параметры надо передать именно объектами, дабы EF сам обернул их в
SqlParameter
чтобы избежать, например SQL-инъекций и передачи дат в неправильном формате.
mayorovp
30.01.2018 06:34Мне вот эта часть жутко не нравится:
// собираем бесконтекстное лямбда-выражение var lex = Expression.Lambda(cArg); // компилим var compiled = lex.Compile(); // вычисляем var result = compiled.DynamicInvoke();
Неужели никто не знает способа ускорить это дело?
msin
30.01.2018 09:51Компиляция выражения в FastExpressionCompiler декларируется в 15 раз быстрее, чем в .NET
(я не проверял)
pnovikov Автор
30.01.2018 10:39Мне она тоже не нравится. Написал из того, что было под рукой. Там вон ниже человек предложил FastExpressionCompiler, но сдается мне, что если вы самостоятельно вычислите параметры в замыканиях и будете подставлять в запрос готовые переменные, то существенного прироста в скорости от изменения способа компиляции не будет (сугубо ИМХО).
Ну и да. По сравнению с проходом метаданных EF, .Compile/.DynamicInvoke — это быстро :)aikixd
30.01.2018 11:33Лямбду можно скомпилировать в конкретный делегат. По скорости будет как если бы вы написали эту лямбду в коде. Замыкания стоит вытащить в аргументы, что-бы не плодить объекты.
Взгляните на GenerateGetHashCode, он по-понятней будет.
pnovikov Автор
30.01.2018 11:45Там человек так же применяет .Compile и, скорее всего, последующий .DynamicInvoke, что полностью эквивалентно решению, изложенному в статье, которым не доволен mayorovp.
aikixd
30.01.2018 11:48Этот человек я =) Нет, вы получаете настоящую взаправдовскую лямбду.
Вызов выглядит так
var x = getHashCodeLambda(obj);
pnovikov Автор
30.01.2018 11:50+1Эм… Вы точно статью читали? Мне не нужна лямбда.
aikixd
30.01.2018 12:01-1Вы исползуете DynamicInvoke, сначит вы используете лямбду. Но ваша композиция не позволяет эффективно использовать кодогенерацию.
К примеру, вы вызываете Compile для каждого параметра, что 1, очень медленно (Сначала вы компилите в IL, потом в машинный код), 2, при длительном использовании программа свалится, когда не будет места для выделения памяти для нового куска кода.
Я бы создал лямбду для каждой связки строка + аргументы и дергал бы их.
pnovikov Автор
30.01.2018 12:06У нас задача: вычислить каждый аргумент и сложить их в массив. Тут максимум что можно сделать — собрать лямбду а-ля NewArrayInit и единожды скомпилировать/посчитать. Никак не возьму в толк что вы предлагаете.
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(); } }
mayorovp
30.01.2018 18:31Выражения компилируются как Dynamic Method, а их .net выгружать умеет.
aikixd
30.01.2018 18:40В доках ничего не сказано о выгрузке кода. И метода для этого я не нашел.
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
pnovikov Автор
30.01.2018 19:58+1Для проблемы, которой передо мной не стоит вы предоставили решение, о котором вас не просили.
pnovikov Автор
30.01.2018 20:05Вдобавок ещё и неправильное.
Cache.ContainsKey(expression)
всегда будет давать false, как вы верно заметили. EqualityComparer для лямбда-выражения, пожалуйста, в студию.
Поймите, наконец, что мне не нужно кэшировать аргументы. Мне их надо высчитывать каждый раз, ибо как оные могут быть разные.
Ознакомьтесь, пожалуйста, со спецификой задачи ещё раз.
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 захвачена в замыкание или является полем какого-нибудь класса)
YetAnotherSlava
30.01.2018 08:24-1Есть еще такая вещь, как linq2db. И одни и те же классы для маппинга таблиц базы можно размечать двумя типами атрибутов сразу — и EF, и linq2db.
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 });
pnovikov Автор
30.01.2018 10:35Ну вот попробуйте и сделайте :)
Спойлер: не вы первый додумались до такого крутого интерфейса использования. Проблема в том, что для него требуется писать свой конструктор запросов, что равноценно переписыванию EF с нуля.
ARad
30.01.2018 13:27Возможно уже все сделано за нас: http://entityframework-plus.net/. Использовать не пробовал.
pnovikov Автор
30.01.2018 13:38Зато я пробовал. Z.EntityFramework.Extensions. 800 баксов стоит эта радость.
Funix
30.01.2018 10:32+1// и этот метод - точно String.Format? if (bdy.Method.DeclaringType != typeof(String) && bdy.Method.Name != "Format") { throw new Exception(err); }
Наверное здесь все-таки задумывалась более жесткая проверка условий через «ИЛИ» (||)?
Yerumaku
30.01.2018 10:33Хотелось бы увидеть зависимости от версий, сейчас MS предлагает NET Standart 2.0 и для него EF 2.0, вот только код из старых версий плохо переносится (особенно касается части EF Core, аналогично под SQlite).
копаться в EF-метаданных — это медленно! Кроме шуток. Поэтому кэшируйте вообще всё, до чего дотянетесь. В статье есть ссылка на мой код — там я уже озаботился кэшированием — можете пользоваться.
Совсем недавно столкнулся с использованием службы SQL как прокси-доступа к базе SQL для среды NET с утечками памяти (кто-то посоветовал кешировать метадату), и ужасает, что подобные статьи не содержат примеров правильного использования IDisposable контекстов в using (для наглядности новичкам), зато технологично.pnovikov Автор
30.01.2018 10:34+1Эм… в нормальных системах контексты запихиваются в IoC-контейнер и диспозятся сами, используя настройки лайфтайма контейнера. У меня, кстати, контекст обернут в юзинг. Так что нече.
Про EF Core — сказал же — упражнение читателю
Drag13
30.01.2018 11:02Если нужен sql raw то почему не использовать хранимку?
pnovikov Автор
30.01.2018 11:04Сомневаюсь, что вы горите желанием писать хранимку для каждого однострочного запроса.
Если бы я это сделал на текущем рабочем месте — у нас бы из воздуха появилось ~200 хранимок.
Плюс хранимки не решают проблемы устойчивости к переименованию и рефакторингу.Drag13
30.01.2018 12:19По большому счету каждый такой запрос это способ использовать EF не как ORM а как базу данных. И вы говорите что у вас таких случаев около 200. Возможно проблема не EF? Если не секрет, о чем проект и откуда возникла нужда в таком количестве разных запросов?
pnovikov Автор
30.01.2018 12:27Одинаковых запросов.
Нужда возникла из необходимости удалять тысячи сущностей из БД, не создавая аналогичное число объектов в памяти, когда можно просто указать критерий удаления. Проект об управлении складом.
RyDmi
Наколько я понял по исходникам, есть поддержка TPH и TPC, но не TPT.
Попробуйте ещё его релизовать.
pnovikov Автор
Попробовал. Оказалось чуть сложнее, чем ожидалось.
RyDmi
работаем с «B»
И тут выходит, что потребуется фактически парсить Sql, т.к. от конкретного оператора
зависит преобразование.
в этот delete придётся инжектить еще операторы:
Для общего случая с TPT, имхо, не применимо.
pnovikov Автор
В моей реализации кинет ошибку, мол, "такой колонки нет в этой таблице". К этому решению приходишь весьма естественным путем.
RyDmi
ИМХО, имеет смысл упомянуть в статье об этом ограничении. TPT не такая уж редко используемая конфигурация.
pnovikov Автор
Да я в гитхаб лучше докоммичу решение и там в readme упомяну
К слову: TPT как раз встретился в том проекте, в рамках которого я изобрел этот подход. Добрался до него в ходе тестирования.