NeinLinq는 .NET 함수의 작은 하위 집합만 지원하는 Entity Framework와 같은 LINQ 공급자를 사용하고, 함수를 재사용하고, 쿼리를 다시 작성하고, 쿼리를 Null로부터 안전하게 만들고, 번역 가능한 조건자와 선택기를 사용하여 동적 쿼리를 작성하는 데 유용한 확장을 제공합니다.
다양한 LINQ 구현을 지원하기 위해 다음과 같은 특징을 사용할 수 있습니다. 하나 이상을 선택하세요.
일반 LINQ 쿼리에는 NeinLinq를 사용하세요.
PM > Install-Package NeinLinq
비동기 LINQ 쿼리에는 NeinLinq.Async를 사용합니다.
PM > Install-Package NeinLinq.Async
Entity Framework 6 LINQ 쿼리에는 NeinLinq.EntityFramework를 사용합니다.
PM > Install-Package NeinLinq.EntityFramework
Entity Framework Core LINQ 쿼리에는 NeinLinq.EntityFrameworkCore를 사용합니다.
PM > Install-Package NeinLinq.EntityFrameworkCore
참고: 아래에 설명된 확장 메서드는 일부 충돌을 피하기 위해 위에서 선택한 패키지에 따라 이름이 다릅니다! 예를 들어 다음과 같습니다.
ToInjectable
( NeinLinq )ToAsyncInjectable
( NeinLinq.Async )ToDbInjectable
( NeinLinq.EntityFramework )ToEntityInjectable
( NeinLinq.EntityFrameworkCore )EF6/EFCore에서는 특정 버전을 사용하는 것이 좋습니다. 그렇지 않으면 비동기 쿼리가 작동하지 않습니다.
새로운 기능: 버전 5.1.0
에서는 NeinLinq.EntityFrameworkCore 패키지에 Lambda 주입을 전역적으로 활성화하기 위한 명시적인 DbContext
확장이 도입되었습니다.
services . AddDbContext < MyContext > ( options =>
options . UseSqlOrTheLike ( " ... " ) . WithLambdaInjection ( ) ) ;
참고: WithLambdaInjection
호출은 UseSqlOrTheLike
호출 이후에 발생해야 합니다!
많은 LINQ 공급자는 .NET 기능의 아주 작은 하위 집합만 지원할 수 있으며 자체 "기능"조차 지원할 수 없습니다. 간단한 메서드 LimitText
구현하고 이를 일반 LINQ 쿼리 내에서 사용합니다. 이 쿼리는 Entity Framework를 통해 SQL로 변환됩니다.
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()
)으로 표시되고 이 쿼리 내에서 사용된 함수가 "여기에 삽입"( [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 .. .
이는 쿼리 코드 내에서 (희망적으로) 더 나은 Like
확장 메서드를 사용하기 위해 Entity Framework 의 SqlFunctions
클래스를 추상화할 수 있는 방법에 대한 예입니다. 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년을 쓰고 있는데 여전히 null 값에 대해 걱정해야 합니다.
하지만 우리는 익숙해졌고 괜찮습니다. 그러나 null 검사를 사용하여 로드된 C#으로 쿼리를 작성하는 것은 옳지 않다고 느껴지고, 보기에 끔찍할 뿐이며, 번역된 SQL은 더욱 악화됩니다. SQL db 전용 LINQ 쿼리는 이러한 null 검사를 생략할 수 있으며, 메모리 내 계산 전용 LINQ 쿼리에는 이러한 null 검사가 포함되어야 합니다. 그리고 두 가지 모두에 대한 LINQ 쿼리에는 NeinLinq가 해결하려고 하는 문제(단위 테스트?)가 있습니다.
다음 쿼리는 null 참조를 트리거할 수 있습니다.
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
}
NeinLinq 내의 모든 ToWhatever
도우미와 마찬가지로 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 ) .. .
또한 이를 달성하기 위해 Invoke가 사용되지 않습니다. 많은 LINQ 공급자가 이를 지원하지 않으므로( Entity Framework , 보세요...) 이 솔루션은 상당히 호환되어야 합니다.
술어 선택자와 마찬가지로 선택자에게도 약간의 사랑이 필요합니다. 일부 기본 유형에 대한 기존 선택기가 있고 하나 이상의 구체적인 유형에 대해 이 코드를 재사용하려는 경우 강제로 다시 복사하여 붙여넣어야 합니다. 그러지 마세요!
계약/ViewModel/DTO/무엇이든(AcademyView 및 SpecialAcademyView)에 따라 두 엔터티(Academy 및 SpecialAcademy)를 생각해 보겠습니다.
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
} ) ;
참고: 덜 장황하게 하려면 "원본 번역"/"결과 번역"을 적절한 경우 하나의 부풀린 명령문 내에서 사용할 수 있습니다.