NeinLinq предоставляет полезные расширения для использования поставщиков LINQ, таких как Entity Framework, которые поддерживают только незначительное подмножество функций .NET, повторное использование функций, переписывание запросов, даже делая их нулевыми, а также создание динамических запросов с использованием переводимых предикатов и селекторов.
Для поддержки различных реализаций LINQ доступны следующие варианты. Выберите хотя бы один.
Используйте NeinLinq для простых запросов LINQ:
PM > Install-Package NeinLinq
Используйте NeinLinq.Async для асинхронных запросов LINQ:
PM > Install-Package NeinLinq.Async
Используйте NeinLinq.EntityFramework для запросов LINQ Entity Framework 6:
PM > Install-Package NeinLinq.EntityFramework
Используйте NeinLinq.EntityFrameworkCore для запросов LINQ Entity Framework Core:
PM > Install-Package NeinLinq.EntityFrameworkCore
Примечание. Описанные ниже методы расширения имеют разные имена в зависимости от выбранного выше пакета, чтобы избежать конфликтов! Например, есть:
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )Для EF6/EFCore рекомендуется использовать определенные варианты (в противном случае асинхронные запросы не будут работать).
Новое: в версии 5.1.0
пакет NeinLinq.EntityFrameworkCore представил явное расширение DbContext
для включения глобального внедрения Lambda :
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
Примечание. вызов WithLambdaInjection
должен произойти после вызова UseSqlOrTheLike
!
Многие поставщики LINQ могут поддерживать лишь очень незначительное подмножество функций .NET, они даже не могут поддерживать наши собственные «функции». Скажем, мы реализуем простой метод LimitText
и используем его в обычном запросе LINQ, который будет транслироваться в SQL через Entity Framework ...
LINQ to Entities не распознает метод System.String LimitText(System.String, Int32)», и этот метод невозможно преобразовать в выражение хранилища.
Вот что мы получаем; на самом деле, это действительно раздражает. Нам приходится распределять нашу логику между кодом, который будет транслироваться любым поставщиком запросов LINQ, и кодом, который этого не сделает. Ситуация становится еще хуже: если какая-то логика «переводима», и это хорошо, нам придется копировать и вставлять! Объединение кода в обычную функцию не работает, поскольку провайдер не может транслировать этот простой вызов метода. Мех.
Давайте представим «лямбда-инъекцию»:
[ InjectLambda ]
public static string LimitText ( this string value , int maxLength )
{
if ( value != null && value . Length > maxLength )
return value . Substring ( 0 , maxLength ) ;
return value ;
}
public static Expression < Func < string , int , string > > LimitText ( )
{
return ( v , l ) => v != null && v . Length > l ? v . Substring ( 0 , l ) : v ;
}
// -------------------------------------------------------------------
from d in data . ToInjectable ( )
select new
{
Id = d . Id ,
Value = d . Name . LimitText ( 10 )
}
Если запрос помечен как «вводимый» ( ToInjectable()
), а функция, используемая в этом запросе, помечена как «вводимая здесь» ( [InjectLambda]
), механизм перезаписи NeinLinq заменяет вызов метода соответствующим лямбда-выражением, которое можно перевести в SQL или что-то еще. Таким образом, мы можем инкапсулировать неподдерживаемые функции .NET и даже создавать свои собственные. Ура.
[ InjectLambda ]
public static bool Like ( this string value , string likePattern )
{
throw new NotImplementedException ( ) ;
}
public static Expression < Func < string , string , bool > > Like ( )
{
return ( v , p ) => SqlFunctions . PatIndex ( p , v ) > 0 ;
}
// -------------------------------------------------------------------
from d in data . ToInjectable ( )
where d . Name . Like ( " %na_f% " )
select .. .
Это пример того, как мы можем абстрагировать класс SqlFunctions
Entity Framework , чтобы использовать (надеюсь) более удобный метод расширения Like
в нашем коде запроса. PatIndex
, вероятно, используется для имитации оператора SQL LIKE, почему бы не сделать это так? На самом деле мы можем реализовать «обычный» метод с помощью регулярных выражений для запуска нашего кода, не затрагивая при этом SqlFunctions
...
Новое: в версии 7.0.0
NeinLinq теперь поддерживает собственные поставщики для InjectLambdaAttribute. Эта функция позволяет вам определить собственную логику, чтобы определить, какие методы следует считать внедряемыми, без явного добавления атрибута [InjectLambda]
. Это особенно полезно при работе с внешними библиотеками или кодом, который вы не можете изменить напрямую:
var oldProvider = InjectLambdaAttribute . Provider ;
InjectLambdaAttribute . SetAttributeProvider ( memberInfo =>
{
// Your custom logic here
if ( memberInfo . Name . StartsWith ( " ExternalMethod " ) )
{
return new InjectLambdaAttribute ( typoef ( MyType ) , nameof ( MyType . ExternalMethodExpression ) ) ;
}
// fallback to standard provider
return oldProvider ( memberInfo ) ;
} ) ;
Наконец, давайте посмотрим на этот запрос, используя Entity Framework или что-то подобное:
from d in data . ToInjectable ( )
let e = d . RetrieveWhatever ( )
where d . FulfillsSomeCriteria ( )
select new
{
Id = d . Id ,
Value = d . DoTheFancy ( e )
}
// -------------------------------------------------------------------
[ InjectLambda ]
public static Whatever RetrieveWhatever ( this Entity value )
{
throw new NotImplementedException ( ) ;
}
public static Expression < Func < Entity , Whatever > > RetrieveWhatever ( )
{
return d => d . Whatevers . FirstOrDefault ( e => .. . ) ;
}
[ InjectLambda ]
public static bool FulfillsSomeCriteria ( this Entity value )
{
throw new NotImplementedException ( ) ;
}
public static Expression < Func < Entity , bool > > FulfillsSomeCriteria ( )
{
return d => .. .
}
[ InjectLambda ]
public static decimal DoTheFancy ( this Entity value , Whatever other )
{
throw new NotImplementedException ( ) ;
}
public static Expression < Func < Entity , Whatever , decimal > > DoTheFancy ( )
{
return ( d , e ) => .. .
}
Методы RetrieveWhatever
, FulfillsSomeCriteria
и DoTheFancy
должны быть помечены соответствующим образом, используя атрибут [InjectLambda]
или просто простое соглашение «тот же класс, то же имя, соответствующая подпись» (что, кстати, требует, чтобы класс был внесен в зеленый список). А вызов ToInjectable
может произойти в любом месте цепочки запросов LINQ, поэтому нам не придется загрязнять нашу бизнес-логику.
Примечание. Дублирование кода не требуется. Обычный метод может просто скомпилировать выражение, в идеале только один раз. Простое решение может выглядеть как следующий пример кода (эти вещи можно инкапсулировать/организовать, какими бы сложными они ни казались подходящими, у NeinLinq нет особых требований; не стесняйтесь использовать встроенный «Кэш выражений» или создавать что-то необычное... ):
public static CachedExpression < Func < string , int , string > > LimitTextExpr { get ; }
= new ( ( v , l ) => v != null && v . Length > l ? v . Substring ( 0 , l ) : v )
[ InjectLambda ]
public static string LimitText ( this string value , int maxLength )
=> LimitTextExpr . Compiled ( value , maxLength ) ;
Расширенный: это работает и с методами экземпляра, поэтому фактический код выражения может получать дополнительные данные. Даже интерфейсы и/или базовые классы могут использоваться для абстрагирования всего. Таким образом, мы можем объявить интерфейс/базовый класс без выражений, но предоставить выражение для внедрения с использованием наследования.
public class ParameterizedFunctions
{
private readonly int narf ;
public ParameterizedFunctions ( int narf )
{
this . narf = narf ;
}
[ InjectLambda ( " FooExpr " ) ]
public string Foo ( )
{
.. .
}
public Expression < Func < string > > FooExpr ( )
{
.. . // use the narf!
}
}
// -------------------------------------------------------------------
public interface IFunctions
{
[ InjectLambda ]
string Foo ( Entity value ) ; // use abstraction for queries
}
public class Functions : IFunctions
{
[ InjectLambda ]
public string Foo ( Entity value )
{
.. .
}
public Expression < Func < Entity , string > > Foo ( )
{
.. .
}
}
Примечание . Внедрение методов экземпляра не так эффективно, как внедрение статических методов. Только не используйте первые, если в этом нет особой необходимости. Более того, внедрение методов экземпляра запечатанного типа немного снижает накладные расходы, поскольку есть больше вещей, которые нужно сделать только один раз. Хорошо, здесь нет ничего нового.
Еще одна вещь: чтобы быть более хакерским и использовать меньше церемоний расширения методов, можно вводить свойства непосредственно в модели.
public class Model
{
public double Time { get ; set ; }
public double Distance { get ; set ; }
[ InjectLambda ]
public double Velocity => Distance / Time ;
public static Expression < Func < Model , double > > VelocityExpr => v => v . Distance / v . Time ;
}
Опять же, вместо того, чтобы размещать [InjectLambda]
везде, можно добавить все модели в зеленый список при вызове ToInjectable
.
Мы пишем 2024 год, и нам все еще приходится беспокоиться о нулевых значениях.
Однако мы привыкли и у нас все в порядке. Но писать запросы на C#, нагруженные проверками на null, кажется неправильным, это выглядит просто ужасно, а переведенный SQL становится еще хуже. Запрос LINQ только для баз данных SQL может избавить от этих проверок на null, запрос LINQ только для вычислений в памяти должен включать их. И запрос LINQ для обоих имеет проблему (модульное тестирование?), которую NeinLinq пытается решить.
Следующий запрос может вызвать появление нулевых ссылок:
from a in data
orderby a . SomeInteger
select new
{
Year = a . SomeDate . Year ,
Integer = a . SomeOther . SomeInteger ,
Others = from b in a . SomeOthers
select b . SomeDate . Month ,
More = from c in a . MoreOthers
select c . SomeOther . SomeDate . Day
}
Хотя следующий запрос не должен :
from a in data
where a != null
orderby a . SomeInteger
select new
{
Year = a . SomeDate . Year ,
Integer = a . SomeOther != null
? a . SomeOther . SomeInteger
: 0 ,
Others = a . SomeOthers != null
? from b in a . SomeOthers
select b . SomeDate . Month
: null ,
More = a . MoreOthers != null
? from c in a . MoreOthers
select c . SomeOther != null
? c . SomeOther . SomeDate . Day
: 0
: null
}
Может быть, мы забыли какой-то чек? Или можем расслабиться благодаря NeinLinq :
from a in data . ToNullsafe ( )
orderby a . SomeInteger
select new
{
Year = a . SomeDate . Year ,
Integer = a . SomeOther . SomeInteger ,
Others = from b in a . SomeOthers
select b . SomeDate . Month ,
More = from c in a . MoreOthers
select c . SomeOther . SomeDate . Day
}
Как и любой помощник ToWhatever
в NeinLinq , ToNullsafe
можно вызывать в любом месте цепочки запросов LINQ.
Многим приложениям, управляемым данными, необходимо создавать какие-то динамические запросы. Это может привести к грязным манипуляциям со строками, сложной сантехнике дерева выражений или их комбинации. Простые и / или -союзы уже решаются в рамках других библиотек, а вот с союзами «чужих» предикатов все не так просто.
Давайте подумаем о трех сущностях: в Академии есть курсы, в курсах есть лекции.
Expression < Func < Course , bool > > p = c => .. .
Expression < Func < Course , bool > > q = c => .. .
db . Courses . Where ( p . And ( q ) ) .. .
Хорошо, мы это уже знаем.
Expression < Func < Academy , bool > > p = a => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( c => c . Academy ) ) .. .
Теперь мы можем перевести (комбинированный) предикат для родительской сущности...
Expression < Func < Lecture , bool > > p = l => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( ( c , q ) => c . Lectures . Any ( q ) ) ) .. .
...и даже для дочерних объектов.
Давайте использовать все это в качестве завершающего этапа:
IEnumerable < Expression < Func < Academy , bool > > > predicatesForAcademy = .. .
IEnumerable < Expression < Func < Course , bool > > > predicatesForCourse = .. .
IEnumerable < Expression < Func < Lecture , bool > > > predicatesForLecture = .. .
var singlePredicateForAcademy =
predicatesForAcademy . Aggregate ( ( p , q ) => p . And ( q ) ) ;
var singlePredicateForCourse =
predicatesForCourse . Aggregate ( ( p , q ) => p . And ( q ) ) ;
var singlePredicateForLecture =
predicatesForLecture . Aggregate ( ( p , q ) => p . And ( q ) ) ;
var academyPredicateForCourse =
singlePredicateForAcademy . Translate ( )
. To < Course > ( c => c . Academy ) ;
var coursePredicateForCourse =
singlePredicateForCourse ; // the hard one ^^
var lecturePredicateForCourse =
singlePredicateForLecture . Translate ( )
. To < Course > ( ( c , p ) => c . Lectures . Any ( p ) ) ;
var finalPredicate =
academyPredicateForCourse . And ( coursePredicateForCourse )
. And ( lecturePredicateForCourse ) ;
db . Courses . Where ( finalPredicate ) .. .
Кроме того, для этого не используется Invoke : многие поставщики LINQ его не поддерживают ( Entity Framework , я смотрю на вас...), поэтому это решение должно быть вполне совместимым.
Как и в случае с предикатами, селекторы тоже нуждаются в некоторой любви. Если у нас есть существующий селектор для какого-то базового типа и мы хотим повторно использовать этот код для одного или нескольких конкретных типов, нам придется скопировать и вставить еще раз. Не делай этого!
Давайте подумаем о двух объектах (Academy и SpecialAcademy) с соответствующими контрактами/ViewModels/DTO/что угодно (AcademyView и SpecialAcademyView).
Expression < Func < Academy , AcademyView > > s =
a => new AcademyView { Id = a . Id , Name = a . Name } ;
Expression < Func < SpecialAcademy , SpecialAcademyView > > t =
a => new SpecialAcademyView { Narf = a . Narf } ;
Обратите внимание, что мы опускаем привязки членов первого селектора во втором. Не повторяйся, помнишь?
db . Academies . OfType < SpecialAcademy > ( )
. Select ( s . Translate ( )
. Cross < SpecialAcademy > ( )
. Apply ( t ) ) ;
Хотя вариантов больше, общий сценарий может выглядеть следующим образом: повторно использовать базовый селектор, начать его перевод (выведение типа), сказать, с чего начать (без вывода типа) и, наконец, применить дополнительный селектор (снова выведение типа). .
Теперь рассмотрим отношения родитель/потомок (Академия и Курс).
Expression < Func < Academy , AcademyView > > s =
a => new AcademyView { Id = a . Id , Name = a . Name } ;
Expression < Func < Course , CourseView > > t =
c => new CourseView { Id = c . Id , Name = c . Name } ;
db . Courses . Select ( s . Translate ( )
. Cross < Course > ( c => c . Academy )
. Apply ( c => c . Academy , t ) ) ;
db . Academies . Select ( t . Translate ( )
. Cross < Academy > ( ( a , v ) => a . Courses . Select ( v ) )
. Apply ( a => a . Courses , s ) ) ;
Опять же, помимо других опций, мы можем транслировать от родительского объекта к дочернему: повторно использовать родительский селектор, запустить его перевод, сказать, с чего начать (указав путь к его родительскому объекту) и, наконец, применить дополнительный селектор (указав путь к его родительскому объекту). родительский «представление»). И мы можем перевести и другим способом: повторно использовать дочерний селектор, запустить его перевод, сказать, с чего начать (при наличии выражения для выбора дочерних элементов) и, наконец, применить дополнительный селектор...
Для большей гибкости «Перевод источника»/«Перевод результата» можно использовать индивидуально:
Expression < Func < Academy , AcademyView > > selectAcademy =
a => new AcademyView { Id = a . Id , Name = a . Name } ;
var selectCourseWithAcademy =
selectAcademy . Translate ( )
. Source < Course > ( c => c . Academy )
. Translate ( )
. Result < CourseView > ( c => c . Academy )
. Apply ( a => new CourseView
{
Id = a . Id ,
Name = a . Name
} ) ;
Примечание. Чтобы быть менее многословным, «Перевод источника»/«Перевод результата» можно использовать в одном раздутом операторе, если это необходимо: