NeinLinq proporciona extensiones útiles para usar proveedores LINQ como Entity Framework que admiten solo un subconjunto menor de funciones .NET, reutilizar funciones, reescribir consultas, incluso hacerlas seguras para nulos y crear consultas dinámicas utilizando predicados y selectores traducibles.
Para admitir diferentes implementaciones de LINQ, están disponibles los siguientes tipos. Elige al menos uno.
Utilice NeinLinq para consultas LINQ simples:
PM > Install-Package NeinLinq
Utilice NeinLinq.Async para consultas LINQ asíncronas:
PM > Install-Package NeinLinq.Async
Utilice NeinLinq.EntityFramework para consultas LINQ de Entity Framework 6:
PM > Install-Package NeinLinq.EntityFramework
Utilice NeinLinq.EntityFrameworkCore para consultas LINQ de Entity Framework Core:
PM > Install-Package NeinLinq.EntityFrameworkCore
Nota: los métodos de extensión que se describen a continuación tienen nombres diferentes según el paquete elegido anteriormente, para evitar algunos conflictos. Por ejemplo hay:
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )Se recomienda el uso de versiones específicas para EF6/EFCore (de lo contrario, las consultas asíncronas no funcionarán).
Nuevo: con la versión 5.1.0
el paquete NeinLinq.EntityFrameworkCore introdujo una extensión DbContext
explícita para habilitar la inyección Lambda globalmente:
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
Nota: ¡la llamada a WithLambdaInjection
debe realizarse después de la llamada a UseSqlOrTheLike
!
Muchos proveedores de LINQ sólo pueden admitir un subconjunto muy pequeño de funcionalidades .NET, ni siquiera pueden admitir nuestras propias "funciones". Digamos que implementamos un método simple LimitText
y lo usamos dentro de una consulta LINQ ordinaria, que se traducirá a SQL a través de Entity Framework ...
LINQ to Entities no reconoce el método 'System.String LimitText(System.String, Int32)' y este método no se puede traducir a una expresión de tienda.
Esto es lo que obtenemos; de hecho, es realmente molesto. Tenemos que distribuir nuestra lógica entre código, que será traducido por cualquier proveedor de consultas LINQ, y código que no. Se pone aún peor: si alguna lógica es "traducible", lo cual es bueno, ¡tenemos que copiar y pegar! Consolidar el código dentro de una función ordinaria no funciona ya que el proveedor no puede traducir esta simple llamada al método. Bueno.
Introduzcamos la "inyección 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 una consulta está marcada como "inyectable" ( ToInjectable()
) y una función utilizada dentro de esta consulta está marcada como "inyectar aquí" ( [InjectLambda]
), el motor de reescritura de NeinLinq reemplaza la llamada al método con la expresión lambda coincidente, que se puede traducir a SQL o lo que sea. Por lo tanto, podemos encapsular funcionalidades .NET no compatibles e incluso crear la nuestra propia. Hurra.
[ 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 .. .
Este es un ejemplo de cómo podemos abstraer la clase SqlFunctions
de Entity Framework para usar un método de extensión Like
(con suerte) más agradable dentro de nuestro código de consulta. Es probable que PatIndex
se use para simular una declaración SQL LIKE, ¿por qué no hacerlo así? De hecho, podemos implementar el método "ordinario" con la ayuda de expresiones regulares para ejecutar nuestro código sin tocar SqlFunctions
también...
Nuevo: con la versión 7.0.0
NeinLinq ahora admite proveedores personalizados para InjectLambdaAttribute. Esta característica le permite definir su propia lógica para determinar qué métodos deben considerarse inyectables sin agregar explícitamente el atributo [InjectLambda]
. Esto es particularmente útil cuando se trabaja con bibliotecas externas o código que no se puede modificar directamente:
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 ) ;
} ) ;
Finalmente, veamos esta consulta usando Entity Framework o similar:
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 ) => .. .
}
Los métodos RetrieveWhatever
, FulfillsSomeCriteria
y DoTheFancy
deben marcarse en consecuencia, usando el atributo [InjectLambda]
o simplemente la convención simple "misma clase, mismo nombre, firma coincidente" (que por cierto requiere que la clase esté en la lista verde). Y la llamada ToInjectable
puede ocurrir en cualquier lugar dentro de la cadena de consultas LINQ, por lo que no tenemos que contaminar nuestra lógica de negocios.
Nota: la duplicación de código no debería ser necesaria. El método ordinario puede simplemente compilar la expresión, idealmente solo una vez. Una solución sencilla puede parecerse al siguiente ejemplo de código (es posible encapsular/organizar este material por muy sofisticado que parezca, NeinLinq no tiene requisitos específicos; siéntase libre de usar el "Expression Cache" integrado o crear algo sofisticado... ):
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 ) ;
Avanzado: eso también funciona con métodos de instancia, por lo que el código de expresión real puede recuperar datos adicionales. Incluso se pueden utilizar interfaces y/o clases base para abstraer todas las cosas. Por lo tanto, podemos declarar una interfaz/clase base sin expresiones, pero proporcionar la expresión para inyectar usando herencia.
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 ( )
{
.. .
}
}
Nota : inyectar métodos de instancia no es tan eficiente como inyectar métodos estáticos. Simplemente no uses los anteriores, si no son realmente necesarios. Además, inyectar métodos de instancia de tipo sellado reduce un poco la sobrecarga, ya que hay más cosas que solo deben hacerse una vez. Bien, no hay nada nuevo que decir aquí.
Una cosa más: para ser más ingenioso y utilizar menos ceremonia de métodos de extensión, es posible inyectar propiedades directamente dentro de los modelos.
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 ;
}
Nuevamente, en lugar de colocar [InjectLambda]
en todo, es posible agregar todos los modelos a la lista verde mientras se llama ToInjectable
.
Estamos escribiendo el año 2024 y todavía tenemos que preocuparnos por los valores nulos.
Sin embargo, nos acostumbramos y estamos bien. Pero escribir consultas en C# cargadas con comprobaciones nulas no se siente bien, simplemente se ve horrible, el SQL traducido incluso empeora. Una consulta LINQ solo para bases de datos SQL puede evitar estas comprobaciones nulas; una consulta LINQ solo para cálculos en memoria debe incluirlas. Y una consulta LINQ para ambos tiene un problema (¿pruebas unitarias?), que NeinLinq intenta resolver.
La siguiente consulta puede generar referencias nulas:
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
}
Mientras que la siguiente consulta no debería :
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
}
¿Quizás hemos olvidado algún cheque? O podemos relajarnos gracias a 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
}
Al igual que con todos los asistentes ToWhatever
dentro de NeinLinq , se puede llamar ToNullsafe
en cualquier lugar dentro de la cadena de consultas de LINQ.
Muchas aplicaciones basadas en datos necesitan crear algún tipo de consultas dinámicas. Esto puede llevar a manipulaciones de cadenas sucias, plomería de árboles de expresiones complejas o una combinación de ellas. Las conjunciones simples y / o -ya se resuelven en otras bibliotecas, pero las conjunciones de predicados "extraños" no son tan fáciles.
Pensemos en tres entidades: la Academia tiene Cursos, los Cursos tienen Conferencias.
Expression < Func < Course , bool > > p = c => .. .
Expression < Func < Course , bool > > q = c => .. .
db . Courses . Where ( p . And ( q ) ) .. .
Vale, eso ya lo sabemos.
Expression < Func < Academy , bool > > p = a => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( c => c . Academy ) ) .. .
Ahora podemos traducir un predicado (combinado) para una entidad matriz...
Expression < Func < Lecture , bool > > p = l => .. .
db . Courses . Where ( p . Translate ( )
. To < Course > ( ( c , q ) => c . Lectures . Any ( q ) ) ) .. .
..e incluso para entidades secundarias.
Usemos todo esto como conclusión:
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 ) .. .
Además, no se utiliza Invoke para lograrlo: muchos proveedores de LINQ no lo admiten ( Entity Framework , te estoy mirando...), por lo que esta solución debería ser bastante compatible.
Al igual que con los selectores de predicados, también es necesario un poco de cariño. Si tenemos un selector existente para algún tipo base y queremos reutilizar este código para uno o más tipos concretos, nos vemos obligados a copiar y pegar nuevamente. ¡No hagas eso!
Pensemos en dos entidades (Academy y SpecialAcademy) con contratos/ViewModels/DTO/lo que sea (AcademyView y 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 } ;
Tenga en cuenta que omitimos los enlaces de miembros del primer selector dentro del segundo. No te repitas, ¿recuerdas?
db . Academies . OfType < SpecialAcademy > ( )
. Select ( s . Translate ( )
. Cross < SpecialAcademy > ( )
. Apply ( t ) ) ;
Aunque hay más opciones, el escenario común puede verse así: reutilizar el selector base, iniciar su traducción (inferencia de tipos), decir por dónde empezar (sin inferencia de tipos) y finalmente aplicar el selector adicional (inferencia de tipos, nuevamente). .
Ahora consideremos las relaciones entre padres e hijos (Academia y Curso).
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 ) ) ;
Nuevamente, además de otras opciones, podemos traducir de padre a hijo: reutilizar el selector padre, iniciar su traducción, decir dónde empezar (dada la ruta a su entidad padre) y finalmente aplicar el selector adicional (dada la ruta a su entidad padre). "vista" principal). Y también podemos traducir al revés: reutilizar el selector secundario, iniciar su traducción, decir por dónde empezar (dando una expresión para seleccionar a los secundarios) y finalmente aplicar el selector adicional...
Para ser más flexible, la "Traducción de origen" / "Traducción de resultados" se puede utilizar individualmente:
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
} ) ;
Nota: para ser menos detallado, "Traducción de origen" / "Traducción de resultados" se puede utilizar dentro de una sola declaración ampliada, si corresponde: