نظرات مطالب
Blazor 5x - قسمت چهارم - مبانی Blazor - بخش 1 - Data Binding
یک نکته تکمیلی: امکان انتقال کدهای #C یک کامپوننت، به یک فایل مجزای code behind نیز وجود دارد

روال متداول کار با کامپوننت‌های Razor، قرار دادن کدهای #C مربوط به آن‌ها، در قسمت {}code@ همان فایل، با پسوند razor. است. می‌توان جهت بالا بردن قابلیت نگهداری کدهای کامپوننت‌ها، برخوردار شدن از intellisense بهتری و همچنین کاهش حجم فایل‌های razor. که در نهایت به خوانایی بیشتر و ساده‌تر آن‌ها منتهی می‌شود، قطعه‌ی {}code@ را به یک فایل سی‌شارپ مجزا منتقل کرد؛ با این شرایط و نکات خاص:
- اگر برای مثال نام یک کامپوننت، login.razor است، فایل code behind آن باید با همان نام شروع شود و ختم به cs. شود؛ یعنی در این مثال باید دقیقا نام login.razor.cs را داشته باشد و محتوای ابتدایی آن اکنون به صورت زیر خواهد بود:
namespace ProjectName.Folder1.Folder2
{
    public class Login
    {

    }
}
- پس از آن، یک چنین قطعه کدی، کامپایل نخواهد شد؛ چون کامپوننت login.razor که در پوشه‌ی folder1/folder2 واقع شده‌است نیز توسط کامپایلر به یک چنین کلاسی ترجمه می‌شود. برای رفع این مشکل، باید کلاس را به صورت partial تعریف کرد تا کدهای آن جزئی از کدهای کامپوننت login.razor شوند:
namespace ProjectName.Folder1.Folder2
{
    public partial class Login
    {

    }
}
- اکنون جهت انتقال کدهای قطعه {}code@، فقط کافی است آن‌ها را انتخاب و cut کرده و سپس در بدنه‌ی کلاس فوق، paste کنیم. در این حالت نیازی نیست سطوح دسترسی قسمت‌های مختلف کد، مانند private و protected را تغییر داد و همه چیز مانند قبل خواهد بود.

چند نکته:
- سرویس‌هایی که با دایرکتیو inject@ تعریف می‌شوند، در اینجا به صورت زیر توسط Attributes تعریف خواهند شد:
namespace ProjectName.Folder1.Folder2
{
    public partial class Login
    {
        [Inject]
        public NavigationManager NavigationManager { set; get; }

        // ...
    }
}
- فایل سراسری Imports.razor_ که جهت تعریف فضاهای نام تکراری مورد استفاده قرار می‌گرفت، در اینجا کاربردی نداشته و نیاز است فضاهای نام را همانند کدهای متداول #C، در ابتدای فایل cs. جاری ذکر کرد.
- جهت بهبود Intellisense متناظر با قابلیت‌های Blazor، بهتر است کلاس partial تعریف شده، از کلاس پایه ComponentBase نیز ارث بری کند:
namespace ProjectName.Folder1.Folder2
{
    public partial class Login : ComponentBase
    {
        [Inject]
        public NavigationManager NavigationManager { set; get; }

        // ...
    }
}
مطالب
بررسی Microsoft Anti-Cross Site Scripting Library

هنگام نمایش اطلاعات در وب باید اطلاعات خام دریافتی از کاربر را encode کرده و سپس نمایش داد تا از حملات XSS یا cross site scripting attacks در امان ماند. مثلا وبلاگی را طراحی کرده‌اید و یک نفر اطلاعات زیر را بجای توضیحات ارسال کرده است:
<SCRIPT>alert('XSS')</SCRIPT>

اگر اطلاعات به همین شکل دریافت و بدون تغییر هم نمایش داده شود، یک ضعف امنیتی برای سایت شما به‌حساب خواهد آمد. (بحث دزدیدن اطلاعات کوکی و امثال آن از این طریق با معرفی HttpOnly cookies در IE‌های جدید و فایرفاکس 3 به بعد تقریبا منتفی شده است اما می‌توانند با ارسال انبوهی اسکریپت، مشاهده صفحه را با crash‌ کردن مرورگر کاربران همراه کنند)
مایکروسافت برای این منظور Microsoft Anti-Cross Site Scripting Library را ارائه داده است. نمونه بهبود یافته HttpUtility.HtmlEncode که در فضای نام System.Web موجود است.

در اینجا قصد داریم این کتابخانه را با لیست زیر آزمایش کنیم:
http://ha.ckers.org/xss.html
در همان صفحه اگر دقت کنید، لیست حملات را به صورت یک فایل xml هم ارائه داده است:
http://ha.ckers.org/xssAttacks.xml
برای خواندن این فایل xml در دات نت روش‌های زیادی وجود دارد منجمله XML serialization .

ساختار این فایل به شکل زیر است:
<?xml version="1.0" encoding="UTF-8"?>
<xss>
<attack>
<name>x1</name>
<code>x2</code>
<desc>x3</desc>
<label>x4</label>
<browser>x5</browser>
</attack>
.
.
.

بنابراین شیء‌ نمایانگر آن می‌تواند به صورت لیستی از کلاس زیر باشد:
    public class attack{
public string name { get; set; }
public string code { get; set; }
public string desc { get; set; }
public string label { get; set; }
public string browser { get; set; }
}

برای دریافت این لیست و بارگذاری فایل xml مربوطه با استفاده از روش XML serialization خواهیم داشت:
      
using System.Collections.Generic;
using System.IO;
using System.Xml.Serialization;

public static List<attack> DeserializeFromXML(string path)
{
XmlRootAttribute root = new XmlRootAttribute("xss");
XmlSerializer deserializer =
new XmlSerializer(typeof (List<attack>),root);
using (TextReader textReader = new StreamReader(path))
{
return (List<attack>)deserializer.Deserialize(textReader);
}
}

در ادامه فرض بر این است که ارجاعی از اسمبلی AntiXssLibrary.dll به پروژه اضافه شده است، همچنین فایل xssAttacks.xml فوق نیز در کنار فایل اجرایی برنامه ، مثلا یک برنامه کنسول قرار گرفته است:
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Microsoft.Security.Application;

private static void testMethod()
{
StringBuilder sb = new StringBuilder();
sb.AppendFormat("<html>{0}", Environment.NewLine);
sb.AppendFormat("<body>{0}", Environment.NewLine);

List<attack> data = XMLParser.DeserializeFromXML("xssAttacks.xml");
foreach (attack atk in data)
{
string cleanSafeHtmlInput = AntiXss.HtmlEncode(atk.code);
sb.AppendFormat("{0}<br>{1}", cleanSafeHtmlInput, Environment.NewLine);
}

sb.AppendFormat("</body>{0}", Environment.NewLine);
sb.AppendFormat("</html>");

File.WriteAllText("out.htm", sb.ToString());
}

پس از اجرای تابع فوق، خروجی ما یک فایل html خواهد بود به نام out.htm . آنرا در مرورگر خود باز کنید. بدون هیچ مشکلی باز خواهد شد و خروجی امنی را مشاهده خواهید کرد. برای مشاهده اثر واقعی این کتابخانه، قسمت AntiXss.HtmlEncode را از کد فوق حذف کنید و یکبار دیگر برنامه را اجرا کنید. اکنون فایل نهایی را در مرورگر باز کنید. با انبوهی از alert های جاوا اسکریپتی مواجه خواهید شد که اهمیت کتابخانه فوق را جهت ارائه خروجی امن در صفحات وب مشخص می‌سازد.

مطالب
Test Driven Development #2
در مطلب قبل شما با TDD آشنا شدید اکنون بهتر است با یک مثال نشان دهم منظور از Test Driven Development چیست.  
برای شروع کافی است یک پروژه کنسول ساخته و Nunit را از طریق کنسول Nuget نصب کنید.
PM> Install-Package NUnit
معمولا برای کلاس‌های تست یک پروژه جدا در نظر گرفته می‌شود، ولی برای شروع می‌توانید از همان پروژه اصلی استفاده کنید.
پس از نصب شدن Nunit  می توانیم شروع به ساختن کلاس‌های تست کنیم:
  [TestFixture]
    public class HelloWorldTest
    {
    }

همانطور که ملاحظه می‌کنید کلاس ما با Attribute به نام TestFixture مزین شده است که خاص فریمورک Nunit است، در صورتی که از فریمورک دیگری برای تست استفاده می‌کنید باید تنظیمات مربوط به آن را انجام دهید.متدهای تست ما نیز با Attribute به نام  Test مزین می‌شوند.
  [Test]
   public void ShouldSayHelloWorld()
  {
  }

همانطور که دقت کردید متد ما به صورتی نام گذاری شده است که مشخص کننده کاری باشد که قرار است انجام دهد.این یکی دیگر از مزایای تست نویسی است که یک داکیومنت تقریباً کامل در طول تولید نرم افزار  ایجاد میشود.همچنین متد تست باید  غیر استاتیک با خروجی void  باشد .متد‌های تست بهتر است فقط یک موضوع را تست کنند، به طور مثال نباید هم اضافه شدن یک رکورد و هم ریدایرکت شدن به صفحه ای خاص را  تست کرد .
حالا وقت آن است که قبل از نوشتن کد اول تستش را بنویسیم.
        [Test]
        public void ShouldSayHelloWorld()
        {
            const string result = "Hello World";
            Assert.AreEqual(result, HelloWorld.SayHello());
        }

کلاس Assert  شامل توابعی بسیار قدرتمند است که مارا در اجرای تست بهتر کمک میکند.شامل متد هایی مانند .
  • AreEqual 
  • AreNotEqual 
  • AreNotSame 
  • AssertDoublesAreEqual 
  • Contains 
  • DoesNotThrow 
  • Equals 
  • Fail
  • Greater 
  • GreaterOrEqual 
  • Ignore 
  • IsEmpty 
  • IsInstanceOf 
  • IsNaN 
  • IsNotNull 
  • True 
  • ...
است.
هر کدام از متد‌های بالا کاربرد خاصی را دارند که به طور جداگانه به آن می‌پردازیم.
به علت وجود نداشتن کلاس HelloWorld  در زمان کامپایل با خطا مواجه می‌شویم.سپس کلاس مربوطه را ساخته و متد SayHello طوری پیاده سازی می‌کنیم که تست ما را پاس کند..(برنامه resharper برای اجرای متد‌های تست بسیار کار آمد است) 
    public class HelloWorld
    {
        public static string SayHello()
        {
            return "Hello World";
        }
    }

حال دوباره تست را اجرا کرده و می‌بینید که تست ما پاس شد.

نیازی به مرحله ریفکتورینگ نیست زیرا کلاس ما به اندازه کافی ساده است.
برای مقایسه بین Nunit و ابزار توکار ویژوال استودیو  می توانید به این سوال نگاهی بیاندازید.
در مطلب بعدی با استفاده از تست پذیری Mvc.net شروع به نوشتن تست هایی جدی‌تر خواهیم کرد.
نظرات مطالب
بخش اول - آشنایی و شروع کار با Svelte
حجم زیاد برنامه‌های بزرگ که به طور مثال با angular نوشته شده اند همیشه دردسر ساز بوده و کاربرانی که از اینترنت‌های ضعیف‌تر استفاده میکنند را با مشکل مواجه کرده. بخصوص داخل ایران کاربر کم نداریم که این مشکل رو دارند در نتیجه به نظر من معیار بدی نیست برای مقایسه. 
در مورد کم کردن حجم و زمان کدنویسی هم به مراتب به نظر من svelte آمار بسیار مناسبی نسبت به سایر فریم ورک‌های معتبر داره همینطور یادگیری و کارکردن با آن ساده است که به فرایند تولید نرم افزار بسیار کمک میکنه. حداقل برای من این نکته خیلی مهم هستش. 
مورد آخری که میمونه در مورد performance هست که اتفاقا من از دیشب مجددا پس از نوشتن این مقاله در موردش تحقیق کردم ولی هنگام نوشتن این مقاله نخواستم بهش اشاره کنم چرا که شاید به نظر میومد قصد دارم حقیقت رو به شکل دیگه ای نشون بدم.
در واقع  از نظر performance به طور کلی این کامپالر عملکرد به مراتب بهتری داره نسبت به سایر فریم ورک هایی که تا امروز استفاده کرده ایم. چه در استفاده از مموری و چه در اجرای کد تنها به دلیل اینکه از virtual-DOM استفاده نمیکند. در آمار بالا خیلی از پارامتر‌های مهم لحاظ نشده بودند به همین جهت رتبه بندی به این شکل است, چرا که تنها svelte در این لیست یک کامپایلر است و الباقی فریم ورک هستند که وضعیت حدودا برابری با هم دارند. 
پیشنهاد میکنم اگر در مورد مزیت‌های این کامپایلر نیاز به اطلاعات بیشتر داشتید این چند ویدئو رو مشاهده کنید. با جزئیات به این موارد بخصوص در بحث Performance پرداخته اند.
جواب سوال شما در ویدئو اول داده شده و در مورد performance دو ویدئو بعدی دلایل بهتر بودن آن را بیان میکند.
در آخر منظور من به طور قطعی بهتر یا بدتر بودن این کامپایلر نیست ولی با توجه به امکانات و تفاوت هایی که داره ارزش امتحان کردن رو قطعا داره. درمورد تعداد سوال و مشارکتی که گذاشتید حق با شماست این پروژه کاملا یک کار یک نفره است و هنوز جا نیفتاده, شاید هم نیفته به همین جهت من بزرگترین ایراد اون رو نداشتن پشتوانه قوی بیان کردم در قسمت معایب.
اشتراک‌ها
ویرایش و تغییر سایز عکس با استفاه از کلاس WebImage
حتما در خلال نوشتن یک برنامه نیاز پیدا کردید که سایز یک عکس را تغییر دهید، بچرخانید، واترمارک بزنید و خیلی کارهای دیگر...
خصوصا زمانی که قرار است پروژه، توسط GTmetrix بررسی شود و شما امتیاز بگیرید. حتما متوجه شدید که یکی از ملزومات داشتن یک Seo خوب، داشتن سرعت قابل قبول در بارگذاری تصاویر است و می‌دانیم که کاربران یک سایت، لزوما دیدگاه، دقت و حساسیت یک برنامه نویس را ندارند و بهترین کار این است که بطور قهری سایز تصاویر در پروژه بصورت صحیح تنظیم شود. بعنوان مثلا ممکن است یک کاربر برای داشتن یک آواتار از عکس بسیار بزرگ (PX 400*600) استفاده کند و این درحالی است که چنین اندازه ای برای یک آواتار غیرمنطقی مخرب است و صرفا باعث کاهش زمان بارگذاری سایت خواهد شد.

راه حل :

یکی از راه حل‌های موجود استفاده از کلاس WebImage  است که شما می‌توانید عملیات ویرایش یک عکس را خودتان مدیریت کنید. یادداشت زیر کدهای لازم برای استفاده از این کلاس می‌باشد.
[HttpPost]
    public ActionResult Index(HttpPostedFileBase file)
    {
        WebImage img = new WebImage(file.InputStream);
        if (img.Width > 1000)
            img.Resize(1000, 1000,false);
        img.Save("path");
        return View();
    }
در قطعه کد بالا انتخاب‌ها گوناگونی در ارسال تصویر و ویرایش داریم که شما می‌توانید بسته به نیاز خود آن را تغییر دهید. بعنوان مثال شما می‌توانید بعد از ذخیره عکس در مسیر فیزیکی سرور، آن را به راحتی ویرایش کنید. به مثال زیر توجه کنید
        WebImage img = new WebImage("path");
        if (img.Width > 720)}
            img.Resize(720, 460 ,false);
        img.Save("path");

 برای آگاهی و مطالعه بیشتر در خصوص Constuctors و Properties و Methods مربوط به کلاس WebImage می‌توانید به لینک WebImage Class in MSDN مراجعه فرمایید.
ویرایش و تغییر سایز عکس با استفاه از کلاس WebImage
نظرات مطالب
Blogger auto poster
-زمان را 24 ساعته وارد کنید؛ یعنی ساعت 22 را.
- برنامه به صورت خودکار تمام لینک‌های روز قبل را منتشر می‌کند. اگر نیاز دارید تعداد مشخصی را ارسال کنید، می‌تونید از حالت دستی استفاده کنید. موارد دلخواه را در برنامه تیک بزنید بعد روی دکمه بلاگر که در toolbar هست کلیک کنید. به این صورت فقط این موارد به اکانت شما ارسال می‌شود (صرفنظر از زمان و غیره).
-برنامه هم گوگل پلاس و هم فید ذکر شده را با هم بررسی می‌کند. احتمالا شما ID پیش فرض (موجود در فایل کانفیگ) مربوط به گوگل پلاس را خالی نکردید، یا ID خودتان را جایگزین آن نکردید. حداقل یک مورد باید ذکر شود. هر دو هم بود که چه بهتر. اگر نیازی نیست آن‌را خالی کنید.
مطالب
بیرون نگاه داشتن تنظیمات خصوصی از سورس کنترل
برخی از تنظیمات پروژه نباید به مخازن سورس کنترل ارسال شوند؛ حال یا نیازی به این کار نیست یا مقادیر تنظیمات محرمانه هستند. چند بار پیش آمده‌است که پروژه را از سورس کنترل دریافت و مجبور شده باشید رشته‌های اتصال و دیگر تنظیمات را مجددا ویرایش کنید، چرا که توسعه دهندگان دیگری مثلا فایل‌های Web/App.config خود را به اشتباه push کرده اند؟ حتی اگر تنظیمات پروژه محرمانه هم نباشند (مثلا پسورد دیتابیس‌ها یا ایمیل ها) این موارد می‌توانند دردسر ساز شوند. بدتر از اینها هنگامی است که تنظیمات محرمانه را به مخازنی عمومی (مثلا GitHub) ارسال می‌کنید!

یک فایل web.config معمولی را در نظر بگیرید (اطلاعات غیر ضروری حذف شده اند).

<?xml version="1.0" encoding="utf-8"?>
<!--
  A bunch of ASP.NET MVC web config stuff goes here . . . 
  -->
<configuration>
  <connectionStrings>
    <add name="DefaultConnection" value="YourConnectionStringAndPassword"/>
  </connectionStrings>

  <appSettings file="PrivateSettings.config">
    <add key="owin:AppStartup" value="AspNetIdentity2ExtendingApplicationUser.Startup,AspNetIdentity2ExtendingApplicationUser" />
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="EMAIL_PASSWORD" value="YourEmailPassword"/>
  </appSettings>
</configuration>
در تنظیمات بالا یک رشته اتصال وجود دارد که ترجیحا نمی‌خواهیم به سورس کنترل ارسال کنیم، و یا اینکه این رشته اتصال بین توسعه دهندگان مختلف متفاوت است.
همچنین کلمه عبور یک ایمیل هم وجود دارد که نمی‌خواهیم به مخازن سورس کنترل ارسال شود، و مجددا ممکن است مقدارش بین توسعه دهندگان متفاوت باشد.
از طرفی بسیاری از تنظیمات این فایل متعلق به کل اپلیکیشن است، بنابراین صرفنظر کردن از کل فایل web.config در سورس کنترل گزینه جالبی نیست.

خوشبختانه کلاس ConfigurationManager راه حل هایی پیش پای ما می‌گذارد.

استفاده از خاصیت configSource برای انتقال قسمت هایی از تنظیمات به فایلی مجزا
با استفاده از خاصیت configSource می‌توانیم قسمتی از تنظیمات (configuration section) را به فایلی مجزا منتقل کنیم. بعنوان مثال، رشته‌های اتصال از مواردی هستند که می‌توانند بدین صورت تفکیک شوند.

بدین منظور می‌توانیم فایل تنظیمات جدیدی (مثلا با نام connectionStrings.config) ایجاد کنیم و سپس با استفاده از خاصیت نام برده در فایل web.config به آن ارجاع دهیم. برای این کار فایل تنظیمات جدیدی ایجاد کنید و مقادیر زیر را به آن اضافه کنید (xml header یا هیچ چیز دیگری نباید در این فایل وجود داشته باشد، تنها مقادیر تنظیمات).
<connectionStrings>
  <add name="DefaultConnection" value="YourConnectionStringAndPassword"/>
</connectionStrings>
حال باید فایل web.config را ویرایش کنیم. رشته‌های اتصال را حذف کنید و با استفاده از خاصیت configSource تنها به فایل تنظیمات اشاره کنید.
<connectionStrings configSource="ConnectionStrings.config">
</connectionStrings>
دسترسی به رشته‌های اتصال مانند گذشته انجام می‌شود. به بیان دیگر تمام تنظیمات موجود (حال مستقیم یا ارجاع شده) همگی بصورت یکپارچه دریافت شده و به کد کلاینت تحویل می‌شوند.
var conn = ConfigurationManager.ConnectionStrings["DefaultConnection"];
string connString = conn.ConnectionString;
// etc.
در قطعه کد بالا، دسترسی به رشته‌های اتصال بر اساس نام، آبجکتی از نوع ConnectionStringSettings را بر می‌گرداند. خاصیت configSource برای هر قسمت از تنظیمات پیکربندی می‌تواند استفاده شود.


استفاده از خاصیت file برای انتقال بخشی از تنظیمات به فایلی مجزا
ممکن است فایل تنظیمات شما (مثلا web.config) شامل مقادیری در قسمت <appSettings> باشد که برای کل پروژه تعریف شده اند (global) اما برخی از آنها محرمانه هستند و باید از سورس کنترل دور نگاه داشته شوند. در این سناریو‌ها خاصیتی بنام file وجود دارد که مختص قسمت appSettings است و به ما اجازه می‌دهد مقادیر مورد نظر را به فایلی مجزا انتقال دهیم. هنگام دسترسی به مقادیر این قسمت تمام تنظیمات بصورت یکجا خوانده می‌شوند.

در مثال جاری یک کلمه عبور ایمیل داریم که می‌خواهیم محرمانه بماند. بدین منظور می‌توانیم فایل پیکربندی جدیدی مثلا با نام PrivateSettings.config ایجاد کنیم. این فایل هم نباید xml header یا اطلاعات دیگری داشته باشد، تنها مقادیر appSettings را در آن نگاشت کنید.
<appSettings>
  <add key="MAIL_PASSWORD" value="xspbqmurkjadteck"/>
</appSettings>
حال تنظیمات کلمه عبور را از فایل web.config حذف کنید و با استفاده از خاصیت file، به فایل جدید اشاره کنید.
<appSettings file="PrivateSettings.config">
  <add key="owin:AppStartup" value="AspNetIdentity2ExtendingApplicationUser.Startup,AspNetIdentity2ExtendingApplicationUser" />
  <add key="webpages:Version" value="3.0.0.0" />
  <add key="webpages:Enabled" value="false" />
  <add key="ClientValidationEnabled" value="true" />
  <add key="UnobtrusiveJavaScriptEnabled" value="true" />
</appSettings>
دسترسی به تنظیمات appSettings مانند گذشته انجام می‌شود. همانطور که گفته شد ConfigurationManager بصورت خودکار اینگونه ارجاعات را تشخیص داده و تمام اطلاعات را بصورت یکجا در اختیار client code قرار می‌دهد.
var pwd = ConfigurationManager.AppSettings["MAIL_PASSWORD"];

فایل‌های ویژه را به gitignore. اضافه کنید
حال می‌توانیم فایل web.config را به سورس کنترل اضافه کنیم، فایل‌های ConnectionStrings.config و PrivateSettings.config را به فایل gitignore. اضافه کنیم و پروژه را commit کنیم. در این صورت فایل‌های تنظیمات خصوصی به مخازن سورس کنترل ارسال نخواهند شد.

مستند سازی را فراموش نکنید!
مسلما اگر چنین رویکردی را در پیش بگیرید باید دیگران را از آن مطلع کنید (مثلا با افزودن توضیحاتی به فایل README.txt). بهتر است در فایل web.config خود هرجا که لازم است توضیحات XML خود را درج کنید و به توسعه دهندگان توضیح دهید که چه فایل هایی را روی نسخه‌های محلی خود باید ایجاد کنند و هر کدام از این فایل‌ها چه محتوایی باید داشته باشند.
مطالب
طراحی گردش کاری با استفاده از State machines - قسمت سوم
در این قسمت، یک سری مثال گردش کاری سازگار با Stateless Designer را بررسی خواهیم کرد. خروجی‌های XML زیر را می‌توانید در Stateless Designer وارد کرده و تبدیل به کدهای معادل کنید. اگر نمونه‌ای را هم خودتان طراحی کرده‌اید می‌توانید در قسمت نظرات مطلب جاری به اشتراک بگذارید.


الف) طراحی گردش کاری یک سیستم ردیابی خطاها (Bug tracking system)

در ادامه رویدادها، حالات و انتقالات یک ماشین حالت ردیابی خطاها را مشاهده می‌کنید:

<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>BugTrackingStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Assign</trigger>
    <trigger>Defer</trigger>
    <trigger>Resolve</trigger>
    <trigger>Close</trigger>
  </triggers>
  <states>     
    <state start="yes">Open</state>
    <state>Assigned</state>
    <state>Deferred</state>
    <state>Resolved</state>
    <state>Closed</state>
  </states>
  <transitions>
    <transition trigger="Assign" from="Open" to="Assigned" />
    <transition trigger="Assign" from="Assigned" to="Assigned" />
    <transition trigger="Close" from="Assigned" to="Closed" />
    <transition trigger="Defer" from="Assigned" to="Deferred" />
    <transition trigger="Assign" from="Deferred" to="Assigned" />
    <transition trigger="Resolve" from="Assigned" to="Resolved" />
  </transitions>
</statemachine>
با گرافی معادل:


توضیحات:
یک گزارش خطا حداقل پنج حالت آغاز (Open)، انتساب به شخص، جهت رفع مشکل (Assign)، به تاخیر افتادن/درحال بررسی (Deffered)، برطرف شده (Resolved) و خاتمه یافته/برطرف نخواهد شد (Closed) را می‌تواند داشته باشد.
برای حرکت (Transition) از هر حالت به حالتی دیگر نیاز به یک سری رویداد (Trigger) است که لیست آن‌ها را در بالا مشاهده می‌کنید.
در ابتدا سیستم در حالت انتساب به شخص قرار می‌گیرد. سپس در همین حالت شخص می‌تواند یکی از سه حالت رفع شده، بستن موضوع و یا ارجاع به زمانی دیگر را انتخاب کند. حتی در حالت ارجاع به شخص، شخص می‌تواند مساله را به شخصی دیگر ارجاع دهد. یا در حالت به تاخیر افتادن حل مساله، می‌توان مشکل را به شخصی دیگر انتساب داد.


ب) طراحی گردش کاری درخواست ارتقاء در یک شرکت

مراحل درخواست ارتقاء شغلی را در یک سازمان فرضی، در ذیل مشاهده می‌کنید:
<statemachine xmlns="http://statelessdesigner.codeplex.com/Schema">
  <settings>
    <itemname>RequestPromotionStateMachine</itemname>
    <namespace>StatelessTests</namespace>
    <class>public</class>
  </settings>
  <triggers>     
    <trigger>Complete</trigger>
    <trigger>RequestInfo</trigger>
    <trigger>Deny</trigger>
    <trigger>Approve</trigger>
    <trigger>ManagerJustify</trigger>
  </triggers>
  <states>     
    <state start="yes">RequestPromotionForm</state>
    <state>ManagerReview</state>
    <state>PromotionDenied</state>
    <state>VicePresidentApprove</state>
    <state>Promoted</state>
  </states>
  <transitions>
    <transition trigger="Complete" from="RequestPromotionForm" to="ManagerReview" />

    <transition trigger="RequestInfo" from="ManagerReview" to="RequestPromotionForm" />
    <transition trigger="Deny" from="ManagerReview" to="PromotionDenied" />
    <transition trigger="Approve" from="ManagerReview" to="VicePresidentApprove" />

    <transition trigger="ManagerJustify" from="VicePresidentApprove" to="ManagerReview" />
    <transition trigger="Deny" from="VicePresidentApprove" to="PromotionDenied" />
    <transition trigger="Approve" from="VicePresidentApprove" to="Promoted" />
  </transitions>
</statemachine>
با گرافی معادل:


توضیحات:
کارمند فرم درخواست ارتقاء را تکمیل می‌کند. این فرم به مسئول او ارسال می‌شود. مسئول می‌تواند درخواست را یک ضرب رد کند؛ یا تائید کند که سپس برای مدیرعامل شرکت ارسال می‌شود و یا مجددا به شخص برای تکمیل نواقص فرم ارجاع دهد. مدیرعامل شرکت می‌تواند درخواست را تائید کند که در اینجا کار خاتمه می‌یابد و شخص ارتقاء خواهد یافت. یا می‌تواند درخواست را رد کند و یا برای بررسی بیشتر مجددا به مسئول شخص ارجاع دهد.


تمرین! توضیحات زیر را تبدیل به یک State machine کنید!

چند سال قبل به اداره‌ی بیمه تامین اجتماعی منطقه مراجعه کردم. جهت دریافت ریز سوابق و انتقال آن‌ها به این مرکز ابتدا یک برگه دریافت شد. پر شد، بعد به صورت دستی (توسط بنده) به یک نفر دیگر ارجاع شد تا امضاء کند. سپس به صورت دستی به مسئول قسمت ارجاع شد تا امضاء کند. مجددا به صورت دستی به مدیر کل مجموعه ارجاع شد تا امضاء کند. سپس به صورت دستی به دبیرخانه برای پیگیری ارجاع شد. قرار است ظرف یک ماه تا 45 روز این سوابق از یک واحد دیگر به این واحد منتقل شوند!
بعد از 45 روز:
مراجعه به دبیرخانه: دریافت شماره پرونده رسیده.
گفته شد که به قسمت دریافت شماره مراجعه کنید. مراجعه شد، گفتند برو پرونده‌ات را بگیر بیار. رفتم زیر زمین، گفت که ما اینطوری پرونده نمی‌دیم! برو فرمش رو هم پر کن بیار. مراجعه شد به کارمند مربوطه، ایشان پس از مشورت با سایر همکاران به این نتیجه رسیدند که در این مرحله نیازی به مراجعه به زیر زمین نبوده! و باید به قسمت ثبت نام مجدد مراجعه کنید! چشم!  
اینجا هم مجددا فرم پر شد،‌ارجاع داده شده به معاون قسمت، امضاء کرد گفت برو دبیرخانه شماره بگیر. شماره گرفته شده بعد مجددا به همان قسمت ثبت نام مراجعه کردم، گفتند برو پرونده‌ات را از زیر زمین بگیر بیار! بعد از آوردن پرونده، ارجاع شد به صورت دستی به یک قسمت دیگر که سوابق وارد سیستم شود (هنوز نشده بود!). بعد از ثبت (نیم ساعت یا بیشتر ...)، مجددا به همان قسمت ثبت نام مراجعه کردم، گفت حالا برو یک شماره بگیر بیار. شماره گرفته شد از قسمتی دیگر و مراجعه مجدد به قسمت ثبت نام، یک نامه دیگر تهیه کرد، به سه نفر دیگر + دبیرخانه برای امضاء و شماره گرفتن ارجاع داده شد. اینجا تمام شد. بعد این موارد ارجاع شد به قسمت دیگری از شهر برای دریافت قبض پرداخت بیمه.
مطلب مرتبط
مطالب
شروع به کار با DNTFrameworkCore - قسمت 5 - مکانیزم Eventing و استفاده از سرویس‌های موجودیت‌ها
در قسمت‌های قبل سعی شد یک دید کلی از نحوه استفاده از این زیرساخت ارائه شود؛ در این قسمت علاوه بر بررسی مکانیزم Eventing، با جزئیات بیشتری به استفاده از سرویس‌های پیاده‌سازی شده پرداخته خواهد شد.
‌‌‌‌‌

مکانیزم Eventing

‌‌
استفاده از رخ‌دادها، یکی از راه‌حل‌های رسیدن به  طراحی با Loose Coupling (اتصال سست و ضعیف، وابستگی ضعیف) می‌باشد؛ همچنین برای حذف چرخه در فرآیند وابستگی مولفه‌های سیستم نیز مورد استفاده قرار میگیرد. در این زیرساخت برای Application Layer مبتنی‌بر CRUD، مکانیزم BusinessEvent با هدف در معرض دید قراردادن یکسری نقاط قابل گسترش توسط سایر بخش‌های سیستم، تعبیه شده است. برای استفاده از این مکانیزم لازم است بسته نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore
‌‌‌
سپس امکان این را خواهید داشت که مشترک رخ‌دادهای مرتبط با عملیات CUD متناظر با موجودیت‌های سیستم، شوید. به عنوان مثال، برای اینکه بتوان مشترک رخ‌داد ویرایش مرتبط با موجودیت Task شد، باید به شکل زیر عمل کرد:
public class TaskEditingBusinessEventHandler : BusinessEventHandler<EditingBusinessEvent<TaskModel, int>>
{
    private readonly ILogger<TaskEditingBusinessEventHandler> _logger;

    public TaskEditingBusinessEventHandler(ILogger<TaskEditingBusinessEventHandler> logger)
    {
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public override Task<Result> Handle(EditingBusinessEvent<TaskModel, int> @event)
    {
        foreach (var model in @event.Models)
        {
            _logger.LogInformation($"Title changed from: {model.OriginalValue.Title} to: {model.NewValue.Title}");
        }

        return Task.FromResult(Ok());
    }
}

کار با پیاده‌سازی واسط جنریک IBusinessEventHandler یا ارث‌بری از کلاس جنریک BusinessEventHandler آغاز می‌شود؛ سپس نیاز است Type Parameter متناظر را نیز مشخص کنیم. برای این منظور در تکه کد بالا از رخ‌داد جنریک EditingBusinessEvent استفاده شده است. همچنین همانطور که ملاحظه می‌کنید، نیاز است نوع Model مورد نظر نیز مشخص شده باشد؛ در اینجا از TaskModel به عنوان Model/DTO عملیات CUD موجودیت Task استفاده شده است.

‌‌‌‌رخ‌دادهای Creating/Created/Deleting/Deleted دارای خصوصیتی بنام Models هستند که نوع آن ‎IEnumerable<TModel>‎ می‌باشد. ولی این خصوصیت در رخ‌دادهای Editing/Edited از نوع ‎IEnumerable<ModifiedModel<TModel>> ‎ می‌باشد؛ در این صورت به مقادیر موجود در بانک اطلاعاتی و همچنین مقادیری که توسط استفاده کننده از سرویس جاری به عنوان آرگومان به متد ویرایش ارسال شده است، دسترسی خواهیم داشت.

public class ModifiedModel<TValue>
{
    public TValue NewValue { get; set; }
    public TValue OriginalValue { get; set; }
}
‌‌‌‎‌‌‌‎‌
نکته: همانطور که در قسمت‌های قبل اشاره شد، Application Layer مدنظر ما با یک Model/DTO برای عملیات CUD کار می‌کند؛ از این جهت، منطق تجاری و همچنین قواعد تجاری برفراز همان Model/DTO اجرا خواهند شد و به‌تبع آن، اگر سایر بخش‌های سیستم نیز قصد گسترش منطق تجاری مرتبط با یک موجودیت را دارند، باید با همان Model/DTO کار کنند.
‎‎‌‌
چه زمانی استفاده از مکانیزم BusinessEvent مطرح شده توصیه می‌شود؟
‌‎‎‌‎
به طور کلی محدودیتی در استفاده از آن وجود ندارد؛ در مواردی مشابه اگر قصد اعمال یکسری قواعد تجاری توسط سایر مولفه‌های سیستم را دارید و قصد ندارید ارجاعی به آن مولفه در مولفه جاری وجود داشته باشد یا بدلیل ایجاد چرخه، این امکان وجود ندارد، می‌توان از این مکانیزم بهره برد. برای مثال زمانی که یکسری قواعد تجاری جدید قرار است از سمت مولفه فروش بر روی مولفه مرتبط با مدیریت محصولات اعمال شود. 
‌‌‌
نکته: اگر قصد ارائه یک رخ‌داد سفارشی را دارید، می‌توانید واسط IEventBus را تزریق کرده و از متد TriggerAsync آن استفاده کنید.
‌‌‌‌

استفاده از سرویس‌های موجودیت‌ها

‌‌
OOP : Everything is an object
CRUD-based thinking : Everything is CRUD

استفاده از سرویس‌های موجودیت‌ها به تولید CrudController مرتبط ختم نمی‌شود و در تفکر مبتنی‌بر CRUD، تمام عملیات مرتبط با یک موجودیت از یک تونل واحد عبور خواهند کرد. مسئول این تونل در ابتدا متد Create می‌باشد و در ادامه توسط متد Edit مدیریت می‌شود. به عنوان مثال، اگر امروز در یک سیستم رستورانی با نحوه‌های فروش مختلف، قرار باشد در زمان ثبت گروه کالایی جدید و براساس تنظیمات سیستم، آن گروه کالایی به صورت خودکار به لیست گروه‌های کالایی مرتبط با تمام نحوه‌های فروش اضافه شود، این امر باید از طریق منطق تجاری توسعه داده شده برای نحوه ‌فروش، انجام پذیرد. قرار نیست شما منطق تجاری مرتبط با نحوه فروش را دور بزنید و به صورت دستی شروع به ثبت این اطلاعات در بانک اطلاعاتی کنید. در این شرایط می‌بایست با استفاده از مکانیزم BusinessEvent به شکل زیر عمل کرد:

public class ItemCategoryCreatedBusinessEventHandler : IBusinessEventHandler<CreatedBusinessEvent<ItemCategoryModel, int>>
{
    private readonly ISaleMethodService _saleMethodService;

    public TaskEditingBusinessEventHandler(ISaleMethodService saleMethodService)
    {
        _saleMethodService = saleMethodService ?? throw new ArgumentNullException(nameof(saleMethodService));
    }

    public override Task<Result> Handle(CreatedBusinessEvent<ItemCategoryModel, int> @event)
    {
        var methods = _saleMethodService.FindAsnc();

        foreach (var method in methods)
        {
            foreach (var model in @event.Models)
            {
                method.ItemCategories.Add(new SaleMethodItemCategoryModel
                {
                    ItemCategoryId = model.Id,
                    TrackingState = TrackingState.Added;
            });
        }
    }

     return _saleMethodService.EditAsync(methods);
}
‌‌‌‌‌
این آبونه شدن به رخ‌داد Created مرتبط با گروه کالایی، از سمت مولفه فروش انجام گرفته است. در بدنه متد Handle، ابتدا لیست نحوه‌های فروش موجود در سیستم توسط متد FindAsync بدون پارامتر واکشی شده و سپس با پیمایش خصوصیت Models مرتبط با رخ‌داد مدنظر، به‌ازای تک‌تک گروه‌های کالایی ثبت شده، یک وهله از SaleMethodItemCategoryModel به عنوان Detail موجودیت SaleMethod اضافه می‌شود. سپس با استفاده از متد EditAsync لیست این نحوه‌های فروش را ویرایش خواهیم کرد.
 
نکته: در حد امکان این هندلرها را به صورت تک مسئولیتی طراحی کرده و توسعه دهید؛ این قضیه برای نوشتن آزمون‌های واحد مرتبط با هندلرها، حیاتی می‌باشد.

نکته مهم: در مطلب «معرفی قالب پروژه Web API مبتنی‌بر ASP.NET Core Web API و زیرساخت DNTFrameworkCore» در رابطه با موضوع آزمون جامعیت سرویس‌ها بحث شد؛ توجه داشته باشید که اگر این هندلرها در فرآیند آزمون واحد سرویس‌ها وارد شوند، نگهداری داده‌های تست به‌شدت سخت و طاقت‌فرسا خواهد بود. راهکار پیشنهادی، استفاده از یک StubEventBust و جایگزینی آن با پیاده‌ساز پیش‌فرض، می‌باشد. از این طریق، فراخوانی هندلرهای مرتبط با رخ‌دادها را از فرآیند اصلی متدها حذف کرده‌ایم.
بازخوردهای دوره
انتقال خواص محاسباتی Entity Framework به ViewModelها توسط AutoMapper
نکته تکمیلی
جهت گرفتن تعداد رکورد‌های یک Navigation Property هنگام استفاده از EF میتوان از قوانین توکار خود Automapper استفاده کرد و نیازی به نوشتن ForMember وجود ندارد.
public class ProductInfoViewModel
{
    // long
    public long ProductCommentsLongCount { get; set; }

    // int
    public int ProductCommentsCount { get; set; }
}
اسم Navigation property + اسم متود، اگر کلید اصلی جدول کامنت int  باشه از ProductCommentsCount و اگر هم long باشه از ProductCommentsLongCount استفاده میکنیم. 
public class Product
{
    #region Properties

    public long Id { get; set; }

    [Required]
    [MaxLength(200)]
    public string Title { get; set; }

    public int Price { get; set; }

    #endregion

    #region Relations

    public ICollection<ProductComment> ProductComments { get; set; }

    #endregion
}

public class ProductComment
{
    #region Properties

    public int Id { get; set; }

    public string CommentBody { get; set; }

    public long ProductId { get; set; }

    #endregion

    #region Relations

    public Product Product { get; set; }

    #endregion
}

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        this.CreateMap<Entities.Product, ProductInfoViewModel>();
    }
}
دیگه نیازی به نوشتن ForMember برای گرفتن Count وجود نداره.