البرمجة الموجهة إلى الجانب (AOP) عبر كود المصدر





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



اليوم ، في معظم الحالات ، يتم تنفيذ الجوانب على مستوى الرمز الثانوي ، أي بعد التجميع ، تقوم بعض الأدوات "بتشبيك" شفرة بايت إضافية بدعم من المنطق المطلوب.



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



إذا كنت مهتمًا بالتفاصيل ، من فضلك انظر القط.



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



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



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



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



الميزة الإضافية هي أن الكود الخاص بك لم يعد يبدو كشخص مصاب بالجدري ، لأنه مليء بهذه الوظيفة وعند قراءة الكود يبدو وكأنه ضوضاء مزعجة.



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



من واقع خبرتي ، في النظام البيئي .Net ، تعتبر البرمجة الموجهة إلى الجانب أقل شعبية مقارنة بنظام Java البيئي. أعتقد أن السبب الرئيسي هو عدم وجود أدوات مجانية ومفتوحة المصدر يمكن مقارنتها بوظائف وجودة Java.



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



يمكنك مقارنة إمكانيات الأدوات (يجب أن يؤخذ في الاعتبار أن المقارنة تم إجراؤها بواسطة مالك PostSharp ، لكنها تعطي بعض الصورة).



طريقنا إلى البرمجة الموجهة



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



ونظرًا لمحدودية الموارد ، بالنسبة لنا ، فإن إحدى أهم المهام هي القدرة على إعادة استخدام التعليمات البرمجية واستخدام الأدوات التي تنقذ المطور من المهام الروتينية.



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



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



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



لسوء الحظ ، لا تزال أداتنا بعيدة عن المثالية ، لذلك أود تقسيم الوصف إلى جزأين ، الأول هو كيف أرى تنفيذ هذه الوظيفة في عالم مثالي والثاني كيف يتم ذلك هنا.



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



كيف يتم ذلك في عالم مثالي



بعد عدة سنوات من استخدام أداتنا ، لدي رؤية حول الطريقة التي أود أن يعمل بها هذا إذا عشنا في عالم مثالي.



في رؤيتي لعالم مثالي ، تسمح مواصفات اللغة باستخدام تحويلات كود المصدر ، وهناك دعم للمترجم و IDE.



الفكرة مستوحاة من إدراج المُعدِّل "الجزئي" في مواصفات لغة C #. هذا المفهوم البسيط إلى حد ما (القدرة على تحديد فئة أو بنية أو واجهة في عدة ملفات) أدى إلى تحسين وتبسيط دعم الأدوات بشكل كبير لإنشاء شفرة المصدر تلقائيًا. أولئك. إنه نوع من التقسيم الأفقي للكود المصدري لفئة بين عدة ملفات. بالنسبة لأولئك الذين لا يعرفون لغة C # ، مثال صغير.



لنفترض أن لدينا نموذجًا بسيطًا موصوفًا في الملف Example1.aspx

<%@ Page Language="C#" AutoEventWireup="True" %>
// . . .
<asp:Button id="btnSubmit"
           Text="Submit"
           OnClick=" btnSubmit_Click" 
           runat="server"/>
// . . .


والمنطق المخصص (على سبيل المثال ، تغيير لون الزر إلى اللون الأحمر عند النقر فوقه) في ملف Example1.aspx.cs



public partial class ExamplePage1 : System.Web.UI.Page, IMyInterface
{
  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


يسمح التواجد في اللغة للإمكانيات التي يوفرها "الجزئي" لمجموعة الأدوات بتحليل ملف Example1.aspx وإنشاء ملف Example1.aspx.designer.cs تلقائيًا.



public partial class ExamplePage1 : System.Web.UI.Page
{
  protected global::System.Web.UI.WebControls.Button btnSubmit;
}


أولئك. لدينا القدرة على تخزين جزء من التعليمات البرمجية لفئة ExamplePage1 في ملف واحد بواسطة المبرمج القابل للتحديث (Example1.aspx.cs) وجزء في ملف Example1.aspx.designer.cs بواسطة مجموعة الأدوات التي تم إنشاؤها تلقائيًا. بالنسبة للمترجم ، في النهاية ، يبدو وكأنه فئة عامة واحدة



public class ExamplePage1 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }
}


باستخدام المثال مع تعريف الوراثة لواجهة IMyInterface ، يمكنك أن ترى أن النتيجة النهائية هي مجموعة من تعريفات الفئات من ملفات مختلفة.



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



وفقًا لذلك ، تتمثل فكرتي في تضمين معدلين إضافيين في مواصفات اللغة ، مما يسهل تضمين الجوانب في شفرة المصدر.



المعدل الأول أصلي ونضيفه إلى تعريف الفئة التي يجب أن تكون قابلة للتحويل.



تتم معالجة الثانية وهي ترمز إلى أن هذا هو تعريف الفئة النهائي الذي تم الحصول عليه بواسطة أداة تحويل المصدر ويجب قبوله من قبل المترجم لإنشاء الرمز الثانوي.



التسلسل شيء من هذا القبيل



  1. يعمل المستخدم مع الكود المصدري للفئة التي تحتوي على المُعدِّل الأصلي في ملف .cs (على سبيل المثال ، Example1.cs)
  2. عند التجميع ، يتحقق المترجم من صحة شفرة المصدر ، وإذا تم تجميع الفئة بنجاح ، فإنه يتحقق من وجود الأصل
  3. إذا كان الأصل موجودًا ، فإن المترجم يعطي الكود المصدري لهذا الملف لعملية التحويل (وهو صندوق أسود للمترجم).
  4. .processed.cs .processed.cs.map ( .cs .processed.cs, IDE)
  5. .processed.cs ( Example1.processed.cs) .
  6. ,



    a. original processed

    b. .cs .processed.cs
  7. , .processed.cs .


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



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



تمت إضافة ميزة جديدة في .NET 5 - مولدات الكود المصدريالذي يسمح لك بالفعل بإنشاء ملفات شفرة مصدر جديدة أثناء التجميع وهذه حركة في الاتجاه الصحيح. لسوء الحظ ، يسمح لك فقط بإنشاء شفرة مصدر جديدة ، ولكن لا يسمح لك بتعديل الرمز الموجود. لذلك ما زلنا ننتظر.



مثال على عملية مماثلة. يقوم المستخدم بإنشاء ملف Example2.cs

public original class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    btnSubmit.Color = Color.Red;
  }	
}


يعمل للتجميع ، إذا كان كل شيء على ما يرام ورأى المحول البرمجي المعدل الأصلي ، فإنه يعطي الكود المصدري لعملية التحويل ، والتي تنشئ ملف Example2.processed.cs (في أبسط الحالات ، يمكن أن يكون مجرد نسخة طبق الأصل من Example2.cs مع استبدال الأصل بمعالج) ...



في حالتنا ، سنفترض أن عملية التحويل أضافت جانب تسجيل والنتيجة تبدو كما يلي:

public processed class ExamplePage2 : System.Web.UI.Page, IMyInterface
{ 
  protected global::System.Web.UI.WebControls.Button btnSubmit;

  protected void btnSubmit_Click(Object sender, EventArgs e) 
  {
    try
    {
      btnSubmit.Color = Color.Red;
    } 
    catch(Exception ex)
    {
      ErrorLog(ex);
      throw;
    }

    SuccessLog();
  }	

  private static processed ErrorLog(Exception ex)
  {
    // some error logic here
  }

  private static processed SuccessLog([System.Runtime.CompilerServices.CallerMemberName] string memberName = "")
  {
    // some success logic here
  }
}


الخطوة التالية هي التحقق من التوقيعات. التواقيع _main_ متطابقة وتفي بشرط أن تكون التعريفات الأصلية والمعالجة متطابقة تمامًا.



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



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



قام المترجم بتجميع هذا الرمز وإذا كان كل شيء على ما يرام ، فقد أخذ رمز البايت لمواصلة العملية.



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



وظائف IDE الأساسية في عالم مثالي



دعم العمل مع الكود المصدري لملفات .processed.cs في IDE هو بشكل أساسي في التنقل الصحيح بين الفئات الأصلية / المعالجة والانتقالات أثناء التصحيح خطوة بخطوة.



الميزة الثانية الأكثر أهمية لـ IDE (من وجهة نظري) هي المساعدة في قراءة كود الفصول المعالجة. يمكن أن تحتوي الفئة المعالجة على العديد من أجزاء التعليمات البرمجية التي تمت إضافتها بواسطة عدة جوانب. يبدو أن تنفيذ عرض مشابه لمفهوم الطبقات في محرر رسومات هو الخيار الأكثر ملاءمة لتحقيق هذا الهدف. يقوم المكون الإضافي الحالي الخاص بنا بتنفيذ شيء مشابه والاستجابة من المستخدمين إيجابية للغاية.



ميزة أخرى من شأنها أن تساعد في إدخال AOP في الحياة اليومية وهي وظيفة إعادة البناء. يمكن للمستخدم ، الذي يبرز جزءًا من الكود ، أن يقول "Extract To AOP Template" وأنشأ IDE الملفات الصحيحة ، وأنشأ الكود الأولي وبعد تحليل كود المشروع ، اقترح مرشحين لاستخدام نموذج من فئات أخرى.



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



أنا متأكد من أنه إذا تولى منشئو أداة إعادة التوزيع العمل ، فسيتم ضمان السحر.



كتابة كود الجانب في عالم مثالي



لإعادة صياغة TRIZ ، فإن الكتابة المثالية للكود لتنفيذ الجوانب هي عدم كتابة كود إضافي موجود فقط لدعم عمليات الأجهزة.



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



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



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



حسنًا ، بحيث تكون بنية القالب أقرب ما يمكن إلى بناء جملة لغة C # ، من الناحية المثالية وظيفة إضافية ثانوية ، بالإضافة إلى بعض الكلمات الرئيسية والعناصر النائبة.



تنفيذنا الحالي



لسوء الحظ ، نحن لا نعيش في عالم مثالي ، لذلك علينا إعادة اختراع الدراجات وركوبها.



حقن الكود وتجميعه وتصحيحه



نموذجنا الحالي هو إنشاء نسختين من المشروع. الأول هو الأصل الذي يعمل به المبرمج ، والثاني يتحول ، والذي يستخدم للتجميع والتنفيذ.



السيناريو شيء من هذا القبيل



  • , , ..
  • , , , .
  • , , , WPF , ..


لتصحيح الأخطاء ، يتم إطلاق النسخة الثانية من IDE ، ويتم فتح نسخة من المشروع تم تشكيلها على مستوى الدولة وتعمل مع النسخة التي تم تطبيق التحويل عليها.



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



IDE



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



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



الاحتمال الآخر هو إظهار الفرق بين الملف الأصلي والملف المحول. نظرًا لأن IDE يعرف الموقع النسبي لنسخة الملف في المشروع ، فيمكنه عرض الاختلافات بين الملفات الأصلية والملفات المُنشأة في البلد.



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



ترتيب



مهمة منفصلة هي تعيين قواعد التحويل ، أي على أي فئات وطرق سنطبق التحول.



نحن نستخدم عدة مستويات.



المستوى الأول هو ملف التكوين ذي المستوى الأعلى. يمكننا تعيين القواعد اعتمادًا على المسار في نظام الملفات ، أو الأنماط في اسم الملفات ، أو الفئات أو الطرق ، أو نطاقات الفئات ، أو الأساليب أو الخصائص.



المستوى الثاني هو إشارة إلى تطبيق قواعد التحويل على مستوى سمات الفئات أو الطرق أو المجالات.



الثالث على مستوى كتلة الكود والرابع هو إشارة صريحة لتضمين نتائج تحويل القالب في مكان محدد في الكود المصدري.



القوالب



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



بالنسبة لأولئك الذين لم يسبق لهم العمل مع T4 ، فإن أبسط تناظري هو تقديم تنسيق ASPX ، والذي بدلاً من HTML يولد كود المصدر في C # ويتم تنفيذه ليس على IIS ، ولكن كأداة مساعدة منفصلة مع إخراج النتيجة إلى وحدة التحكم (أو إلى ملف).



أمثلة على



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



عينة التعليمات البرمجية المصدر قبل التحويل
// ##aspect=AutoComment

using AOP.Common;
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{

    [AopTemplate("ClassLevelTemplateForMethods", NameFilter = "First")]
    [AopTemplate("StaticAnalyzer", Action = AopTemplateAction.Classes)]
    [AopTemplate("DependencyInjection", AdvicePriority = 500, Action = AopTemplateAction.PostProcessingClasses)]
    [AopTemplate("ResourceReplacer", AdvicePriority = 1000, ExtraTag = "ResourceFile=Demo.resx,ResourceClass=Demo", Action = AopTemplateAction.PostProcessingClasses)]
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");

            // ##aspect="FirstDemoComment" extra data here

            return new Person()
            {
                FirstName = firstName,
                LastName = lastName,
                Age = age,
            };
        }

        private static IConfigurationRoot _configuration = inject;
        private IDataService _service { get; } = inject;
        private Person _somePerson = inject;

        [AopTemplate("LogExceptionMethod")]
        [AopTemplate("StopWatchMethod")]
        [AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]
        public Customer[] SecondDemo(Person[] people)
        {
            IEnumerable<Customer> Customers;

            Console.Out.WriteLine("SecondDemo: 1");

            Console.Out.WriteLine(i18("SecondDemo: i18"));

            int configDelayMS = inject;
            string configServerName = inject;

            using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
            {

                Customers = people.Select(s => new Customer()
                {
                    FirstName = s.FirstName,
                    LastName = s.LastName,
                    Age = s.Age,
                    Id = s.Id
                });

                _service.Init(Customers);

                foreach (var customer in Customers)
                {
                    Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));
                    Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name   ") + customer.LastName);
                }
            }

            Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
            Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
            Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

            return Customers.ToArray();
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;

        [AopTemplate("NotifyPropertyChangedClass", Action = AopTemplateAction.Classes)]
        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Person
        {
            [AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
            public string FullName
            {
                get
                {
                    // ##aspect="FullNameComment" extra data here
                    return $"{FirstName} {LastName}";
                }
            }

            public int Id { get; set; }
            public string FirstName { get; set; }
            public string LastName { get; set; }
            public int Age { get; set; }
        }

        [AopTemplate("NotifyPropertyChanged", Action = AopTemplateAction.Properties)]
        public class Customer : Person
        {
            public double CreditScore { get; set; }
        }

        public interface IDataService
        {
            void Init(IEnumerable<Customer> customers);
            string GetCustomerName(int customerId);
        }

        public class DataService: IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                if(customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));

                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));

                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}




النسخة الكاملة من الكود المصدري بعد التحويل
//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: ekmmxFSeH5ev8Epvl7QvDL+D77DHwq1gHDnCxzeBWcw
//  Created By: JohnSmith
//  Created Machine: 127.0.0.1
//  Created At: 2020-09-19T23:18:07.2061273-04:00
//
// </auto-generated>
//------------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;

namespace Aspectimum.Demo.Lib
{
    public class ConsoleDemo
    {
        public virtual Person FirstDemo(string firstName, string lastName, int age)
        {
            Console.Out.WriteLine("FirstDemo: 1");
            // FirstDemoComment replacement extra data here
            return new Person()
            {FirstName = firstName, LastName = lastName, Age = age, };
        }

        private static IConfigurationRoot _configuration = new ConfigurationBuilder()
            .SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
            .AddJsonFile("appsettings.json", optional: true)
            .Build();
        
        private IDataService _service { get; } = new DataService();

#error Cannot find injection rule for Person _somePerson
        private Person _somePerson = inject;

        public Customer[] SecondDemo(Person[] people)
        {
            try
            {
#error variable "Customers" doesn't match code standard rules
                IEnumerable<Customer> Customers;
                
                Console.Out.WriteLine("SecondDemo: 1");

#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
                Console.Out.WriteLine(i18("SecondDemo: i18"));

                int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
                string configServerName = _configuration["server_name"];
                {
                    // second demo test extra
                    {
                        Customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, Id = s.Id});
                        _service.Init(Customers);
                        foreach (var customer in Customers)
                        {
                            Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));
                            Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + "   ") + customer.LastName);
                        }
                    }
                }

#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
#warning Please replace String.Format with string interpolation format.
                Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
                Console.Out.WriteLine($"Server {configServerName} default delay {configDelayMS}");
                Console.Out.WriteLine($"Customer for ID=5 is {_service.GetCustomerName(5)}");

                return Customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }

        protected static string i18(string s) => s;
        protected static dynamic inject;
        public class Person : System.ComponentModel.INotifyPropertyChanged
        {
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

            public string FullName
            {
                get
                {
                    System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
                    string cachedData = cache["name_of_cache_key"] as string;
                    if (cachedData == null)
                    {
                        cachedData = GetPropertyData();
                        if (cachedData != null)
                        {
                            cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
                        }
                    }

                    return cachedData;
                    string GetPropertyData()
                    {
                        // FullNameComment FullName
                        return $"{FirstName} {LastName}";
                    }
                }
            }

            private int _id;
            public int Id
            {
                get
                {
                    return _id;
                }

                set
                {
                    if (_id != value)
                    {
                        _id = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _firstName;
            public string FirstName
            {
                get
                {
                    return _firstName;
                }

                set
                {
                    if (_firstName != value)
                    {
                        _firstName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private string _lastName;
            public string LastName
            {
                get
                {
                    return _lastName;
                }

                set
                {
                    if (_lastName != value)
                    {
                        _lastName = value;
                        NotifyPropertyChanged();
                    }
                }
            }

            private int _age;
            public int Age
            {
                get
                {
                    return _age;
                }

                set
                {
                    if (_age != value)
                    {
                        _age = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public class Customer : Person
        {
            private double _creditScore;
            public double CreditScore
            {
                get
                {
                    return _creditScore;
                }

                set
                {
                    if (_creditScore != value)
                    {
                        _creditScore = value;
                        NotifyPropertyChanged();
                    }
                }
            }
        }

        public interface IDataService
        {
            void Init(IEnumerable<Customer> customers);
            string GetCustomerName(int customerId);
        }

        public class DataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                _customers = customers;
            }

            public string GetCustomerName(int customerId)
            {
                return _customers.FirstOrDefault(w => w.Id == customerId)?.FullName;
            }
        }

        public class MockDataService : IDataService
        {
            private IEnumerable<Customer> _customers;
            public void Init(IEnumerable<Customer> customers)
            {
                if (customers == null)
                    throw (new Exception("IDataService.Init(customers == null)"));
            }

            public string GetCustomerName(int customerId)
            {
                if (customerId < 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be negative"));
                if (customerId == 0)
                    throw (new Exception("IDataService.GetCustomerName: customerId cannot be zero"));
                return $"FirstName{customerId} LastName{customerId}";
            }
        }
    }
}

// ##template=AutoComment sha256=Qz6vshTZl2/u+NgtcV4u5W5RZMb9JPkJ2Zj0yvQBH9w
// ##template=AopCsharp.ttinclude sha256=2QR7LE4yvfWYNl+JVKQzvEBwcWvReeupVpslWTSWQ0c
// ##template=FirstDemoComment sha256=eIleHCim5r9F/33Mv9B7pcNQ/dlfEhDVXJVhA7+3OgY
// ##template=FullNameComment sha256=2/Ipn8fk2y+o/FVQHAWnrOlhqS5ka204YctZkwl/CUs
// ##template=NotifyPropertyChangedClass sha256=sxRrSjUSrynQSPjo85tmQywQ7K4fXFR7nN2mX87fCnk
// ##template=StaticAnalyzer sha256=zmJsj/FWmjqDDnpZXhoAxQB61nYujd41ILaQ4whcHyY
// ##template=LogExceptionMethod sha256=+zTre3r3LR9dm+bLPEEXg6u2OtjFg+/V6aCnJKijfcg
// ##template=NotifyPropertyChanged sha256=PMgorLSwEChpIPnEWXfEuUzUm4GO/6pMmoJdF7qcgn8
// ##template=CacheProperty sha256=oktDGTfC2hHoqpbKkeNABQaPdq6SrVLRFEQdNMoY4zE
// ##template=DependencyInjection sha256=nPq/ZxVBpgrDzyH+uLtJvD1aKbajKinX/DUBQ4BGG9g
// ##template=ResourceReplacer sha256=ZyUljjKKj0jLlM2nUIr1oJc1L7otYUI8WqWN7um6NxI







شروحات ورموز القالب



قالب AutoComment



// ##aspect=AutoComment


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



رمز القالب AutoComment.t4



<#@ include file="AopCsharp.ttinclude" #>

//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: <#= FileName #>
//  ##sha256: <#= FileSha256 #>
//  Created By: <#= User #>
//  Created Machine: <#= MachineName #>
//  Created At: <#= Now #>
//
// </auto-generated>
//------------------------------------------------------------------------------


يتم تصدير المتغيرات FileName و FileSha256 و User و MachineName و Now إلى القالب من عملية التحويل.



نتيجة التحول



//------------------------------------------------------------------------------
// <auto-generated> 
//     This code was generated from a template.
// 
//     Manual changes to this file may cause unexpected behavior in your application.
//     Manual changes to this file will be overwritten if the code is regenerated.
//
//  Generated base on file: ConsoleDemo.cs
//  ##sha256: PV3lHNDftTzVYnzNCZbKvtHCbscT0uIcHGRR/NJFx20
//  Created By: EuGenie
//  Created Machine: 192.168.0.1
//  Created At: 2017-12-09T14:49:26.7173975-05:00
//
// </auto-generated>
//------------------------------------------------------------------------------


تم تحديد التحويل التالي



كسمة للفئة [AopTemplate ("ClassLevelTemplateForMethods"، NameFilter = "First")] تشير



هذه السمة إلى أنه يجب تطبيق القالب على جميع طرق الفئة التي تحتوي على الكلمة "First". معلمة NameFilter عبارة عن نمط تعبير عادي يُستخدم لتحديد الطرق التي يجب تضمينها في التحويل.



رمز القالب ClassLevelTemplateForMethods.t4



<#@ include file="AopCsharp.ttinclude" #>

// class level template
<#= MethodStart() #><#= MethodBody() #><#= MethodEnd() #>


هذا هو أبسط مثال يضيف تعليقاً // class level templateقبل



نتيجة تحويل كود الطريقة



// class level template
public virtual Person FirstDemo(string firstName, string lastName, int age)
{
  Console.Out.WriteLine("FirstDemo: 1");

  // ##aspect="FirstDemoComment" extra data here

  return new Person()
      {
        FirstName = firstName,
        LastName = lastName,
        Age = age,
      };
}


تم تحديد التحويلات التالية كسمات للطريقة لتوضيح تحويلات متعددة مطبقة على نفس الطريقة. قالب LogExceptionMethod.t4



[AopTemplate("LogExceptionMethod")]

[AopTemplate("StopWatchMethod")]

[AopTemplate("MethodFinallyDemo", AdvicePriority = 100)]






<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System"); #>
<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
catch(Exception logExpn)
{
	Console.Error.WriteLine($"Exception in <#= MethodName #>\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
	throw;
}

<#= MethodEnd() #>


قالب StopWatchMethod.t4

<#@ include file="AopCsharp.ttinclude" #>
<# EnsureUsing("System.Diagnostics"); #>
<#= MethodStart() #>

var stopwatch = Stopwatch.StartNew(); 

try
{
<#= MethodBody() #>
} 
finally
{
	stopwatch.Stop();
	Console.Out.WriteLine($"Method <#= MethodName #>: {stopwatch.ElapsedMilliseconds}");

}

<#= MethodEnd() #>


نموذج FinallyDemo.t4

<#@ include file="AopCsharp.ttinclude" #>

<#= MethodStart() #>
try
{
<#= MethodBody() #>
} 
finally 
{
	// whatever logic you need to include for a method
}

<#= MethodEnd() #>


نتيجة التحولات

public Customer[] SecondDemo(Person[] people)
{
    try
    {
        var stopwatch = Stopwatch.StartNew();
        try
        {
            try
            {
                IEnumerable<Customer> customers;
                Console.Out.WriteLine("SecondDemo: 1");
                {
                    // second demo test extra
                    {
                        customers = people.Select(s => new Customer()
                        {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
                        foreach (var customer in customers)
                        {
                            Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
                        }
                    }
                }

                Console.Out.WriteLine("SecondDemo: 3");
                return customers.ToArray();
            }
            catch (Exception logExpn)
            {
                Console.Error.WriteLine($"Exception in SecondDemo\r\n{logExpn.Message}\r\n{logExpn.StackTrace}");
                throw;
            }
        }
        finally
        {
            stopwatch.Stop();
            Console.Out.WriteLine($"Method SecondDemo: {stopwatch.ElapsedMilliseconds}");
        }
    }
    finally
    {
    // whatever logic you need to include for a method
    }
}


يتم إعطاء التحويل التالي لكتلة مقصورة على بنية مستخدمة



using (new AopTemplate("SecondDemoUsing", extraTag: "test extra"))
{
    customers = people.Select(s => new Customer()
    {
        FirstName = s.FirstName,
        LastName = s.LastName,
        Age = s.Age,
    });

    foreach (var customer in customers)
    {
        Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
    }
}


قالب SecondDemoUsing.t4

<#@ include file="AopCsharp.ttinclude" #>

// second demo <#= ExtraTag #>

<#= StatementBody() #>


ExtraTag عبارة عن سلسلة يتم تمريرها كمعامل. يمكن أن يكون هذا مفيدًا للأدوية التي يمكن أن يكون لها سلوك مختلف قليلاً اعتمادًا على معلمات الإدخال.



نتيجة التحول



{
  // second demo test extra
  {
      customers = people.Select(s => new Customer()
      {FirstName = s.FirstName, LastName = s.LastName, Age = s.Age, });
      foreach (var customer in customers)
      {
          Console.Out.WriteLine($"SecondDemo: 2 {customer.FirstName} {customer.LastName}");
      }
  }
}


يتم تحديد التحويل التالي من خلال سمات فئة NotifyPropertyChanged . هذا مثال كلاسيكي ، والذي يتم تقديمه مع مثال التسجيل في معظم أمثلة البرمجة الموجهة إلى الجانب.



[AopTemplate("NotifyPropertyChangedClass", Action = AopTemplaceAction.Classes)]

[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]








يتم تطبيق قالب NotifyPropertyChangedClass.t4 على كود الفئة
<#@ include file="AopCsharp.ttinclude" #>
<#
	// the class already implements INotifyPropertyChanged, nothing to do here
	if(ImplementsBaseType(ClassNode, "INotifyPropertyChanged", "System.ComponentModel.INotifyPropertyChanged"))
		return null;

	var classNode = AddBaseTypes<ClassDeclarationSyntax>(ClassNode, "System.ComponentModel.INotifyPropertyChanged"); 
#>

<#= ClassStart(classNode) #>
            public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;

            protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
            {
                PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
            }

<#= ClassBody(classNode) #>
<#= ClassEnd(classNode) #>


.



Fogy
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;

public partial class ModuleWeaver
{
    public void InjectINotifyPropertyChangedInterface(TypeDefinition targetType)
    {
        targetType.Interfaces.Add(new InterfaceImplementation(PropChangedInterfaceReference));
        WeaveEvent(targetType);
    }

    void WeaveEvent(TypeDefinition type)
    {
        var propertyChangedFieldDef = new FieldDefinition("PropertyChanged", FieldAttributes.Private | FieldAttributes.NotSerialized, PropChangedHandlerReference);
        type.Fields.Add(propertyChangedFieldDef);
        var propertyChangedField = propertyChangedFieldDef.GetGeneric();

        var eventDefinition = new EventDefinition("PropertyChanged", EventAttributes.None, PropChangedHandlerReference)
            {
                AddMethod = CreateEventMethod("add_PropertyChanged", DelegateCombineMethodRef, propertyChangedField),
                RemoveMethod = CreateEventMethod("remove_PropertyChanged", DelegateRemoveMethodRef, propertyChangedField)
            };

        type.Methods.Add(eventDefinition.AddMethod);
        type.Methods.Add(eventDefinition.RemoveMethod);
        type.Events.Add(eventDefinition);
    }

    MethodDefinition CreateEventMethod(string methodName, MethodReference delegateMethodReference, FieldReference propertyChangedField)
    {
        const MethodAttributes Attributes = MethodAttributes.Public |
                                            MethodAttributes.HideBySig |
                                            MethodAttributes.Final |
                                            MethodAttributes.SpecialName |
                                            MethodAttributes.NewSlot |
                                            MethodAttributes.Virtual;

        var method = new MethodDefinition(methodName, Attributes, TypeSystem.VoidReference);

        method.Parameters.Add(new ParameterDefinition("value", ParameterAttributes.None, PropChangedHandlerReference));
        var handlerVariable0 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable0);
        var handlerVariable1 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable1);
        var handlerVariable2 = new VariableDefinition(PropChangedHandlerReference);
        method.Body.Variables.Add(handlerVariable2);

        var loopBegin = Instruction.Create(OpCodes.Ldloc, handlerVariable0);
        method.Body.Instructions.Append(
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldfld, propertyChangedField),
            Instruction.Create(OpCodes.Stloc, handlerVariable0),
            loopBegin,
            Instruction.Create(OpCodes.Stloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Ldarg_1),
            Instruction.Create(OpCodes.Call, delegateMethodReference),
            Instruction.Create(OpCodes.Castclass, PropChangedHandlerReference),
            Instruction.Create(OpCodes.Stloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldarg_0),
            Instruction.Create(OpCodes.Ldflda, propertyChangedField),
            Instruction.Create(OpCodes.Ldloc, handlerVariable2),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Call, InterlockedCompareExchangeForPropChangedHandler),
            Instruction.Create(OpCodes.Stloc, handlerVariable0),
            Instruction.Create(OpCodes.Ldloc, handlerVariable0),
            Instruction.Create(OpCodes.Ldloc, handlerVariable1),
            Instruction.Create(OpCodes.Bne_Un_S, loopBegin), // go to begin of loop
            Instruction.Create(OpCodes.Ret));
        method.Body.InitLocals = true;
        method.Body.OptimizeMacros();

        return method;
    }
}


, AOP .Net


تم تطبيق قالب NotifyPropertyChanged.t4 على خصائص الفئة
<#@ include file="AopCsharp.ttinclude" #>
<#
 	if(!(PropertyHasEmptyGetBlock() && PropertyHasEmptySetBlock()))
		return null;

	string privateUnqiueName = GetUniquePrivatePropertyName(ClassNode, PropertyNode.Identifier.ToString());
#>

	private <#= PropertyNode.Type.ToFullString() #> <#= privateUnqiueName #><#= PropertyNode.Initializer != null ? " = " + PropertyNode.Initializer.ToFullString() : "" #>;

<#= PropertyNode.AttributeLists.ToFullString() + PropertyNode.Modifiers.ToFullString() + PropertyNode.Type.ToFullString() + PropertyNode.Identifier.ToFullString() #>
	{
		get { return <#= privateUnqiueName #>; }
		set 
		{
			if(<#= privateUnqiueName #> != value)
			{
				<#= privateUnqiueName #> = value;
				NotifyPropertyChanged();
			}
		}
	}


الكود الأصلي للفئة والخصائص

public class Person
{
    public int Id { get; set; }

// ...
}


نتيجة التحول

public class Person : System.ComponentModel.INotifyPropertyChanged
{
    public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
    {
        PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
    }

    private int _id;
    public int Id
    {
        get
        {
            return _id;
        }

        set
        {
            if (_id != value)
            {
                _id = value;
                NotifyPropertyChanged();
            }
        }
    }

// ...
}


مثال على قالب لتخزين نتائج الخاصية مؤقتًا ، تم تحديده بواسطة



[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]



معاملات القالب المحددة كسمة JSON. في حالة عدم وجود معلمات صريحة ، يتم استخدام المعلمات الافتراضية.



قالب CacheProperty.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
	// The template accepts a configuration value from extraTag in two ways
	// 1. as a number of minutes to use for expiration (example: 8)
	// 2. as a string in JSON in format { CacheKey: "name_of_cache_key", CacheKeyVariable: "name_of_variable", ExpiresInMinutes: 10, ExpiresVariable: "name_of_variable" }
	//
	//    CacheKey (optional) name of the cache key, the name will be used as a literal string (example: my_key)
	//    CacheKeyVariable (optional) name of variable that holds the cache key (example: GlobalConsts.MyKeyName)
	//
	//    ExpiresInMinutes (optional) number minutes that the cache value will expires (example: 12)
	//    ExpiresVariable (optional) name of a variable that the expiration value will be get from (example: AppConfig.EXPIRE_CACHE)
	//
	// if any of expiration values are not specified, 5 minutes default expiration will be used

	if(!PropertyHasAnyGetBlock())
		return null;

	const int DEFAULT_EXPIRES_IN_MINUTES = 5;

	string propertyName = PropertyNode.Identifier.ToFullString().Trim();
	string propertyType = PropertyNode.Type.ToFullString().Trim();
	string expiresInMinutes = DEFAULT_EXPIRES_IN_MINUTES.ToString();
	string cacheKey = "\"" + ClassNode.Identifier.ToFullString() + ":" + propertyName + "\"";

	if(!String.IsNullOrEmpty(ExtraTag))
	{
		if(Int32.TryParse(ExtraTag, out int exp))
		{
			expiresInMinutes = exp.ToString();
		}
		else
		{
			JsonDocument json = ExtraTagAsJson();
			if(json != null && json.RootElement.ValueKind  == JsonValueKind.Object)
			{
				if(json.RootElement.TryGetProperty("CacheKey", out JsonElement cacheKeyElement))
				{
					string s = cacheKeyElement.GetString();
					if(!String.IsNullOrEmpty(s))
						cacheKey = "\"" + s + "\"";
				}
				else if(json.RootElement.TryGetProperty("CacheKeyVariable", out JsonElement cacheVariableElement))
				{
					string s = cacheVariableElement.GetString();
					if(!String.IsNullOrEmpty(s))
						cacheKey = s;
				}

				if(json.RootElement.TryGetProperty("ExpiresInMinutes", out JsonElement expiresInMinutesElement))
				{
					if(expiresInMinutesElement.TryGetInt32(out int v) && v > 0)
						expiresInMinutes = "" + v;
				} 
				else if(json.RootElement.TryGetProperty("ExpiresVariable", out JsonElement expiresVariableElement))
				{				
					string s = expiresVariableElement.GetString();
					if(!String.IsNullOrEmpty(s))
						expiresInMinutes = s;
				}
			}
		}
	}

#>


<#= PropertyDefinition() #>
	{
		get 
		{ 
			System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;			

			<#= propertyType #> cachedData = cache[<#= cacheKey #>] as <#= propertyType #>;
			if(cachedData == null)
			{
				cachedData = GetPropertyData();
				if(cachedData != null)
				{					
					cache.Set(<#= cacheKey #>, cachedData, System.DateTimeOffset.Now.AddMinutes(<#= expiresInMinutes #>)); 
				}
			}

			return cachedData;

			<#= propertyType #> GetPropertyData()
			{
				<# if(PropertyNode.ExpressionBody != null ) { #>
				return (<#= PropertyNode.ExpressionBody.Expression.ToFullString() #>);
				<# } else if(PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get") != null) { #>
				return (<#= PropertyNode?.AccessorList?.Accessors.FirstOrDefault(w => w.ExpressionBody != null && w.Keyword.ToString() == "get").ExpressionBody.Expression.ToFullString() #>);
				<# } else { #>
				<#= PropertyGetBlock() #>
				<# } #>
			}
       }

		<#
		
		if(PropertyHasAnySetBlock()) { #>
		set 
		{
			System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;  

			cache.Remove(<#= cacheKey #>); // invalidate cache for the property		
			
			<#= PropertySetBlock() #>			
		}
		<# } #>

	}


مصدر

[AopTemplate("CacheProperty", extraTag: "{ \"CacheKey\": \"name_of_cache_key\", \"ExpiresInMinutes\": 10 }")]
public string FullName
{
    get
    {
        return $"{FirstName} {LastName}";
    }
}


نتيجة تحويل CacheProperty.t4

public string FullName
{
    get
    {
        System.Runtime.Caching.ObjectCache cache = System.Runtime.Caching.MemoryCache.Default;
        string cachedData = cache["name_of_cache_key"] as string;
        if (cachedData == null)
        {
            cachedData = GetPropertyData();
            if (cachedData != null)
            {
                cache.Set("name_of_cache_key", cachedData, System.DateTimeOffset.Now.AddMinutes(10));
            }
        }

        return cachedData;
        string GetPropertyData()
        {
            // FullNameComment FullName
            return $"{FirstName} {LastName}";
        }
    }
}


الاستدعاء التالي للقالب مرة أخرى من التعليق

// ##aspect="FullNameComment" extra data here


قالب FullNameComment.t4

<#@ include file="AopCsharp.ttinclude" #>

// FullNameComment <#= PropertyNode.Identifier #>


يشبه إلى حد كبير نموذج AutoComment.t4 ، لكننا هنا نوضح استخدام PropertyNode. أيضًا ، تتوفر بيانات "البيانات الإضافية هنا" لقالب FullNameComment.t4 من خلال معلمة ExtraTag (ولكن في هذا المثال لا نستخدمها ، لذلك يتم تجاهلها ببساطة)



نتيجة التحويل

// FullNameComment FullName


يتم تحديد التحويل التالي في الملف بواسطة السمة



[AopTemplate("NotifyPropertyChanged", Action = AopTemplaceAction.Properties)]



AND وهو مطابق لتلك الخاصة بفئة الشخص. تم بالفعل تضمين الكود المصدري لنموذج NotifyPropertyChanged.t4 أعلاه.



نتيجة التحول

public class Customer : Person
{
    private double _creditScore;
    public double CreditScore
    {
        get
        {
            return _creditScore;
        }

        set
        {
            if (_creditScore != value)
            {
                _creditScore = value;
                NotifyPropertyChanged();
            }
        }
    }
}


الجزء الأخير



على الرغم من أن هذه المقالة تركز على البرمجة الموجهة إلى الجانب ، إلا أن تقنية تحويل كود المصدر عالمية ويمكن ، من حيث المبدأ ، استخدامها للمهام التي لا تتعلق بـ AOP.



على سبيل المثال ، يمكن استخدامه لحقن التبعية ، أي نقوم بتغيير كود إنشاء المورد اعتمادًا على معاملات البناء.



نموذج DependencyInjection.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
	var syntaxNode = FieldsInjection(SyntaxNode);
	syntaxNode = VariablesInjection(syntaxNode);
	syntaxNode = PropertiesInjection(syntaxNode);	

	if(syntaxNode == SyntaxNode)
		return null;
#>

<#= syntaxNode.ToFullString() #>

<#+
	private SyntaxNode VariablesInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<LocalDeclarationStatementSyntax >(syntaxNode, OnLocalVariablesInjection);	
	
		SyntaxNode OnLocalVariablesInjection(LocalDeclarationStatementSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode PropertiesInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<PropertyDeclarationSyntax>(syntaxNode, OnPropertyInjection);	
	
		SyntaxNode OnPropertyInjection(PropertyDeclarationSyntax node)
		{
			if(node.Initializer?.Value?.ToString() != "inject")
				return node;

			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, node.Type, errorMsgs);

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode FieldsInjection(SyntaxNode syntaxNode)
	{
		return RewriteNodes<BaseFieldDeclarationSyntax>(syntaxNode, OnFieldsInjection);	
	
		SyntaxNode OnFieldsInjection(BaseFieldDeclarationSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();

			SyntaxNode syntaxNode = RewriteNodes<VariableDeclaratorSyntax>(node, (n) => OnVariableDeclaratorVisit(n, node.Declaration.Type, errorMsgs));

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

			return syntaxNode;
		}
	}

	private SyntaxNode OnVariableDeclaratorVisit(VariableDeclaratorSyntax node, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
	{
		if(node.Initializer?.Value?.ToString() != "inject")
			return node;

		return DoInjection(node, node.Identifier.ToString().Trim(), node.Initializer.Value, typeSyntax, errorMsgs);
	}

	private SyntaxNode DoInjection(SyntaxNode node, string varName, ExpressionSyntax initializerNode, TypeSyntax typeSyntax, System.Text.StringBuilder errorMsgs)
	{		
		string varType = typeSyntax.ToString().Trim();

		Log($"{varName} {varType} {initializerNode.ToString()}");

		if(varName.StartsWith("config"))
		{
			string configName = Regex.Replace(Regex.Replace(varName, "^config", ""), "([a-z])([A-Z])", (m) => m.Groups[1].Value + "_" + m.Groups[2].Value).ToLower();
			ExpressionSyntax configNode = CreateElementAccess("_configuration", CreateStringLiteral(configName));

			if(varType == "int")
			{
				configNode = CreateMemberAccessInvocation("Int32", "Parse", configNode);
			}

			return node.ReplaceNode(initializerNode, configNode);
		}

		switch(varType)
		{
			case "Microsoft.Extensions.Configuration.IConfigurationRoot":
			case "IConfigurationRoot":
				EnsureUsing("Microsoft.Extensions.Configuration");

				ExpressionSyntax pathCombineArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));

				ExpressionSyntax builderNode = CreateNewType("ConfigurationBuilder").WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));
				builderNode  = CreateMemberAccessInvocation(builderNode, "SetBasePath", pathCombineArg).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));

				ExpressionSyntax addJsonFileArg = CreateMemberAccessInvocation("System.IO.Path", "Combine", CreateMemberAccess("AppContext", "BaseDirectory"));

				builderNode  = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile", 
																		(null, CreateStringLiteral("appsettings.json")), 
																		("optional",  SyntaxFactory.LiteralExpression(SyntaxKind.TrueLiteralExpression))).WithTrailingTrivia(SyntaxFactory.EndOfLine("\r\n"));

				if(GetGlobalSetting("env")?.ToLower() == "test")
				{
					builderNode  = CreateMemberAccessInvocationNamedArgs(builderNode, "AddJsonFile", 
																			(null, CreateStringLiteral("appsettings.test.json")), 
																			("optional",  SyntaxFactory.LiteralExpression(SyntaxKind.FalseLiteralExpression)));
				}

				builderNode  = CreateMemberAccessInvocation(builderNode, "Build");

				return node.ReplaceNode(initializerNode, builderNode);
				
			case "IDataService":
			{
				string className = (GetGlobalSetting("env")?.ToLower() == "test" ? "MockDataService" : "DataService");

				return node.ReplaceNode(initializerNode, CreateNewType(className));
			}
		}

		errorMsgs.AppendLine($"Cannot find injection rule for {varType} {varName}");

		return node;
	}

#>




في الكود المصدري (هنا يتم استخدام ميزة المتغير الديناميكي ، والتي تتيح تخصيصها لأي نوع) ، أي للتعبير ، توصلنا نوعًا ما إلى كلمة رئيسية جديدة.

private static IConfigurationRoot _configuration = inject;
private IDataService _service { get; } = inject;
// ...
public Customer[] SecondDemo(Person[] people)
{
     int configDelayMS = inject; // we are going to inject dependency to local variables
     string configServerName = inject;
}
// ...
protected static dynamic inject;


أثناء التحويل ، يتم استخدام المقارنة GetGlobalSetting ("env") == "test" واعتمادًا على هذه الحالة ، سيتم إدخال إما DataService () جديد أو MockDataService () جديد.



نتيجة التحول


private static IConfigurationRoot _configuration = new ConfigurationBuilder()
    .SetBasePath(System.IO.Path.Combine(AppContext.BaseDirectory))
    .AddJsonFile("appsettings.json", optional: true)
    .Build();

private IDataService _service { get; } = new DataService();
// ...
public Customer[] SecondDemo(Person[] people)
{
       int configDelayMS = Int32.Parse(_configuration["delay_ms"]);
       string configServerName = _configuration["server_name"];
}
// ...


أو يمكنك استخدام هذه الأداة كتحليل ثابت لـ "رجل فقير" (ولكن من الأصح بكثير تنفيذ أدوات التحليل باستخدام الوظيفة الأصلية لـ Roslyn) ، نقوم بتحليل التعليمات البرمجية لقواعدنا وإدخالها في التعليمات البرمجية المصدر.



#error our error message here



سيؤدي ذلك إلى حدوث خطأ في وقت التجميع.



#warning our warning message here



والذي سيكون بمثابة تحذير في IDE أو عند التجميع.



قالب StaticAnalyzer.t4
<#@ include file="AopCsharp.ttinclude" #>
<#
	var syntaxNode = AnalyzeLocalVariables(SyntaxNode);
	syntaxNode = AnalyzeStringFormat(syntaxNode);	

	if(syntaxNode == SyntaxNode)
		return null;
#>

<#= syntaxNode.ToFullString() #>

<#+

	private SyntaxNode AnalyzeLocalVariables(SyntaxNode syntaxNode)
	{
		return RewriteNodes<LocalDeclarationStatementSyntax>(syntaxNode, OnAnalyzeLocalVariablesNodeVisit);	
	
		SyntaxNode OnAnalyzeLocalVariablesNodeVisit(LocalDeclarationStatementSyntax node)
		{
			var errorMsgs = new System.Text.StringBuilder();
			
			string d = "";
			foreach(VariableDeclaratorSyntax variableNode in node.DescendantNodes().OfType<VariableDeclaratorSyntax>().Where(w => Regex.IsMatch(w.Identifier.ToString(), "^[A-Z]")))
			{
				LogDebug($"variable: {variableNode.Identifier.ToString()}");

				errorMsgs.Append(d + $"variable \"{variableNode.Identifier.ToString()}\" doesn't match code standard rules");
				d = ", ";
			}

			if(errorMsgs.Length > 0)
				return AddErrorMessageTrivia(node, errorMsgs.ToString());

			return node;
		}
	}


	private SyntaxNode AnalyzeStringFormat(SyntaxNode syntaxNode)
	{
		return RewriteLeafStatementNodes(syntaxNode, OnAnalyzeStringFormat);	
	
		SyntaxNode OnAnalyzeStringFormat(StatementSyntax node)
		{
			bool hasStringFormat = false;

			foreach(MemberAccessExpressionSyntax memberAccessNode in node.DescendantNodes().OfType<MemberAccessExpressionSyntax>())
			{
				if(memberAccessNode.Name.ToString().Trim() != "Format")
					continue;

				string expr = memberAccessNode.Expression.ToString().Trim().ToLower();
				if(expr != "string" && expr != "system.string")
					continue;

				hasStringFormat = true;
				break;
			}

			if(hasStringFormat)
				return AddWarningMessageTrivia(node, "Please replace String.Format with string interpolation format.");

			return node;
		}
	}
#>




نتيجة التحول

#error variable "Customers" doesn't match code standard rules
IEnumerable<Customer> Customers;
// ...
#warning Please replace String.Format with string interpolation format.
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));


أو كأداة تلقائية لترجمة تطبيق ما ، أي ابحث عن جميع السلاسل في الفصول واستبدلها باستخدام الموارد المناسبة.



نموذج ResourceReplacer.t4
<#@ include file="AopCsharp.ttinclude" #>
<#

	Dictionary<string, string> options = ExtraTagAsDictionary();
	_resources = LoadResources(options["ResourceFile"]);
	_resourceClass = options["ResourceClass"];

	var syntaxNode = RewriteLeafStatementNodes(SyntaxNode, OnStatementNodeVisit);	
#>

<#= syntaxNode.ToFullString() #>

<#+ 
	private SyntaxNode OnStatementNodeVisit(StatementSyntax node)
	{
		if(!node.DescendantNodes().OfType<InvocationExpressionSyntax>().Any(w => (w.Expression is IdentifierNameSyntax) && ((IdentifierNameSyntax)w.Expression).Identifier.ToString() == "i18"  ))
			return node;

		var errorMsgs = new System.Text.StringBuilder();

		SyntaxNode syntaxNode = RewriteNodes<InvocationExpressionSyntax>(node, (n) => OnInvocationExpressionVisit(n, errorMsgs));

		if(errorMsgs.Length > 0)
			return AddErrorMessageTrivia(syntaxNode, errorMsgs.ToString());

		return syntaxNode;
	}

    private SyntaxNode OnInvocationExpressionVisit(InvocationExpressionSyntax node, System.Text.StringBuilder errorMsgs)
	{
		if(!(node.Expression is IdentifierNameSyntax && ((IdentifierNameSyntax)node.Expression).Identifier.ToString() == "i18"  ))
			return node;

		ArgumentSyntax arg = node.ArgumentList.Arguments.Single(); // We know that i18 method accepts only one argument. Keep in mind that it is just a demo and in real life you could be more inventive
		
		var expr = arg.Expression;
		if(!(expr is LiteralExpressionSyntax || expr is InterpolatedStringExpressionSyntax))
		{
			errorMsgs.AppendLine($"Argument for i18 method must be either string literal or interpolated string, but instead got {arg.Expression.GetType().ToString()}");

			return node;
		}
		
		string s = expr.ToString();
		if(s.StartsWith("$"))
		{
			(string format, List<ExpressionSyntax> expressions) = ConvertInterpolatedStringToFormat((InterpolatedStringExpressionSyntax)expr);

			ExpressionSyntax stringNode = ReplaceStringWithResource("\"" + format + "\"", errorMsgs);
			if(stringNode != null)
			{
				var memberAccess = CreateMemberAccess("String", "Format");
			
				var arguments = new List<ArgumentSyntax>();
	
				arguments.Add(SyntaxFactory.Argument(stringNode));
				expressions.ForEach(item => arguments.Add(SyntaxFactory.Argument(item)));

				var argumentList = SyntaxFactory.SeparatedList(arguments);

				return SyntaxFactory.InvocationExpression(memberAccess, SyntaxFactory.ArgumentList(argumentList));
			}
		}
		else
		{
			SyntaxNode stringNode = ReplaceStringWithResource(s, errorMsgs);
			if(stringNode != null)
				return stringNode;
		}

		return node;
	}

	private ExpressionSyntax ReplaceStringWithResource(string s, System.Text.StringBuilder errorMsgs)
	{
		Match m = System.Text.RegularExpressions.Regex.Match(s, "^\"(\\s*)(.*?)(\\s*)\"$");
		if(!m.Success)
		{
			errorMsgs.AppendLine($"String doesn't match search criteria");

			return null;
		}

		if(!_resources.TryGetValue(m.Groups[2].Value, out string resourceName))
		{

			errorMsgs.AppendLine($"Cannot find resource for a string {s}, please add it to resources");
			return null;
		}

		string csharpName = Regex.Replace(resourceName, "[^A-Za-z0-9]", "_");

		ExpressionSyntax stringNode = CreateMemberAccess(_resourceClass, csharpName);

		if(!String.IsNullOrEmpty(m.Groups[1].Value) || !String.IsNullOrEmpty(m.Groups[3].Value))
		{
			if(!String.IsNullOrEmpty(m.Groups[1].Value))
			{
				stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, 
																CreateStringLiteral(m.Groups[1].Value), 
																stringNode);
			}

			if(!String.IsNullOrEmpty(m.Groups[3].Value))
			{
				stringNode = SyntaxFactory.BinaryExpression(SyntaxKind.AddExpression, 
															stringNode, 
															CreateStringLiteral(m.Groups[3].Value));
			}

			stringNode = SyntaxFactory.ParenthesizedExpression(stringNode);
		}

		return stringNode;
	}	

	private string _resourceClass;
	private Dictionary<string,string> _resources;
#>




مصدر


Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(i18($"First Name {customer.FirstName} Last Name {customer.LastName}"));

Console.Out.WriteLine("SecondDemo: 2 " + i18("First Name ") + customer.FirstName + i18(" Last Name   ") + customer.LastName);
// ...
 Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));
// ...
protected static string i18(string s) => s;


في ملف المورد Demo.resx ، على سبيل المثال ، أنشأنا الأسطر التالية

<data name="First Last Names Formatted" xml:space="preserve">
  <value>First Name {0} Last Name {1}</value>
</data>
<data name="First Name" xml:space="preserve">
    <value>First Name</value>
</data>
<data name="Last Name" xml:space="preserve">
  <value>Last Name</value>
</data>


والشفرة التي تم إنشاؤها تلقائيًا لملف Demo.Designer.cs
public class Demo 
{
// ...

    public static string First_Last_Names_Formatted
    {
        get
        {
            return ResourceManager.GetString("First Last Names Formatted", resourceCulture);
        }
    }

    public static string First_Name
    {
        get
        {
            return ResourceManager.GetString("First Name", resourceCulture);
        }
    }

    public static string Last_Name
    {
        get
        {
            return ResourceManager.GetString("Last Name", resourceCulture);
        }
    }
}


نتيجة التحويل (لاحظ أنه تم استبدال السلسلة المقحمة بـ String.Format وتم استخدام مورد "First Name {0} Last Name {1}"). بالنسبة للأسطر غير الموجودة في ملف المورد أو التي لا تتطابق مع التنسيق الخاص بنا ، تتم إضافة رسالة خطأ

//#error Cannot find resource for a string "SecondDemo: i18", please add it to resources
Console.Out.WriteLine(i18("SecondDemo: i18"));
// ...
Console.Out.WriteLine(String.Format(Demo.First_Last_Names_Formatted, customer.FirstName, customer.LastName));

Console.Out.WriteLine("SecondDemo: 2 " + (Demo.First_Name + " ") + customer.FirstName + (" " + Demo.Last_Name + "   ") + customer.LastName);
// ...
//#error Argument for i18 method must be either string literal or interpolated string, but instead got Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax
Console.Out.WriteLine(i18(String.Format("SecondDemo: {0}", "3")));


بالإضافة إلى ذلك ، تتيح لك أداة التحويل العمل ليس فقط مع ملفات C # ، ولكن أيضًا مع أي نوع ملف (بالطبع ، مع قيود معينة). إذا كان لديك محلل يمكنه إنشاء AST للغتك ، فيمكنك استبدال Roslyn بهذا المحلل اللغوي ، وتعديل تنفيذ معالج الكود وسيعمل. لسوء الحظ ، يوجد عدد محدود جدًا من المكتبات ذات وظائف قريبة من Roslyn ويتطلب استخدامها مزيدًا من الجهد. بالإضافة إلى C # ، نستخدم تحويلات لمشاريع JavaScript و TypeScript ، ولكن بالتأكيد ليس بشكل شامل مثل C #.



مرة أخرى ، أكرر أن كود المثال والقوالب مقدمة كتوضيح لإمكانيات مثل هذا النهج ، وكما يقولون ، السماء هي الحد الأقصى.



شكرا على وقتك.



تمت كتابة الجزء الرئيسي من هذا المقال قبل عامين ، ولكن للأسف ، ولأسباب معينة ، كان من الممكن نشره الآن فقط.



تم تطوير أداتنا الأصلية على .Net Framework ، لكننا بدأنا العمل على إصدار مبسط مفتوح المصدر بموجب ترخيص MIT لـ NET Core. في الوقت الحالي ، النتيجة تعمل بكامل طاقتها وجاهزة بنسبة 90٪ ، وهناك تحسينات طفيفة ، وتصفيفة شعر للكود ، وإنشاء وثائق وأمثلة ، ولكن بدون كل هذا سيكون من الصعب الدخول في المشروع ، وستكون الفكرة نفسها معرضة للخطر وستكون DX سلبية.



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



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



سأكون سعيدًا بتلقي النقد البناء (النقد غير البناء لن يزعجني أيضًا ، لذلك لا تتردد).



بفضل Phil Rangin (Fillpackart) للتحفيز في كتابة المقال. قواعد قناة "نحن محكوم عليهم"!



All Articles