Русский
Русский
English
Статистика
Реклама

Expression trees

Cоздание переиспользуемых Linq фильтров (построителей предикатов для Where), которые можно применять для разных типов

15.04.2021 22:17:49 | Автор: admin

Способ создания переиспользуемых Linq фильтров (построителей предикатов для условия Where), которые можно применять для разных типов объектов. Поля объектов для фильтрации указываются с помощью MemberExpression.

Способ подходит для Entity Framework, включая Async операции.

Основная идея. Что такое переиспользуемый фильтр?

Например есть приказы:

class Order { public DateTime Start { get; set; }public DateTime? End { get; set; }}

Пусть нужно найти все приказы которые будут действовать в ближайшие 7 дней.

С помощью переиспользуемого построителя фильтра (если бы он был реализован) найти приказы можно так:

var ordersFiltred = orders.WhereOverlap(// с помощью MemberExpressions// указываем по каким полям производить поискfromField: oo => oo.Start,toField: oo => oo.End,// указываем период поискаfrom: DateTime.Now,to: DateTime.Now.AddDays(7)).ToList();

Этот же WhereOverlap можно переиспользовать и применить к другому типу. Например, для поиска командировок:

class Trip { public DateTime? From { get; set; }public DateTime? To { get; set; }}
var tripsFiltred = trips.WhereOverlap(// с помощью MemberExpressions// указываем по каким полям производить поискfromField: oo => oo.From,toField: oo => oo.To,from: DateTime.Now,to: DateTime.Now.AddDays(7)).ToList();

Приказы и командировки - это разные типы объектов, у них нет общего интерфейса, поля для поиска называются по-разному. И все таки для обоих типов (и приказов и командировок) применяется один переиспользуемый фильтр WhereOverlap.

Ниже описано как можно делать такие переиспользуемые построители предикатов.

Как сделать переиспользуемый фильтр

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

Пусть есть выплаты и премии:

class Payout { public decimal Total { get; set; }public bool UnderControl { get; set; }}class Premium {public decimal Sum { get; set; }public bool RequiresConfirmation { get; set; }}

Сделаем переиспользуемый фильтр для поиска платежей больше определенной суммы:

class UnderControlPayFilter {readonly decimal Limit;public UnderControlPayFilter(decimal limit) {Limit = limit;}public Expression<Func<TEnt, bool>> Create<TEnt>(Expression<Func<TEnt, decimal>> sumField) {// GreaterOrEqual - нужно реализовать// GreaterOrEqual - это extension, который принимает//  - указание на поле (Expression sumField)//  - и значение с которым нужно сравнивать (Limit)return sumField.GreaterOrEqual(Limit);}}

Пример использования UnderControlPayFilter фильтра:

// фильтр поиска платежей требующих дополнительного контроля//// конкретный предел (здесь 1000) можно вынести в настройки,// а UnderControlPayFilter зарегистрировать в IoC-контейнере.// Тогда можно централизовано (через найстройки приложения)// управлять максимальным пределомvar underControlPayFilter = new UnderControlPayFilter(1000);//// Применение переиспользуемого фильтра для выплатvar payoutPredicate =underControlPayFilter.Create<Payout>(pp => pp.Total);// здесь, для упрощения, payouts - это массив,// в реальном приложении это может быть Entity Framework DbSet var payouts = new[] {new Payout{ Total = 100 },new Payout{ Total = 50, UnderControl = true },new Payout{ Total = 25.5m },new Payout{ Total = 1050.67m }}.AsQueryable().Where(payoutPredicate).ToList();//// Применение переиспользуемого фильтра для премийvar premiumPredicate =underControlPayFilter.Create<Premium>(pp => pp.Sum);// здесь, для упрощения, premiums - это массив,// в реальном приложении это может быть Entity Framework DbSet var premiums = new[] {new Premium{ Sum = 2000 },new Premium{ Sum = 50.08m },new Premium{ Sum = 25.5m, RequiresConfirmation = true },new Premium{ Sum = 1070.07m }}.AsQueryable().Where(premiumPredicate).ToList();

Все готово, осталось только реализовать GreaterOrEqual extension:

public static class MemberExpressionExtensions {    public static Expression<Func<TEnt, bool>> GreaterOrEqual<TEnt, TProp>(        this Expression<Func<TEnt, TProp>> field, TProp val)            => Expression.Lambda<Func<TEnt, bool>>(                Expression.GreaterThanOrEqual(field.Body, Expression.Constant(val, typeof(TProp))),                 field.Parameters);}

По аналогии можно реализовать extension-ы LessOrEqual, Equal, HasNoVal и другие.

Более сложные переиспользуемые фильтры с операторами И и ИЛИ

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

Дополним UnderControlPayFilter:

class UnderControlPayFilter {readonly decimal Limit;public UnderControlPayFilter(decimal limit) {Limit = limit;}public Expression<Func<TEnt, bool>> Create<TEnt>(Expression<Func<TEnt, decimal>> sumField,Expression<Func<TEnt, bool>> controlMarkField) {// PredicateBuilder нужно реализовать (см. ниже)return PredicateBuilder.Or(sumField.GreaterOrEqual(Limit),controlMarkField.Equal(true));}}

Пример использования:

// для выплатvar payoutPredicate =underControlPayFilter.Create<Payout>(sumField: pp => pp.Total,controlMarkField: pp => pp.UnderControl);// для премийvar premiumPredicate = underControlPayFilter.Create<Premium>(sumField: pp => pp.Sum,controlMarkField: pp => pp.RequiresConfirmation);

PredicateBuilder это A universal PredicateBuilder сделанный Pete Montgomery.

Заключение

Чтобы делать свои переиспользуемые фильтры, нужен только PredicateBuilder и MemberExpressionExtensions. Просто скопируйте их в свой проект. Переиспользуемые фильтры можно оформить как extension (как WhereOverlap), как статический хелпер или класс (как UnderControlPayFilter).

Я сделал парочку переиспользуемых фильтров - GitHub, NuGet (включает PredicateBuilder и MemberExpressionExtensions).

Подробнее..

Оживляем деревья выражений кодогенерацией

02.01.2021 00:07:07 | Автор: admin

Деревья выражений System.Linq.Expressions дают возможность выразить намерения не только самим кодом, но и его структурой, синтаксисом.

Их создание из лямбда-выражений это, по сути, синтаксический сахар, при котором пишется обычный код, а компилятор строит из него синтаксическое дерево (AST), которое в том числе включает ссылки на объекты в памяти, захватывает переменные. Это позволяет манипулировать не только данными, но и кодом, в контексте которого они используются: переписывать, дополнять, пересылать, а уже потом компилировать и выполнять.

Run-time компиляция порождает производительные делегаты, которые часто быстрее тех, что компилируются во время сборки (за счет меньшего оверхеда). Однако сама компиляция происходит до десятков тысяч раз дольше, чем вызов результата компиляции.

(бенчмарк)

Действие

Время, нс

Cached Compile Invoke

0.5895 0.0132 ns

Compile and Invoke

83,292.3139 922.4315 ns

Это особенно обидно, когда выражение простое, например содержит только доступ к свойству (в библиотеках для маппинга, сериализации, дата-байндинга), вызову конструктора или метода (для IoC/DI решений).

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

Для уменьшения времени получения делегатов из деревьев выражений используют:

  • Встроенную интерпретацию.
    Необходимость использования интерпретатора вместо компилятора указывается соответствующим флагом:

    Expression.Compile(preferInterpretation: true)
    

    Происходит через рефлексию, но с накладными расходами на формирование стека инструкций.

    Для платформ Xamarin.iOS, Xamarin.watchOS, Xamarin.tvOS, Mono.PS4 и Mono.XBox стандартная компиляция через генерацию IL (System.Reflection.Emit) долгое время была недоступна и на данный момент под капотом всегда откатывается к этому варианту.

  • FastExpressionCompile от @dadhi.
    Ускоряет компиляцию за счет оптимизиpованной генерации IL и с меньшим количеством проверок совместимости.

    На платформах без поддержки JIT компиляции может использоваться только с включенным Mono Interpreter.

  • Ручную интерпретацию.
    Используется для оптимизации вызовов рефлексии под специальные сценарии использования, например для добавления кэширования отдельных вызовов.

    Интерпретируя вручную, уже можно воспользоваться способами ускорения рефлексии. Самые эффективные из них, например Fasterflect, используют System.Reflection.Emit и на некоторых платформах так же могут требовать включения Mono Interpreter.

Для случаев, когда производительности указанных методов недостаточно, напрашивается решение:

Компилировать выражения или какие-то их части во время написания кода (design-time) или сборки (compile-time).

Для compile-time компиляции делегатов к фрагментам деревьев выражений требуется сгенерировать соответствующий код.

API доступа к делегатам не стоит генерировать вместе с ними, лучше держать его в отдельной сборке. Причина заключается в том, что анализ и использование деревьев выражений часто происходит в проектах, отдельных от их инициализации. Фреймворки для дата-байндинга и DI это сторонние библиотеки, да и сами программы часто разбиваются на несколько сборок из соображений архитектуры.

От самого API требуется только давать нужный делегат по ключу, как в словаре. У интересующих нас фрагментов кода: методов, конструкторов и свойств на стыке run-time и compile-time естественный идентификатор это сигнатура. По ней генерируемый код будет класть делегаты в словарь, а клиенты забирать.

Например, для класса со свойством

namespace Namespace{  public class TestClass  {    public int Property { get; set; }  }}

используемым внутри System.Linq.Expressions.Expression<T> лямбды

Expression<Func<TestClass, int>> expression = o => o.Property;

делегатами чтения и записи в общем виде являются

Func<object, object> _ = obj => ((Namespace.TestClass)obj).Property;Action<object, object> _ => (t, m) => ((Namespace.TestClass)t).Property  = (System.Int32)m;

и генерируемый код для их регистрации будет примерно таким:

namespace ExpressionDelegates.AccessorRegistration{  public static class ModuleInitializer  {    public static void Initialize()    {      ExpressionDelegates.Accessors.Add("Namespace.TestClass.Property",        getter: obj => ((Namespace.TestClass)obj).Property,        setter: (t, m) => ((Namespace.TestClass)t).Property = (System.Int32)m);    }  }}

Генерация

Наиболее известные решения для кодогенерации, на мой взгляд, это:

Отдельная область применения есть у каждого решения, и только Roslyn Source Generators умеет анализировать исходный C# код даже в процессе его набора.

Кроме того, именно Roslyn Source Generators видятся более или менее стандартом для кодогенерации, т. к. были представлены как фича основного компилятора языка и используют Roslyn API, используемый в анализаторах и code-fix.

Принцип работы Roslyn Source Generators описан в дизайн-документе (местами не актуален!) и гайде.

Вкратце: для создания генератора требуется создать реализацию интерфейса

namespace Microsoft.CodeAnalysis{  public interface ISourceGenerator  {    void Initialize(GeneratorInitializationContext context);    void Execute(GeneratorExecutionContext context);  }}

и подключить ее к проекту как анализатор.

Метод Initialize пригодится для выполнения какой-либо единоразовой логики. GeneratorInitializationContext на данный момент может быть полезен только для подключения посетителя узлов синтаксиса кода.

В Execute имеется контекст, из которого можно как собрать информацию по существующему коду, так и, собственно, добавить новый.

Для каждого файла исходного кода Roslyn предоставляет синтаксическое дерево в виде объекта SyntaxTree:

GeneratorExecutionContext.Compilation.SyntaxTrees

а так же семантическую модель:

semanticModel =  GeneratorExecutionContext.Compilation.GetSemanticModel(SyntaxTree)

Последняя нужна, чтобы по участку кода (узлу синтаксиса) понять его связи с другими частями программы, типами, другими сборками.

Среди всех узлов синтаксических деревьев сборки нам нужно найти только интересующие нас лямбда-выражения типа System.Linq.Expressions.Expression<T> и отобрать из их узлов-потомков выражения, описывающие доступ к членам классов, создание объектов и вызов методов:

По семантике узла, так называемому символу (Symbol), можно определять:

  • типы, используемые выражением;

  • область видимости;

  • IsStatic, IsConst, IsReadOnly и другие характеристики.

На основе такой информации и будем генерировать подходящий код.

В Roslyn API (Microsoft.CodeAnalysis) построить сигнатуру намного проще, чем c API рефлексии (System.Reflection). Достаточно сконвертировать символ в строку при помощи методаISymbol.ToDisplayString(SymbolDisplayFormat) c подходящим форматом:

Зная сигнатуры свойства/поля, его типа и обладателя формируем строки для добавления делегатов:

Оформляем код добавления делегатов в класс и отдаем компилятору:

var sourceBuilder = new StringBuilder(@"namespace ExpressionDelegates.AccessorRegistration{  public static class ModuleInitializer  {    public static void Initialize()    {");      foreach (var line in registrationLines)      {        sourceBuilder.AppendLine();        sourceBuilder.Append(' ', 6).Append(line);      }      sourceBuilder.Append(@"    }  }}");GeneratorExecutionContext.AddSource(  "AccessorRegistration",  SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));

Этот код обязательно будет добавлен в сборку ...если генератор сможет отработать :)

Дело в том, что хоть Source Generators технически и не фича языка, поддерживаются они только в проектах с C# 9+. Позволить такую роскошь без костылей и ограничений на данный момент могут только проекты на .NET 5.

Совместимость

Поддержку Roslyn Source Generators API для .NET Standard, платформ .NET Core, .NET Framework и даже Xamarin поможет организовать Uno.SourceGeneration.

Uno.SourceGeneration предоставляет собственные копии интерфейса ISourceGenerator и атрибута [Generator], которые при миграции на С# 9 меняются на оригинальные из пространства имен Microsoft.CodeAnalysis простым удалением импортов Uno:

using Uno.SourceGeneration;using GeneratorAttribute = Uno.SourceGeneration.GeneratorAttribute;using ISourceGenerator = Uno.SourceGeneration.ISourceGenerator;
Для подключения достаточно добавить несколько строк в файл проекта.

В проект, где генератор будет использоваться:

<ItemGroup>  <SourceGenerator Include="PATH\TO\GENERATOR.dll" /></ItemGroup>

Например, распространяя генератор через nuget, подключение можно осуществлять вложением MSBuild props файла со следующим путём:

Инициализация

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

Для этих целей отлично подходит Module Initializer. Это конструктор сборки (а точнее ее модуля), который запускается сразу после ее загрузки и до вызовов к остальному коду. Он давно есть в CLR, но к сожалению, в C# его поддержка c атрибутом [ModuleInitializer] добавлена только в 9 версии.

Решение по добавлению конструктора в сборку с более широкой поддержкой платформ есть у Fody плагин Fody.ModuleInit. После компиляции добавляет классы с именами ModuleInitializer в конструктор сборки. В такой класс и будем оборачивать инициализацию сгенерированных делегатов.

Подключение Fody.ModuleInit через MSBuild свойства вместо FodyWeavers.xml исключит конфликты с другими Weaver-ами Fody в проекте клиента.

Использование

Таким образом, при сборке проекта:

  1. Source Generator добавит в сборку код, регистрирующий делегаты для деревьев выражений, в обертке класса ModuleInitializer.

  2. Fody.ModuleInit добавит ModuleInitializer в конструктор сборки.

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

Проверяем:

Expression<Func<string, int>> expression = s => s.Length;MemberInfo accessorInfo = ((MemberExpression)expression.Body).Member;Accessor lengthAccessor = ExpressionDelegates.Accessors.Find(accessorInfo);var length = lengthAccessor.Get("17 letters string");// length == 17

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

Бенчмарки

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

Действие

Время, нс

Вызов простого делегата конструктора

4.6937 0.0443

Вызов сгенерированного делегата конструктора

5.8940 0.0459

Поиск и вызов сгенерированного делегата конструктора

191.1785 2.0766

Компиляция выражения и вызов конструктора

88,701.7674 962.4325

Вызов простого делегата доступа к свойству

1.7740 0.0291

Вызов сгенерированного делегата доступа к свойству

5.8792 0.1525

Поиск и вызов сгенерированного делегата доступа к свойству

163.2990 1.4388

Компиляция выражения и вызов геттера

88,103.7519 235.3721

Вызов простого делегата метода

1.1767 0.0289

Вызов сгенерированного делегата метода

4.1000 0.0185

Поиск и вызов сгенерированного делегата метода

186.4856 2.5224

Компиляция выражения и вызов метода

83,292.3139 922.4315

Полный вариант таблицы, с бенчмарками интерпретации.

А судя по результату профилирования поиска сгенерированного делегата, самое долгое построение сигнатуры, ключа для поиска.

Flame-график бенчмарка поиска и вызова сгенерированного делегата доступа к свойствуFlame-график бенчмарка поиска и вызова сгенерированного делегата доступа к свойству

Идеи насчёт оптимизации построения сигнатур по System.Reflection.MemberInfo приветствуются. Реализация на момент написания.

Заключение

По итогу получилось современное решение для кодогенерации с актуальной совместимостью и автоматической инициализацией.

Полный код можно посмотреть на: github/ExpressionDelegates, а подключить через nuget.

Для тех, кто будет пробовать Source Generators хотелось бы отметить несколько полезностей:

  • Source Generator Playground (github).
    Позволяет экспериментировать с Roslyn Source Generators в браузере, онлайн.

  • Окно визуализации синтаксиса для Visual Studio.
    Удобный инструмент для знакомства с Roslyn Syntax API на собственном коде.

  • Отлаживается Source Generator вызовом отладчика из его кода. Пример.
    Для этого нужен компонент Visual Studio Just-In-Time debugger и включенная настройка Tools -> Options -> Debugging -> Just-In-Time Debugging -> Managed.

  • В сгенерированных *.cs файлах срабатывают брейкпоинты, проверено в Visual Studio16.8.
    При генерации через Uno.SourceGeneration файлы размещаются по пути: \obj\{configuration}\{platform}\g\.
    С Roslyn Source Generators их появление включается через MSBuild свойство EmitCompilerGeneratedFiles.
    Стандартный путь: \obj\{configuration}\{platform}\generated\, переопределяется в свойстве CompilerGeneratedFilesOutputPath.

  • Source Generators можно конфигурировать свойствами MSBuild.
    При использовании Uno.SourceGeneration значение получают вызовом

    GeneratorExecutionContext.GetMSBuildPropertyValue(string)
    

    Для Roslyn Source Generators требуемые свойства необходимо сперва отдельно обозначить в MSBuild группе CompilerVisibleProperty и только после вызывать:

    GeneratorExecutionContext.AnalyzerConfigOptions.GlobalOptions  .TryGetValue("build_property.<PROPERTY_NAME>", out var propertyValue)
    
  • Из генератора можно кидать предупреждения и ошибки сборки.

    //Roslyn Source GeneratorsGeneratorExecutionContext.ReportDiagnostic(Diagnostic)//Uno.SourceGeneration:GeneratorExecutionContext.GetLogger().Warn/Error().
    
Подробнее..

Транслируй меня полностью

24.02.2021 14:21:40 | Автор: admin


Вы когда-нибудь работали с Entity Framework или другим ORM и получали NotSupportedException? Многие люди получали:


InvalidOperationException: Error generated for warning 'Microsoft.EntityFrameworkCore.Query.QueryClientEvaluationWarning: The LINQ expression could not be translated and will be evaluated locally.'

Марк Симан твердо убежден, что, за одним исключением, все существующие реализации нарушают LSP. Он даже готов отправить бесплатную копию своей книги первому читателю, который укажет ему на реальную, общедоступную реализацию IQueryable<T>, которая может принять любое выражение и не выбросить исключение. За девять лет книга так и не нашла своего обладателя:)


  • Hi Mark,
    I am writing a blog post that refers to your artticle. I am wondering if you have ever sent a free copy of your book to someone. Presumably not:)
  • Hi Maxim
    Thats right: I havent.
    Regards
    Mark Seemann

В поддержку этой точки зрения можно привести и другие аргументы. Например, ToListAsync вообще отсутствует в наборе методов расширения из коробки. Вместо этого он определен в пакетах конкретных ORM. Значит ли это, что не стоит раскрывать IQueryable<T> в публичных API? Я думаю, что ответ на этот вопрос зависит


ToListAsync


Для начала разберемся с ToListAsync. Здесь все однозначно. Метод построен на попытке привести IQueryable<TSource> к IAsyncEnumerable<TSource> с помощью метода AsAsyncEnumerable:


public static async Task<List<TSource>> ToListAsync<TSource>(   [NotNull] this IQueryable<TSource> source,   CancellationToken cancellationToken = default){   var list = new List<TSource>();   await foreach (var element in source.AsAsyncEnumerable().WithCancellation(cancellationToken))   {       list.Add(element);   }   return list;}

Который, в свою очередь, выбрасывает исключение, если аргумент не реализует соответствующий интерфейс:


public static IAsyncEnumerable<TSource> AsAsyncEnumerable<TSource>(   [NotNull] this IQueryable<TSource> source){   Check.NotNull(source, nameof(source));   if (source is IAsyncEnumerable<TSource> asyncEnumerable)   {       return asyncEnumerable;   }   throw new InvalidOperationException(CoreStrings.IQueryableNotAsync(typeof(TSource)));}

Не очень-то заменимы реализации этого интерфейса. Да, можно обложить этот метод еще одной оберткой и в случае отсутствия интерфейса завернуть синхронный метод в Task.FromResult, но это позиция обезьянок на картинке для привлечения внимания.


Раскрывать ли IQueryable в public API?


Пишем IQueryable держим Entity Framework / NHibernate / Linq2Db в уме. Почти никогда в рамках одного проекта не используется больше одной ORM. Замена одной ORM на другую крайне редкое и невероятно затратное мероприятие.


Связка Linq2Db и Entity Framework кажется многообещающей, но я ее не пробовал в реальных проектах, поэтому рекомендовать не могу.

Фасад из протекающей абстракции, к сожалению, никак не меняет этого факта. Поэтому вопрос в заголовке можно переформулировать как раскрывать ли зависимости от фреймворка в API?


Out Of Process


Я думаю, что светить IQueryable за пределами вашего приложения скорее неудачная идея. Поэтому я настороженно отношусь к GraphQl или OData. Безусловно, существуют сценарии, когда использование этих технологий может быть оправдано. Однако, обычные rest-like API гораздо проще в разработке, поддержке и использовании. Даже если методов для фильтрации достаточно много Query Objects все еще могут неплохо справляться со своей задачей с минимальным дублированием кода.


In process


Что касается передачи IQueryable во внутренних слоях приложения, я думаю, что этот сценарий допустим, при условии, что такой объект появился в результате рефакторинга. Бывают случаи, когда условия получения данных настолько запутаны и сложны, что одними спецификациями выразить их не получается. Приходится создавать промежуточные слои la DataQueryHandler в терминологии CQRS/Vertical Slices. В этом случае передача IQueryable допустимо, просто потому что остальные варианты еще хуже.


Что учесть при работе с IQueryable


Мы условились, что под IQueryable всегда понимается реализация конкретного поставщика запросов. Поэтому, придется учитывать его ограничения. Будем рассматривать варианты запросов на примере вот такого простого класса пользователя:


public enum UserType: byte{   Regular,   Vip}public class User : IdentityUser{   [NotMapped]   public int Age { get; set; }   public Organization Organization { get; set; }   public UserType UserType { get; set; }   public string FirstName { get; set; }   public string LastName { get; set; }   public string FullName => $"{FirstName} {LastName}";}

Вызов методов


Первое от чего нужно отказаться использование методов, написанных вами внутри выражений:


public static class Demo{   public static bool Filter(User user) =>           user.FirstName.StartsWith("М");   public static object Works(IEnumerable<User> users) =>       users           .Where(x => Filter(x))           .ToList();     public static object ThrowsException(IQueryable<User> users) =>       users           .Where(x => Filter(x))           .ToList();}

Если вы не понимаете, почему одинаковые (на самом деле нет) LINQ-выражения в одном случае работают, а в другом нет, посмотрите или почитайте мой доклад о деревьях выражений и попробуйте написать Visitor, чтобы посмотреть разницу в выражениях. Должно стать понятно.

К сожалению, Мадс Торгерсен подтвердил мне в ходе Q&A сессии DotNext Moscow 2020, что у них нет планов по реализации декомпилятора делегатов в выражения в BCL. Кроме того, существуют объективные технические сложности для реализации такого метода. Поэтому, у ORM нет простых способов заинлайнить методы в выражении. В какой SQL должен транслироваться вызов функции Filter? В общем случае ответ да хрен его знает. Ровно поэтому поставщики запросов и выбрасывают исключения.


Вы можете подсказать Entity Framework, что такая функция есть у вас в БД. В этому случае есть смысл объявить об этом явно:


public static class DbFunctions{   public static bool Filter(User user) =>           user.FirstName.StartsWith("М"); }//...       users           .Where(DbFunctions.Filter)           .ToList();

Исключения, подтверждающее правило


public static object Exception (IQueryable<User> users) =>   users       .Where(x => x.FirstName.StartsWith("М"))       .ToList();

Метод StartsWith или Contains будут транслироваться, потому что трансляция этих методов в SQL достаточно проста: LIKE М% и LIKE %М%, соответственно. Важное различие заключается в том, что эти методы входят в BCL и поставщики типов знают об их существовании на этапе компиляции. Кроме этого, вы никогда не застрахованы от неожиданностей вроде:


// падаетpublic static object EnumException (IQueryable<User> users) =>   users       .Select(x => x.UserType.ToString())       .Distinct()       .ToList();// а так работаетpublic static object EnumWorks (IQueryable<User> users) =>   users       .Select(x => ((byte)x.UserType).ToString())       .Distinct()       .ToList();

Видимо, приведение к Underlying Type позволяет проигнорировать создание Enum как такового и интерпретировать ToString как обычный Convert.


Вообще, что именно будет транслировано в плане MethodCallExpression, а что нет это та еще кроличья нора. К тому же, нет никакой гарантии, что в новых версиях ORM правила трансляции не будут изменены, как это уже случалось много раз.

[NotMapped]


Здесь все очевидно. Если поле не имеет отражения на БД, то и транслировать такое выражение не во что:


public static object NotMappedException (IQueryable<User> users) =>   users       .Where(x => x.Age > 18)       .ToList();

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


Конструктор


И на закуску еще сценарий, часто вводящих в ступор. Вот такое выражение транслируется последними версиями EF Core (может транслируется и в EF 6, напишите в комментариях, если знаете точно):


public static object ConstructorWorks (IQueryable<User> users) =>   users       .Select(x => x.FullName)       .ToList();

А вот такой уже нет:


public static object ConstructorException (IQueryable<User> users) =>   users       .Where(x => x.FullName.StartsWith("М"))          .Select(x => x.FullName)       .ToList();

Это связано с оптимизацией EF Core. Если Select не получается транслировать, то он может вытащить всю сущность и выполнить вызов x => x.FullName уже в памяти. Оптимизация может приводить к более изощенным казусам, вроде:


public static object OrganizationWorksOrNot (IQueryable<User> users) =>   users       .Select(x => new { x.Organization.FullName })       //.Where(x => x.FullName.StartsWith("М"))        .ToList();

Код выше может работать или падать с ошибкой, в зависимости от того, что еще входит в выражение. Еще хуже все становится, если где-то в Select попадают сущности целиком. В этом случае успех операции может зависеть от того включен ли Lazy Loading или указан ли Include.


Вообще Include в сочетание с Select это либо тот еще запашок, либо полное непонимание того, что проекции не попадают в change tracker (если вы не инициализируете свойства проекции классами сущностей). Вообще, по поводу Include и Lazy Loading очень рекомендую статью In Defense of Lazy Loading, особенно если вы хотите структурировать бизнес-логику в DDD-стиле.

Интерполяция строк


Это еще несколько палок в колеса. Я не буду подробно останавливаться на деталях, потому что они уже прекрасно описаны в статье Помогаем Queryable Provider разобраться с интерполированными строками. Замечу лишь, что:


FullName => FirstName + " " + LastName

гораздо безопаснее, чем


FullName => $"{FirstName} {LastName}"

Выводы


Использование IQueryable во внутренних API допустимо, но сопряжено с неожиданностями. Чего больше: плюсов или минусов каждый решает сам для себя. Делитесь в комментариях вашим опытом увлекательной отладки нетранслируемых запросов. Добавим интересные истории в статью, чтобы жить C#-программистам стало чуточку легче.

Подробнее..
Категории: C , Net , Expression trees , Iqueryable , Iqueryprovider

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru