Pada artikel ini, saya akan berbagi pengalaman serialisasi tipe biner antara majelis, tanpa referensi satu sama lain. Ternyata, ada kasus nyata dan "sah" ketika Anda perlu melakukan deserialisasi data tanpa memiliki tautan ke majelis tempat dinyatakan. Dalam artikel saya akan berbicara tentang skenario yang diperlukan, saya akan menjelaskan metode solusi, dan saya juga akan berbicara tentang kesalahan menengah yang dibuat selama pencarian
Pendahuluan Pernyataan masalah
Kami bekerja sama dengan perusahaan besar yang bergerak di bidang geologi. Secara historis, korporasi telah menulis perangkat lunak yang sangat berbeda untuk bekerja dengan data yang berasal dari berbagai jenis peralatan + analisis data + perkiraan. Sayangnya, semua perangkat lunak ini jauh dari selalu "ramah" satu sama lain, dan lebih sering sama sekali tidak ramah. Untuk menggabungkan informasi, entah bagaimana portal web sekarang sedang dibuat, di mana berbagai program mengunggah data mereka dalam bentuk xml. Dan portal sedang mencoba untuk membuat tampilan plus-minus-lengkap. Nuansa penting: karena pengembang portal tidak kuat di bidang subjek setiap aplikasi, masing-masing tim menyediakan modul pengubah data / parser dari xml ke struktur data portal.
Saya bekerja dalam tim yang mengembangkan salah satu aplikasi dan kami dengan mudah menulis mekanisme ekspor untuk bagian data kami. Tetapi di sini, analis bisnis memutuskan bahwa portal pusat membutuhkan salah satu laporan yang sedang dibangun oleh program kami. Di sinilah masalah pertama muncul: laporan dibangun kembali setiap kali dan hasilnya tidak disimpan di mana pun.
"Jadi selamatkanlah!" Pembaca mungkin akan berpikir. Saya juga berpikir demikian, tetapi sangat kecewa dengan persyaratan bahwa laporan sudah dibuat untuk data yang diunduh. Tidak ada hubungannya - Anda perlu mentransfer logika.
Tahap 0. Refactoring. Tidak ada masalah tubuh
Diputuskan untuk memisahkan logika membangun laporan (pada kenyataannya, ini adalah label 4-kolom, tetapi logikanya adalah kereta dan troli besar) ke dalam kelas yang terpisah, dan termasuk file dengan kelas ini dengan referensi dalam rakitan parser. Dengan ini kita:
- Hindari penyalinan langsung
- Melindungi dari perbedaan versi
Memisahkan logika menjadi kelas yang terpisah bukanlah tugas yang sulit. Tapi kemudian semuanya tidak begitu cerah: algoritma didasarkan pada objek bisnis, transfer yang tidak sesuai dengan konsep kami. Saya harus menulis ulang metode sehingga mereka hanya menerima tipe sederhana dan beroperasi pada mereka. Itu tidak selalu sederhana dan di beberapa tempat, dibutuhkan keputusan, keindahan yang masih dipertanyakan, tetapi secara keseluruhan, solusi yang andal diperoleh tanpa tongkat penyangga yang jelas.
Ada satu detail yang, seperti yang Anda tahu, sering berfungsi sebagai tempat perlindungan yang nyaman bagi iblis: kami mewarisi pendekatan aneh dari generasi pengembang sebelumnya, yang menurutnya beberapa data yang diperlukan untuk membuat laporan disimpan dalam database sebagai objek .Net biner-serial. pertanyaan "mengapa?", "kaaak?", dll. sayangnya, akan tetap tidak terjawab karena kurangnya alamat yang dituju). Dan dalam input perhitungan, kita, tentu saja, harus membatalkan deserialisasi.
Jenis-jenis ini, yang tidak mungkin dihilangkan, kami juga memasukkan "dengan referensi", terutama karena mereka agak tidak rumit.
Tahap 1. Deserialisasi. Ingat nama lengkapnya
Setelah melakukan manipulasi di atas dan melakukan uji coba, saya tiba-tiba menerima kesalahan runtime itu
[A] Namespace.TypeA tidak dapat dilemparkan ke [B] Namespace.TypeA. Tipe A berasal dari 'Assembley.Application, Versi = 1.0.0.0, Culture = netral, PublicKeyToken = null' dalam konteks 'Default' at location '...'. Tipe B berasal dari 'Assmbley.Portal, Versi = 1.0.0.0, Culture = netral, PublicKeyToken = null' dalam konteks 'Default' at location ''.
Tautan Google pertama kali memberi tahu saya bahwa faktanya adalah BinaryFormatter tidak hanya menulis data, tetapi juga mengetik informasi ke arus keluaran, yang logis. Dan mengingat bahwa nama lengkap dari tipe tersebut mengandung rakitan yang dideklarasikan, gambar dari apa yang saya coba deserialize satu tipe sama sekali berbeda dari sudut pandang .Net
Setelah menggaruk-garuk kepala, saya, seperti yang terjadi, membuat keputusan yang jelas, tetapi sayangnya, ganas untuk mengganti tipe TypeA tertentu selama deserialisasi yang
dinamis . Semuanya berhasil. Hasil laporan terkonvergensi dari satu ke yang lain, tes pada server build berlalu. Dengan rasa keberhasilan, kami mengirim tugas ke penguji.
Tahap 2. Utama. Serialisasi antara majelis
Penghitungan ulang datang dengan cepat dalam bentuk bug yang terdaftar oleh penguji, yang menyatakan bahwa parser di sisi portal jatuh dengan pengecualian bahwa itu tidak dapat memuat Assembley assembly. Aplikasi (assembly dari aplikasi kami). Pikiran pertama - saya tidak membersihkan referensi. Tapi - tidak, semuanya baik-baik saja, tidak ada yang merujuk. Saya mencoba menjalankannya lagi di kotak pasir - semuanya berfungsi. Saya mulai mencurigai kesalahan build, tetapi di sini muncul ide yang tidak menyenangkan saya: Saya mengubah jalur output untuk parser ke folder terpisah, dan bukan ke direktori bin bersama aplikasi. Dan voila - saya mendapatkan pengecualian yang dijelaskan. Analisis stectrace mengkonfirmasi dugaan yang tidak jelas - deserialisasi menurun.
Kesadaran itu cepat dan menyakitkan: mengganti tipe tertentu dengan dinamis tidak mengubah apa pun, BinaryFormatter masih menciptakan tipe dari rakitan eksternal, hanya jika rakitan dengan tipe itu di dekatnya, runtime memuatnya secara alami, dan ketika rakitan itu hilang - kami mendapatkan kesalahan.
Ada alasan untuk bersedih. Tapi googling memberi harapan dalam bentuk
Kelas SerializationBinder . Ternyata, ini memungkinkan Anda untuk menentukan jenis deserialized data kami. Untuk melakukan ini, buat pewaris dan tentukan metode berikut di dalamnya.
public abstract Type BindToType(String assemblyName, String typeName);
di mana Anda dapat mengembalikan jenis apa pun untuk kondisi tertentu.
Kelas BinaryFormatter memiliki properti
Binder tempat Anda dapat menyuntikkan implementasi Anda.
Tampaknya tidak ada masalah. Tetapi sekali lagi, detail tetap ada (lihat di atas).
Pertama, Anda harus memproses permintaan untuk
semua jenis (dan standar juga).
Opsi implementasi yang menarik ditemukan di Internet di sini , tetapi mereka mencoba menggunakan binder default dari BinaryFormatter, dalam bentuk konstruksi
var defaultBinder = new BinaryFormatter().Binder
Namun pada kenyataannya, properti Binder adalah null secara default. Analisis kode sumber menunjukkan bahwa di dalam BinaryFormatter, apakah Binder diperiksa, jika demikian, metodenya disebut, jika tidak, logika internal digunakan, yang akhirnya bermuara pada
var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName);
Tanpa basa-basi lagi, saya mengulangi logika yang sama dalam diri saya.
Inilah yang terjadi pada implementasi pertama
public class MyBinder : SerializationBinder { public override Type BindToType(string assemblyName, string typeName) { if (assemblyName.Contains("<ObligatoryPartOfNamespace>") ) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } private Type LoadTypeFromAssembly(string assemblyName, string typeName) { if (string.IsNullOrEmpty(assemblyName) || string.IsNullOrEmpty(typeName)) return null; var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); } }
Yaitu itu diperiksa jika namespace milik proyek - kami mengembalikan jenis dari domain saat ini, jika jenis sistem - kami memuat dari perakitan yang sesuai
Itu terlihat logis. Kami mulai menguji: tipe kami datang - kami ganti, itu dibuat. Hore! String datang - kita pergi sepanjang cabang dengan memuat dari rakitan. Itu berhasil! Buka sampanye virtual ...
Tapi di sini ... Kamus dilengkapi dengan elemen tipe pengguna: karena ini adalah tipe sistem, maka ... jelas, kami mencoba memuatnya dari perakitan, tetapi karena elemen yang dimilikinya adalah tipe kami, apalagi, dengan kualifikasi penuh (perakitan, versi, kunci ), lalu kita jatuh lagi. (harus ada senyum sedih).
Jelas, Anda perlu mengubah nama input dari tipe, menggantikan tautan ke rakitan yang diinginkan. Saya benar-benar berharap untuk nama tipe, ada analog dari kelas
AssemblyName , tapi saya tidak menemukan yang serupa. Menulis parser universal dengan penggantian bukanlah tugas yang mudah. Setelah serangkaian percobaan, saya sampai pada solusi berikut: di konstruktor statis, saya kurangi jenis untuk diganti, dan kemudian saya mencari nama mereka di baris dengan nama jenis yang dibuat, dan ketika saya menemukannya, saya mengganti nama perakitan
Seperti yang Anda lihat, saya mulai dari fakta bahwa PublicKeyToken adalah yang terakhir dalam deskripsi tipe. Mungkin ini tidak 100% dapat diandalkan, tetapi dalam tes saya, saya tidak menemukan kasus di mana ini tidak begitu.
Jadi, sebaris bentuk
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Application, Versi = 1.0.0.0, Budaya = netral, PublicKeyToken = null], [System.Byte [], mscorlib, Versi = 4.0.0.0, Budaya = netral, PublicKeyToken = b77a5c561934e089]] ยป
berubah menjadi
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType, Assembley.Portal, Versi = 1.0.0.0, Budaya = netral, PublicKeyToken = null], [System.Byte [], mscorlib, Versi = 4.0.0.0, Budaya = netral, PublicKeyToken = b77a5c561934e089]] ยป
Sekarang semuanya akhirnya bekerja "seperti jam." Ada beberapa seluk beluk teknis kecil: jika Anda ingat, file yang kami sertakan termasuk dalam tautan dari aplikasi utama. Tetapi dalam aplikasi utama semua tarian ini tidak diperlukan. Oleh karena itu, mekanisme kompilasi bersyarat dari formulir
BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif
Dengan demikian, dalam perakitan portal kami mendefinisikan EXTERNAL_LIB makro, tetapi dalam aplikasi utama - tidak
"Penyimpangan non-lirik"
Bahkan, dalam proses pengkodean, untuk memeriksa solusi dengan cepat, saya membuat satu kesalahan perhitungan, yang mungkin membuat saya harus membayar sejumlah sel syaraf: untuk permulaan, saya hanya meng-hardcode substitusi jenis untuk Dicitionary. Akibatnya, setelah deserialization, itu ternyata menjadi Kamus kosong, yang juga "macet" ketika mencoba untuk melakukan beberapa operasi dengannya. Saya sudah mulai berpikir bahwa Anda tidak bisa menipu BinaryFormatter , dan saya memulai eksperimen putus asa dengan upaya untuk menulis pewaris Kamus. Untungnya, saya berhenti hampir pada waktunya dan kembali menulis mekanisme substitusi universal dan, mengimplementasikannya, saya menyadari bahwa untuk membuat Kamus tidak cukup untuk mendefinisikan kembali jenisnya: Anda masih perlu mengurus jenis untuk KeyValuePair <TKey, TValue>, Comparer, yang juga diminta dari Binder
Ini adalah petualangan serialisasi biner. Saya akan berterima kasih atas umpan baliknya.