NeinLinq提供了有用的扩展,可用于使用 LINQ 提供程序(例如仅支持 .NET 函数的一小部分的实体框架)、重用函数、重写查询,甚至使它们成为 null 安全,以及使用可翻译谓词和选择器构建动态查询。
为了支持不同的 LINQ 实现,可以使用以下风格。至少选择一项。
使用NeinLinq进行普通 LINQ 查询:
PM > Install-Package NeinLinq
使用NeinLinq.Async进行异步 LINQ 查询:
PM > Install-Package NeinLinq.Async
使用NeinLinq.EntityFramework进行 Entity Framework 6 LINQ 查询:
PM > Install-Package NeinLinq.EntityFramework
使用NeinLinq.EntityFrameworkCore进行 Entity Framework Core LINQ 查询:
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 ...
LINQ to Entities 无法识别“System.String LimitText(System.String, Int32)”方法,并且此方法无法转换为存储表达式。
这就是我们得到的;事实上,这真的很烦人。我们必须将逻辑分散在将由任何 LINQ 查询提供程序翻译的代码和不会由任何 LINQ 查询提供程序翻译的代码之间。情况变得更糟:如果某些逻辑是“可翻译的”,这很好,我们必须复制和粘贴!将代码合并到普通函数中是行不通的,因为提供者无法转换这个简单的方法调用。嗯。
让我们介绍一下“lambda注入”:
[ 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的重写引擎会用匹配的 lambda 表达式替换方法调用,其中可以翻译成 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
类,以便在我们的查询代码中使用(希望)更好的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 ) ;
} ) ;
最后,让我们使用实体框架等来看看这个查询:
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 ( )
{
.. .
}
}
注意:注入实例方法不如注入静态方法有效。如果不是真的有必要,请不要使用前面的那些。此外,注入密封类型的实例方法会稍微减少开销,因为有更多的事情只需要完成一次。好吧,这里没什么新的可说的。
另一件事:为了更加 hacky 并使用更少的扩展方法仪式,可以直接在模型中注入属性。
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 ;
}
同样,在调用ToInjectable
时,可以将所有模型添加到绿色列表中,而不是将[InjectLambda]
放在所有内容上。
我们正在编写 2024 年,但仍然需要担心空值。
不过,我们已经习惯了,一切都很好。但是用 C# 编写带有 null 检查的查询感觉不太对,它看起来很糟糕,翻译后的 SQL 甚至变得更糟。仅用于 SQL 数据库的 LINQ 查询可以省去这些空检查,仅用于内存计算的 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
}
与NeinLinq中的每个ToWhatever
帮助器一样,可以在 LINQ 查询链中的任何位置调用ToNullsafe
。
许多数据驱动的应用程序需要构建某种动态查询。这可能会导致脏字符串操作、复杂的表达式树管道或这些的组合。简单的和/或-连词已经在其他库中解决了,但是“外国”谓词的连词并不那么容易。
让我们考虑三个实体:学院有课程,课程有讲座。
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 提供程序不支持它(实体框架,我正在看着你......),因此这个解决方案应该非常兼容。
与谓词选择器一样,选择器也需要一些爱。如果我们有某种基本类型的现有选择器,并且想要为一种或多种具体类型重用此代码,我们将被迫再次复制和粘贴。不要那样做!
让我们考虑两个实体(Academy 和 SpecialAcademy)以及相应的 Contracts / 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
} ) ;
注意:为了不那么冗长,如果合适的话,可以在单个臃肿的语句中使用“源翻译”/“结果翻译”: