.Net تسلسل ثنائي بدون الرجوع إلى التجميع مع نوع المصدر أو كيفية التفاوض مع BinaryFormatter

في هذه المقالة ، سأشارك تجربة تسلسل النوع الثنائي بين التجميعات ، دون الرجوع إلى بعضها البعض. كما اتضح ، هناك حالات حقيقية و "شرعية" عندما تحتاج إلى إلغاء تسلسل البيانات دون أن يكون هناك رابط للتجميع حيث يتم الإعلان عنها. في المقالة سوف أتحدث عن السيناريو الذي كان مطلوبًا فيه ، سأصف طريقة الحل ، وسأتحدث أيضًا عن الأخطاء المتوسطة التي تم ارتكابها أثناء البحث

مقدمة بيان المشكلة


نتعاون مع شركة كبيرة تعمل في مجال الجيولوجيا. تاريخيا ، كتبت الشركة برمجيات مختلفة للغاية للعمل مع البيانات القادمة من أنواع مختلفة من المعدات + تحليل البيانات + التنبؤ. للأسف ، كل هذا البرنامج بعيد كل البعد عن كونه "ودودًا" مع بعضهم البعض ، وغالبًا ما يكون غير ودي على الإطلاق. من أجل دمج المعلومات بطريقة أو بأخرى ، يتم الآن إنشاء بوابة ويب ، حيث تقوم برامج مختلفة بتحميل بياناتها في شكل XML. وتحاول البوابة إنشاء عرض زائد ناقص. فارق بسيط مهم: نظرًا لأن مطوري البوابة ليسوا أقوياء في مجالات موضوع كل تطبيق ، قدم كل فريق محولًا نموذجيًا / محول بيانات من xml إلى هياكل بيانات البوابة.

أعمل في فريق يعمل على تطوير أحد التطبيقات وكتبنا بسهولة آلية تصدير لجزء البيانات الخاص بنا. ولكن هنا ، قرر محلل الأعمال أن البوابة المركزية بحاجة إلى أحد التقارير التي كان برنامجنا يقوم ببنائها. هنا ظهرت المشكلة الأولى: يتم إنشاء التقرير من جديد في كل مرة ولا يتم حفظ النتائج في أي مكان.
"لذا احفظه!" ربما يعتقد القارئ. فكرت بذلك أيضًا ، ولكن شعرت بخيبة أمل شديدة من مطلب إنشاء التقرير بالفعل للبيانات التي تم تنزيلها. لا يوجد شيء للقيام به - تحتاج إلى نقل المنطق.

المرحلة 0. إعادة بيع ديون. لا شيء يبشر بالمتاعب


تقرر فصل منطق بناء التقرير (في الواقع ، هذا عبارة عن ملصق مكون من 4 أعمدة ، ولكن المنطق عبارة عن عربة وعربة كبيرة) في فئة منفصلة ، وتضمين الملف مع هذه الفئة بالإشارة في تجميع المحلل. من خلال هذا نحن:

  1. تجنب النسخ المباشر
  2. الحماية من التناقضات في الإصدار

إن فصل المنطق إلى فصل منفصل ليس مهمة صعبة. ولكن بعد ذلك لم يكن كل شيء ورديًا: فقد كانت الخوارزمية تعتمد على كائنات الأعمال ، والتي لم يتناسب نقلها مع مفهومنا. اضطررت إلى إعادة كتابة الأساليب بحيث تقبل الأنواع البسيطة فقط وتعمل عليها. لم يكن الأمر دائمًا بسيطًا وفي الأماكن ، كان يتطلب قرارات ، ظل جمالها محل تساؤل ، ولكن بشكل عام ، تم الحصول على حل موثوق بدون عكازات واضحة.

كان هناك تفصيل واحد ، كما تعلمون ، غالبًا ما يعمل كملاذ مريح للشيطان: لقد ورثنا نهجًا غريبًا من الأجيال السابقة من المطورين ، حيث يتم تخزين بعض البيانات المطلوبة لإنشاء تقرير في قاعدة البيانات على أنها كائنات ثنائية متسلسلة. الأسئلة "لماذا؟" ، "كعك؟" ، إلخ ، للأسف ، ستبقى دون إجابة بسبب نقص المرسل إليهم). وفي مدخلات الحسابات ، بالطبع ، يجب أن نزيلها.

هذه الأنواع ، التي كان من المستحيل التخلص منها ، قمنا أيضًا بتضمينها "بالإشارة" ، خاصة أنها لم تكن معقدة.

المرحلة 1. نزع التسلسل. تذكر اسم النوع الكامل


بعد القيام بالتلاعبات المذكورة أعلاه وإجراء تشغيل اختباري ، تلقيت خطأ وقت التشغيل بشكل غير متوقع
لا يمكن إرسال [A] Namespace.TypeA إلى [B] Namespace.TypeA. النوع أ نشأ من "Assembley.Application، Version = 1.0.0.0، Culture = Neutral، PublicKeyToken = null" في السياق "افتراضي" في الموقع "...". النوع B ينشأ من "Assmbley.Portal ، الإصدار = 1.0.0.0 ، الثقافة = محايد ، PublicKeyToken = فارغ" في السياق "افتراضي" في الموقع "".
أخبرتني أول روابط Google أن الحقيقة هي أن BinaryFormatter لا يكتب البيانات فحسب ، بل يكتب أيضًا المعلومات إلى دفق الإخراج ، وهو أمر منطقي. ومع الأخذ في الاعتبار أن الاسم الكامل للنوع يحتوي على التجميع الذي تم التصريح به ، فإن صورة ما حاولت إلغاء نوع منه هو مختلف تمامًا عن وجهة نظر .Net

بعد أن خدشت رأسي ، اتخذت ، كما يحدث ، قرارًا واضحًا ، ولكن للأسف ، شرير ، لاستبدال نوع TypeA معين أثناء إزالة التسلسل الديناميكي . كل شيء يعمل. تقاربت نتائج التقرير من أعلى إلى أسفل ، مرت الاختبارات على خادم البناء. مع شعور بالإنجاز ، نرسل المهمة إلى المختبرين.

المرحلة 2. الرئيسية. التسلسل بين التجمعات


جاء الحساب بسرعة على شكل أخطاء مسجلة من قبل المختبرين ، والتي ذكرت أن المحلل اللغوي على جانب المدخل سقط باستثناء أنه لا يمكن تحميل التجميع Assembley.Application (تجميع من تطبيقنا). الفكر الأول - لم أنظف المراجع. ولكن - لا ، كل شيء على ما يرام ، لا أحد يشير. أحاول تشغيله مرة أخرى في وضع الحماية - كل شيء يعمل. أبدأ في الشك في وجود خطأ في البناء ، ولكن هنا يتبادر إلى ذهني فكرة لا ترضيني: أقوم بتغيير مسار الإخراج للمحلل إلى مجلد منفصل ، وليس إلى دليل الحاوية المشتركة للتطبيق. وفويلا - أحصل على الاستثناء الموصوف. يؤكد تحليل Stectrace التخمينات المبهمة - إزالة التسلسل آخذة في الانخفاض.

كان الوعي سريعًا ومؤلمًا: استبدال نوع معين بديناميكي لم يغير أي شيء ، لا يزال BinaryFormatter ينشئ نوعًا من تجميع خارجي ، فقط عندما يكون التجميع مع النوع قريبًا ، ووقت التشغيل الذي تم تحميله بشكل طبيعي ، وعندما اختفى التجميع - نحصل على خطأ.

كان هناك سبب للحزن. لكن غوغلينغ أعطى الأمل في شكل SerializationBinder Class . كما اتضح ، فإنه يسمح لك بتحديد النوع الذي يتم فيه إلغاء تسلسل بياناتنا. للقيام بذلك ، قم بإنشاء وريث وحدد الطريقة التالية فيه.

public abstract Type BindToType(String assemblyName, String typeName); 

حيث يمكنك إرجاع أي نوع لظروف معينة.
تحتوي فئة BinaryFormatter على خاصية Binder حيث يمكنك إدخال التطبيق الخاص بك.

يبدو أنه لا توجد مشكلة. ولكن مرة أخرى ، تبقى التفاصيل (انظر أعلاه).

أولاً ، يجب عليك معالجة الطلبات لجميع الأنواع (والمعيارية أيضًا).
تم العثور على خيار تنفيذ مثير للاهتمام على الإنترنت هنا ، لكنهم يحاولون استخدام الرابط الافتراضي من BinaryFormatter ، في شكل إنشاء

 var defaultBinder = new BinaryFormatter().Binder 

ولكن في الواقع ، تكون خاصية Binder فارغة بشكل افتراضي. أظهر تحليل لشفرة المصدر أنه داخل BinaryFormatter ، سواء تم فحص Binder ، إذا كان الأمر كذلك ، يتم استدعاء أساليبها ، إذا لم يتم ذلك ، يتم استخدام المنطق الداخلي ، والذي يتلخص في النهاية في

  var assembly = Assembly.Load(assemblyName); return FormatterServices.GetTypeFromAssembly(assembly, typeName); 

دون مزيد من اللغط ، كررت نفس المنطق في نفسي.

إليك ما حدث في التنفيذ الأول

 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); } } 

أي يتم فحص ما إذا كانت مساحة الاسم تنتمي إلى المشروع - نعيد النوع من المجال الحالي ، إذا كان نوع النظام - نقوم بالتحميل من التجميع المقابل

يبدو منطقيا. نبدأ الاختبار: يأتي نوعنا - نستبدل ، يتم إنشاؤه. الصيحة! تأتي السلسلة - نذهب على طول الفرع مع التحميل من التجميع. يعمل! فتح الشمبانيا الافتراضية ...

ولكن هنا ... يأتي القاموس مع عناصر من أنواع المستخدمين: نظرًا لأن هذا هو نوع النظام ، فمن الواضح ... أننا نحاول تحميله من التجميع ، ولكن نظرًا لأن العناصر الموجودة فيه هي أنواعنا ، علاوة على ذلك ، مع التأهيل الكامل (التجميع ، الإصدار ، المفتاح ) ، ثم نسقط مرة أخرى. (يجب أن تكون هناك ابتسامة حزينة).

من الواضح أنك تحتاج إلى تغيير اسم الإدخال من النوع ، واستبدال الروابط بالتجميع المطلوب. كنت أتمنى حقًا أنه بالنسبة لاسم النوع ، هناك نظير من فئة AssemblyName ، لكنني لم أجد أي شيء مماثل. إن كتابة محلل شامل مع الاستبدال ليست مهمة سهلة. بعد سلسلة من التجارب ، توصلت إلى الحل التالي: في المنشئ الثابت ، أطرح الأنواع لاستبدالها ، ثم أبحث عن أسمائهم في السطر مع اسم النوع الذي تم إنشاؤه ، وعندما أجده ، أستبدل اسم التجميع

  /// <summary> /// The types that may be changed to local /// </summary> protected static IEnumerable<Type> _changedTypes; static MyBinder() { var executingAssembly = Assembly.GetCallingAssembly(); var name = executingAssembly.GetName().Name; _changedTypes = executingAssembly.GetTypes().Where(t => t.Namespace != null && !t.Namespace.Contains(name) && !t.Name.StartsWith("<")); //!t.Namespace.Contains(name) - .     ,         // "<'      -     } private static string CorrectTypeName(string name) { foreach (var changedType in _changedTypes) { var ind = name.IndexOf(changedType.FullName); if (ind != -1) { var endIndex = name.IndexOf("PublicKeyToken", ind) ; if (endIndex != -1) { endIndex += +"PublicKeyToken".Length + 1; while (char.IsLetterOrDigit(name[endIndex++])) { } var sb = new StringBuilder(); sb.Append(name.Substring(0, ind)); sb.Append(changedType.AssemblyQualifiedName); sb.Append(name.Substring(endIndex-1)); name = sb.ToString(); } } } return name; } /// <summary> /// look up the type locally if the assembly-name is "NA" /// </summary> /// <param name="assemblyName"></param> /// <param name="typeName"></param> /// <returns></returns> public override Type BindToType(string assemblyName, string typeName) { typeName = CorrectTypeName(typeName); if (assemblyName.Contains("<ObligatoryPartOfNamespace>") || assemblyName.Equals("NA")) { var bindToType = Type.GetType(typeName); return bindToType; } else { var bindToType = LoadTypeFromAssembly(assemblyName, typeName); return bindToType; } } 

كما ترى ، لقد بدأت من حقيقة أن PublicKeyToken هو الأخير في وصف النوع. ربما هذا ليس موثوقًا بنسبة 100 ٪ ، ولكن في اختباراتي لم أجد حالات حيث لم يكن الأمر كذلك.

وبالتالي ، خط النموذج
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType، Assembley.Application، Version = 1.0.0.0، Culture = Neutral، PublicKeyToken = null]، [System.Byte []، mscorlib، الإصدار = 4.0.0.0، الثقافة = محايد ، PublicKeyToken = b77a5c561934e089]] »

يتحول إلى
"System.Collections.Generic.Dictionary`2 [[SomeNamespace.CustomType، Assembley.Portal، Version = 1.0.0.0، Culture = Neutral، PublicKeyToken = null]، [System.Byte []، mscorlib، الإصدار = 4.0.0.0، الثقافة = محايد ، PublicKeyToken = b77a5c561934e089]] »

الآن عمل كل شيء أخيرًا "مثل الساعة". كانت هناك خفايا فنية ثانوية: إذا كنت تتذكر ، فقد تم تضمين الملفات التي قمنا بتضمينها في الرابط من التطبيق الرئيسي. ولكن في التطبيق الرئيسي ليست هناك حاجة إلى كل هذه الرقصات. لذلك ، آلية تجميع مشروطة للنموذج

 BinaryFormatter binForm = new BinaryFormatter(); #if EXTERNAL_LIB binForm.Binder = new MyBinder(); #endif 

وفقًا لذلك ، في تجميع المدخل نحدد الماكرو EXTERNAL_LIB ، ولكن في التطبيق الرئيسي - لا

"انحراف غير غنائي"


في الواقع ، في عملية الترميز ، من أجل التحقق من الحل بسرعة ، قمت بحساب خاطئ واحد ، والذي ربما يكلفني عددًا معينًا من الخلايا العصبية: بالنسبة للمبتدئين ، قمت فقط بترميز نوع استبدال Dicitionary. ونتيجة لذلك ، بعد إلغاء التسلسل ، تبين أنه قاموس فارغ ، والذي "انهار" أيضًا عند محاولة إجراء بعض العمليات معه. لقد بدأت بالفعل في التفكير في أنه لا يمكنك خداع BinaryFormatter ، وبدأت تجارب يائسة في محاولة لكتابة وريث القاموس. لحسن الحظ ، توقفت في الوقت المحدد تقريبًا وعدت لكتابة آلية استبدال عالمية ، وأدركت أنها لإنشاء قاموس لا يكفي لإعادة تعريف نوعه: ما زلت بحاجة إلى رعاية أنواع KeyValuePair <TKey ، TValue> ، المقارنة ، والمطلوبة أيضًا من بيندر


هذه هي مغامرات التسلسل الثنائي. سأكون ممتنا لردود الفعل.

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


All Articles