NeinLinq bietet hilfreiche Erweiterungen für die Verwendung von LINQ-Anbietern wie Entity Framework, die nur eine kleine Teilmenge von .NET-Funktionen unterstützen, Funktionen wiederverwenden, Abfragen neu schreiben, sie sogar nullsicher machen und dynamische Abfragen mithilfe übersetzbarer Prädikate und Selektoren erstellen.
Zur Unterstützung verschiedener LINQ-Implementierungen stehen die folgenden Varianten zur Verfügung. Wählen Sie mindestens eine aus.
Verwenden Sie NeinLinq für einfache LINQ-Abfragen:
PM > Install-Package NeinLinq
Verwenden Sie NeinLinq.Async für asynchrone LINQ-Abfragen:
PM > Install-Package NeinLinq.Async
Verwenden Sie NeinLinq.EntityFramework für Entity Framework 6 LINQ-Abfragen:
PM > Install-Package NeinLinq.EntityFramework
Verwenden Sie NeinLinq.EntityFrameworkCore für Entity Framework Core LINQ-Abfragen:
PM > Install-Package NeinLinq.EntityFrameworkCore
Hinweis: Die unten beschriebenen Erweiterungsmethoden haben je nach oben ausgewähltem Paket unterschiedliche Namen, um Konflikte zu vermeiden! Es gibt zum Beispiel:
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )Die Verwendung bestimmter Varianten wird für EF6/EFCore empfohlen (andernfalls funktionieren asynchrone Abfragen nicht).
Neu: Mit Version 5.1.0
führte das Paket NeinLinq.EntityFrameworkCore eine explizite DbContext
Erweiterung ein, um die Lambda-Injection global zu ermöglichen:
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
Hinweis: Der Aufruf von WithLambdaInjection
muss nach dem Aufruf von UseSqlOrTheLike
erfolgen!
Viele LINQ-Anbieter können nur einen sehr kleinen Teil der .NET-Funktionalität unterstützen, sie können nicht einmal unsere eigenen „Funktionen“ unterstützen. Angenommen, wir implementieren eine einfache Methode LimitText
und verwenden sie in einer gewöhnlichen LINQ-Abfrage, die über Entity Framework in SQL übersetzt wird ...
LINQ to Entities erkennt die Methode „System.String LimitText(System.String, Int32)“ nicht und diese Methode kann nicht in einen Speicherausdruck übersetzt werden.
Das ist es, was wir bekommen; Tatsächlich ist es wirklich nervig. Wir müssen unsere Logik zwischen Code, der von jedem LINQ-Abfrageanbieter übersetzt wird, und Code, der dies nicht übersetzt, aufteilen. Es kommt noch schlimmer: Wenn eine Logik „übersetzbar“ ist, was gut ist, müssen wir sie kopieren und einfügen! Das Konsolidieren des Codes innerhalb einer gewöhnlichen Funktion funktioniert nicht, da der Anbieter diesen einfachen Methodenaufruf nicht übersetzen kann. Meh.
Lassen Sie uns die „Lambda-Einspritzung“ vorstellen:
[ 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 )
}
Wenn eine Abfrage als „injizierbar“ ( ToInjectable()
) markiert ist und eine in dieser Abfrage verwendete Funktion als „hier injizieren“ ( [InjectLambda]
) markiert ist, ersetzt die Rewrite-Engine von NeinLinq den Methodenaufruf durch den passenden Lambda-Ausdruck kann in SQL oder was auch immer übersetzt werden. Dadurch sind wir in der Lage, nicht unterstützte .NET-Funktionalitäten zu kapseln und sogar eigene zu erstellen. Ja.
[ 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 .. .
Dies ist ein Beispiel dafür, wie wir die SqlFunctions
-Klasse von Entity Framework abstrahieren können, um eine (hoffentlich) schönere Like
-Erweiterungsmethode in unserem Abfragecode zu verwenden – PatIndex
wird wahrscheinlich verwendet, um eine SQL-LIKE-Anweisung zu simulieren, warum nicht auch so machen? Wir können die „gewöhnliche“ Methode tatsächlich mithilfe regulärer Ausdrücke implementieren, um unseren Code auszuführen, ohne auch SqlFunctions
zu berühren ...
Neu: Mit Version 7.0.0
unterstützt NeinLinq jetzt benutzerdefinierte Anbieter für das InjectLambdaAttribute. Mit dieser Funktion können Sie Ihre eigene Logik definieren, um zu bestimmen, welche Methoden als injizierbar betrachtet werden sollen, ohne das Attribut [InjectLambda]
explizit hinzuzufügen. Dies ist besonders nützlich, wenn Sie mit externen Bibliotheken oder Code arbeiten, den Sie nicht direkt ändern können:
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 ) ;
} ) ;
Schauen wir uns abschließend diese Abfrage mit Entity Framework oder ähnlichem an:
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 ) => .. .
}
Die Methoden RetrieveWhatever
, FulfillsSomeCriteria
und DoTheFancy
sollten entsprechend gekennzeichnet werden, indem das Attribut [InjectLambda]
oder einfach die einfache Konvention „gleiche Klasse, gleicher Name, passende Signatur“ verwendet wird (was übrigens erfordert, dass die Klasse in der grünen Liste aufgeführt ist). Und der Aufruf von ToInjectable
kann überall in der LINQ-Abfragekette erfolgen, sodass wir unsere Geschäftslogik nicht verunreinigen müssen.
Hinweis: Eine Codeduplizierung sollte nicht erforderlich sein. Die gewöhnliche Methode kann den Ausdruck einfach kompilieren, idealerweise nur einmal. Eine einfache Lösung kann wie das folgende Codebeispiel aussehen (es ist möglich, dieses Zeug zu kapseln/organisieren, egal wie anspruchsvoll es passt. NeinLinq hat keine besonderen Anforderungen; Sie können gerne den eingebauten „Expression Cache“ verwenden oder etwas Ausgefallenes erstellen ... ):
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 ) ;
Erweitert: Funktioniert auch mit Instanzmethoden, sodass der eigentliche Ausdruckscode zusätzliche Daten abrufen kann. Sogar Schnittstellen und/oder Basisklassen können verwendet werden, um alle Dinge zu abstrahieren. Somit können wir eine Schnittstelle/Basisklasse ohne Ausdrücke deklarieren, den zu injizierenden Ausdruck jedoch durch Vererbung bereitstellen.
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 ( )
{
.. .
}
}
Hinweis : Das Einfügen von Instanzmethoden ist nicht so effizient wie das Einfügen statischer Methoden. Verwenden Sie die ersteren einfach nicht, wenn sie nicht wirklich notwendig sind. Darüber hinaus reduziert das Einfügen versiegelter Instanzmethoden den Overhead etwas, da mehr Dinge nur einmal erledigt werden müssen. Okay, hier gibt es nichts Neues zu sagen.
Noch etwas: Um hackiger zu sein und weniger zeremonielle Erweiterungsmethoden zu verwenden, ist es möglich, Eigenschaften direkt in Modelle einzufügen.
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 ;
}
Anstatt [InjectLambda]
auf alles zu setzen, ist es wiederum möglich, alle Modelle zur grünen Liste hinzuzufügen, während ToInjectable
aufgerufen wird.
Wir schreiben das Jahr 2024 und müssen uns immer noch um Nullwerte kümmern.
Aber wir haben uns daran gewöhnt und es geht uns gut. Aber das Schreiben von Abfragen in C#, die mit Nullprüfungen geladen sind, fühlt sich nicht richtig an, es sieht einfach schrecklich aus, das übersetzte SQL wird sogar noch schlimmer. Eine LINQ-Abfrage nur für SQL-Datenbanken kann diese Nullprüfungen ersparen, eine LINQ-Abfrage nur für In-Memory-Berechnungen muss sie enthalten. Und eine LINQ-Abfrage für beide hat ein Problem (Unit-Test?), das NeinLinq zu lösen versucht.
Die folgende Abfrage kann Nullverweise auslösen:
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
}
Während die folgende Abfrage Folgendes nicht tun sollte :
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
}
Vielleicht haben wir einen Scheck vergessen? Oder wir können dank NeinLinq entspannen:
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
}
Wie jeder ToWhatever
Helfer in NeinLinq kann ToNullsafe
überall in der LINQ-Abfragekette aufgerufen werden.
Viele datengesteuerte Anwendungen müssen dynamische Abfragen erstellen. Dies kann zu unsauberen Zeichenfolgenmanipulationen, komplexer Ausdrucksbaumstruktur oder einer Kombination davon führen. Einfache und / oder -Konjunktionen werden bereits in anderen Bibliotheken gelöst, Konjunktionen „fremder“ Prädikate sind jedoch nicht so einfach.
Stellen wir uns drei Einheiten vor: Die Akademie hat Kurse, die Kurse haben Vorlesungen.
Expression < Func < Course , bool > > p = c => .. .
Expression < Func < Course , bool > > q = c => .. .
db . Courses . Where ( p . And ( q ) ) .. .
Ok, das wissen wir schon.
Expression < Func < Academy , bool > > p = a => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( c => c . Academy ) ) .. .
Wir können jetzt ein (kombiniertes) Prädikat für eine übergeordnete Entität übersetzen ...
Expression < Func < Lecture , bool > > p = l => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( ( c , q ) => c . Lectures . Any ( q ) ) ) .. .
..und sogar für untergeordnete Entitäten.
Lassen Sie uns das alles als Zusammenfassung nutzen:
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 ) .. .
Darüber hinaus wird kein Invoke verwendet, um dies zu erreichen: Viele LINQ-Anbieter unterstützen es nicht ( Entity Framework , ich sehe Sie ...), daher sollte diese Lösung ziemlich kompatibel sein.
Wie bei Prädikaten brauchen auch Selektoren etwas Liebe. Wenn wir einen vorhandenen Selektor für einen Basistyp haben und diesen Code für einen oder mehrere konkrete Typen wiederverwenden möchten, müssen wir ihn erneut kopieren und einfügen. Tu das nicht!
Stellen wir uns zwei Entitäten vor (Academy und SpecialAcademy) mit entsprechenden Verträgen/ViewModels/DTOs/Was auch immer (AcademyView und 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 } ;
Beachten Sie, dass wir die Member-Bindungen des ersten Selektors im zweiten weglassen. Wiederholen Sie sich nicht, schon vergessen?
db . Academies . OfType < SpecialAcademy > ( )
. Select ( s . Translate ( )
. Cross < SpecialAcademy > ( )
. Apply ( t ) ) ;
Obwohl es mehr Optionen gibt, kann das übliche Szenario so aussehen: Den Basisselektor wiederverwenden, mit der Übersetzung beginnen (Typinferenz), sagen, wo begonnen werden soll (keine Typinferenz) und schließlich den zusätzlichen Selektor anwenden (wieder Typinferenz). .
Betrachten wir nun die Eltern-Kind-Beziehungen (Akademie und Kurs).
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 ) ) ;
Abgesehen von anderen Optionen können wir auch hier vom übergeordneten zum untergeordneten Element übersetzen: den übergeordneten Selektor wiederverwenden, mit der Übersetzung beginnen, sagen, wo beginnen soll (angegebener Pfad zur übergeordneten Entität) und schließlich den zusätzlichen Selektor anwenden (angegebener Pfad zu seiner übergeordneten Entität). übergeordnete „Ansicht“). Und wir können auch in die andere Richtung übersetzen: den untergeordneten Selektor wiederverwenden, mit der Übersetzung beginnen, sagen, wo beginnen soll (bei gegebenem Ausdruck zur Auswahl der untergeordneten Elemente) und schließlich den zusätzlichen Selektor anwenden ...
Um flexibler zu sein, kann die „Quellenübersetzung“ / „Ergebnisübersetzung“ einzeln verwendet werden:
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
} ) ;
Hinweis: Um weniger ausführlich zu sein, kann „Quellenübersetzung“/„Ergebnisübersetzung“ gegebenenfalls in einer einzigen aufgeblähten Anweisung verwendet werden: