NeinLinq は、.NET 関数の少数のサブセットのみをサポートする Entity Framework などの LINQ プロバイダーの使用、関数の再利用、クエリの書き換え、クエリの null セーフ化、翻訳可能な述語とセレクターを使用した動的クエリの構築に役立つ拡張機能を提供します。
さまざまな LINQ 実装をサポートするために、次のフレーバーが利用可能です。少なくとも 1 つ選択してください。
プレーンな 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 .. .
これは、 Entity FrameworkのSqlFunctions
クラスを抽象化して、(できれば) より優れた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 クエリ チェーン内のどこでも実行できるため、ビジネス ロジックを汚す必要はありません。
注:コードの重複は必要ありません。通常の方法では、式をコンパイルするだけでよく、理想的には 1 回だけです。簡単な解決策は、次のコード サンプルのようになります (適切に見えても、これをカプセル化/整理することは可能です。NeinLinqには特別な要件はありません。ビルトインの「式キャッシュ」を自由に使用するか、何か派手なものを構築してください... ):
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 ( )
{
.. .
}
}
注: インスタンス メソッドの注入は、静的メソッドの注入ほど効率的ではありません。本当に必要でない場合は、前者は使用しないでください。さらに、シールされた型のインスタンス メソッドを挿入すると、一度だけ実行する必要がある処理が増えるため、オーバーヘッドが少し削減されます。さて、ここで新たに言うことは何もありません。
もう 1 つ:よりハッキングして拡張メソッドの式を少なくするために、モデル内にプロパティを直接注入することができます。
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 データベース専用の 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 クエリ チェーン内のどこでも呼び出すことができます。
多くのデータ駆動型アプリケーションは、ある種の動的クエリを構築する必要があります。これにより、不正な文字列操作、複雑な式ツリーの配管、またはそれらの組み合わせが発生する可能性があります。単純結合や-結合は他のライブラリ内ですでに解決されていますが、「外部」述語の結合はそれほど簡単ではありません。
3 つのエンティティについて考えてみましょう。アカデミーにはコースがあり、コースには講義があります。
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 、注目しています...)、このソリューションはかなり互換性があるはずです。
述語と同様に、セレクターにも愛情が必要です。何らかの基本型の既存のセレクターがあり、このコードを 1 つ以上の具象型に再利用したい場合は、再度コピーして貼り付ける必要があります。そんなことしないでください!
適切なコントラクト / ビューモデル / DTO / 何でも (AcademyView と SpecialAcademyView) を持つ 2 つのエンティティ (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 } ;
2 番目のセレクター内の最初のセレクターのメンバー バインディングを省略していることに注意してください。同じことを繰り返さないでください、覚えていますか?
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
} ) ;
注:冗長さを減らすために、必要に応じて、単一の肥大化したステートメント内で「ソース翻訳」/「結果翻訳」を使用できます。