NeinLinq fournit des extensions utiles pour l'utilisation de fournisseurs LINQ tels que Entity Framework qui ne prennent en charge qu'un sous-ensemble mineur de fonctions .NET, réutilisant des fonctions, réécrivant des requêtes, les rendant même nulles et créant des requêtes dynamiques à l'aide de prédicats et de sélecteurs traduisibles.
Pour prendre en charge différentes implémentations LINQ, les versions suivantes sont disponibles. Choisissez-en au moins un.
Utilisez NeinLinq pour les requêtes LINQ simples :
PM > Install-Package NeinLinq
Utilisez NeinLinq.Async pour les requêtes asynchrones LINQ :
PM > Install-Package NeinLinq.Async
Utilisez NeinLinq.EntityFramework pour les requêtes Entity Framework 6 LINQ :
PM > Install-Package NeinLinq.EntityFramework
Utilisez NeinLinq.EntityFrameworkCore pour les requêtes Entity Framework Core LINQ :
PM > Install-Package NeinLinq.EntityFrameworkCore
Remarque : les méthodes d'extension décrites ci-dessous portent des noms différents selon le package choisi ci-dessus, afin d'éviter certains conflits ! Il y a par exemple :
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )L'utilisation de versions spécifiques est encouragée pour EF6/EFCore (sinon les requêtes asynchrones ne fonctionneront pas).
Nouveau : avec la version 5.1.0
le package NeinLinq.EntityFrameworkCore a introduit une extension DbContext
explicite pour activer l'injection Lambda globalement :
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
Remarque : l'appel à WithLambdaInjection
doit avoir lieu après l'appel à UseSqlOrTheLike
!
De nombreux fournisseurs LINQ ne peuvent prendre en charge qu'un sous-ensemble très mineur de fonctionnalités .NET ; ils ne peuvent même pas prendre en charge nos propres « fonctions ». Disons que nous implémentons une méthode simple LimitText
et l'utilisons dans une requête LINQ ordinaire, qui sera traduite en SQL via Entity Framework ...
LINQ to Entities ne reconnaît pas la méthode « System.String LimitText(System.String, Int32) » et cette méthode ne peut pas être traduite en une expression de magasin.
C'est ce que nous obtenons ; en fait, c'est vraiment ennuyeux. Nous devons disperser notre logique entre le code, qui sera traduit par n'importe quel fournisseur de requêtes LINQ, et le code, qui ne le sera pas. C'est encore pire : si une logique est « traduisible », ce qui est bien, il faut copier-coller ! Consolider le code au sein d'une fonction ordinaire ne fonctionne pas puisque le fournisseur est incapable de traduire cet appel de méthode simple. Meh.
Introduisons "l'injection 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 )
}
Si une requête est marquée comme « injectable » ( ToInjectable()
) et qu'une fonction utilisée dans cette requête est marquée comme « injecter ici » ( [InjectLambda]
), le moteur de réécriture de NeinLinq remplace l'appel de méthode par l'expression lambda correspondante, qui peut être traduit en SQL ou autre. Ainsi, nous sommes en mesure d'encapsuler des fonctionnalités .NET non prises en charge et même de créer les nôtres. Ouais.
[ 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 .. .
Ceci est un exemple de la façon dont nous pouvons faire abstraction de la classe SqlFunctions
d' Entity Framework pour utiliser une méthode d'extension Like
(espérons-le) plus agréable dans notre code de requête - PatIndex
est probablement utilisé pour simuler une instruction SQL LIKE, pourquoi ne pas en faire ainsi ? Nous pouvons en fait implémenter la méthode "ordinaire" à l'aide d'expressions régulières pour exécuter notre code sans trop toucher SqlFunctions
...
Nouveau : avec la version 7.0.0
NeinLinq prend désormais en charge les fournisseurs personnalisés pour InjectLambdaAttribute. Cette fonctionnalité vous permet de définir votre propre logique pour déterminer quelles méthodes doivent être considérées comme injectables sans ajouter explicitement l'attribut [InjectLambda]
. Ceci est particulièrement utile lorsque vous travaillez avec des bibliothèques externes ou du code que vous ne pouvez pas modifier directement :
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 ) ;
} ) ;
Enfin, examinons cette requête en utilisant Entity Framework ou similaire :
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 ) => .. .
}
Les méthodes RetrieveWhatever
, FulfillsSomeCriteria
et DoTheFancy
doivent être marquées en conséquence, en utilisant l'attribut [InjectLambda]
ou simplement la simple convention "même classe, même nom, signature correspondante" (qui nécessite d'ailleurs que la classe soit répertoriée en vert). Et l'appel ToInjectable
peut avoir lieu n'importe où dans la chaîne de requêtes LINQ, nous n'avons donc pas à polluer notre logique métier.
Remarque : la duplication de code ne devrait pas être nécessaire. La méthode ordinaire permet simplement de compiler l’expression, idéalement une seule fois. Une solution simple peut ressembler à l'exemple de code suivant (il est possible d'encapsuler/organiser ce contenu aussi sophistiqué que cela semble approprié, NeinLinq n'a pas d'exigences spécifiques ; n'hésitez pas à utiliser le "Cache d'expression" intégré ou à créer quelque chose de sophistiqué... ) :
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 ) ;
Avancé : cela fonctionne également avec les méthodes d'instance, de sorte que le code d'expression réel est capable de récupérer des données supplémentaires. Même les interfaces et/ou les classes de base peuvent être utilisées pour tout abstraire. Ainsi, nous pouvons déclarer une interface/classe de base sans expressions, mais fournir l'expression à injecter en utilisant l'héritage.
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 ( )
{
.. .
}
}
Remarque : l'injection de méthodes d'instance n'est pas aussi efficace que l'injection de méthodes statiques. N'utilisez simplement pas les premiers, si ce n'est pas vraiment nécessaire. De plus, l'injection de méthodes d'instance de type scellé réduit un peu la surcharge, car il y a plus de choses qui ne doivent être faites qu'une seule fois. Bon, rien de nouveau à dire ici.
Encore une chose : pour être plus hacky et utiliser moins de cérémonies de méthodes d'extension, il est possible d'injecter des propriétés directement dans les modèles.
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 ;
}
Encore une fois, au lieu de placer [InjectLambda]
sur tout, il est possible d'ajouter tous les modèles à la liste verte en appelant ToInjectable
.
Nous écrivons l’année 2024 et devons toujours nous soucier des valeurs nulles.
Cependant, nous nous y sommes habitués et tout va bien. Mais écrire des requêtes en C# chargées de vérifications nulles ne semble pas correct, cela a l'air horrible, le SQL traduit est encore pire. Une requête LINQ uniquement pour les bases de données SQL peut épargner ces vérifications nulles, une requête LINQ uniquement pour les calculs en mémoire doit les inclure. Et une requête LINQ pour les deux a un problème (test unitaire ?), que NeinLinq essaie de résoudre.
La requête suivante peut déclencher des références nulles :
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
}
Alors que la requête suivante ne devrait pas :
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
}
Peut-être avons-nous oublié un chèque ? Ou on peut se détendre grâce à 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
}
Comme pour chaque assistant ToWhatever
au sein de NeinLinq , ToNullsafe
peut être appelé n'importe où dans la chaîne de requêtes LINQ.
De nombreuses applications basées sur les données doivent créer une sorte de requêtes dynamiques. Cela peut conduire à des manipulations de chaînes sales, à une plomberie d'arbre d'expression complexe ou à une combinaison de ceux-ci. Les conjonctions simples et / ou - sont déjà résolues dans d'autres bibliothèques, mais les conjonctions de prédicats « étrangers » ne sont pas si simples.
Pensons à trois entités : l'Académie a des cours, les cours ont des conférences.
Expression < Func < Course , bool > > p = c => .. .
Expression < Func < Course , bool > > q = c => .. .
db . Courses . Where ( p . And ( q ) ) .. .
Ok, nous le savons déjà.
Expression < Func < Academy , bool > > p = a => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( c => c . Academy ) ) .. .
Nous pouvons maintenant traduire un prédicat (combiné) pour une entité parent...
Expression < Func < Lecture , bool > > p = l => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( ( c , q ) => c . Lectures . Any ( q ) ) ) .. .
..et même pour les entités enfants.
Utilisons tout cela comme une conclusion :
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 ) .. .
En plus de cela, aucun Invoke n'est utilisé pour y parvenir : de nombreux fournisseurs LINQ ne le supportent pas ( Entity Framework , je vous regarde...), cette solution devrait donc être tout à fait compatible.
Comme pour les prédicats, les sélecteurs ont aussi besoin d’un peu d’amour. Si nous avons un sélecteur existant pour un type de base et que nous souhaitons réutiliser ce code pour un ou plusieurs types concrets, nous sommes obligés de copier et coller à nouveau. Ne fais pas ça !
Pensons à deux entités (Academy et SpecialAcademy) avec des Contrats / ViewModels / DTO / Quoi qu'il en soit (AcademyView et 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 } ;
Notez que nous omettons les liaisons de membres du premier sélecteur dans le second. Ne te répète pas, tu te souviens ?
db . Academies . OfType < SpecialAcademy > ( )
. Select ( s . Translate ( )
. Cross < SpecialAcademy > ( )
. Apply ( t ) ) ;
Bien qu'il existe plus d'options, le scénario courant peut ressembler à ceci : réutiliser le sélecteur de base, démarrer sa traduction (inférence de type), dire par où commencer (pas d'inférence de type) et enfin appliquer le sélecteur supplémentaire (inférence de type, encore une fois) .
Considérons maintenant les relations parents/enfants (Académie et Cours).
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 ) ) ;
Encore une fois, outre d'autres options, nous pouvons traduire du parent vers l'enfant : réutiliser le sélecteur parent, démarrer sa traduction, dire par où commencer (étant donné le chemin d'accès à son entité parent), et enfin appliquer le sélecteur supplémentaire (étant donné le chemin d'accès à son entité parent). "vue") parente. Et nous pouvons aussi traduire dans l'autre sens : réutiliser le sélecteur d'enfants, lancer sa traduction, dire par où commencer (étant donné une expression pour sélectionner les enfants), et enfin appliquer le sélecteur supplémentaire...
Pour plus de flexibilité, la « Traduction source » / « Traduction résultat » peut être utilisée individuellement :
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
} ) ;
Remarque : pour être moins verbeux, "Traduction de la source" / "Traduction du résultat" peut être utilisée dans une seule instruction volumineuse, le cas échéant :