Saya ingin memulai dengan deskripsi masalah yang saya temui. Ada entitas dalam database yang perlu ditampilkan sebagai tabel di UI. Entity Framework digunakan untuk mengakses database. Ada filter untuk kolom tabel ini.
Anda perlu menulis kode untuk memfilter entitas berdasarkan parameter.
Misalnya, ada dua entitas:Pengguna dan Produk.
Pengguna kelas publik{ public int Id { get; mengatur; } Nama string publik { dapatkan; mengatur; }}Produk kelas publik{ public int Id { dapatkan; mengatur; } Nama string publik { dapatkan; mengatur; }}
Asumsikan kita perlu memfilter pengguna dan produk berdasarkan nama. Kami membuat metode untuk memfilter setiap entitas.
public IQueryableFilterUsersByName(IQueryable users, string text){ pengguna kembali.Where(user => user.Name.Contains(text));}public IQueryable FilterProductsByName(IQueryable produk, teks string){ mengembalikan produk.Di mana(produk => produk.Nama.Berisi(teks));}
Seperti yang Anda lihat, kedua metode ini hampir identik dan hanya berbeda dalam properti entitas, yang digunakan untuk memfilter data.
Mungkin menjadi tantangan jika kita memiliki lusinan entitas dengan lusinan bidang yang memerlukan pemfilteran. Kompleksitas terletak pada dukungan kode, penyalinan tanpa berpikir, dan akibatnya, pengembangan lambat dan kemungkinan kesalahan tinggi.
Mengutip Fowler, itu mulai berbau. Saya ingin menulis sesuatu yang standar daripada duplikasi kode. Misalnya:
public IQueryableFilterUsersByName(IQueryable users, string text){ return FilterContainsText(users, user => user.Name, text);}public IQueryable FilterProductsByName(IQueryable produk, string text){ return FilterContainsText(products, propduct => propduct.Name, text);}public IQueryable FilterContainsText (entity IQueryable , Func getProperty, string text){ mengembalikan entitas. Where(entity => getProperty(entity).Contains(text));}
Sayangnya, jika kami mencoba memfilter:
public void TestFilter(){ using (var context =new Context()) { var filteredProducts =FilterProductsByName(context.Products, "name").ToArray(); }}
Kami akan mendapatkan kesalahan «Test method ExpressionTests.ExpressionTest.TestFilter melemparkan pengecualian:
System.NotSupportedException :Jenis simpul ekspresi LINQ 'Invoke' tidak didukung di LINQ ke Entitas.
Ekspresi
Mari kita periksa apa yang salah.
Metode Where menerima parameter tipe Expression
Ekspresi menjelaskan pohon sintaks. Untuk lebih memahami bagaimana strukturnya, pertimbangkan ekspresi, yang memeriksa bahwa nama sama dengan baris.
Ekspresi> yang diharapkan =produk => produk.Nama =="target";
Saat debugging, kita dapat melihat struktur ekspresi ini (properti kunci ditandai dengan warna merah).
Kami memiliki pohon berikut:
Saat kita meneruskan delegasi sebagai parameter, pohon berbeda akan dibuat, yang memanggil metode Invoke pada parameter (delegasi) alih-alih memanggil properti entitas.
Saat Linq mencoba membuat kueri SQL dengan pohon ini, Linq tidak tahu cara menginterpretasikan metode Invoke dan melempar NotSupportedException.
Jadi, tugas kita adalah mengganti cast ke properti entitas (bagian pohon yang ditandai dengan warna merah) dengan ekspresi yang diteruskan melalui parameter ini.
Mari kita coba:
Expression> propertyGetter =product => product.Name;Expression > filter =product => propertyGetter(product) =="target"
Sekarang, kita dapat melihat kesalahan «Nama metode yang diharapkan» pada tahap kompilasi.
Masalahnya adalah bahwa ekspresi adalah kelas yang mewakili node dari pohon sintaks, bukan delegasi dan tidak dapat dipanggil secara langsung. Sekarang, tugas utamanya adalah menemukan cara untuk membuat ekspresi yang meneruskan parameter lain ke dalamnya.
Pengunjung
Setelah pencarian Google singkat, saya menemukan solusi untuk masalah serupa di StackOverflow.
Untuk bekerja dengan ekspresi, ada kelas ExpressionVisitor, yang menggunakan pola Pengunjung. Hal ini dirancang untuk melintasi semua node dari pohon ekspresi dalam urutan parsing pohon sintaks dan memungkinkan memodifikasi mereka atau mengembalikan node lain sebagai gantinya. Jika baik node maupun node anaknya tidak diubah, ekspresi aslinya akan dikembalikan.
Saat mewarisi dari kelas ExpressionVisitor, kita dapat mengganti simpul pohon apa pun dengan ekspresi, yang kita lewati melalui parameter. Jadi, kita perlu meletakkan beberapa label-simpul, yang akan kita ganti dengan parameter, ke dalam pohon. Untuk melakukannya, tulis metode ekstensi yang akan mensimulasikan panggilan ekspresi dan akan menjadi penanda.
public static class ExpressionExtension{ public static TFunc Call(ekspresi ini ) { throw new InvalidOperationException("Metode ini tidak boleh dipanggil. Ini adalah penanda untuk mengganti."); }}
Sekarang, kita dapat mengganti satu ekspresi dengan yang lain
Expression> propertyGetter =product => product.Name;Expression > filter =product => propertyGetter.Call()(product) =="target"; Pengunjung perlu ditulis, yang akan menggantikan metode Panggilan dengan parameternya di pohon ekspresi:
kelas publik SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtension).GetMethod(nameof(ExpressionExtension.Call)).GetGenericMethodDefinition(); } protected override Expression VisitMethodCall(MethodCallExpression node) { if (IsMarker(node)) { kembali Visit(ExtractExpression(node)); } kembalikan basis.VisitMethodCall(simpul); } private LambdaExpression ExtractExpression(MethodCallExpression node) { var target =node.Arguments[0]; kembali (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { kembali node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }}Kami dapat mengganti penanda kami:
ekspresi statis publikSubstituteMarker (ekspresi ini ekspresi){ var visitor =new SubstituteExpressionCallVisitor(); return (Expression )visitor.Visit(expression);}Ekspresi > propertyGetter =product => product.Name;Expression > filter =product => propertyGetter.Call ()(produk).Contains("123");Ekspresi > finalFilter =filter.SubstituteMarker(); Dalam debugging, kita dapat melihat bahwa ekspresi tidak seperti yang kita harapkan. Filter masih berisi metode Invoke.
Faktanya adalah bahwa ekspresi parameterGetter dan finalFilter menggunakan dua argumen yang berbeda. Jadi, kita perlu mengganti argumen di parameterGetter dengan argumen di finalFilter. Untuk melakukan ini, kami membuat pengunjung lain:
Hasilnya adalah sebagai berikut:
kelas publik SubstituteParameterVisitor :ExpressionVisitor{ private readonly LambdaExpression _expressionToVisit; Kamus hanya-baca pribadi_substitutionByParameter; public SubstituteParameterVisitor(Expression[] parameterSubstitutions, LambdaExpression expressionToVisit) { _expressionToVisit =expressionToVisit; _substitutionByParameter =expressionToVisit .Parameters .Select((parameter, index) => new {Parameter =parameter, Index =index}) .ToDictionary(pair => pair.Parameter, pair => parameterSubstitutions[pair.Index]); } public Expression Replace() { kembali Visit(_expressionToVisit.Body); } protected override Expression VisitParameter(ParameterExpression node) { Penggantian ekspresi; if (_substitutionByParameter.TryGetValue(simpul, substitusi keluar)) { kunjungan kembali(substitusi); } kembalikan basis.VisitParameter(simpul); }}kelas publik SubstituteExpressionCallVisitor :ExpressionVisitor{ private readonly MethodInfo _markerDesctiprion; public SubstituteExpressionCallVisitor() { _markerDesctiprion =typeof(ExpressionExtensions) .GetMethod(nameof(ExpressionExtensions.Call)) .GetGenericMethodDefinition(); } protected override Expression VisitInvocation(InvocationExpression node) { var isMarkerCall =node.Expression.NodeType ==ExpressionType.Call &&IsMarker((MethodCallExpression) node.Expression); if (isMarkerCall) { var parameterReplacer =new SubstituteParameterVisitor(node.Arguments.ToArray(), Unwrap((MethodCallExpression) node.Expression))); var target =parameterReplacer.Replace(); Kunjungan kembali (target); } kembalikan base.VisitInvocation(simpul); } private LambdaExpression Unwrap(MethodCallExpression node) { var target =node.Arguments[0]; kembali (LambdaExpression)Expression.Lambda(target).Compile().DynamicInvoke(); } private bool IsMarker(MethodCallExpression node) { kembali node.Method.IsGenericMethod &&node.Method.GetGenericMethodDefinition() ==_markerDesctiprion; }} Sekarang, semuanya berfungsi sebagaimana mestinya dan kami, akhirnya, dapat menulis metode penyaringan kami
public IQueryableFilterContainsText (IQueryable entitas, Ekspresi > getProperty, teks string){ Expression > filter =entitas => getProperty. Panggilan()(entitas).Berisi(teks); kembalikan entitas.Where(filter.SubstituteMarker());} Kesimpulan
Pendekatan dengan penggantian ekspresi dapat digunakan tidak hanya untuk pemfilteran tetapi juga untuk pengurutan dan kueri apa pun ke database.
Selain itu, metode ini memungkinkan penyimpanan ekspresi bersama dengan logika bisnis secara terpisah dari kueri ke database.
Anda dapat melihat kodenya di GitHub.
Artikel ini didasarkan pada balasan StackOverflow.