NeinLinq มอบส่วนขยายที่เป็นประโยชน์สำหรับการใช้ผู้ให้บริการ LINQ เช่น Entity Framework ที่รองรับเฉพาะฟังก์ชัน .NET บางส่วนเท่านั้น การใช้ฟังก์ชันซ้ำ การเขียนคิวรีใหม่ แม้แต่การทำให้คิวรีปลอดภัยเป็นโมฆะ และการสร้างคิวรีแบบไดนามิกโดยใช้เพรดิเคตและตัวเลือกที่แปลได้
เพื่อรองรับการใช้งาน LINQ ที่แตกต่างกัน จึงมีให้เลือกใช้งานดังต่อไปนี้ เลือกอย่างน้อยหนึ่งรายการ
ใช้ NeinLinq สำหรับการสืบค้น LINQ ธรรมดา:
PM > Install-Package NeinLinq
ใช้ NeinLinq.Async สำหรับการสืบค้น 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 injector ทั่วโลก:
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 )
}
หากแบบสอบถามถูกทำเครื่องหมายเป็น "injectable" ( ToInjectable()
) และฟังก์ชันที่ใช้ในแบบสอบถามนี้ถูกทำเครื่องหมายเป็น "inject here" ( [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 ไม่มีข้อกำหนดเฉพาะ อย่าลังเลที่จะใช้ "Expression Cache" ในตัวหรือสร้างสิ่งแฟนซี... ):
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# ที่โหลดด้วยการตรวจสอบค่าว่างนั้นรู้สึกไม่ถูกต้อง มันแค่ดูแย่มาก และ SQL ที่แปลก็แย่ลงไปอีก การสืบค้น LINQ สำหรับ SQL dbs เท่านั้นสามารถสำรองการตรวจสอบค่าว่างเหล่านี้ได้ การสืบค้น 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
แอปพลิเคชันที่ขับเคลื่อนด้วยข้อมูลจำนวนมากจำเป็นต้องสร้างแบบสอบถามแบบไดนามิกบางประเภท ซึ่งอาจนำไปสู่การยักยอกสายที่สกปรก การวางท่อต้นไม้ที่ซับซ้อน หรือทั้งสองอย่างรวมกัน คำสันธานแบบง่าย และ / หรือ - ได้รับการแก้ไขแล้วในไลบรารีอื่น แต่คำสันธานของเพรดิเคต "ต่างประเทศ" นั้นไม่ใช่เรื่องง่าย
ลองนึกถึงสามสิ่ง: Academy มีหลักสูตร, Courses มีการบรรยาย
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 ) .. .
นอกจากนั้น ไม่มีการเรียกใช้ Inrigg เพื่อให้บรรลุเป้าหมายดังกล่าว: ผู้ให้บริการ LINQ จำนวนมากไม่รองรับ ( Entity Framework ฉันกำลังมองคุณอยู่...) ดังนั้นโซลูชันนี้จึงค่อนข้างเข้ากันได้
เช่นเดียวกับภาคแสดง ซีเลกเตอร์ก็ต้องการความรักเช่นกัน หากเรามีตัวเลือกอยู่แล้วสำหรับประเภทพื้นฐานบางประเภท และต้องการใช้โค้ดนี้ซ้ำกับประเภทที่เป็นรูปธรรมตั้งแต่หนึ่งประเภทขึ้นไป เราจะถูกบังคับให้คัดลอกและวางอีกครั้ง อย่าทำอย่างนั้น!
ให้เราคิดถึงสองเอนทิตี (Academy และ SpecialAcademy) ตามสัญญา / ViewModels / DTOs / อะไรก็ตาม (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 } ;
โปรดทราบว่าเราละเว้น การเชื่อมโยง Member ของตัวเลือกตัวแรกภายในตัวเลือกที่สอง อย่าพูดซ้ำตัวเองจำได้ไหม?
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
} ) ;
หมายเหตุ: หากต้องการใช้ "การแปลแหล่งที่มา" / "การแปลผลลัพธ์" แบบละเอียดน้อยลง สามารถใช้ภายในคำสั่งเดียวได้ หากเหมาะสม: