مطالب
اتصال ویژوال پارادایم به ساب ورژن
در فرآیند تولید نرم افزار بصورت تیمی، یکپارچگی در انجام کارها بسیار حائز اهمیت است. بطوریکه می‌توان نقطه‌ی اتکای تیم را بر اساس یکپارچگی ابزارها و نرم افزارها دانست. در این بین برای استفاده‌ی از ابزارهای مدلینگ بصورت تیمی و پشتیبانی از این ابزارها، استفاده‌ی از ابزار‌های سورس کنترل در اولویتی بالاتر قرار دارند. دراین مقاله سعی شده‌است تا روش اتصال ابزار بسیار قدرتمند ویژوال پارادایم، به سورس کنترلر Subversion توضیح داده شود.
در ابتدا در سرور svn  که من از برنامه‌ی VisualSVN استفاده کرده‌ام، یک repository ایجاد کرده و یا قسمتی از repository موجود را برای پروژه ویژوال پارادایم اختصاص میدهیم:

سپس طبق استاندارد، فولدرهای استاندارد برای کار با svn را در حین ایجاد repository میسازیم: 


در ادامه آدرس فولدر trunk را کپی کرده و برنامه ویژوال پارادایم را باز میکنیم: 

از منوی Teamwork گزینه‌ی Open Teamwork Client را انتخاب می‌کنیم (همانطور که در تصویر مشخص است فعالیت‌های svn در این منو وجود دارد مانند branch و tag):  

لیستی از سورس کنترلرها در اینجا وجود دارد. تمام فیلدهای لازم را برای کار با ساب ورژن تکمیل میکنیم:  

نکته: فیلد آخر، آدرس برنامه ساب ورژن است که همراه برنامه‌ی ویژوال پارادایم نصب شده است.

در پنجره‌ی Teamwork Client منوی Project گزینه Import Project to Repository را انتخاب میکنیم:

در پنجره بعدی دو حالت برای اتمام اتصال داریم:
1) افزودن یک پروژه‌ی جدید ویژوال پارادایم
2) افزودن پروژه در دست

و بعد از انتخاب (در اینجا من برای سهولت کار از Create new Project استفاده کردم) گزینه‌ی Open Project را انتخاب میکنیم:

اکنون همانطور که می‌بینید اعمال مربوط به کار کردن با svn در دسترس می‌باشند:

مطالب
Microsoft Test Manager - قسمت سوم

در کنار کاربرگ contents کاربرگی با نام  Propertiesوجود دارد که می‌توانید یک سری تنظیمات را برای plan خود انجام دهید. این تنظیمات از قبیل تغییر عنوان plan، تعیین مسیر پروژه، تاریخ شروع و پایان، کاربری که مالک این plan است، وضعیت جاری تست‌های plan و تعیین مرورگر و ویندوز نیز می‌باشد که می‌توانید در تصویر زیر آن را مشاهده کنید. 

اگر در لیست کشویی مربوط به test settings مقدار <default> قرار داشت می‌توانید با انتخاب آیتم new از لیست settings جدیدی را ایجاد نمایید و یا می‌توانید لیست test settings هایی را که قبلا ایجاد کرده اید انتخاب نمایید و برای ویرایش آن با کلیک بر روی لینک open که کنار لیست قرار دارد، می‌توانید تنظیمات را ویرایش نمایید.

همانطور که در تصویر بالا مشاهده می‌کنید، در سمت چپ، بخش هایی برای انجام تنظیمات مربوط به تست وجود دارد. در قسمت general تنظیماتی از قبیل عنوان test settings، شرح و نوع اجرای دستی یا اتومات بودن تستتان وجود دارد. در بخش roles می‌توانید نقش هایی را برای این تست انتخاب نمایید و در قسمت data and diagnostics می‌توانید یک سری اطلاعاتی را که می‌خواهید در زمان تست دریافت کنید، انتخاب کنید. برای اطلاعات بیشتر در مورد این بخش می‌توانید در سایت مایکروسافت مطالعه کنید.

حالا بر می‌گردیم به بخش contents و موارد تست خود را می‌سازیم. همانطور که در تصویر پایین مشاهده می‌کنید در بخش contents و در سمت راست پنجره یک گزینه ای به نام configuration وجود دارد.

در configuration شما می‌توانید یک سری تنظیمات مربوط به test شما است انجام دهید مثلا نوع مرورگری که می‌خواهید تست خود را اجرا کنید و یا اولویت تست را مشخص نمایید یا حتی نوع سیستم عامل را مشخص کنید. هم چنین می‌توانید چندین configuration تعریف کنید و از هر کدام برای یک test suite استفاده کنید. به صورت پیش فرض test suite از تنظیمات config والد خودش یعنی test plan استفاده می‌کند.

دوباره برمی گردیم به بخش contents و می‌خواهیم یک test suite با استفاده از add requirements بسازیم. همانطور که در بخش‌های قبل توضیح دادم می‌توانیم به چند روش test suite بسازیم که یکی از آن‌ها همین add requirements بود که می‌توانستید از test suite هایی که قبلا ساخته اید به این پروژه تستتان اضافه کنید.

با انتخاب گزینه add requirements پنجره ای باز می‌شود که می‌توانید همه test suite‌ها را مشاهده کنید و حتی می‌توانید براساس عنوان و یا وضعیت تست و ... فیلتر کنید.

بعد از اینکه در قسمت بالا کوئری خود را تنظیم کردید با انتخاب گزینه run می‌توانید کوئری خود را اجرا کرده و لیست test suite‌ها را براساس آن کوئری فیلتر کنید. می‌توانید یک یا چند سطر را انتخاب کرده و با زدن دکمه add requirements to plan آن‌ها را به plan خود اضافه نمایید. حالا ما یک test suite با استفاده از test suite هایی که قبلا ساخته ایم ایجاد کردیم. حالا باید مورد تست‌های مان را به این test suite اضافه کنیم. در سمت راست با کلیک بر روی گزینه add پنجره ای مشابه پنجره بالا باز می‌شود که شما می‌توانید test case‌ها را فیلتر کنید و یک یا چند مورد را انتخاب کرده و با زدن دکمه add test cases آن‌ها را به test suite تان اضافه کنید. برای اضافه کردن مورد تست جدید هم می‌توانید با کلیک بر روی new که در کنار گزینه Add قرار دارد مورد تست جدیدی را بسازید.

در تصویر زیر می‌توانید بخش‌های مختلف تست را که در بخش‌های قبل هم توضیح دادم ببینید.

نظرات مطالب
ایجاد کپچایی (captcha) سریع و ساده در ASP.NET MVC 5
با سلام و با تشکر؛ با اجازه بنده کد فوق رو کامل‌تر کردم و یک سری کد جدید بهش اضافه کردم و برخی بخش‌ها رو هم تغییر داده ام:
1- به جای سوال ، بنده یک عبارت رو نمایش میدم
2- ارسال دیتا از طریق کوئری استرینگ که باعث میشه سشن دیگه نیاز نباشه و از مصرف حافظه رو تا حد زیادی کاسته بشه.
البته این مورد برای سایت‌های پربازدید خیلی قابل لمس است و ممکنه روی سایت‌های معمولی تفاوت زیادی احساس نشه.
3- ارسال داده بصورت هش شده ، که این رو بنده خودم با یک کلاس دست ساز معمولی به روش TripleDes انجام داده ام که دوستان به هر روشی می‌تونن داده هاشون رو هش کنن.
4- یکم حروف رو چرخوندم و فاصله بین حروف رو هم طوری تنظیم کردم که در عرض تصویر پخش بشن (از کل عرض تصویر استفاده بشه)
* شایان ذکر است که به نظر من روش فوق در ایجاد نویز‌های دایره ای بسیار زیبا بود، چون همیشه همه جا با یک سری خط ساده نویز ایجاد می‌کنن ولی روش فوق واقعا خلاقانه و قشنگ بود :)
ساختار کنترلر ریکپچای من :
public class CaptchaController : Controller
    {
        private static readonly Brush ForeColor = Brushes.Black;
        private const string FontName = "tahoma";
        private const int FontSize = 14;
        private const int Width = 130;
        private const int Height = 35;

        [HttpGet]
        public ActionResult Image(string cc)
        {
            if (string.IsNullOrEmpty(cc) || string.IsNullOrWhiteSpace(cc))
                return null;

            var captchaData = CustomHashing.DecryptTpl(cc);

            var rand = new Random((int)DateTime.Now.Ticks);

            // image stream
            FileContentResult img = null;

            using (var mem = new MemoryStream())
            using (var bmp = new Bitmap(Width, Height))
            using (var mtrx = new Matrix())
            using (var gfx = Graphics.FromImage((Image)bmp))
            {
                gfx.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
                gfx.SmoothingMode = SmoothingMode.AntiAlias;
                gfx.FillRectangle(Brushes.White, new Rectangle(0, 0, bmp.Width, bmp.Height));

                //add noise
                int rn, xn, yn;
                var pen = new Pen(Color.Yellow);

                for (int i = 1; i < 10; i++)
                {
                    pen.Color = Color.FromArgb((rand.Next(0, 255)), (rand.Next(0, 255)), (rand.Next(0, 255)));

                    rn = rand.Next(0, (130 / 3));
                    xn = rand.Next(0, 130);
                    yn = rand.Next(0, 30);

                    gfx.DrawEllipse(pen, xn - rn, yn - rn, rn, rn);
                }

                //add chars
                #region draw pic

                float x = 1, y = 1;
                int degree = 10;

                for (int i = 0; i < captchaData.Length; i++)
                {
                    mtrx.Reset();

                    x = (float)(Width * (0.19 * i));

                    y = (float)(Height * 0.19);

                    degree = rand.Next(-25, 25);

                    if (i == 0 && degree > 20)
                    {
                        x += (FontSize + 5);
                        y -= 15;
                    }

                    mtrx.RotateAt(degree, new PointF(x, y));

                    gfx.Transform = mtrx;

                    gfx.DrawString(captchaData[i].ToString(), new Font(FontName, FontSize), ForeColor, x, y);

                    gfx.ResetTransform();
                }
                #endregion

                //render as Jpeg
                bmp.Save(mem, System.Drawing.Imaging.ImageFormat.Jpeg);
                img = this.File(mem.GetBuffer(), "image/Jpeg");
            }

            return img;
        }
برای استفاده هم داریم :
@{
    var r = new Web.Tools.CustomRandom();
    string hash = Web.Tools.CustomHashing.EncryptTpl(r.CraeteCapchaNumericData(4));
} 

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>test Index</title>
</head>
<body>
<div>

    <img src="@Url.Action("Image", "Captcha", new { cc = hash })" />

</div>
</body>
</html>
محتوای کلاس CustomRandom :
این کلاس به تعداد مورد نیاز کاراکتر عددی/عددی-حروفی می‌سازه و به شما تحویل میده
public class CustomRandom
 {
        /// <summary>
        /// ساخت یک عبارت عددی رندوم
        /// </summary>
        public string CraeteCapchaNumericData(int length)
        {
            var rnd = new Random((int) DateTime.Now.Ticks);
            var temp = new StringBuilder();

            for (var i = 0; i < length; i++)
                temp.Append(Convert.ToChar(rnd.Next(49, 58)));

            return temp.ToString();
        }

        /// <summary>
        /// ساخت یک عبارت رندوم
        /// </summary>
        public string CreateRandomName(int length)
        {
            var rnd = new Random((int) DateTime.Now.Ticks);
            var temp = new StringBuilder();
            var flag = 1;

            for (var i = 0; i < length; i++)
            {
                flag = rnd.Next(0, 15);

                if (flag < 5)
                    temp.Append(Convert.ToChar(rnd.Next(97, 123))); // lower
                else if (flag >= 5 && flag < 10)
                    temp.Append(Convert.ToChar(rnd.Next(49, 58))); // numeric
                else
                    temp.Append(Convert.ToChar(rnd.Next(65, 91))); // biger
            }

            return temp.ToString();
        } 
}
همانطور که گفتم پیاده سازی متد های DecryptTpl   و EncryptTpl  کلاس CustomHashing   رو به خود دوستان واگذار می‌کنم تا با هر الگوریتمی که دوست دارن این کار رو انجام بدن. (^)
امیدوارم کد بنده به دوستان کمک کنه.
موفق باشید
مطالب
استفاده از BroadcastChannel در جاوا اسکریپت

یکی از API ‌های کاربردی و جدید در دنیای وب، BroadcastChannel است که امکان ارسال اطلاعات بین window‌ها ، Tab‌ها و iframe‌های مختلف را که در یک دامنه هستند، فراهم می‌کند.  برای مثال اگر شما در مرورگری در پنجره‌های مختلف یک سایت را باز کرده باشید، با تغییر در یکی از این پنجره‌ها، قادر خواهید بود سایر پنجر‌ها را هم مطلع کنید تا در صورت نیاز، مجددا بارگذاری شوند. 


چرا از این API استفاده کنیم؟ 

یکی از وب سایت‌های مورد علاقه‌ی خود را در مرورگر باز کنید. مثلا یوتیوب و لاگین کنید. حالا در پنجره‌ی جدیدی، همین وب سایت را مجددا باز کرده و لاگین کنید. حالا در یکی از پنجره‌ها، از یوتیوب Logout کنید. خب شما حالا در یکی از پنجره‌ها لاگین هستید و در یکی دیگر Logout کرده‌اید. حالا پنجره‌های مرورگر شما دارای دو وضعیت متفاوت هستند. Logged-in در برابر Logged-out و این گاهی باعث دردسر خواهد شد.

این وضعیت حتی میتواند باعث خطرهای امنیتی نیز بشود. تصور کنید که کاربری در یک فضای عمومی مثل یک کافی شاپ وارد سایت شما شده‌است و داشبرد مخصوص به خود را باز کرده‌است. بنا به دلایلی کاربر قصد ترک محل را کرده و طبیعتا از برنامه شما Logout خواهد کرد . در این حالت اگر این کاربر برنامه شما را در صفحات مختلف مرورگر باز کرده باشد و لاگین نیز کرده باشد، هر کسی که بعد از او قصد استفاده از این کامپیوتر را داشته باشد ،  میتواند به اطلاعات کاربر مورد نظر در آن صفحات دسترسی پیدا کند؛ چه این اطلاعات روی صفحه باشد و چه مثلا اطلاعات یک JWT token.  چون کاربر فراموش کرده در صفحات دیگر هم Logout کند.


کد نویسی BroadcastChannel API 

در نگاه اول، استفاده از این API  ممکن است سخت به نظر برسد؛ ولی در واقع خیلی راحت است. برای نمونه قطعه کد زیر را درنظر بگیرید: 

<!DOCTYPE html>
<body>
  <!-- The title will change to greet the user -->
  <h1 id="title">Hey</h1>
  <input id="name-field" placeholder="Enter Your Name"/>
</body>
<script>
var bc = new BroadcastChannel('gator_channel');
(()=>{
  const title = document.getElementById('title');
  const nameField = document.getElementById('name-field');
  const setTitle = (userName) => {
    title.innerHTML = 'Hey ' + userName;
  }
  bc.onmessage = (messageEvent) => {
    // If our broadcast message is 'update_title' then get the new title from localStorage
    if (messageEvent.data === 'update_title') {
      // localStorage is domain specific so when it changes in one window it changes in the other
      setTitle(localStorage.getItem('title'));
    }
  }

  // When the page loads check if the title is in our localStorage
  if (localStorage.getItem('title')) {
    setTitle(localStorage.getItem('title'));
  } else {
    setTitle('please tell us your name');
  }
  nameField.onchange = (e) => {
    const inputValue = e.target.value;
    // In the localStorage we set title to the user's input
    localStorage.setItem('title', inputValue);
    // Update the title on the current page
    setTitle(inputValue);
    // Tell the other pages to update the title
    bc.postMessage('update_title');
  }
})()
</script>

این کد شامل یک Input باکس و یک title است. وقتی کاربر نام خود را در Input باکس وارد می‌کند، برنامه آن را در Localstorage با کلیدی به نام userName ذخیره می‌کند و بعد title صفحه جاری را به سلام + userName تغییر می‌دهد. مثلا اگر کاربر در Input باکس، عبارت بابک را وارد کند، title صفحه به سلام بابک تغییر داده میشود.

بدون BroadcastChannel، چنانچه کاربر در پنجره‌های مختلف مرورگر، برنامه را باز کرده باشد، تغییری در Title آن صفحات داده نخواهد شد؛ مگر اینکه مجددا توسط کاربر بارگذاری شود. 

در کد فوق ما یک وهله از BroadcastChannel را به نام gator_channel ایجاد کرده‌ایم و بعد onmessage را مساوی متدی با یک آرگومان جهت دریافت پیام قرار داده‌ایم. در این متد چک شده که اگر نام پیام، مساوی update_title باشد، متغیر ذخیره شده‌ی در LocalStorage خوانده شود.

هربار که متد postMessage ، از BroadcastChannel را فراخوانی میکنیم، این متد، باعث اجرای متد onmessage در سایر پنجره‌ها می‌شود. پس اگر در پنجره‌ی جاری در Input باکس، کلمه فرهاد را بنویسیم، متد bc.postMessage('update_title') در پنجره جاری اجرا شده و باعث اجرای متد onmessage در سایر پنجره‌هایی که سایتمان در آن باز است میشود.


این API در چه حالت‌هایی کار میکند

برخلاف سایر API‌ها مثل window.postMessage، شما لازم نیست چیزی در مورد اینکه چند تا صفحه از سایتتان بر روی مرورگر جاری باز شده را بدانید. (توجه کنید که روی عبارت «مرورگر جاری» تاکید میکنم. چون اگر برنامه روی دو مرورگر مثلا Chrome و Firefox به صورت همزمان باز باشد، این API فقط روی صفحات باز مرورگر جاری فراخوانی خواهد شد و نه مرورگر دوم؛ توضیحات بیشتر در ادامه داده شده است)

BroadcastChannel فقط روی مرورگر جاری و صفحاتی از یک دامنه، اجرا خواهد شد. این به این معنا است که شما می‌توانید پیام‌هایتان را از دامنه مثلا : https://alligator.io به دامنه https://alligator.io/js/broadcast_channels ارسال کنید. تنها نکته‌ای که لازم است تا رعایت شود این است که آبجکت BroadcastChannel در هر دو صفحه، از یک نام برای channel استفاده کرده باشند:

const bc = new BroadcastChannel('alligator_channel');
bc.onmessage = (eventMessage) => {
  // do something different on each page
}


در حالتهای زیر این API کار نخواهد کرد:

هاست‌های متفاوت:

https://alligator.io 

https://www.aligator.io

پورت‌های متفاوت:

https://alligator.io 

https://alligator.io :8080

پروتکل‌های متفاوت:

https://alligator.io 

http://alligator.io 

و یا برای مثال اگر مثلا در مرورگر Chrome یکی از صفحات به صورت Incognito باز شده باشد.


سازگاری این API با مرورگرهای مختلف

با توجه به اطلاعات سایت caniuse.com، این API در 75.6% مرورگرها پشتیبانی میشود. ولی مرورگرهای Safari و Internet Explorer از این API پشتیبانی نمی‌کنند. همچنین امکان استفاده از این API توسط کتابخانه sysend.js نیز فراهم شده‌است.


چه نوع پیامهایی را می‌توانید به کمک این API ارسال کنید

  • تمامی تایپ‌ها (Boolean,Null, Undefined,Number,BigInt, String) به غیر از symbol
  • آبجکتهای Boolean و String
  • Dates
  • Regular Expressions
  • Blobs
  • Files, File Lists
  • Array Buffer, ArrayBufferViews
  • ImageBitmaps, ImageDates
  • Arrays,Objects,Maps and Sets
در صورت ارسال تایپی از نوع symbol، با خطایی در سمت ارسال کننده مواجه خواهید شد.

قطعه کد زیر، بجای string، یک object را ارسال می‌کند:

bc.onmessage = (messageEvent) => {
  const data = messageEvent.data
  // If our broadcast message is 'update_title' then get the new title from localStorage
  switch (data.type) {
    case 'update_title':
      if (data.title){
        setTitle(data.title);
      }
      else
        setTitle(localStorage.getItem('title'));
      break
    default:
      console.log('we received a message')
  }
};
// ... Skipping Code
bc.postMessage({type: 'update_title', title: inputValue});


چه کارهایی را میتوانید به کمک این API انجام دهید

چیزهای زیادی را میتوان مجسم کرد. محتمل‌ترین گزینه، به اشتراک گذاری state جاری برنامه است. برای مثال اگه از کتابخانه‌های flux یا redux برای مدیریت state برنامه استفاده می‌کنید، به کمک این API می‌توانید state جاری را در تمامی صفحات باز برنامه، بروز رسانی کنید. حتی می‌توانید به چیزی شبیه به machine state فکر کنید.

یا مثلا آخرین وضعیت سبد خرید کالای مشتری و یا موجودی کالاها، در یک سایت خرید آنلاین. همچنین به اشتراک گذاری فایلهای حجیم مثل عکس و غیره جهت جلوگیری از دانلود مجدد آن‌‌ها در سایر صفحات.

به کمک دستور ()bc.close در هر زمانی میتوانید channel باز شده را ببندید و مجددا بسته به وضعیت برنامه، آن را باز کنید.

نظرات مطالب
طراحی گردش کاری با استفاده از State machines - قسمت اول
برای به گردش درآوردن آبجکت در Flow ایجاد شده تحت وب ایده ای می‌توانید بدهید؟
قاعدتا چندین راه (مانند موارد دیگر) برای آن وجود دارد اما راه بهینه بدون استفاده از WF و مستقل از شیء چیست؟
با جستجوی در وب چیزی دستگیرم نشد. اولین راه حلی که به نظرم می‌رسد در سمت application با mvc کنترلی با تحریف OnActionExecuting  و OnResultExecuted داشته باشیم برای پردازش State‌های مختلف و در متدهای ذکر شده ذخیره در بانک اطلاعاتی را انجام دهیم و در سمت دیتابیس هم جدولی داشته باشیم که با تغییر وضعیت، شناسه Workflow و شناسه State را برای نگهداشتن تاریخچه وضعیت‌ها و پیگیری آن را ذخیره کنیم. البته همچنان برای بررسی شروط هنگام transition‌ها به نتیجه ای نرسیده ام
مطالب
بهبود کارآیی حلقه‌های foreach در دات نت 7
بالاخره تفاوت کارآیی بین حلقه‌های for و foreach در دات نت 7 برطرف شده‌است که این مورد نیز یکی دیگر از دلایل بهبود کارآیی LINQ در دات نت 7 است. در این مطلب به همراه آزمایشی، این مورد را بررسی خواهیم کرد.


تدارک یک آزمایش برای بررسی کارآیی حلقه‌های for و foreach در دات نت 7

یک برنامه‌ی کنسول جدید را ایجاد کرده و سپس کتابخانه‌ی BenchmarkDotNet را با TargetFramework دات نت 7 به صورت زیر به پروژه اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net7.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
  </ItemGroup>
</Project>
در ادامه به این پروژه، کلاس زیر را اضافه می‌کنیم:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;

namespace NET7Loops;

[SimpleJob(RuntimeMoniker.Net60)]
[SimpleJob(RuntimeMoniker.Net70)]
[MemoryDiagnoser(false)]
public class Benchmarks
{
    private int[] ItemsArray;
    private List<int> ItemsList;

    [GlobalSetup]
    public void Setup()
    {
        var random = new Random(420);
        var randomItems = Enumerable.Range(0, 1000).Select(_ => random.Next());
        ItemsArray = randomItems.ToArray();
        ItemsList = randomItems.ToList();
    }

    [Benchmark]
    public void For_Array()
    {
        for (var i = 0; i < ItemsArray.Length; i++)
        {
            var item = ItemsArray[i];
        }
    }

    [Benchmark]
    public void For_List()
    {
        for (var i = 0; i < ItemsList.Count; i++)
        {
            var item = ItemsList[i];
        }
    }

    [Benchmark]
    public void ForEach_Array()
    {
        foreach (var item in ItemsArray)
        {
        }
    }

    [Benchmark]
    public void ForEach_List()
    {
        foreach (var item in ItemsList)
        {
        }
    }
}
که توسط دستورات زیر در حالت release اجرا شده و نتایج نهایی را نمایش می‌دهد:
using BenchmarkDotNet.Running;
using NET7Loops;

BenchmarkRunner.Run<Benchmarks>();
توضیحات:

- می‌توان یک پروژه را یکبار بر اساس دات نت 7 و یکبار هم بر اساس دات نت 6 با تغییر target framework آن‌ها کامپایل و اجرا کرد تا بتوان نتایج این دو را با هم مقایسه کرد و یا می‌توان با ذکر [SimpleJob(RuntimeMoniker.Net60)] و همچنین [SimpleJob(RuntimeMoniker.Net70)]، این مورد را به صورت خودکار به BenchmarkDotNet دات نت واگذار کرد.
- در این آزمایش، ابتدا یک آرایه و یک لیست را تهیه می‌کنیم.
- سپس یکبار حلقه‌های for و foreach را بر روی آرایه و همین عملیات را بر روی لیست تهیه شده، تکرار می‌کنیم.

نتایج حاصل به صورت زیر هستند:


همانطور که در نتایج فوق هم مشاهده می‌کنید:
در دات نت 6
- تفاوتی بین کارآیی حلقه‌ها‌ی for و foreach، زمانیکه بر روی یک آرایه اجرا می‌شوند، وجود ندارد.
- اما کارآیی حلقه‌ی foreach نسبت به حلقه‌ی for، زمانیکه بر روی یک لیست اجرا می‌شوند، تقریبا 50 درصد کمتر است.

در دات نت 7
- تفاوتی بین کارآیی حلقه‌ها‌ی for و forach، زمانیکه بر روی یک آرایه اجرا می‌شوند، وجود ندارد. بنابراین از این لحاظ با دات نت 6 تفاوتی ندارد.
- اما کارآیی حلقه‌ی foreach نسبت به حلقه‌ی for، زمانیکه بر روی یک لیست اجرا می‌شود، تقریبا یکسان و قابل چشم‌پوشی است. یعنی در دات نت 7، کارآیی این دو حلقه یکی شده‌است. اما چرا؟


روشی در جهت یافتن یکی بودن سرعت حلقه‌های for و foreach بر اساس خروجی کامپایلر

با مشاهده‌ی نتایج حاصل از BenchmarkDotNet می‌توان به بهبود کارآیی حاصل پی‌برد؛ اما برای مثال چرا زمانیکه از آرایه استفاده می‌شود، حتی در دات نت 6، تفاوتی بین دو حلقه‌ی for و foreach وجود ندارد، اما زمانیکه از لیست‌ها استفاده می‌شود، این کارآیی 50 درصد افت می‌کند؟
برای پاسخ به این سؤال می‌توان از IL Viewer موجود در Rider استفاده کرد که آخرین نگارش آن به همراه نمایش #Low-level C هم هست:

این همان خروجی است که توسط کامپایلر، پیش از تولید کدهای باینری نهایی، تهیه می‌شود. یعنی اگر قصد داشته باشیم تا درک کامپایلر را نسبت به قطعه کدی مشاهده کنیم، می‌توان به این خروجی مراجعه کرد که به صورت زیر است:
// Decompiled with JetBrains decompiler
// Type: NET7Loops.Benchmarks
// Assembly: NET7Loops, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
// MVID: E398BEE7-8123-4C55-AF9A-F7D83DDA73F1
// Assembly location: C:\Prog\1401\Net7Tests\NET7Loops\bin\Debug\net7.0\NET7Loops.dll
// Compiler-generated code is shown

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;

namespace NET7Loops
{
  [NullableContext(1)]
  [Nullable(0)]
  [SimpleJob(RuntimeMoniker.Net60, -1, -1, -1, -1, null, false)]
  [SimpleJob(RuntimeMoniker.Net70, -1, -1, -1, -1, null, false)]
  [MemoryDiagnoser(false)]
  public class Benchmarks
  {
    private int[] ItemsArray;
    private List<int> ItemsList;

    [GlobalSetup]
    public void Setup()
    {
      Benchmarks.<>c__DisplayClass2_0 cDisplayClass20 = new Benchmarks.<>c__DisplayClass2_0();
      cDisplayClass20.random = new Random(420);
      IEnumerable<int> source = Enumerable.Range(0, 1000).Select<int, int>(new Func<int, int>((object) cDisplayClass20, __methodptr(<Setup>b__0)));
      this.ItemsArray = source.ToArray<int>();
      this.ItemsList = source.ToList<int>();
    }

    [Benchmark(23, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void For_Array()
    {
      for (int index = 0; index < this.ItemsArray.Length; ++index)
      {
        int items = this.ItemsArray[index];
      }
    }

    [Benchmark(32, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void For_List()
    {
      for (int index = 0; index < this.ItemsList.Count; ++index)
      {
        int items = this.ItemsList[index];
      }
    }

    [Benchmark(41, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void ForEach_Array()
    {
      int[] itemsArray = this.ItemsArray;
      for (int index = 0; index < itemsArray.Length; ++index)
      {
        int num = itemsArray[index];
      }
    }

    [Benchmark(49, "C:\\Prog\\1401\\Net7Tests\\NET7Loops\\Benchmarks.cs")]
    public void ForEach_List()
    {
      List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator();
      try
      {
        while (enumerator.MoveNext())
        {
          int current = enumerator.Current;
        }
      }
      finally
      {
        enumerator.Dispose();
      }
    }

    public Benchmarks()
    {
      base..ctor();
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass2_0
    {
      [Nullable(0)]
      public Random random;

      public <>c__DisplayClass2_0()
      {
        base..ctor();
      }

      internal int <Setup>b__0(int _)
      {
        return this.random.Next();
      }
    }
  }
}
در این خروجی بهتر می‌توان مشاهده کرد که چرا در حالت استفاده‌ی از آرایه‌ها، تفاوتی بین حلقه‌های for و foreach نیست؛ چون هر دو به صورت حلقه‌ی for تفسیر می‌شوند:
for (int index = 0; index < this.ItemsArray.Length; ++index)
{
   int items = this.ItemsArray[index];
}
اما زمانیکه به لیست‌ها می‌رسیم، حلقه‌ی foreach به صورت زیر تفسیر می‌شود که بدیهی است نسبت به حلقه‌ی for، کندتر اجرا خواهد شد:
      List<int>.Enumerator enumerator = this.ItemsList.GetEnumerator();
      try
      {
        while (enumerator.MoveNext())
        {
          int current = enumerator.Current;
        }
      }
      finally
      {
        enumerator.Dispose();
      }
اگر این خروجی را برای دات نت 6 و دات نت 7 تهیه کنیم، به یک جواب خواهیم رسید. یعنی از دیدگاه #Low-level C، تفاوتی بین IL دات نت 6 و 7 از این لحاظ وجود ندارد. تفاوتی اصلی در بهبودهای JIT دات نت 7 است که سبب شده، خروجی نهایی حلقه‌‌های foreach با for یکی باشد.
مطالب
کارهایی جهت بالابردن کارآیی Entity Framework #2

در ادامه‌ی مطلب قبلی، نکاتی دیگر را جهت افزایش کارآیی سیستم‌های مبتنی بر EF اشاره خواهیم کرد:

عدم استفاده از کوئری‌های کلی

فرض کنید در یک فرم جستجو، 4 تکست باکس FirstName, LastName, City و PostalZipCode برای عملیات جستجو در نظر گرفته شده است و کاربر می‌تواند بر اساس آنها جستجو را انجام دهد.

var searchModel = new Pupil
{
    FirstName = "Ben",
    LastName = null,
    City = null,
    PostalZipCode = null
};

List<Pupil> pupils = db.Pupils.Where(p =>
        (searchModel.FirstName == null || p.FirstName == searchModel.FirstName)
        && (searchModel.LastName == null || p.LastName == searchModel.LastName)
        && (searchModel.City == null || p.LastName == searchModel.City)
        && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode))
    .Take(100)
    .ToList()
در کد بالا کاربر فقط فیلد نام را برای جستجو انتخاب کرده است و سایر فیلدها را خالی در نظر گرفته است و کوئری بالا صادر شده است و دستورات زیر به اس کیو ال ارسال شده‌اند:
 USE [EFSchoolSystem]
DECLARE @p__linq__0 NVarChar(4000) SET @p__linq__0 = 'Ben'
DECLARE @p__linq__1 NVarChar(4000) SET @p__linq__1 = 'Ben'
DECLARE @p__linq__2 NVarChar(4000) SET @p__linq__2 = ''
DECLARE @p__linq__3 NVarChar(4000) SET @p__linq__3 = ''
DECLARE @p__linq__4 NVarChar(4000) SET @p__linq__4 = ''
DECLARE @p__linq__5 NVarChar(4000) SET @p__linq__5 = ''
DECLARE @p__linq__6 NVarChar(4000) SET @p__linq__6 = ''
DECLARE @p__linq__7 NVarChar(4000) SET @p__linq__7 = ''

-- Executed query
SELECT TOP (100)
        [Extent1].[PupilId] AS [PupilId] ,
        [Extent1].[FirstName] AS [FirstName] ,
        [Extent1].[LastName] AS [LastName] ,
        [Extent1].[Address1] AS [Address1] ,
        [Extent1].[Adderss2] AS [Adderss2] ,
        [Extent1].[PostalZipCode] AS [PostalZipCode] ,
        [Extent1].[City] AS [City] ,
        [Extent1].[PhoneNumber] AS [PhoneNumber] ,
        [Extent1].[SchoolId] AS [SchoolId] ,
        [Extent1].[Picture] AS [Picture]
FROM    [dbo].[Pupils] AS [Extent1]
WHERE   (@p__linq__0 IS NULL OR [Extent1].[FirstName] = @p__linq__1)
        AND (@p__linq__2 IS NULL OR [Extent1].[LastName] = @p__linq__3)
        AND (@p__linq__4 IS NULL OR [Extent1].[LastName] = @p__linq__5)
        AND (@p__linq__6 IS NULL OR [Extent1].[PostalZipCode] = @p__linq__7)
هنگامیکه کوئری، سمت اس کیو ال سرور اجرا می‌شود، در صورتیکه پلن مربوط به آن وجود نداشته باشد، اس کیو ال سرور اقدام به ایجاد پلن، براساس پارامترهای ورودی می‌کند و از این به بعد، کوئری‌هایی یکسان، با پارامترهای متفاوت، از پلن کش شده استفاده خواهند کرد و از ایجاد پلن دیگری بدلیل هزینه بر بودن آن جلوگیری می‌شود.
مشکلی که در این دسته از کوئری‌های عمومی ایجاد می‌گردد آن است که ممکن است پلنی که برای یک گروه از پارامترهای ورودی مناسب باشد (جستجو بر اساس نام) برای سایر پارامترهای ورودی نامناسب باشد. تصور کنید تمام دانش آموزان، در شهر نیویورک یا بوستون زندگی می‌کنند. بنابراین این ستون از تنوع انتخاب کمتری برخوردار است در مقایسه با ستون نام خانوادگی و فرض کنید پلن، براساس پارامتر شهر ایجاد شده است. بنابراین ایجاد این پلن برای سایر پارامترها از کارآیی کافی برخوردار نیست. این مشکل با نام Bad Parameter Sniffing شناخته می‌شود و درباره‌ی Parameter Sniffing در اینجا به تفصیل اشاره شده است.
این مشکل زمانی بیشتر مشکل ساز خواهد شد که 99 درصد دانش آموزان در شهر نیویورک و فقط 1 درصد آن‌ها در شهر بوستون زندگی می‌کنند و پلن ایجاد شده بر اساس پارامتر شهر بوستون باشد.

راه حل اول:
برای حل این مشکل تنها یک راه حل خاص وجود ندارد و باید براساس شرایط برنامه، کوئری از حالت عمومی خارج گردد؛ مانند زیر:
 if (searchModel.City == null)
{
    pupils = db.Pupils.Where(p =>
        (searchModel.FirstName == null || p.FirstName == searchModel.FirstName)
        && (searchModel.LastName == null || p.LastName == searchModel.LastName)
        && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode))
        .Take(100)
        .ToList();
}
else
{
    pupils = db.Pupils.Where(p =>
        (searchModel.FirstName == null || p.FirstName == searchModel.FirstName)
        && (searchModel.LastName == null || p.LastName == searchModel.LastName)
        && (searchModel.City == null || p.LastName == searchModel.City)
        && (searchModel.PostalZipCode == null || p.PostalZipCode == searchModel.PostalZipCode))
        .Take(100)
        .ToList();
}

راه حل دوم:

کامپایل مجدد پلن در اجرای هر کوئری، اما این راه حل سرباری را تحمیل می‌کند. بدین منظور مترجم زیر را ایجاد کنید:
public class RecompileDbCommandInterceptor : IDbCommandInterceptor
{
    public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
    {
        if(!command.CommandText.EndsWith(" option(recompile)"))
        {
            command.CommandText += " option(recompile)";
        }
    }
    //and implement other interface members
}
و مانند زیر استفاده کنید:
 var interceptor = new RecompileDbCommandInterceptor();
DbInterception.Add(interceptor);
var pupils = db.Pupils.Where(p => p.City = city).ToList();
DbInterception.Remove(interceptor);

راه حل سوم:

استفاده از اجرای به تعویق افتاده به شکل زیر است:

var result = db.Pupils.AsQueryable();

if(searchModel.FirstName != null )
     result = result.Where(p => p.FirstName == searchModel.FirstName);

if(searchModel.LastName != null )
     result = result.Where(p => p.LastName == searchModel.LastName);

if(searchModel.PostalZipCode != null )
     result = result.Where(p => p.PostalZipCode == searchModel.PostalZipCode);

if(searchModel.City != null )
     result = result.Where(p => p.City == searchModel.City);

افزونگی کشِ پلن

استفاده‌ی مجدد از پلن بدلیل عدم ایجاد مجدد آن در زمان اجرای هر کوئری، بسیار خوب است. برای استفاده‌ی مجدد از پلن، باید دستورات ارسالی یکسان باشند؛ مانند کوئری‌های پارامتریک. در EF هنگامیکه از متغیرها استفاده کنید، کوئری‌ها پارامتریک تولید می‌کند؛ اما یک استثناء وجود دارد: ()Skip و ()Take

2 متد فوق بیشتر جهت صفحه بندی استفاده می‌شوند:

var schools = db.Schools
    .OrderBy(s => s.PostalZipCode)
    .Skip(model.Page * model.ResultsPerPage)
    .Take(model.ResultsPerPage)
    .ToList();
فرض کنید مقدار ResultsPerPage برابر 100 باشد و مقدار Page برابر 417. کوئری ارسالی به شکل زیر خواهد بود:

حال اگر قصد دریافت اطلاعات صفحه‌ی 500 را داشته باشید، مقادیر کوئری بعدی بترتیب 100 و 50000 خواهد بود و بجای مقادیر تصویر بالا 100 و 50000 قرار داده می‌شوند و کوئری متفاوتی با پلن متفاوتی ایجاد خواهد شد و اس کیو ال پلن کوئری قبلی را مورد استفاده قرار نخواهد داد و با اجرای کوئری دوم، پلن متفاوتی ایجاد خواهد کرد که این باعث ایجاد افزونگی پلن‌ها خواهد شد و همانگونه که قبلا اشاره شد ایجاد پلن جدید هزینه بر است.

نکته: جهت مشاهده پلن‌های کش شده در اس کیو ال، دستور زیر اجرا کنید:
SELECT  text, query_plan
FROM    sys.dm_exec_cached_plans
        CROSS APPLY sys.dm_exec_query_plan(plan_handle)
        CROSS APPLY sys.dm_exec_sql_text(plan_handle)
درصورتیکه کوئری دوم را برای صفحه 500 اجرا کنید، مشاهده خواهید کرد پلن کش شده‌ی دیگری ایجاد شده است. این افزونگی، نامناسب است به دو دلیل:
  1. صدمه به کارآیی؛ هربار EF یک کوئری و اس کیو ال یک پلن جدید را ایجاد می‌کنند.
  2. افزایش اشغال حافظه؛ کش شدن کوئری‌ها توسط EF سمت کلاینت و کش شدن پلن‌ها در اس کیو ال سرور (کش بی رویه‌ی پلن‌ها باعث حذف سایر پلن‌های مورد استفاده بدلیل محدودیت حافظه می‌شود که امکان بروز اختلال در کارآیی را به‌همراه خواهد داشت.)


علت بروز مشکل:

هنگامیکه یک مقدار int، به متدهای ()Skip و ()Take ارسال می‌شود، EF نمی‌تواند تشخیص دهد این مقدار ارسالی ثابت (absolute) مانند (100)Take است یا توسط یک متغیر مانند (متغیر)Take تولید شده است. به همین خاطر EF مقدار ارسال شده را پارامتریک در نظر نمی‌گیرد.


راه حل:

در EF6 پیاده سازی دیگری برای متدهای ()Skip و ()Take ارائه شده است که برای حل مشکل فوق می‌توان به کار گرفت، این پیاده سازی امکان دریافت lambada بجای int را دارد که باعث ایجاد کوئری‌های پارامتریک خواهد شد.

int resultsToSkip = model.Page * model.ResultsPerPage;
var schools = db.Schools
    .OrderBy(s => s.PostalZipCode)
    .Skip(() => resultsToSkip) //must pre-calculate this value
    .Take(() => model.ResultsPerPage)
    .ToList();
اکنون کوئری ارسال شده به اس کیو ال به صورت زیر خواهد بود:

همانطور که مشاهده می‌کنید این بار  EF کوئری پارامتریک ایجاد و ارسال کرده است.
مطالب
مبانی TypeScript؛ اینترفیس‌ها
اینترفیس، مانند قراردادی است که یک نوع را تعریف می‌کند. کامپایلر از اینترفیس‌ها جهت بررسی نوع‌ها و اجبار به رعایت قرارداد استفاده می‌کند. در این حالت اگر متدها یا خواص معرفی شده‌ی در نوع اینترفیس، توسط استفاده کننده بکار گرفته نشوند، خطایی توسط کامپایلر گزارش خواهد شد.
از آنجائیکه اینترفیس‌ها به معنای نوع‌های سفارشی هستند و جاوا اسکریپت از آن‌ها پشتیبانی نمی‌کند، توسط کامپایلر TypeScript، به هیچ نوع کد معادلی در جاوا اسکریپت، ترجمه و تبدیل نخواهند شد. کامپایلر TypeScript تنها از آن‌ها جهت بررسی نوع‌ها استفاده می‌کند.
اینترفیس‌ها به صورت مجموعه‌ای از تعاریف خواص و متدها، بدون پیاده سازی آن‌ها تعریف می‌شوند. پیاده سازی این اینترفیس‌ها، توسط کلاس‌ها و یا سایر اشیاء صورت خواهند گرفت. برای مثال یک قرارداد اجاره، مشخص می‌کند که آخر هر ماه چه مقداری را باید پرداخت کرد. اما این قرار داد مشخص نمی‌کند که چگونه باید این پرداخت صورت گیرد و از هر شخصی به شخص دیگری می‌تواند متفاوت باشد. به این حالت duck typing هم می‌گویند. به این معنا که قرار داد، شکل یک شیء را مشخص می‌کند و تا زمانیکه پیاده سازی کننده‌ی آن بتواند این قرارداد را تامین کند، می‌تواند بجای نوع اصلی نیز بکار گرفته شود.


Duck typing چیست؟

duck typing به این معنا است که اگر پرنده‌ای بتواند مانند یک اردک راه برود، شنا کند و صدا در بیاورد، یک اردک نامیده می‌شود. بنابراین همینقدر که یک شیء بتواند قراردادی را پیاده سازی کند، نوع آن با نوع اینترفیس یکی درنظر گرفته می‌شود. برای نمونه به مثال ذیل دقت کنید:
interface Duck {
    walk: () => void;
    swim: () => void;
    quack: () => void;
}

let probablyADuck = {
    walk: () => console.log('walking like a duck'),
    swim: () => console.log('swimming like a duck'),
    quack: () => console.log('quacking like a duck')
}

function FlyOverWater(bird: Duck) { }

FlyOverWater(probablyADuck); // works!
در این مثال اینترفیس Duck، متدهایی را تعریف کرده‌است که یک Duck می‌تواند انجام دهد.
در ادامه متغیر و شیءایی بدون تعریف نوع آن ایجاد شده‌است که همان متدهای اینترفیس Duck را پیاده سازی می‌کند و امضای آن‌ها با امضای متدهای اینترفیس Duck یکی هستند.
سپس متد FlyOverWater تعریف شده که در آن، نوع پارامتر ورودی آن به صورت صریحی به نوع اینترفیس Duck مقید شده‌است.
در سطر بعدی، این متد با دریافت شیء probablyADuck فراخوانی شده‌است و چون این شیء تمام اجزای قرارداد Duck را پیاده سازی کرده‌است، مشکلی در اجرای آن نخواهد بود. به این حالت duck typing می‌گویند.


نحوه‌ی تعریف یک اینترفیس در TypeScript

تعریف یک اینترفیس با واژه‌ی کلیدی interface شروع شده و سپس خواص و متدهای مدنظر این قرارداد، به همراه نوع آن‌ها تعریف خواهند شد:
interface Book {
    id: number;
    title: string;
    author: string;
    pages?: number;
    markDamaged: (reason: string) => void;
}
در این مثال خواص id، title و author اجباری هستند و پیاده سازی کننده موظف است آن‌ها را به همراه داشته باشد.
در اینترفیس‌های TypeScript می‌توان خواص اختیاری و optional را نیز تعریف کرد. نمونه‌ی آن خاصیت pages در این مثال است که با ? مشخص شده‌است و نمونه‌ی آن‌را در حین تعریف پارامترهای اختیاری متدها نیز پیشتر ملاحظه کرده بودید.
تعریف متدها در یک اینترفیس، با مشخص سازی نام آن متد و ذکر یک کولن و سپس مشخص سازی امضای پارامترهای دریافتی  انجام می‌شود. نوع خروجی متد، در سمت راست علامت <= قرار خواهد گرفت.


استفاده از اینترفیس‌ها برای تعریف نوع خروجی توابع

در مثال زیر، متد CreateCustomerID دارای دو پارامتر ورودی از نوع‌های رشته‌ای و عددی است و خروجی آن نیز از نوع رشته‌ای تعریف شده‌است:
function CreateCustomerID(name: string, id: number): string {
    return name + id;
}
در ادامه تعریف متغیری را مشاهده می‌کنید که نوع آن، متدی است که با امضای متد CreateCustomerID یکسان است:
 let IdGenerator: (chars: string, nums: number) => string;
به این ترتیب امکان انتساب متد CreateCustomerID به متغیر IdGenerator وجود خواهد داشت:
IdGenerator = CreateCustomerID;
جهت مدیریت بهتر یک چنین تعریف‌هایی و همچنین امکان استفاده‌ی مجدد از آن‌ها، می‌توان از اینترفیس‌ها کمک گرفت:
interface StringGenerator {
     (chars: string, nums: number): string;
}
اینترفیس StringGenerator نام بهتر و با قابلیت استفاده‌ی مجددی را به نوع متدی که قابل انتساب است به متغیر IdGenerator، تعریف می‌کند. در اینجا syntax تعریف نوع متد، در اینترفیس StringGenerator اندکی با حالت‌های قبلی متفاوت است. در اینجا بجای استفاده از <= جهت مشخص کردن نوع خروجی متد، از کولن استفاده شده‌است.
اکنون می‌توان نحوه‌ی تعریف متغیر IdGenerator را به صورت زیر Refactor کرد و تغییر داد:
 let IdGenerator: StringGenerator;
به عنوان نمونه می‌توان یک چنین تغییری را در نحوه‌ی تعریف اینترفیس Book ابتدای بحث و تغییر متد markDamaged آن نیز اعمال کرد.


بسط و توسعه‌ی اینترفیس‌ها

بسط و توسعه‌ی اینترفیس‌ها شبیه به مباحث ارث بری هستند. به این ترتیب که با بسط یک اینترفیس از طریق اینترفیسی دیگر، می‌توان به نوعی مرکب رسید:
interface LibraryResource {
   catalogNumber: number;
}

interface LibraryBook {
   title: string;
}

interface Encyclopedia extends LibraryResource, LibraryBook {
   volume: number;
}
در این مثال، ابتدا دو اینترفیس منابع و کتاب‌های یک کتابخانه تعریف شده‌اند. سپس اینترفیس جدیدی به نام Encyclopedia با بسط این دو اینترفیس توسط واژه‌ی کلیدی extends ایجاد شده‌است.
این نوع مرکب، علاوه بر دارا بودن خاصیت volume مختص به خودش، اکنون حاوی دو خاصیت موجود در سایر اینترفیس‌های ذکر شده‌ی در قسمت extends نیز هست.
حال اگر متغیر جدیدی را از نوع Encyclopedia تعریف کنیم، جهت برآورده شده تمام اجزای قرارداد، لازم است هر سه خاصیت را مقدار دهی نمائیم:
let refBook: Encyclopedia = {
   catalogNumber: 1234,
   title: 'The Book of Everything',
   volume: 1
}


نوع کلاس‌ها

مبحث کلاس‌ها به صورت جداگانه‌ای در این سری بررسی خواهند شد. اما جهت تکمیل بحث جاری نیاز است اشاره‌ی کوتاهی به آنها شود.
همانطور که عنوان شد، اینترفیس‌ها تنها شکل و قرارداد پیاده سازی یک شیء را تعریف می‌کنند؛ بدون ارائه‌ی پیاده سازی خاصی از آن‌‌ها. تا اینجا در بحث جاری، اشیاء را توسط object literals داخل {} تعریف کردیم (مانند متغیر refBook مثال قبل). اما کلاس‌ها روش بهتری برای انجام این‌کار و تعریف اشیاء هستند.
در ذیل تعریف اینترفیس کتابدار را با تک متد doWork آن ملاحظه می‌کنید:
interface Librarian {
   doWork: () => void;
}
متد doWork دارای پارامتری نیست و خروجی نیز ندارد. سپس با استفاده از واژه‌ی کلیدی class، یک کلاس جدید را ایجاد کرده‌ایم که با استفاده‌ی واژه‌ی کلیدی implements، یک پیاده سازی مشخص از اینترفیس Librarian را ارائه می‌دهد:
class ElementarySchoolLibrarian implements Librarian {
   doWork() {
     console.log('Reading to and teaching children...');
   }
}
اکنون داخل این کلاس، پیاده سازی خاصی از متد doWork مشخص شده‌ی در قرارداد و اینترفیس Librarian را مشاهده می‌کنید.
در ادامه برای ایجاد شیءایی از روی این تعریف، به نحو ذیل عمل می‌کنیم:
 let kidsLibrarian: Librarian = new ElementarySchoolLibrarian();
kidsLibrarian.doWork();
در اینجا متغیر kidsLibrarian از نوع اینترفیس کتابدار تعریف شده‌است. به این معنا که شیءایی که به آن انتساب داده می‌شود باید این اینترفیس را پیاده سازی کند. این شیء نیز توسط واژه‌ی کلیدی new، نمونه سازی/وهله سازی می‌شود. در ادامه می‌توان به متدها و خواص شیء kidsLibrarian دسترسی یافت و آن‌ها را فراخوانی کرد.
مطالب
آشنایی با NHibernate - قسمت پنجم

استفاده از LINQ جهت انجام کوئری‌ها توسط NHibernate

نگارش نهایی 1.0 کتابخانه‌ی LINQ to NHibernate اخیرا (حدود سه ماه قبل) منتشر شده است. در این قسمت قصد داریم با کمک این کتابخانه، اعمال متداول انجام کوئری‌ها را بر روی دیتابیس قسمت قبل انجام دهیم.
توسط این نگارش ارائه شده، کلیه اعمال قابل انجام با criteria API این فریم ورک را می‌توان از طریق LINQ نیز انجام داد (NHibernate برای کار با داده‌ها و جستجوهای پیشرفته بر روی آن‌ها، HQL : Hibernate Query Language و Criteria API را سال‌ها قبل توسعه داده است).

جهت دریافت پروایدر LINQ مخصوص NHibernate به آدرس زیر مراجعه نمائید:


پس از دریافت آن، به همان برنامه کنسول قسمت قبل، دو ارجاع را باید افزود:
الف) ارجاعی به اسمبلی NHibernate.Linq.dll
ب) ارجاعی به اسمبلی استاندارد System.Data.Services.dll دات نت فریم ورک سه و نیم

در ابتدای متد Main برنامه قصد داریم تعدادی مشتری را به دیتابیس اضافه نمائیم. به همین منظور متد AddNewCustomers را به کلاس CDbOperations برنامه کنسول قسمت قبل اضافه نمائید. این متد لیستی از مشتری‌ها را دریافت کرده و آن‌ها را در طی یک تراکنش به دیتابیس اضافه می‌کند:

public void AddNewCustomers(params Customer[] customers)
{
using (ISession session = _factory.OpenSession())
{
using (ITransaction transaction = session.BeginTransaction())
{
foreach (var data in customers)
session.Save(data);

session.Flush();

transaction.Commit();
}
}
}
در اینجا استفاده از واژه کلیدی params سبب می‌شود که بجای تعریف الزامی یک آرایه از نوع مشتری‌ها، بتوانیم تعداد دلخواهی پارامتر از نوع مشتری را به این متد ارسال کنیم.

پس از افزودن این ارجاعات، کلاس جدیدی را به نام CLinqTest به برنامه کنسول اضافه نمائید. ساختار کلی این کلاس که قصد استفاده از پروایدر LINQ مخصوص NHibernate را دارد باید به شکل زیر باشد (به کلاس پایه NHibernateContext دقت نمائید):

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{ }
}
اکنون پس از مشخص شدن context یا زمینه، نحوه ایجاد یک کوئری ساده LINQ to NHibernate به صورت زیر می‌تواند باشد:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CLinqTest : NHibernateContext
{
ISessionFactory _factory;

public CLinqTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetAllCustomers()
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>() select x;
return query.ToList();
}
}
}
}
ابتدا علاوه بر سایر فضاهای نام مورد نیاز، فضای نام NHibernate.Linq به پروژه افزوده می‌شود. سپس از extension متدی به نام Linq بر روی اشیاء ISession از نوع یکی از موجودیت‌های تعریف شده در برنامه در قسمت‌های قبل، می‌توان جهت تهیه کوئری‌های Linq مورد نظر بهره برد.
در این کوئری، لیست تمامی مشتری‌ها بازگشت داده می‌شود.

سپس جهت استفاده و بررسی آن در متد Main برنامه خواهیم داشت:

static void Main(string[] args)
{
using (ISessionFactory session = Config.CreateSessionFactory(
MsSqlConfiguration
.MsSql2008
.ConnectionString("Data Source=(local);Initial Catalog=HelloNHibernate;Integrated Security = true")
.ShowSql()
))
{

var customer1 = new Customer()
{
FirstName = "Vahid",
LastName = "Nasiri",
AddressLine1 = "Addr1",
AddressLine2 = "Addr2",
PostalCode = "1234",
City = "Tehran",
CountryCode = "IR"
};

var customer2 = new Customer()
{
FirstName = "Ali",
LastName = "Hasani",
AddressLine1 = "Addr..1",
AddressLine2 = "Addr..2",
PostalCode = "4321",
City = "Shiraz",
CountryCode = "IR"
};

var customer3 = new Customer()
{
FirstName = "Mohsen",
LastName = "Shams",
AddressLine1 = "Addr...1",
AddressLine2 = "Addr...2",
PostalCode = "5678",
City = "Ahwaz",
CountryCode = "IR"
};

CDbOperations db = new CDbOperations(session);
db.AddNewCustomers(customer1, customer2, customer3);

CLinqTest lt = new CLinqTest(session);
foreach (Customer customer in lt.GetAllCustomers())
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
}

Console.WriteLine("Press a key...");
Console.ReadKey();

}
در این متد ابتدا تعدادی رکورد تعریف و سپس به دیتابیس اضافه شدند. در ادامه لیست تمامی آن‌ها از دیتابیس دریافت و نمایش داده می‌شود.

مهمترین مزیت استفاده از LINQ در این نوع کوئری‌ها نسبت به روش‌های دیگر، استفاده از کدهای strongly typed دات نتی تحت نظر کامپایلر است، نسبت به رشته‌های معمولی SQL که کامپایلر کنترلی را بر روی آن‌ها نمی‌تواند داشته باشد (برای مثال اگر نوع یک ستون تغییر کند یا نام آن‌، در حالت استفاده از LINQ بلافاصله یک خطا را از کامپایلر جهت تصحیح مشکلات دریافت خواهیم کرد که این مورد در زمان استفاده از یک رشته معمولی صادق نیست). همچنین مزیت فراهم بودن Intellisense را حین نوشتن کوئری‌هایی از این دست نیز نمی‌توان ندید گرفت.

مثالی دیگر:
لیست تمام مشتری‌های شیرازی را نمایش دهید:
ابتدا متد GetCustomersByCity را به کلاس CLinqTest فوق اضافه می‌کنیم:

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
var query = from x in session.Linq<Customer>()
where x.City == city
select x;
return query.ToList();
}
}
سپس برای استفاده از آن، چند سطر ساده زیر به ادامه متد Main اضافه می‌شوند:

foreach (Customer customer in lt.GetCustomersByCity("Shiraz"))
{
Console.WriteLine("Customer: LastName = {0}", customer.LastName);
}
یکی دیگر از مزایای استفاده از LINQ to NHibernate ، امکان بکارگیری LINQ بر روی تمامی دیتابیس‌های پشتیبانی شده توسط NHibernate است؛ برای مثال مای اس کیوال، اوراکل و ....
لیست کامل دیتابیس‌های پشتیبانی شده توسط NHibernate را در این آدرس می‌توانید مشاهده نمائید. (البته به نظر لیست آن، آنچنان هم به روز نیست؛ چون در نگارش آخر NHibernate ، پشتیبانی از اس کیوال سرور 2008 هم اضافه شده است)


نکته:
در کوئری‌های مثال‌های فوق همواره باید session.Linq<T> را ذکر کرد. اگر علاقمند بودید شبیه به روشی که در LINQ to SQL موجود است مثلا db.TableName بجای session.Linq<T> در کوئری‌ها ذکر گردد، می‌توان اصلاحاتی را به صورت زیر اعمال کرد:
یک کلاس جدید را به نام SampleContext به برنامه کنسول جاری با محتویات زیر اضافه نمائید:

using System.Linq;
using NHibernate;
using NHibernate.Linq;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class SampleContext : NHibernateContext
{
public SampleContext(ISession session)
: base(session)
{ }

public IOrderedQueryable<Customer> Customers
{
get { return Session.Linq<Customer>(); }
}

public IOrderedQueryable<Employee> Employees
{
get { return Session.Linq<Employee>(); }
}

public IOrderedQueryable<Order> Orders
{
get { return Session.Linq<Order>(); }
}

public IOrderedQueryable<OrderItem> OrderItems
{
get { return Session.Linq<OrderItem>(); }
}

public IOrderedQueryable<Product> Products
{
get { return Session.Linq<Product>(); }
}
}
}
در این کلاس به ازای تمام موجودیت‌های تعریف شده در پوشه domain برنامه اصلی خود (همان NHSample1 قسمت‌های اول و دوم)، یک متد از نوع IOrderedQueryable را باید تشکیل دهیم که پیاده سازی آن‌را ملاحظه می‌نمائید.
سپس بازنویسی متد GetCustomersByCity بر اساس SampleContext فوق به صورت زیر خواهد بود که به کوئری‌های LINQ to SQL بسیار شبیه است:

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHSample1.Domain;

namespace ConsoleTestApplication
{
class CSampleContextTest
{
ISessionFactory _factory;

public CSampleContextTest(ISessionFactory factory)
{
_factory = factory;
}

public List<Customer> GetCustomersByCity(string city)
{
using (ISession session = _factory.OpenSession())
{
using (SampleContext db = new SampleContext(session))
{
var query = from x in db.Customers
where x.City == city
select x;
return query.ToList();
}
}
}
}
}
دریافت سورس برنامه تا این قسمت

و در تکمیل این بحث، می‌توان به لیستی از 101 مثال LINQ ارائه شده در MSDN اشاره کرد که یکی از بهترین و سریع ترین مراجع یادگیری مبحث LINQ است.


ادامه دارد ...


مطالب
کار با SignalR Core از طریق یک کلاینت جاوا اسکریپتی
کلاینت جاوا اسکریپتی SignalR Core، بازنویسی کامل شده‌است و دیگر وابستگی به jQuery ندارد. این کلاینت از طریق npm توزیع می‌شود:
 npm install @aspnet/signalr-client --save
فایل‌های آن نیز شامل فایل‌های جاوا اسکریپتی مرتبط و همچنین Typings مورد استفاده‌ی در TypeScript است که نمونه‌ای از نحوه‌ی استفاده از این Typings را در مطلب «کار با SignalR Core از طریق یک کلاینت Angular» مطالعه کردید.


بررسی محتوای پوشه‌ی node_modules\@aspnet\signalr-client

پس از نصب بسته‌ی «aspnet/signalr-client@»، در مسیر node_modules\@aspnet\signalr-client\dist دو پوشه‌ی src و browser را خواهید یافت. پوشه‌ی src حاوی منبع کامل این کلاینت و همچنین فایل‌های Typings مخصوص تایپ‌اسکریپت است.


و پوشه‌ی browser آن شامل دو گروه فایل است:


- در اینجا گروهی از فایل‌ها، حاوی عبارت ES5 هستند و تعدادی خیر. SignalR JavaScript بر اساس ES 6 یا EcmaScript 2015 تهیه شده‌است و از مفاهیمی مانند Promises  و  arrow functions استفاده می‌کند. باید دقت داشت که تعدادی از مرورگرها مانند IE از این قابلیت‌ها پیشتیبانی نمی‌کنند. در بین این فایل‌ها، آن‌هایی که حاوی عبارت ES5 نیستند، یعنی بر اساس ES 6 تهیه شده‌اند. سایر فایل‌ها توسط قابلیت Transpile مربوط به TypeScript به ES5 ترجمه شده‌اند. به علاوه حجم این فایل‌ها نیز بیشتر می‌باشد؛ چون حاوی تعاریف وابستگی‌هایی هستند که در ES 5 وجود خارجی ندارند. بنابراین بسته به نوع مرورگر مدنظر، یکی از این دو گروه را باید انتخاب کرد؛ ES 6 برای مرورگرهای جدید و ES 5 برای مرورگرهای قدیمی.
- به علاوه در اینجا تعدادی از فایل‌ها حاوی عبارت msgpackprotocol هستند. نگارش جدید SignalR از پروتکل‌های هاب سفارشی مانند پروتکل‌های باینری نیز پشتیبانی می‌کند. همچنین حاوی یک پیاده سازی توکار از پروتکل‌های باینری بر اساس MessagePack نیز هست. چون حجم کدهای پشتیبانی کننده‌ی از این پروتکل ویژه بالا است، آن‌را به یک فایل مجزا انتقال داده‌اند تا در صورت نیاز مورد استفاده قرار گیرد. بنابراین اگر از این پروتکل استفاده نمی‌کنید، نیازی هم به الحاق آن در صفحات خود نخواهید داشت. فایل third-party-notices.txt نیز مربوط است به یادآوری مجوز استفاده‌ی از MessagePack که MIT می‌باشد.
- در هر گروه نیز، دو فایل min و معمولی قابل مشاهده‌است. فایل‌های min برای توزیع نهایی مناسب هستند و فایل‌های غیرفشرده شده برای حالت دیباگ.


استفاده از کلاینت جاوا اسکریپتی SignalR Core

برای کار با کلاینت جاوا اسکریپتی SignalR Core از همان فایل‌های موجود در پوشه‌ی node_modules/@aspnet/signalr-client/dist/browser استفاده می‌کنیم. تفاوت این کلاینت با نگارش قبلی SignalR به صورت یک ذیل است:
1) ارجاع به فایل قدیمی signalR-2.2.1.min.js با فایل جدید signalR-client-1.0.0-alpha1.js جایگزین می‌شود. اگر می‌خواهید مرورگرهای قدیمی را پشتیبانی کنید، نگارش ES5 آن‌را لحاظ کنید.
2) پروکسی‌ها با new HubConnection جایگزین شده‌اند.
3) برای ثبت callbackهای سمت کلاینت، از متد جدید on استفاده می‌شود.
4) بجای متد done مربوط به jQuery، در اینجا از متد then مربوط به ES6 کمک گرفته شده‌است.
5) کار فراخوانی متدهای هاب توسط متد invoke انجام می‌شود.


یک مثال: بازنویسی قسمت سمت کلاینت مثال «کار با  SignalR Core از طریق یک کلاینت Angular» با jQuery

هرچند کلاینت جدید SignalR Core وابستگی به jQuery ندارد، اما جهت سهولت کار با DOM، کدهای سمت کلاینت مثال قبلی را با jQuery بازنویسی می‌کنیم. تمام کدهای سمت سرور این مثال با مطلب «کار با SignalR Core از طریق یک کلاینت Angular» یکی است؛ مانند ایجاد هاب، فعالسازی SiganlR در فایل آغازین برنامه و ثبت مسیرهاب. بنابراین در اینجا، این قسمت از کدهای سمت سرور را مجددا تکرار نمی‌کنیم و تمام نکات آن یکی هستند.

برای کار با کلاینت جاوا اسکریپتی SignalR Core، اینبار دستور ذیل را در ریشه‌ی پروژه‌ی وب اجرا می‌کنیم (یا هر پروژه‌ای که قرار است مدیریت فایل‌های سمت کلاینت و Viewهای برنامه را انجام دهد):
npm init
npm install @aspnet/signalr-client --save
bower install
دستور اول یک فایل package.json خالی را ایجاد می‌کند و دستور دوم بسته‌ی جاوا اسکریپتی SiganlR Core را نصب خواهد کرد. به علاوه این وابستگی را در فایل package.json نیز ثبت می‌کند. دستور سوم نیز وابستگی‌های قید شده‌ی در فایل bower.json را نصب می‌کند.

مرحله‌ی بعدی کار، تنظیمات فایل bundleconfig.json است؛ تا تمام اسکریپت‌های مورد نیاز جمع‌آوری و یکی شوند:
[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/lib/bootstrap/dist/css/bootstrap.min.css",
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/lib/jquery/dist/jquery.min.js",
      "wwwroot/lib/bootstrap/dist/js/bootstrap.min.js",
      "node_modules/@aspnet/signalr-client/dist/browser/signalr-client-1.0.0-alpha1-final.min.js",
      "wwwroot/lib/jquery-validation/dist/jquery.validate.min.js",
      "wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js",
      "wwwroot/lib/jquery-ajax-unobtrusive/jquery.unobtrusive-ajax.min.js",
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": false,
      "renameLocals": false
    },
    "sourceMap": false
  }
]
در اینجا نحوه‌ی ثبت فایل signalr-client-1.0.0-alpha1-final.min.js مبتنی بر ES 6 را مشاهده می‌کنید. اگر می‌خواهید نگارش ES 5 آن‌را ذکر کنید، از فایل signalr-clientES5-1.0.0-alpha1-final.min.js استفاده نمائید.
با توجه به خروجی‌های نهایی فایل bundleconfig.json، تنها نیاز است مداخل ذیل را به فایل layout برنامه اضافه کرد:
<link href="~/css/site.min.css" rel="stylesheet" asp-append-version="true" />
<script src="~/js/site.min.js" type="text/javascript" asp-append-version="true"></script>

مرحله‌ی بعد، تغییر نام متد send قسمت قبل به broadcastMessage است:
public class MessageHub : Hub
{
   public Task Send(string message)
   {
     return Clients.All.InvokeAsync("broadcastMessage", message);
   }
}
به این ترتیب می‌توان به تمایز بهتری بین نام callback سمت کلاینت و متد Send سمت سرور رسید. بهتر است این‌دو هم‌نام نباشند.
در ادامه یک کنترلر ساده را به نام JsClientController با View ذیل ایجاد می‌کنیم:
<form method="post"
      asp-action="Index"
      asp-controller="Home"
      data-ajax="true"
      role="form">
  <div class="form-group">
     <label label-for="message">Message: </label>
     <input id="message" name="message" class="form-control"/>
  </div>
  <button class="btn btn-primary" type="submit">Send To Home/Index</button>
  <button class="btn btn-success" id="sendmessageDirect" type="button">Send To /message hub directly</button>
</form>

<div id="discussion">
</div>
کار آن نمایش فرم ذیل است:


از اولین دکمه برای ارسال یک پیام به کنترلر Home که در آن توسط <IHubContext<MessageHub پیامی به تمام کلاینت‌ها ارسال می‌شود، استفاده شده‌است. دومین دکمه متد Send هاب را مستقیما فراخوانی می‌کند؛ با این کدهای سمت کلاینت:
@section Scripts
{
   <script type="text/javascript" asp-append-version="true">
   $(function() {
      var connection = new signalR.HubConnection('/message');
      connection.on('broadcastMessage', function (message) {
          // Add the message to the page.
          var encodedMsg = $('<div />').text(message).html();
          $('#discussion').append('<li>' + encodedMsg + '</li>');
      });

      connection.start().then(function () {
          console.log('connected.');
          $('#sendmessageDirect').click(function () {
              // Call the Send method on the hub.
              connection.invoke('send', $('#message').val());
          });
      });
   });
   </script>
}
- ابتدا یک شیء جدید signalR.HubConnection ایجاد می‌شود. این شیء به آدرس Hub تعریف شده‌ی در فایل آغازین برنامه اشاره می‌کند.
- سپس در متد on هست که مشخص می‌کنیم متد سمت کلاینتی که قرار است از سمت سرور فراخوانی شود، چه نامی دارد. نام آن‌را در این مثال broadcastMessage درنظر گرفته‌ایم. در اینجا پارامتر message از سمت سرور دریافت شده و سپس در صفحه‌ی جاری نمایش داده می‌شود.
بدیهی است متد Send می‌تواند تعداد پارامترهای بیشتری را بپذیرد و همچنین متد broadcastMessage نیز محدودیتی از لحاظ تعداد پارامتر ندارد. اگر پارامترهای بیشتری را تعریف کردید، در همینجا باید قید شوند.
- در ادامه کار شروع این اتصال آغاز می‌شود. در متد then هست که باید کار اتصال دکمه‌ی sendmessageDirect صورت گیرد. چون عملیات اتصال ممکن است زمانبر باشد و connection ارسالی هنوز آغاز نشده باشد. در اینجا نحوه‌ی فراخوانی مستقیم متد Send سمت سرور را با یک پارامتر ملاحظه می‌کنید. این متد نیز می‌تواند بر اساس امضای متد Send سمت سرور، تعداد پارامترهای بیشتری را قبول کند.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: SignalRCore2WebApp02.zip
برای اجرا آن باید این دستورات را به ترتیب وارد کنید:
dotnet restore
npm install
npm install -g bower
bower install
dotnet watch run