Mapper sendiri atau sedikit tentang ExpressionTrees

gambar

Hari ini kita akan berbicara tentang cara menulis AutoMapper Anda. Ya, saya benar-benar ingin memberi tahu Anda tentang ini, tetapi saya tidak bisa. Faktanya adalah bahwa solusi seperti itu sangat besar, memiliki sejarah coba-coba, dan juga sudah jauh untuk aplikasi. Saya hanya bisa memberikan pemahaman tentang bagaimana ini bekerja, memberikan titik awal bagi mereka yang ingin memahami mekanisme kerja para "pemetaan". Anda bahkan dapat mengatakan bahwa kami akan menulis sepeda kami.

Penafian


Saya ingatkan sekali lagi: kami akan menulis mapper primitif. Jika Anda tiba-tiba memutuskan untuk memodifikasinya dan menggunakannya di prod - jangan lakukan ini. Ambil solusi yang sudah jadi yang tahu tumpukan masalah di area subjek ini dan sudah tahu bagaimana menyelesaikannya. Ada beberapa alasan yang kurang lebih signifikan untuk menulis dan menggunakan mapper sepeda Anda:

  • Perlu beberapa penyesuaian khusus.
  • Anda memerlukan kinerja maksimum dalam kondisi Anda dan Anda siap untuk mengisi kerucut.
  • Anda ingin memahami cara kerja mapper.
  • Anda suka bersepeda.

Apa yang disebut kata "mapper"?


Ini adalah subsistem yang bertanggung jawab untuk mengambil objek dan mengonversi (menyalin nilainya) ke yang lain. Tugas khas adalah mengubah DTO menjadi objek lapisan bisnis. Mapper yang paling primitif β€œdijalankan” melalui properti dari sumber data dan membandingkannya dengan properti dari tipe data yang akan di-output. Setelah pencocokan, nilai diekstraksi dari sumber dan ditulis ke objek, yang akan menjadi hasil konversi. Di suatu tempat di sepanjang jalan, kemungkinan besar, masih akan diperlukan untuk menciptakan "hasil" ini.

Bagi konsumen, mapper adalah layanan yang menyediakan antarmuka berikut:

public interface IMapper<out TOut> { TOut Map(object source); } 

Saya menekankan: ini adalah antarmuka yang paling primitif, yang, dari sudut pandang saya, nyaman untuk penjelasan. Pada kenyataannya, kita kemungkinan besar akan berurusan dengan mapper yang lebih spesifik (IMapper <TIn, TOut>) atau dengan fasad yang lebih umum (IMapper), yang dengan sendirinya akan memilih mapper spesifik untuk jenis objek input-output yang ditentukan.

Implementasi naif


Catatan: bahkan implementasi naif dari mapper membutuhkan pengetahuan dasar tentang Reflection dan ExpressionTrees . Jika Anda belum mengikuti tautan atau mendengar apa pun tentang teknologi ini - lakukan, baca. Saya berjanji dunia tidak akan pernah sama.

Namun, kami menulis mapper Anda sendiri. Untuk memulai, mari kita dapatkan semua properti ( PropertyInfo ) dari tipe data yang akan di-output (selanjutnya saya akan menyebutnya TOUT ). Ini cukup sederhana: kita tahu jenisnya, karena kita menulis implementasi kelas generik yang diparameterisasi dengan tipe TOut. Selanjutnya, dengan menggunakan instance dari kelas Type, kita mendapatkan semua propertinya.

 Type outType = typeof(TOut); PropertyInfo[] outProperties = outType.GetProperties(); 

Saat mendapatkan properti, saya menghilangkan fitur. Misalnya, beberapa di antaranya mungkin tanpa fungsi setter, beberapa mungkin ditandai sebagai diabaikan oleh atribut, beberapa mungkin dengan akses khusus. Kami sedang mempertimbangkan opsi paling sederhana.

Kami melangkah lebih jauh. Akan lebih baik untuk dapat membuat turunan dari tipe TOut, yaitu objek yang kita "peta" objek yang masuk. Di C #, ada beberapa cara untuk melakukan ini. Sebagai contoh, kita dapat melakukan ini: System.Activator.CreateInstance (). Atau bahkan hanya TOut baru (), tetapi untuk ini Anda perlu membuat batasan untuk TOut, yang tidak ingin Anda lakukan di antarmuka umum. Namun, kami berdua tahu sesuatu tentang ExpressionTrees, yang berarti kami bisa melakukannya seperti ini:

 ConstructorInfo outConstructor = outType.GetConstructor(Array.Empty<Type>()); Func<TOut> activator = outConstructor == null ? throw new Exception($"Default constructor for {outType.Name} not found") : Expression.Lambda<Func<TOut>>(Expression.New(outConstructor)).Compile(); 

Kenapa begitu? Karena kita tahu bahwa instance kelas Type dapat memberikan informasi tentang konstruktor apa yang dimilikinya - ini sangat cocok untuk kasus ketika kita memutuskan untuk mengembangkan mapper kita sehingga kita akan meneruskan data apa pun ke konstruktor. Kami juga belajar lebih banyak tentang ExpressionTrees, yaitu, mereka memungkinkan plak untuk membuat dan mengkompilasi kode, yang kemudian dapat digunakan kembali. Dalam kasus ini, ini adalah fungsi yang benar-benar terlihat seperti () => new TOut ().

Sekarang Anda perlu menulis metode mapper utama, yang akan menyalin nilai-nilai. Kita akan pergi dengan cara yang paling sederhana: kita pergi melalui properti dari objek yang datang kepada kita di pintu masuk, dan mencari properti dengan nama yang sama di antara properti dari objek yang keluar. Jika ditemukan - salin, jika tidak - pindah.

 TOut outInstance = _activator(); PropertyInfo[] sourceProperties = source.GetType().GetProperties(); for (var i = 0; i < sourceProperties.Length; i++) { PropertyInfo sourceProperty = sourceProperties[i]; string propertyName = sourceProperty.Name; if (_outProperties.TryGetValue(propertyName, out PropertyInfo outProperty)) { object sourceValue = sourceProperty.GetValue(source); outProperty.SetValue(outInstance, sourceValue); } } return outInstance; 

Dengan demikian, kami telah sepenuhnya membentuk kelas BasicMapper . Anda dapat membiasakan diri dengan tes-tesnya di sini . Harap perhatikan bahwa sumber dapat berupa objek jenis apa pun atau objek anonim.

Performa dan tinju


Refleksi itu bagus, tetapi lambat. Selain itu, penggunaannya yang sering meningkatkan lalu lintas memori, yang berarti memuat GC, yang berarti memperlambat aplikasi lebih banyak lagi. Misalnya, kami hanya menggunakan metode PropertyInfo.SetValue dan PropertyInfo.GetValue . Metode GetValue mengembalikan objek di mana nilai tertentu dibungkus (tinju). Ini artinya kami menerima alokasi dari awal.

Pemetaan biasanya terletak di mana Anda perlu mengubah satu objek menjadi yang lain ... Tidak, bukan satu, tetapi banyak objek. Misalnya, ketika kita mengambil sesuatu dari database. Di tempat ini, saya ingin melihat kinerja normal dan tidak kehilangan memori pada operasi dasar.

Apa yang bisa kita lakukan ExpressionTrees akan membantu kami lagi. Faktanya adalah. NET memungkinkan Anda untuk membuat dan mengkompilasi kode "on the fly": kami menggambarkannya dalam representasi objek, katakan apa dan di mana kami akan menggunakannya ... dan kompilasi. Hampir tidak ada sihir.

Mapper yang dikompilasi


Sebenarnya, semuanya relatif sederhana: kami sudah melakukan yang baru dengan Expression.New (ConstructorInfo). Anda mungkin telah memperhatikan bahwa metode New statis disebut persis sama dengan operator. Faktanya adalah bahwa hampir semua sintaks C # tercermin dalam bentuk metode statis dari kelas Ekspresi. Jika ada sesuatu yang hilang, itu berarti Anda mencari apa yang disebut "Gula sintaksis."

Berikut ini beberapa operasi yang akan kami gunakan di mapper kami:

  • Deklarasi variabel - Ekspresi. Variabel (Jenis, string). Argumen Type memberitahu tipe variabel apa yang akan dibuat, dan string adalah nama variabel.
  • Penugasan - Ekspresi. Menugaskan (Ekspresi, Ekspresi). Argumen pertama adalah apa yang kita tetapkan, dan argumen kedua adalah apa yang kita tetapkan.
  • Akses ke properti objek adalah Expression.Property (Expression, PropertyInfo). Ekspresi adalah pemilik properti, dan PropertyInfo adalah representasi objek dari properti yang diperoleh melalui Refleksi.

Dengan pengetahuan ini, kita bisa membuat variabel, mengakses properti objek, dan menetapkan nilai ke properti objek. Kemungkinan besar, kami juga memahami bahwa ExpressionTree perlu dikompilasi menjadi delegasi dari bentuk Fungsi <objek, TOut> . Rencananya adalah ini: kita mendapatkan variabel yang berisi data input, membuat instance bertipe TOut dan membuat ekspresi yang menetapkan satu properti ke yang lain.

Sayangnya, kode ini tidak terlalu kompak, jadi saya sarankan Anda melihat implementasi CompiledMapper segera. Saya membawa ke sini hanya poin-poin penting.

Pertama, kita membuat representasi objek dari parameter fungsi kita. Karena mengambil objek sebagai input, objek akan menjadi parameter.

 var parameter = Expression.Parameter(typeof(object), "source"); 

Selanjutnya, kita membuat dua variabel dan daftar Ekspresi di mana kita akan secara berurutan menambahkan ekspresi penugasan. Urutan itu penting, karena begitulah perintah akan dieksekusi ketika kita memanggil metode yang dikompilasi. Misalnya, kami tidak dapat menetapkan nilai ke variabel yang belum dideklarasikan.

Lebih jauh, dengan cara yang sama seperti dalam kasus implementasi naif, kita melihat daftar properti tipe dan mencoba mencocokkannya dengan nama. Namun, alih-alih segera menetapkan nilai, kami membuat ekspresi untuk mengekstraksi nilai dan menetapkan nilai untuk setiap properti terkait.

 Expression sourceValue = Expression.Property(sourceInstance, sourceProperty); Expression outValue = Expression.Property(outInstance, outProperty); expressions.Add(Expression.Assign(outValue, sourceValue)); 

Poin penting: setelah kita membuat semua operasi penugasan, kita perlu mengembalikan hasilnya dari fungsi. Untuk melakukan ini, ekspresi terakhir dalam daftar harus berupa Ekspresi, yang berisi instance kelas yang kami buat. Saya meninggalkan komentar di sebelah baris ini. Mengapa perilaku yang terkait dengan kata kunci kembali di ExpressionTree terlihat seperti ini? Saya khawatir ini masalah tersendiri. Sekarang saya sarankan agar mudah diingat.

Ya, pada akhirnya, kita harus mengkompilasi semua ekspresi yang kita buat. Apa yang kami minati di sini? Variabel tubuh berisi "tubuh" fungsi. "Fungsi normal" memiliki tubuh, bukan? Nah, yang kami sertakan dalam kurung kurawal. Jadi, Expression. Blocklock persis seperti itu. Karena kurung kurawal juga merupakan ruang lingkup, kita harus memberikan variabel yang akan digunakan di sana - dalam case sourceInstance dan outInstance kita.

 var body = Expression.Block(new[] {sourceInstance, outInstance}, expressions); return Expression.Lambda<Func<object, TOut>>(body, parameter).Compile(); 

Pada output, kita mendapatkan Func <object, TOut>, i.e. fungsi yang dapat mengkonversi data dari satu objek ke objek lainnya. Mengapa kesulitan seperti itu, Anda bertanya? Saya mengingatkan Anda bahwa, pertama, kami ingin menghindari tinju saat menyalin nilai ValueType, dan kedua, kami ingin meninggalkan metode PropertyInfo.GetValue dan PropertyInfo.SetValue, karena agak lambat.

Kenapa tidak bertinju? Karena ExpressionTree yang dikompilasi adalah IL nyata, dan untuk runtime, sepertinya (hampir) seperti kode Anda. Mengapa "kompilasi mapper" lebih cepat? Sekali lagi: karena itu hanyalah IL biasa. Ngomong-ngomong, kami dapat dengan mudah mengkonfirmasi kecepatan menggunakan perpustakaan BenchmarkDotNet , dan tolok ukur itu sendiri dapat dilihat di sini .
MetodeBerartiKesalahanStddevRasioDialokasikan
Pemeta Otomatis1.291,6 kita3.3173 kami3.1030 kami1,00312,5 KB
Velo_BasicMapper11.987,0 kami33.8389 kami28.2570 kami9.283437.5 KB
Velo_CompiledMapper341.3 kita2.8230 kami2.6407 kami0,26312,5 KB

Di kolom Rasio, "CompiledMapper" (CompiledMapper) menunjukkan hasil yang sangat baik, bahkan dibandingkan dengan AutoMapper (ini adalah baseline, mis. 1). Namun, jangan bersukacita: AutoMapper memiliki kemampuan yang jauh lebih besar dibandingkan dengan motor kami. Dengan plat ini saya hanya ingin menunjukkan bahwa ExpressionTrees jauh lebih cepat daripada "pendekatan Refleksi klasik".

Ringkasan


Saya harap saya bisa menunjukkan bahwa menulis mapper Anda cukup sederhana. Reflection dan ExpressionTrees adalah alat yang sangat kuat yang digunakan pengembang untuk menyelesaikan banyak tugas berbeda. Ketergantungan injeksi, Serialization / Deserialization, repositori CRUD, membangun query SQL, menggunakan bahasa lain sebagai skrip untuk aplikasi .NET - semua ini dilakukan dengan menggunakan Reflection, Reflection.Emit dan ExpressionTrees.

Bagaimana dengan mapper? Mapper adalah contoh yang bagus untuk mempelajari semua ini.

PS: Jika Anda ingin ExpressionTrees lagi, saya sarankan membaca tentang bagaimana membuat konverter JSON Anda menggunakan teknologi ini.

Source: https://habr.com/ru/post/id463961/


All Articles