NeinLinq menyediakan ekstensi yang berguna untuk menggunakan penyedia LINQ seperti Entity Framework yang hanya mendukung sebagian kecil fungsi .NET, menggunakan kembali fungsi, menulis ulang kueri, bahkan menjadikannya aman untuk null, dan membuat kueri dinamis menggunakan predikat dan penyeleksi yang dapat diterjemahkan.
Untuk mendukung implementasi LINQ yang berbeda, tersedia varian berikut. Pilih setidaknya satu.
Gunakan NeinLinq untuk pertanyaan LINQ biasa:
PM > Install-Package NeinLinq
Gunakan NeinLinq.Async untuk kueri LINQ asinkron:
PM > Install-Package NeinLinq.Async
Gunakan NeinLinq.EntityFramework untuk kueri LINQ Entity Framework 6:
PM > Install-Package NeinLinq.EntityFramework
Gunakan NeinLinq.EntityFrameworkCore untuk kueri LINQ Entity Framework Core:
PM > Install-Package NeinLinq.EntityFrameworkCore
Catatan: metode ekstensi yang dijelaskan di bawah ini memiliki nama berbeda tergantung pada paket yang dipilih di atas, untuk menghindari beberapa konflik! Misalnya ada:
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )Penggunaan ragam tertentu dianjurkan untuk EF6/EFCore (jika tidak, kueri asinkron tidak akan berfungsi).
Baru: dengan Versi 5.1.0
paket NeinLinq.EntityFrameworkCore memperkenalkan ekstensi DbContext
eksplisit untuk mengaktifkan injeksi Lambda secara global:
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
Catatan: panggilan ke WithLambdaInjection
perlu dilakukan setelah panggilan ke UseSqlOrTheLike
!
Banyak penyedia LINQ hanya dapat mendukung sebagian kecil dari fungsionalitas .NET, mereka bahkan tidak dapat mendukung "fungsi" kami sendiri. Katakanlah, kita menerapkan metode sederhana LimitText
dan menggunakannya dalam kueri LINQ biasa, yang akan diterjemahkan ke SQL melalui Entity Framework ...
LINQ ke Entitas tidak mengenali metode metode 'System.String LimitText(System.String, Int32)', dan metode ini tidak dapat diterjemahkan ke dalam ekspresi penyimpanan.
Inilah yang kami dapatkan; sebenarnya, itu sangat menjengkelkan. Kita harus menyebarkan logika kita antara kode, yang akan diterjemahkan oleh penyedia kueri LINQ mana pun, dan kode, yang tidak akan diterjemahkan. Lebih buruk lagi: jika ada logika yang "dapat diterjemahkan", dan itu bagus, kita harus menyalin dan menempel! Menggabungkan kode dalam fungsi biasa tidak berfungsi karena penyedia tidak dapat menerjemahkan panggilan metode sederhana ini. Ya ampun.
Mari kita perkenalkan "injeksi 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 )
}
Jika kueri ditandai sebagai "injectable" ( ToInjectable()
) dan fungsi yang digunakan dalam kueri ini ditandai sebagai "inject here" ( [InjectLambda]
), mesin penulisan ulang NeinLinq akan menggantikan pemanggilan metode dengan ekspresi lambda yang cocok, yang mana bisa menerjemahkan ke SQL atau apa pun. Dengan demikian, kami dapat merangkum fungsionalitas .NET yang tidak didukung dan bahkan membuatnya sendiri. Yay.
[ 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 .. .
Ini adalah contoh bagaimana kita dapat mengabstraksi kelas SqlFunctions
dari Entity Framework untuk menggunakan metode ekstensi Like
yang (mudah-mudahan) lebih bagus dalam kode kueri kita -- PatIndex
kemungkinan digunakan untuk mensimulasikan pernyataan SQL LIKE, mengapa tidak membuatnya begitu? Kita sebenarnya dapat mengimplementasikan metode "biasa" dengan bantuan ekspresi reguler untuk menjalankan kode kita tanpa menyentuh SqlFunctions
juga...
Baru: dengan Versi 7.0.0
NeinLinq sekarang mendukung penyedia khusus untuk InjectLambdaAttribute. Fitur ini memungkinkan Anda menentukan logika Anda sendiri untuk menentukan metode mana yang dianggap dapat disuntikkan tanpa menambahkan atribut [InjectLambda]
secara eksplisit. Ini sangat berguna ketika bekerja dengan perpustakaan eksternal atau kode yang tidak dapat Anda modifikasi secara langsung:
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 ) ;
} ) ;
Terakhir, mari kita lihat query ini menggunakan Entity Framework atau sejenisnya:
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 ) => .. .
}
Metode RetrieveWhatever
, FulfillsSomeCriteria
dan DoTheFancy
harus ditandai dengan tepat, menggunakan atribut [InjectLambda]
atau hanya konvensi sederhana "kelas yang sama, nama yang sama, tanda tangan yang cocok" (yang mengharuskan kelas tersebut masuk dalam daftar hijau). Dan panggilan ToInjectable
bisa terjadi di mana saja dalam rantai kueri LINQ, jadi kita tidak perlu mencemari logika bisnis kita.
Catatan: duplikasi kode tidak diperlukan. Metode biasa hanya dapat mengkompilasi ekspresi, idealnya hanya sekali. Solusi langsung dapat terlihat seperti contoh kode berikut (dimungkinkan untuk merangkum / mengatur hal-hal ini betapapun canggihnya tampaknya cocok, NeinLinq tidak memiliki persyaratan khusus; silakan gunakan "Cache Ekspresi" bawaan atau buat sesuatu yang mewah... ):
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 ) ;
Lanjutan: yang juga berfungsi dengan metode instan, sehingga kode ekspresi sebenarnya dapat mengambil data tambahan. Bahkan antarmuka dan/atau kelas dasar dapat digunakan untuk mengabstraksi semua hal. Dengan demikian, kita dapat mendeklarasikan antarmuka/kelas dasar tanpa ekspresi, tetapi menyediakan ekspresi untuk disuntikkan menggunakan warisan.
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 ( )
{
.. .
}
}
Catatan : memasukkan metode instan tidak seefisien memasukkan metode statis. Hanya saja, jangan gunakan yang pertama, jika tidak terlalu diperlukan. Selain itu, memasukkan metode instance dari tipe tersegel sedikit mengurangi overhead, karena ada lebih banyak hal yang hanya perlu dilakukan satu kali. Oke, tidak ada hal baru untuk dikatakan di sini.
Satu hal lagi: agar lebih hacky dan menggunakan lebih sedikit upacara metode ekstensi, dimungkinkan untuk memasukkan properti langsung ke dalam model.
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 ;
}
Sekali lagi, alih-alih menempatkan [InjectLambda]
pada segala hal, Anda dapat menambahkan semua model ke daftar hijau sambil memanggil ToInjectable
.
Kami menulis tahun 2024 dan masih harus mengkhawatirkan nilai nol.
Namun, kami sudah terbiasa dan kami baik-baik saja. Tetapi menulis kueri dalam C# yang dimuat dengan pemeriksaan nol rasanya tidak benar, hanya terlihat buruk, SQL yang diterjemahkan bahkan menjadi lebih buruk. Kueri LINQ hanya untuk SQL dbs dapat menghindari pemeriksaan nol ini, kueri LINQ hanya untuk penghitungan dalam memori harus menyertakannya. Dan kueri LINQ untuk keduanya memiliki masalah (pengujian unit?), yang coba dipecahkan oleh NeinLinq .
Kueri berikut mungkin memicu referensi nol:
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
}
Sedangkan pertanyaan berikut tidak seharusnya :
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
}
Mungkin kita lupa beberapa cek? Atau kita bisa bersantai berkat 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
}
Seperti halnya setiap pembantu ToWhatever
dalam NeinLinq , ToNullsafe
dapat dipanggil di mana pun dalam rantai kueri LINQ.
Banyak aplikasi berbasis data perlu membuat semacam kueri dinamis. Hal ini dapat menyebabkan manipulasi string yang kotor, ekspresi pohon yang rumit, atau kombinasi dari semuanya. Konjungsi sederhana dan / atau - sudah diselesaikan di perpustakaan lain, tetapi konjungsi predikat "asing" tidak semudah itu.
Mari kita pikirkan tiga entitas: Akademi memiliki Kursus, Kursus memiliki Kuliah.
Expression < Func < Course , bool > > p = c => .. .
Expression < Func < Course , bool > > q = c => .. .
db . Courses . Where ( p . And ( q ) ) .. .
Oke, kita sudah mengetahuinya.
Expression < Func < Academy , bool > > p = a => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( c => c . Academy ) ) .. .
Kami sekarang dapat menerjemahkan predikat (gabungan) untuk entitas induk...
Expression < Func < Lecture , bool > > p = l => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( ( c , q ) => c . Lectures . Any ( q ) ) ) .. .
..dan bahkan untuk entitas anak.
Mari kita gunakan semua ini sebagai penutup:
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 ) .. .
Selain itu, tidak ada Invoke yang digunakan untuk mencapai hal itu: banyak penyedia LINQ tidak mendukungnya ( Entity Framework , saya melihat Anda...), jadi solusi ini seharusnya cukup kompatibel.
Seperti halnya penyeleksi predikat juga membutuhkan perhatian. Jika kita sudah memiliki pemilih untuk beberapa tipe dasar dan ingin menggunakan kembali kode ini untuk satu atau lebih tipe konkret, kita terpaksa menyalin dan menempelkannya lagi. Jangan lakukan itu!
Mari kita pikirkan dua entitas (Academy dan SpecialAcademy) dengan Kontrak/ViewModels/DTO/Apa pun yang sesuai (AcademyView dan 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 } ;
Perhatikan bahwa kami menghilangkan pengikatan Anggota dari pemilih pertama dalam yang kedua. Jangan ulangi lagi, ingat?
db . Academies . OfType < SpecialAcademy > ( )
. Select ( s . Translate ( )
. Cross < SpecialAcademy > ( )
. Apply ( t ) ) ;
Meskipun ada lebih banyak pilihan, skenario umum dapat terlihat seperti itu: gunakan kembali pemilih dasar, mulai terjemahannya (ketik inferensi), sebutkan di mana harus memulai (tidak ada inferensi jenis), dan terakhir terapkan pemilih tambahan (ketik inferensi, lagi) .
Sekarang mari kita pertimbangkan hubungan orang tua/anak (Akademi dan Kursus).
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 ) ) ;
Sekali lagi, selain opsi lain, kita dapat menerjemahkan dari induk ke anak: menggunakan kembali pemilih induk, memulai penerjemahannya, menyebutkan di mana harus memulai (mengingat jalur ke entitas induknya), dan terakhir menerapkan pemilih tambahan (mengingat jalur ke entitas induknya), dan terakhir menerapkan pemilih tambahan (mengingat jalur ke entitas induknya). induk "tampilan"). Dan kita juga dapat menerjemahkan dengan cara lain: menggunakan kembali pemilih anak, memulai terjemahannya, menyebutkan di mana harus memulai (diberikan ekspresi untuk memilih anak), dan terakhir menerapkan pemilih tambahan...
Agar lebih fleksibel, "Terjemahan sumber" / "Terjemahan hasil" dapat digunakan secara individual:
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
} ) ;
Catatan: agar tidak terlalu bertele-tele, "Terjemahan sumber" / "Terjemahan hasil" dapat digunakan dalam satu pernyataan yang terlalu besar, jika sesuai: