يوفر NeinLinq امتدادات مفيدة لاستخدام موفري LINQ مثل Entity Framework الذي يدعم فقط مجموعة فرعية صغيرة من وظائف .NET، وإعادة استخدام الوظائف، وإعادة كتابة الاستعلامات، وحتى جعلها خالية من الأخطاء، وإنشاء استعلامات ديناميكية باستخدام المسندات والمحددات القابلة للترجمة.
لدعم تطبيقات 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 من خلال 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()
) وتم وضع علامة على الوظيفة المستخدمة في هذا الاستعلام على أنها "inject here" ( [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
من 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.
تحتاج العديد من التطبيقات المعتمدة على البيانات إلى إنشاء نوع من الاستعلامات الديناميكية. يمكن أن يؤدي هذا إلى معالجة سلسلة قذرة، أو سباكة شجرة التعبير المعقدة، أو مزيج من ذلك. لقد تم بالفعل حل الاقترانات البسيطة و / أو -في المكتبات الأخرى، ولكن اقترانات المسندات "الأجنبية" ليست بهذه السهولة.
دعونا نفكر في ثلاثة كيانات: الأكاديمية لديها دورات، والدورات لها محاضرات.
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 ) .. .
بالإضافة إلى ذلك، لا يتم استخدام استدعاء لتحقيق ذلك: العديد من موفري 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 } ;
لاحظ أننا نحذف روابط الأعضاء الخاصة بالمحدد الأول ضمن المحدد الثاني. لا تكرر نفسك، أتذكر؟
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
} ) ;
ملحوظة: لكي تكون أقل إسهابًا، يمكن استخدام "ترجمة المصدر"/"ترجمة النتيجة" ضمن عبارة واحدة متضخمة، إذا كان ذلك مناسبًا: