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# 寫有空檢查的查詢感覺不太對,它看起來很糟糕,翻譯後的 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
} ) ;
注意:為了不那麼冗長,如果適當的話,可以在單一臃腫的語句中使用「來源翻譯」/「結果翻譯」: