
اليوم سنتحدث عن كيفية كتابة
AutoMapper . نعم ، أود حقاً إخبارك بهذا ، لكن لا يمكنني ذلك. والحقيقة هي أن هذه الحلول كبيرة للغاية ، ولها تاريخ من التجربة والخطأ ، كما قطعت شوطًا طويلاً في التطبيق. لا يمكنني إلا أن أفهم كيف يعمل هذا ، وأضف نقطة انطلاق لأولئك الذين يرغبون في فهم آلية عمل "المصممين". يمكنك أن تقول أننا سنكتب دراجتنا.
تنصل
أذكرك مرة أخرى: سنكتب مخططًا بدائيًا. إذا قررت فجأة تعديله واستخدامه في المنتج - فلا تفعل هذا. خذ حلاً جاهزًا يعرف مجموعة المشاكل في هذا الموضوع
ويعرف بالفعل كيفية حلها. هناك العديد من الأسباب الأكثر أهمية أو أقل لكتابة واستخدام مصمم الدراجة الخاص بك:
- تحتاج الى بعض التخصيص الخاص.
- أنت بحاجة إلى أقصى درجات الأداء في ظروفك وأنت مستعد لملء الأقماع.
- تريد أن تفهم كيف يعمل معين.
- أنت فقط تحب ركوب الدراجات.
ما يسمى كلمة "معين"؟
هذا هو النظام الفرعي المسؤول عن أخذ كائن وتحويله (نسخ قيمه) إلى كائن آخر. المهمة النموذجية هي تحويل DTO إلى كائن طبقة عمل. يعمل "المخطط" الأكثر بدائية عبر خصائص مصدر البيانات ويقارنها بخصائص نوع البيانات التي سيتم إخراجها. بعد المطابقة ، يتم استخراج القيم من المصدر وكتابتها إلى الكائن ، والتي ستكون نتيجة التحويل. في مكان ما ، على الأرجح ، سيكون من الضروري إنشاء هذه "النتيجة" ذاتها.
بالنسبة للمستهلك ، فإن المخطط هو خدمة توفر الواجهة التالية:
public interface IMapper<out TOut> { TOut Map(object source); }
وأؤكد: هذه هي الواجهة الأكثر بدائية والتي ، من وجهة نظري ، مريحة للتفسير. في الواقع ، من المحتمل أن نتعامل مع معين أكثر تحديدًا (IMapper <TIn ، TOut>) أو مع واجهة أكثر عمومية (IMapper) ، والتي ستحدد بنفسها معينًا محددًا للأنواع المحددة من كائنات الإدخال والإخراج.
تنفيذ ساذج
ملاحظة: حتى التنفيذ الساذج لمخطط معين يتطلب معرفة أساسية بـ
Reflection و
ExpressionTrees . إذا لم تكن قد اتبعت الروابط أو سمعت أي شيء عن هذه التقنيات - فقم بذلك ، اقرأها. أعدك أن العالم لن يكون هو نفسه.
ومع ذلك ، نكتب مصمم الخرائط الخاص بك. للبدء ، دعونا نحصل على جميع الخصائص (
PropertyInfo ) لنوع البيانات التي سيتم إخراجها (فيما يلي سوف أسميها
TOut ). هذا بسيط للغاية: نحن نعرف النوع ، حيث نكتب تنفيذ فئة عامة ذات معلمات بنوع TOut. بعد ذلك ، باستخدام مثيل لفئة Type ، نحصل على جميع خصائصه.
Type outType = typeof(TOut); PropertyInfo[] outProperties = outType.GetProperties();
عند الحصول على العقارات ، أغفل الميزات. على سبيل المثال ، قد يكون بعضها بدون وظيفة setter ، وقد يتم تعليم البعض على أنها متجاهلة من قبل السمة ، وقد يكون بعضها مع وصول خاص. نحن ندرس أبسط خيار.
نذهب أبعد من ذلك. سيكون من الجيد أن تكون قادرًا على إنشاء مثيل من النوع TOut ، أي الكائن نفسه الذي "نقوم" بتخطيط الكائن الوارد فيه. في C # ، هناك عدة طرق للقيام بذلك. على سبيل المثال ، يمكننا القيام بذلك: System.Activator.CreateInstance (). أو حتى فقط TOut () جديد ، ولكن لهذا تحتاج إلى إنشاء تقييد لـ TOut ، والذي لا تريد القيام به في الواجهة العامة. ومع ذلك ، كلانا يعرف شيئًا عن ExpressionTrees ، مما يعني أنه يمكننا القيام بذلك على النحو التالي:
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();
لماذا هكذا؟ نظرًا لأننا نعلم أن مثيل فئة Type يمكن أن يقدم معلومات حول المنشئات الموجودة به - فهذا مناسب جدًا للحالات التي نقرر فيها تطوير معيننا حتى نتمكن من تمرير أي بيانات إلى المُنشئ. أيضًا ، تعلمنا المزيد حول ExpressionTrees ، أي أنها تسمح للوحة بإنشاء وتجميع التعليمات البرمجية ، والتي يمكن إعادة استخدامها بعد ذلك. في هذه الحالة ، هي وظيفة تبدو بالفعل () => TOut () جديدة.
أنت الآن بحاجة إلى كتابة طريقة معين معين ، والتي سوف تنسخ القيم. سنقوم بمتابعة أبسط الطرق: تصفح خصائص الكائن الذي جاء إلينا عند المدخل ، وابحث عن الخصائص التي تحمل الاسم نفسه بين خصائص الكائن الصادر. إذا وجدت - نسخة ، إن لم يكن - المضي قدما.
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;
وبالتالي ، قمنا بتكوين فئة
BasicMapper بالكامل. يمكنك التعرف على اختباراته
هنا . يرجى ملاحظة أن المصدر يمكن أن يكون إما كائنًا من أي نوع معين أو كائنًا مجهول الهوية.
الأداء والملاكمة
انعكاس كبير ، ولكن بطيئة. علاوة على ذلك ، يؤدي الاستخدام المتكرر إلى زيادة حركة الذاكرة ، مما يعني أنه يقوم بتحميل GC ، مما يعني أنه يبطئ التطبيق أكثر. على سبيل المثال ، استخدمنا فقط أساليب
PropertyInfo.SetValue و
PropertyInfo.GetValue . إرجاع الأسلوب GetValue كائن فيه التفاف قيمة معينة (الملاكمة). هذا يعني أننا تلقينا مخصصًا من البداية.
عادةً ما توجد المخططات حيث تحتاج إلى تحويل كائن إلى كائن آخر ... لا ، ليس كائنًا واحدًا ، ولكن يوجد العديد من الكائنات. على سبيل المثال ، عندما نأخذ شيئا من قاعدة البيانات. في هذا المكان ، أود أن أرى الأداء العادي وألا أفقد الذاكرة في العملية الأولية.
ماذا يمكننا أن نفعل؟ سوف
ExpressionTrees مساعدتنا مرة أخرى. والحقيقة هي أن .NET يسمح لك بإنشاء وتجميع التعليمات البرمجية "سريعًا": نصفها في تمثيل الكائن ونقول ماذا وأين سنستخدمه ... وتجميعه. تقريبا لا السحر.
مخطط معين
في الواقع ، كل شيء بسيط نسبيًا: لقد فعلنا جديدًا بالفعل مع Expression.New (ConstructorInfo). ربما لاحظت أن الطريقة الجديدة الثابتة تسمى بالضبط نفس المشغل. الحقيقة هي أن كل لغة C # تقريبًا تنعكس في شكل طرق ثابتة لفئة التعبير. إذا كان هناك شيء مفقود ، فهذا يعني أنك تبحث عن ما يسمى "السكر النحوي."
إليك بعض العمليات التي سنستخدمها في مخططنا:
- إعلان متغير - Expression.Variable (النوع ، سلسلة). تخبر وسيطة Type نوع المتغير الذي سيتم إنشاؤه ، والسلسلة هي اسم المتغير.
- الواجب - التعبير. التعيين (التعبير ، التعبير). الوسيطة الأولى هي ما نخصصه ، والحجة الثانية هي ما نخصصه.
- الوصول إلى خاصية كائن هو Expression.Property (التعبير ، PropertyInfo). Expression هو مالك العقار ، و PropertyInfo هو تمثيل كائن الخاصية التي تم الحصول عليها من خلال Reflection.
مع هذه المعرفة ، يمكننا إنشاء متغيرات ، والوصول إلى خصائص الكائنات ، وتعيين قيم لخصائص الكائنات. على الأرجح ، نحن نفهم أيضًا أن ExpressionTree يحتاج إلى ترجمة إلى مفوض النموذج
Func <object، TOut> . الخطة هي: نحصل على متغير يحتوي على بيانات الإدخال ، وإنشاء مثيل من نوع TOut وإنشاء تعبيرات تعين خاصية واحدة إلى أخرى.
لسوء الحظ ، الكود ليس مضغوطًا جدًا ، لذلك أقترح عليك إلقاء نظرة على تطبيق
CompiledMapper على الفور. احضرت هنا فقط النقاط الرئيسية.
أولاً ، نقوم بإنشاء تمثيل كائن لمعلمة وظيفتنا. نظرًا لأنه يأخذ كائنًا كمدخل ، فإن الكائن سيكون معلمة.
var parameter = Expression.Parameter(typeof(object), "source");
بعد ذلك ، ننشئ متغيرين وقائمة تعبيرات نضيف إليها تعبيرات التعيين بالتسلسل. الترتيب مهم ، لأن هذه هي الطريقة التي سيتم بها تنفيذ الأوامر عندما نسميها الطريقة المترجمة. على سبيل المثال ، لا يمكننا تعيين قيمة لمتغير لم يتم الإعلان عنه بعد.
علاوة على ذلك ، بالطريقة نفسها كما في حالة التطبيق الساذج ، نذهب إلى قائمة خصائص النوع ونحاول مطابقتها بالاسم. ومع ذلك ، بدلاً من تعيين القيم على الفور ، نقوم بإنشاء تعبيرات لاستخراج القيم وتعيين القيم لكل خاصية مرتبطة.
Expression sourceValue = Expression.Property(sourceInstance, sourceProperty); Expression outValue = Expression.Property(outInstance, outProperty); expressions.Add(Expression.Assign(outValue, sourceValue));
نقطة مهمة: بعد أن أنشأنا جميع عمليات التعيين ، نحتاج إلى إرجاع النتيجة من الوظيفة. للقيام بذلك ، يجب أن يكون التعبير الأخير في القائمة هو التعبير ، والذي يحتوي على مثيل الفئة التي أنشأناها. تركت تعليق بجانب هذا الخط. لماذا يبدو السلوك المقابل للكلمة الرئيسية المرتجعة في ExpressionTree بهذا الشكل؟ أخشى أن هذه قضية منفصلة. الآن أقترح أنه من السهل أن نتذكر.
حسنًا ، في النهاية ، علينا أن نجمع كل التعبيرات التي بنيناها. ما الذي يهمنا هنا؟ يحتوي متغير الجسم على "نص" الوظيفة. "وظائف عادية" لها جسم ، أليس كذلك؟ حسنا ، الذي نرفقه في الأقواس. لذلك ، Expression.Block هو ذلك بالضبط. نظرًا لأن الأقواس المتعرجة هي أيضًا نطاق ، يجب أن نمر هناك إلى المتغيرات التي سيتم استخدامها هناك - في حالتنا sourceInstance و outInstance.
var body = Expression.Block(new[] {sourceInstance, outInstance}, expressions); return Expression.Lambda<Func<object, TOut>>(body, parameter).Compile();
في الإخراج ، نحصل على Func <object ، TOut> ، أي دالة يمكنها تحويل البيانات من كائن إلى آخر. لماذا هذه الصعوبات ، تسأل؟ أذكرك بأننا أولاً ، أردنا تجنب الملاكمة عند نسخ قيم ValueType ، وثانياً ، أردنا التخلي عن أساليب PropertyInfo.GetValue و PropertyInfo.SetValue ، لأنها بطيئة بعض الشيء.
لماذا لا الملاكمة؟ نظرًا لأن ExpressionTree المترجمة عبارة عن IL حقيقي ، ولأنه يشبه وقت التشغيل (تقريبًا) مثل التعليمات البرمجية الخاصة بك. لماذا "مخطط المترجمة" أسرع؟ مرة أخرى: لأنها مجرد IL IL. بالمناسبة ، يمكننا بسهولة تأكيد السرعة باستخدام مكتبة
BenchmarkDotNet ، ويمكن الاطلاع على المؤشر نفسه
هنا .
في عمود "النسبة" أظهر "مخطط المترجمة" (CompiledMapper) نتيجة جيدة للغاية ، حتى بالمقارنة مع "رسم تلقائي" (وهو الأساس ، أي 1). ومع ذلك ، دعونا لا نفرح: يتمتع AutoMapper بقدرات أكبر بكثير مقارنة بالدراجة. مع هذه اللوحة ، أردت فقط أن أوضح أن ExpressionTrees أسرع بكثير من "طريقة التفكير الكلاسيكية".
ملخص
آمل أن أتمكن من إظهار أن كتابة مخططك بسيط للغاية. إن Reflection و ExpressionTrees أدوات قوية للغاية يستخدمها المطورين لحل العديد من المهام المختلفة. حقن التبعية ، التسلسل / إلغاء التسلسل ، مستودعات CRUD ، إنشاء استعلامات SQL ، باستخدام لغات أخرى كنصوص برمجية لتطبيقات .NET - كل هذا يتم باستخدام Reflection و Reflection.Emit و ExpressionTrees.
ماذا عن مخطط؟ يعد مخطط الخرائط مثالًا رائعًا حيث يمكنك تعلم كل هذا.
ملاحظة: إذا كنت تريد المزيد من ExpressionTrees ، أقترح القراءة حول كيفية
جعل محول JSON الخاص بك باستخدام هذه التقنية.