مطالب
پیاده‌سازی الگوی Transaction Per Request در EF
قبلاً در سایت جاری در رابطه با پیاده‌سازی الگوی Context Per Request مطالبی منتشر شده است. در ادامه می‌خواهیم تمامی درخواست‌های خود را اتمیک کنیم. همانطور که قبلاً در این مطلب مطالعه کردید یکی از مزایای الگوی Context Per Request، استفاده‌ی صحیح از تراکنش‌ها می‌باشد. به عنوان مثال اگر در حین فراخوانی متد SaveChanges، خطایی رخ دهد، کلیه‌ی عملیات RollBack خواهد شد. اما حالت زیر را در نظر بگیرید:
_categoryService.AddNewCategory(category);
_uow.SaveAllChanges();

throw new InvalidOperationException();

return RedirectToAction("Index");
همانطور که در کدهای فوق مشاهده می‌کنید، قبل از ریدایرکت شدن صفحه، یک استثناء را صادر کرده‌ایم. در این حالت، تغییرات درون دیتابیس ذخیره می‌شوند! یعنی حتی اگر یک استثناء نیز در طول درخواست رخ دهد، قسمتی از درخواست که در اینجا ذخیره‌سازی گروه محصولات است، درون دیتایس ذخیره خواهد شد؛ در نتیجه درخواست ما اتمیک نیست.
برای رفع این مشکل می‌توانیم یکسری وظایف (Tasks) را تعریف کنیم که در نقاط مختلف چرخه‌ی حیات برنامه اجرا شوند. هر کدام از این وظایف تنها کاری که انجام می‌دهند فراخوانی متد Execute خودشان است. در ادامه می‌خواهیم از این وظایف جهت پیاده‌سازی الگوی Transaction Per Request استفاده کنیم. در نتیجه اینترفیس‌های زیر را ایجاد خواهیم کرد:
public interface IRunAtInit
{
       void Execute();
}
public interface IRunAfterEachRequest
{
       void Execute(); 
}
public interface IRunAtStartUp
{
       void Execute(); 
}
public interface IRunOnEachRequest
{
       void Execute(); 
}
public interface IRunOnError
{
       void Execute(); 
}
خوب، این اینترفیس‌ها همانطور که از نامشان پیداست، همان اعمال را پیاده سازی خواهند کرد:
IRunAtInit: اجرای وظایف در زمان بارگذاری اولیه‌ی برنامه.
IRunAfterEachRequest: اجرای وظایف بعد از اینکه درخواستی فراخوانی (ارسال) شد.
IRunAtStartUp: اجرای وظایف در زمان StartUp برنامه.
IRunOnEachRequest: اجرای وظایف در ابتدای هر درخواست.
IRunOnError: اجرای وظایف در زمان بروز خطا یا استثناء‌های مدیریت نشده‌ی برنامه.
خوب، یک کلاس می‌تواند با پیاده‌سازی هر کدام از اینترفیس‌های فوق تبدیل به یک task شود. همچنین از این جهت که اینترفیس‌های ما ساده هستند و هر اینترفیس یک متد Execute دارد، عملکرد آن‌ها تنها اجرای یکسری دستورات در حالات مختلف می‌باشد.
قدم بعدی افزودن قابلیت پشتیبانی از این وظایف در برنامه‌مان است. اینکار را با پیاده‌سازی ریجستری زیر انجام خواهیم داد:
public class TaskRegistry : StructureMap.Configuration.DSL.Registry
{
        public TaskRegistry()
        {
            Scan(scan =>
            {
                scan.Assembliy("yourAssemblyName");
                scan.AddAllTypesOf<IRunAtInit>();
                scan.AddAllTypesOf<IRunAtStartUp>();
                scan.AddAllTypesOf<IRunOnEachRequest>();
                scan.AddAllTypesOf<IRunOnError>();
                scan.AddAllTypesOf<IRunAfterEachRequest>();
            });
        }
}
با این کار استراکچرمپ اسمبلی معرفی شده را بررسی کرده و هر کلاسی که اینترفیس‌های ذکر شده را پیاده‌سازی کرده باشد، رجیستر می‌کند. قدم بعدی افزودن رجیستری فوق و بارگذاری آن درون کانتینرمان است:
ioc.AddRegistry(new TaskRegistry());
اکنون وظایف درون کانتینرمان بارگذاری شده‌اند. سپس نوبت به استفاده‌ی از این وظایف است. 
خوب، باید درون فایل Global.asax کدهای زیر را قرار دهیم. چون همانطور که عنوان شد وظایف ایجاد شده می‌بایستی در نقاط مختلف برنامه اجرا شوند:
protected void Application_Start()
{
   // other code
   foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAtInit>())
   {
                task.Execute();
    }
}
protected void Application_BeginRequest()
 {
           foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnEachRequest>())
           {
                task.Execute();
           }
}
protected void Application_EndRequest(object sender, EventArgs e)
{
            try
            {
                foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunAfterEachRequest>())
                {
                    task.Execute();
                }
            }
            finally
            {
                HttpContextLifecycle.DisposeAndClearAll();
                MiniProfiler.Stop();
            }
}
protected void Application_Error()
{
            foreach (var task in SmObjectFactory.Container.GetAllInstances<IRunOnError>())
            {
                task.Execute();
            }
}
همانطور که مشاهده می‌کنید، هر task در قسمت خاص خود فراخوانی خواهد شد. مثلاً IRunOnError درون رویداد Application_Error و دیگر وظایف نیز به همین ترتیب.
اکنون برنامه به صورت کامل از وظایف پشتیبانی می‌کند. در ادامه، کلاس زیر را ایجاد خواهیم کرد. این کلاس چندین اینترفیس را از اینترفیس‌های ذکر شده، پیاده‌سازی می‌کند:
public class TransactionPerRequest : IRunOnEachRequest, IRunOnError, IRunAfterEachRequest
{
        private readonly IUnitOfWork _uow;
        private readonly HttpContextBase _httpContext;
        public TransactionPerRequest(IUnitOfWork uow, HttpContextBase httpContext)
        {
            _uow = uow;
            _httpContext = httpContext;
        }


        void IRunOnEachRequest.Execute()
        {
            _httpContext.Items["_Transaction"] =
                _uow.Database.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
        }

        void IRunOnError.Execute()
        {
            _httpContext.Items["_Error"] = true;
        }

        void IRunAfterEachRequest.Execute()
        {
            var transaction = (DbContextTransaction) _httpContext.Items["_Transaction"];
            if (_httpContext.Items["_Error"] != null)
            {
                transaction.Rollback();
            }
            else
            {
                transaction.Commit();
            }
        }
}
توضیحات کلاس فوق:
در کلاس TransactionPerRequest به دو وابستگی نیاز خواهیم داشت: IUnitOfWork برای کار با تراکنش‌ها و HttpContextBase برای دریافت درخواست جاری. همانطور که مشاهده می‌کنید در متد IRunOnEachRequest.Execute یک تراکنش را آغاز کرده‌ایم و در IRunAfterEachRequest.Execute یعنی در پایان یک درخواست، تراکنش را commit کرده‌ایم. این مورد را با چک کردن یک فلگ در صورت عدم بروز خطا انجام داده‌ایم. اگر خطایی نیز وجود داشته باشد، کل عملیات roll back خواهد شد. لازم به ذکر است که فلگ خطا نیز درون متد IRunOnError.Execute به true مقداردهی شده است.
خوب، پیاده‌سازی الگوی Transaction Per Request به صورت کامل انجام گرفته است. اکنون اگر برنامه را در حالت زیر اجرا کنید:
_categoryService.AddNewCategory(category);
_uow.SaveAllChanges();

throw new InvalidOperationException();

return RedirectToAction("Index");
خواهید دید که عملیات roll back شده و تغییرات در دیتابیس (در اینجا ذخیره سازی گروه محصولات) اعمال نخواهد شد.
مطالب
امکان تعریف حلقه‌ی foreach بر روی هر نوع مجموعه‌ای از داده‌ها در C# 9.0
عبارت foreach در زبان #C، امکان پیمایش اعضای یک مجموعه را میسر می‌کند؛ اما نه هر مجموعه‌ای. این مجموعه‌ی خاص باید به این صورت تعریف شده باشد:
الف) <IEnumerable<T را پیاده سازی کرده باشد.
ب) و یا ... مهم نیست که این مجموعه حتما <IEnumerable<T را پیاده سازی کرده باشد. اگر این مجموعه به همراه یک متد عمومی خاص با نام GetEnumerator باشد که خروجی آن دارای خاصیت عمومی T Current است (یکی از اعضای اینترفیس <IEnumerable<T) و همچنین به همراه متد عمومی bool MoveNext نیز هست (یکی از اعضای اینترفیس IEnumerator)، قابلیت کار با حلقه‌ی foreach را پیدا می‌کند و ... اکنون در C# 9.0 می‌توان متد GetEnumerator را به صورت یک متد الحاقی، به هر نوع دلخواهی اعمال کرد! یعنی می‌توان برای هر نوعی در صورت نیاز، یک GetEnumerator خاص را طراحی کرد که سبب به کار افتادن حلقه‌ی foreach بر روی آن شود.


مثال 1: نوع <IEnumerator<T با حلقه‌ی foreach سازگار نیست

نوع <IEnumerator<T به دلیل نداشتن متد عمومی GetEnumerator که ذکر شد:
    public interface IEnumerator<out T> : IEnumerator, IDisposable
    {
        //
        // Summary:
        //     Gets the element in the collection at the current position of the enumerator.
        //
        // Returns:
        //     The element in the collection at the current position of the enumerator.
        T Current { get; }
    }
قابلیت پیمایش توسط حلقه‌ی foreach را ندارد. اگر در C# 8.0 این حلقه را بر روی آن اعمال کنیم، به خطای کامپایلر زیر می‌رسیم:
Error CS1579 foreach statement cannot operate on variables of type ‘IEnumerator’
because ‘IEnumerator’ does not contain a public instance or extension definition for ‘GetEnumerator’
 اما می‌توان به صورت زیر در C# 9.0، این متد را به آن اضافه کرد:
static class Extensions
{
   public static IEnumerator<T> GetEnumerator<T>(this IEnumerator<T> enumerator) => enumerator;
}

اکنون حلقه‌ی foreach را می‌توان بر روی نوع‌های <IEnumerator<T نیز بکار گرفت:
class Program
{
    void Main()
    {
        var enumerator = Enumerable.Range(0, 10).GetEnumerator();
        foreach (var item in enumerator)
        {
            Console.WriteLine(item);
        }
    }
}

این نکته بر روی نمونه‌ی async آن نیز قابل اعمال است که مثالی از آن‌را در ادامه مشاهده می‌کنید:
static class Extensions
{
    public static IAsyncEnumerator<T> GetAsyncEnumerator<T>(this IAsyncEnumerator<T> enumerator) => enumerator;
}

class Program
{
    static async Task Main()
    {
        var enumerator = GetAsyncEnumerator();
        await foreach (var item in enumerator)
        {
            Console.WriteLine(item);
        }
    }

    static async IAsyncEnumerator<int> GetAsyncEnumerator()
    {
        yield return 0;
        await Task.Delay(1);
        yield return 1;
    }
}


مثال 2: اضافه کردن پشتیبانی از حلقه‌ی foreach بر روی نوع‌های tuple

مثال زیر را درنظر بگیرید:
class Program
{
    static void Main()
    {
        foreach (var item in (1, 2, 3))
        {
            Console.WriteLine(item);
        }
    }
}
در اینجا سعی کرده‌ایم تا حلقه‌ی foreach را بر روی یک tuple سه عضوی، اعمال کنیم. اما با خطای کامپایلر زیر مواجه می‌شویم:
foreach statement cannot operate on variables of type '(int, int, int)'
because '(int, int, int)' does not contain a public instance or extension definition
for 'GetEnumerator' [CS9Features]csharp(CS1579)
برای رفع این خطا در C# 9.0 تنها کافی است متد الحاقی GetEnumerator مخصوص نوع آن‌را طراحی و به برنامه اضافه کرد:
static class Extensions
{
    public static IEnumerator<object> GetEnumerator<T1, T2, T3>(this ValueTuple<T1, T2, T3> tuple)
    {
        yield return tuple.Item1;
        yield return tuple.Item2;
    }
}
نظرات مطالب
اعمال کنترل دسترسی پویا در پروژه‌های ASP.NET Core با استفاده از AuthorizationPolicyProvider سفارشی
با توجه به اینکه برای هر درخواست رسیده به اکشن متد تزئین شده با PermissionAuthorizeAttribute لازم است تا وهله سازی جدید از AuthorizationPolicy برای policyName دریافتی صورت پذیرد، شاید بهتر باشد برای بهبود کارایی سیستم، تغییرات زیر را اعمال کرد:
 public class AuthorizationPolicyProvider : DefaultAuthorizationPolicyProvider
    {
        private readonly LazyConcurrentDictionary<string, AuthorizationPolicy> _policies =
            new LazyConcurrentDictionary<string, AuthorizationPolicy>();

        public AuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
            : base(options)
        {
        }

        public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
        {
           //...
        }
    }
با توجه به مطلب «الگویی برای مدیریت دسترسی همزمان به ConcurrentDictionary»، مخزنی امن برای دسترسی همزمان به لیست سیاست‌های درخواست شده قبلی تحت عنوان ‎ _policies ایجاد شده است. سپس در بدنه متد GetPolicyAsync تغییرات زیر را اعمال کنید:
        public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
        {
          //...

            var policy = _policies.GetOrAdd(policyName, name =>
            {
                var permissionNames = policyName.Substring(PermissionAuthorizeAttribute.PolicyPrefix.Length).Split(',');

                return new AuthorizationPolicyBuilder()
                    .RequireClaim(CustomClaimTypes.Permission, permissionNames)
                    .Build();
            });

            return policy;
        }

مطالب
اضافه کردن Watermark به تصاویر یک برنامه ASP.NET MVC در صورت لینک شدن در سایتی دیگر
درگیر شدن با سایت‌های دیگر که چرا مطالب ما را کپی کرده‌اید نهایتا بجز فرسایش عصبی حاصل دیگری را به همراه ندارد. اساسا زمانیکه مطلبی را به صورت باز در اینترنت انتشار می‌دهید، قید کپی شدن یا نشدن آن‌را باید زد. اما ... می‌توان همین سایت‌ها را تبدیل به تبلیغ کننده‌های رایگان کار خود نمود که در ادامه نحوه انجام آن‌را در یک برنامه ASP.NET MVC بررسی خواهیم کرد:

الف) نیاز است ارائه تصاویر تحت کنترل برنامه باشند.

using System.IO;
using System.Net.Mime;
using System.Web.Mvc;

namespace MvcWatermark.Controllers
{
    public class HomeController : Controller
    {
        const int ADay = 86400;

        public ActionResult Index()
        {
            return View();
        }

        [OutputCache(VaryByParam = "fileName", Duration = ADay)]
        public ActionResult Image(string fileName)
        {
            fileName = Path.GetFileName(fileName); // تمیز سازی امنیتی است
            var rootPath = Server.MapPath("~/App_Data/Images");
            var path = Path.Combine(rootPath, fileName);
            if (!System.IO.File.Exists(path))
            {
                var notFoundImage = "notFound.png";
                path = Path.Combine(rootPath, notFoundImage);
                return File(path, MediaTypeNames.Image.Gif, notFoundImage);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
        }
    }
}
در اینجا یک کنترلر را مشاهده می‌کنید که در اکشن متد Image آن، نام یک فایل دریافت شده و سپس این نام در پوشه App_Data/Images جستجو گردیده و نهایتا در مرورگر کاربر Flush می‌شود. از آنجائیکه الزامی ندارد fileName، واقعا یک fileName صحیح باشد، نیاز است توسط متد استاندارد Path.GetFileName این نام دریافتی اندکی تمیز شده و سپس مورد استفاده قرار گیرد. همچنین جهت کاهش بار سرور، از یک OutputCache به مدت یک روز نیز استفاده گردیده است.
نحوه استفاده از این اکشن متد نیز به نحو زیر است:
<img src="@Url.Action(actionName: "Image", controllerName: "Home", routeValues: new { fileName = "EF_Stra_08.gif" })" />


ب) آیا فراخوان تصویر ما را مستقیما در سایت خودش قرار داده است؟

        private bool isEmbeddedIntoAnotherDomain
        {
            get
            {
                return this.HttpContext.Request.UrlReferrer != null &&
                       !this.HttpContext.Request.Url.Host.Equals(this.HttpContext.Request.UrlReferrer.Host,
                                                                   StringComparison.InvariantCultureIgnoreCase);
            }
        }
در ادامه توسط خاصیت سفارشی isEmbeddedIntoAnotherDomain درخواهیم یافت که درخواست رسیده، از دومین جاری صادر شده است یا خیر. اینکار توسط بررسی UrlReferrer ارسال شده توسط مرورگر صورت می‌گیرد. اگر Host این UrlReferrer با Host درخواست جاری یکی بود، یعنی تصویر از سایت خودمان فراخوانی شده‌است.


ج) افزودن خودکار Watermark در صورت کپی شدن در سایتی دیگر

        private byte[] addWaterMark(string filePath, string text)
        {
            var image = new WebImage(filePath);
            image.AddTextWatermark(text);
            return image.GetBytes();
        }
کلاسی در فضای نام System.Web.Helpers وجود دارد به نام WebImage که کار افزودن Watermark را بسیار ساده کرده است. نمونه‌ای از نحوه استفاده از آن‌را در متد فوق ملاحظه می‌کنید.
اما ... پس از امتحان تصاویر مختلف ممکن است گاها با خطای زیر مواجه شویم:
 A Graphics object cannot be created from an image that has an indexed pixel format.
مشکل از اینجا است که تصاویر با فرمت ذیل برای انجام کار Watermark پشتیبانی نمی‌شوند:
PixelFormatUndefined
PixelFormatDontCare
PixelFormat1bppIndexed
PixelFormat4bppIndexed
PixelFormat8bppIndexed
PixelFormat16bppGrayScale
PixelFormat16bppARGB1555
اما می‌توان تصویر دریافتی را ابتدا تبدیل به BMP کرد و سپس Watermark دار نمود:
        private byte[] addWaterMark(string filePath, string text)
        {
            using (var img = System.Drawing.Image.FromFile(filePath))
            {
                using (var memStream = new MemoryStream())
                {
                    using (var bitmap = new Bitmap(img))//avoid gdi+ errors
                    {
                        bitmap.Save(memStream, ImageFormat.Png);                        
                        var webImage = new WebImage(memStream);
                        webImage.AddTextWatermark(text, verticalAlign: "Top", horizontalAlign: "Left", fontColor: "Brown");
                        return webImage.GetBytes();
                    }
                }
            }
        }
در اینجا نمونه اصلاح شده متد addWaterMark فوق را بر اساس کار با تصاویر bmp و سپس تبدیل آن‌ها به png، ملاحظه می‌کنید. به این ترتیب دیگر به خطای یاد شده بر نخواهیم خورد.
در ادامه، قسمت آخر کار، اعمال این مراحل به اکشن متد Image است:
            if (isEmbeddedIntoAnotherDomain)
            {
                var text = Url.Action(actionName: "Index", controllerName: "Home", routeValues: null, protocol: "http");
                var content = addWaterMark(path, text);
                return File(content, MediaTypeNames.Image.Gif, fileName);
            }
            return File(path, MediaTypeNames.Image.Gif, fileName);
در اینجا اگر تشخیص داده شود که تصویر، در دومین دیگری لینک شده است، آدرس سایت ما به صورت خودکار در بالای تصویر درج خواهد شد.

کدهای نهایی این کنترلر را از اینجا می‌توانید دریافت کنید:
HomeController.cs
به همراه نمونه تصویری که استثنای یاد شده را تولید می‌کند؛ جهت آزمایش بیشتر:
EFStra08.gif
نظرات مطالب
Vue.js - بحث Event Handling - آشنایی و شیوۀ استفاده از رویدادها - قسمت پنجم
یک نکته‌ی تکمیلی: نحوه‌ی انتخاب یک المنت در Vue.js

در Vue.js  چندین روش برای انتخاب یک المنت وجود دارند که در ادامه آن‌ها را مورد بررسی قرار میدهیم.
 کد زیر را در نظر بگیرید، ما قصد داریم value موجود در input  را نمایش دهیم.

روش اول: با استفاده از جاوا اسکریپت
<!DOCTYPE html>

<head>
    <meta charset="UTF-8">
    <title>Vue.js گرفتن یک المنت در </title>
</head>

<body>
    <div id="main">
        <input type="text" name="fullName" id="fullName">
        <button @click='getFullNameValue()'>Show Me</button>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
        new Vue({
            el: '#main',
            data() {
                return {

                }
            },
            methods: {
                getFullNameValue() {
                    let fullNameValue = document.querySelector('#fullName').value;
                    alert(fullNameValue);
                }
            }
        });
    </script>
</body>

</html>
با استفاده از دستور زیر در جاوا اسکریپت براحتی میتوانیم یک المنت را انتخاب کنیم و آن را به یک متغیر انتساب دهیم.
 let fullNameValue = document.querySelector('#fullName')

روش دوم: انتخاب یک المنت با جی کوئری (با سلکتور دلخواه)
getFullNameValue() {
                    //let fullNameValue = document.querySelector('#fullName').value;
                    let fullNameValue = $('#fullName');
                    alert(fullNameValue.val());
}

روش سوم: انتخاب یک المنت در حیطه نمونه سازی شده  Vue.js
getFullNameValue() {
                    //let fullNameValue = document.querySelector('#fullName').value;
                    //let fullNameValue = $('#fullName').val();
                    let fullNameValue = this.$el.querySelector('#fullName');
                    alert(fullNameValue.value );
}

روش چهارم: انتخاب یک المنت از طریق امکان ref  در  Vue.js
<!DOCTYPE html>

<head>
    <meta charset="UTF-8">
    <title>Vue.js گرفتن یک المنت در </title>
</head>

<body>
    <div id="main">
        <input type="text" name="fullName" id="fullName" ref="refFullName">
        <button @click='getFullNameValue()'>Show Me</button>
    </div>
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue"></script>
    <script>
        new Vue({
            el: '#main',
            data() {
                return {

                }
            },
            methods: {
                getFullNameValue() {
                    //let fullNameValue = document.querySelector('#fullName').value;
                    //let fullNameValue = $('#fullName').val();
                    //let fullNameValue = this.$el.querySelector('#fullName').value;
                    let fullNameValue = this.$refs.refFullName;
                    alert(fullNameValue.value);
                }
            }
        });
    </script>
</body>

</html>

نتیجه گیری:
از لحاظ performance  روش سوم و چهارم بهینه می‌باشند؛ زیرا فقط در حیطه نمونه سازی شده  Vue.js  مربوطه جستجوی المنت را انجام میدهند. درحالیکه در روش اول و دوم کل DOM  مورد بررسی قرار میگیرد. روش چهارم بخاطر سهولت نوشتاری بهتر از سایر موارد می‌باشد.
چند نکته در مورد کد فوق:
A) در ES6 نیازی به نوشتن کلمه کلیدی function برای تعریف یک متد نیست.
 getFullNameValue: function() {
                  ...
                }
و میتوان بصورتی که در مثال کد استفاده شده، آن را خلاصه نمود.
B)  در ورژن جدید  Vue.js  بجای استفاده از دایرکتیو  v-on  ( کد زیر ) برای تعریف یک رویداد میتوان از  @ استفاه نمود. در مثال مورد استفاده‌ی مقاله از @ استفاده شده است.
  <button v-on:click='getFullNameValue()'>Show Me</button>

مطالب
ایجاد ابزارهای سراسری ویژه NET Core.
از زمان ارائه نگارش net core 2.1.، ابزارهای سراسری (Global tools) نیز معرفی شدند. استفاده از این ابزارها در محیط cli در جهت آسان‌تر شدن و سریعتر شدن وظایف، صورت می‌پذیرد. net core sdk. مربوطه، تمامی امکانات لازم از جهت ایجاد، حذف و به روزرسانی ابزارها را از طریق nuget شامل می‌گردد. تعداد بسیار زیادی از این ابزارها در حال حاضر ایجاد شده‌اند که در لیست زیر، تعدادی از آن‌ها را معرفی میکنیم و سپس به نحوه‌ی ایجاد این نوع ابزارها میپردازیم.
  • dotnet-ignore : این ابزار جهت دریافت فایل‌های gitignore. کاربرد داشته و از یک مخزن عمومی گیت هاب جهت دریافت این فایل‌ها استفاده میکند. این مخزن شامل انواع قالب‌های gitignore در پروژه‌های متفاوت میباشد. با استفاده از این ابزار، ایجاد فایل gitignore راحت‌تر و سریعتر امکانپذیر میباشد.
  • dotnet-serve : میزبانی و نمایش لیست فایل‌های استاتیک محلی و اجرای آن‌ها را در بستر http، فراهم مینماید.
  • dotnet-cleanup : جهت پاکسازی محیط بیلد مانند دایرکتوری‌های bin و obj میباشد. همان کار گزینه clean در منوی بیلد را بازی میکند.
  • dotnet-warp : این ابزار در واقع پروژه Warp است که برای ایجاد یک تک فایل اجرایی جهت انتقال راحت‌تر فایل پروژه صورت میگیرد که همه وابستگی‌های آن در همان تک فایل قرار میگیرد.
  • Amazon.ECS.Tools Amazon.ElasticBeanstalk.Tools  و  Amazon.Lambda.Tools  : این ابزارها که به صورت رسمی از طرف آمازون ارائه شده‌اند که جهت deploy شدن راحت‌تر پروژه به محیط‌های توسعه وب آمازون مورد استفاده قرار میگیرند.
جهت مشاهده لیست کامل این ابزارها، به این مخزن گیت هاب مراجعه نمایید. نام ابزار و همچنین لینک‌ها و توضیحات هر کدام، در این مخزن موجود است. همچنین جهت اضافه شدن ابزاری که در لیست نیست، از طریق ایجاد issue یا pull request لیست را به روزرسانی نمایید.

نحوه‌ی نصب، حذف و به روزرسانی ابزارهای سراسری
جهت نصب یک ابزار، از دستور زیر استفاده میکنیم:
dotnet tool install -g dotnet-ignore
سوییچ g به معنای نصب سراسری ابزار و افزوده شدن آن به متغیرهای محیطی PATH میباشد که به راحتی در هر مسیری از محیط کنسول در دسترس خواهد بود و به مسیر dotnet/tools/. محدود نخواهد بود.
جهت مشاهده لیست تمامی ابزاهای سراسری نصب شده بر روی سیستم میتوانید از کامند زیر استفاده نمایید:
dotnet tool list -g
نحوه به روزرسانی ابزار و ارتقا آن به آخرین نسخه پایدار، با دستور زیر میباشد:
dotnet tool update -g dotnet-ignore
دستور حذف:
dotnet tool uninstall -g dotnet-ignore

ایجاد یک ابزار سراسری
جهت ساخت یک ابزار سراسری نیاز است تا یک پروژه را از نوع کنسول ایجاد نمایید و سپس به فایل csproj، خطوط زیر را اضافه کنید:
<PropertyGroup>
    <PackAsTool>true</PackAsTool>
    <ToolCommandName>dotnet-mytool</ToolCommandName>
    <PackageOutputPath>./nupkg</PackageOutputPath>
</PropertyGroup>
گزینه PackAsTool، امکان تبدیل فایل اجرایی شما را به یک ابزار سراسری فراهم میکند. دو گزینه بعدی که اختیاری است، به ترتیب شامل نام ابزار سراسری است که در صورت ذکر نشدن نام فایل پروژه، بدون پسوند csproj. میباشد و سومین مورد نیز مسیر قرارگیری فایل ابزار سراسری به عنوان یک بسته nuget میباشد.
جهت ساخته شدن فایل، ابتدا یکبار پروژه را بیلد کرده و پس از اجرای دستور dotnet pack، فایل پکیج در مسیر ذکر شده ساخته میشود و آماده انتقال به مخازن nuget میباشد. جهت تست و اجرای ابزار بر روی سیستم خود قبل از عرضه نهایی نیاز است تا با دستور زیر آن را بر روی سیستم خود نصب و آزمایش نمایید:
dotnet tool install --global --add-source ./nupkg globaltools
سوییچ global که در بالاتر نیز توضیح داده شد، باعث نصب سراسری ابزار میگردد و سوییچ add-source که بعد از آن مسیر فایل ابزار، آمده است، به این معنا است که به صورت موقت، این دایرکتوری یا مسیر را به عنوان مخزن nuget  شناسایی کرده تا امکان یافتن بسته در آن مسیر مهیا گردد و سپس نام پروژه در پایان ذکر میگردد. در آخر جهت اطمینان از نصب میتوانید ابزار را صدا بزنید:
dotnet-mytool
با توجه به اینکه اصل مطلب گفته در رابطه با ایجاد یک ابزار سراسری در اینجا به پایان میرسد، ولی ایجاد یک ابزار خط فرمانی نیازمند یک سری کدنویسی‌ها جهت ایجاد کامندها و سوییچ‌ها و راهنمای مربوط به آن نیز میباشد. بدین جهت کتابخانه زیر را نصب نمایید:
https://www.nuget.org/packages/McMaster.Extensions.CommandLineUtils
این کتابخانه شامل کلاس هایی جهت ایجاد یک ابزار خط فرمانی راحت‌تر میباشد.

ایجاد یک ابزار عمومی جهت یادداشت نویسی
برای استفاده از این کتابخانه، یک پروژه از نوع کنسول را با نام globaltools ایجاد نمایید و کتابخانه‌ی بالا را نصب نمایید. سپس به ازای هر کامند، یک کلاس را ایجاد میکنیم. ابتدا جهت ایجاد کامندی با نام NewNote یک کلاس را به همین نام میسازیم:
[Command(Description="Add a new note")]
    public class NewNote
    {
        [Required]
        [Option(Description="title of note")]
        public string Title{ get; set; }

        [Option(Description="content of note")]
        public string Body{ get; set; }
    }
با مزین کردن کلاس به ویژگی command، این کلاس را یک کامند معرفی کرده و شرحی از کاری که این کامند را انجام میدهد، نیز وارد می‌کنیم. این شرح بعدا در ابزار تولید شده به عنوان متن راهنما به کار می‌رود. سپس پراپرتی‌هایی را که با ویژگی option مزین گشته‌اند، به عنوان سوییچ معرفی میکنیم. همچنین میتوان از DataAnotation‌ها نیز جهت اعتبار سنجی نیز استفاده نمود. 
بعد از ایجاد موارد بالا، نیاز است که اکشنی که باید این کامند را اجرا کند، به آن اضافه کرد. جهت افزودن این اکشن، یک متد را با نام OnExecute، به بدنه این کلاس اضافه می‌کنیم:
[Command(Description="Add a new note")]
    public class NewNote:BaseClass
    {
        [Required]
        [Option(Description="title of note")]
        public string Title{ get; set; }

        [Option(Description="content of note")]
        public string Body{ get; set; }

        public void OnExecute(IConsole console)
        {

            var dir = GetBaseDirectory();
            if(!Directory.Exists(dir))
            {
                Directory.CreateDirectory(dir);
            }
            var filePath = Path.Combine(dir, Title + ".txt");
            File.WriteAllText(filePath, Body);
            console.WriteLine("the note is saved");
        }
    }
در پارامتر این متد، یک اینترفیس با نام IConsole جهت ارتباط با محیط کنسول دیده میشود که در پایان عملیات، پیام «یادداشت ذخیره شد» توسط آن چاپ میگردد. کار این متد به طور خلاصه این است که مسیر اجرایی ابزار جاری را دریافت کرده و سپس در یک دایرکتوری با نام notes، برای هر یادداشت یک فایل ایجاد شده و محتوای دریافتی از کاربر داخل آن قرار میگرد و نام هر فایل، موضوع یادداشتی است که کاربر وارد کرده‌است. متد GetBaseDirectory که مسیر ذخیره یادداشت‌ها را بر میگرداند، در کلاس BaseClass با محتوای زیر قرار گرفته است:
public class BaseClass
    {
        protected string GetBaseDirectory(){
            var baseDirectory = Environment.CurrentDirectory;
            return (Path.Combine(baseDirectory, "notes"));
        }
    }

کامند بعدی، لیست یادداشت‌های ثبت شده‌است:
public class List:BaseClass
    {
        [Option(Description="search a phrase in notes title")]
        public string Grep{ get; set; }
        public void OnExecute(IConsole console)
        {
            try
            {
                var baseDirectory = GetBaseDirectory();

                var dir = new DirectoryInfo(baseDirectory);
                var files = dir.GetFiles();
                foreach(var file in files)
                {
                    if(!String.IsNullOrEmpty(Grep) && !file.Name.Contains(Grep))
                        continue;

                    console.WriteLine(Path.GetFileNameWithoutExtension(file.Name));
                }
            }
            catch (Exception e)
            {
                console.WriteLine(e.Message);
            }
        }
    }
کار این کلاس، بازگردانی لیستی از یادداشت‌های ثبت شده است که حاوی سوییچ grep برای فیلتر کردن اسامی یادداشت هاست.
کلاس بعدی show نیز جهت نمایش کلاس بر اساس عنوان یادداشت است:
[Command(Description="show contnet of note")]
    public class Show:BaseClass
    {
        [Required]
        [Option(Description="title of note")]
        public string Title{ get; set; }


        public void OnExecute(IConsole console){
            var baseDirectory = GetBaseDirectory();
            var file = Path.Combine(baseDirectory, Title+".txt");

            if(!File.Exists(file))
            {
                console.WriteLine("The Note NotFound...");
                return;
            }
            console.WriteLine(File.ReadAllText(file));

        }
    }
در صورتیکه یادداشت مورد نظر وجود نداشته باشد، با پیام The Note NotFound کار به پایان میرسد.
بعد از اتمام کامندهای مربوطه، به کلاس program رفته و برای آن نیز ویژگی command را اضافه می‌کنیم و همچنین ویژگی subCommand را جهت معرفی کامندهایی که در برنامه در دسترس کاربر قرار میگیرند، اضافه میکنیم:
    [Command(Description="An Immediate Note Saver")]
    [Subcommand(typeof(NewNote),typeof(List),typeof(Show))]
    class Program
    {
        static int Main(string[] args)
        {
            return CommandLineApplication.Execute<Program>(args);
        }

        public int OnExecute(CommandLineApplication app, IConsole console)
        {
            console.WriteLine("You must specify a subcommand.");
            console.WriteLine();
            app.ShowHelp();
            return 1;
        }
    }
از آنجا که کلاس Program نیز به ویژگی command مزین شده‌است، متد OnExecute را اضافه می‌کنیم. تنها تفاوت این متد با متدهای قبلی، در نوع خروجی آن است که هر مقدار غیر از صفر، به منزله خطا میباشد. در این حالت چون کاربر کامندی را صادر نکرده است، ابتدا به کاربر اجباری بودن کامند را گوشزد کرده و سپس از طریق متد ShowHelp، راهنمای کار با ابزار را به او نشان داده و سپس کد یک را به منزله رخ دادن خطا یا اعلام شرایط غیرعادی بازمیگردانیم. نوع خروجی متد OnExecute در صورتی که void باشد، به معنای مقدار 0 میباشد که در کلاس‌های قبلی از آن استفاده کرده‌ایم.
در نهایت متد Main را نیز به شکل زیر تغییر می‌دهیم:
        static int Main(string[] args)
        {
            return CommandLineApplication.Execute<Program>(args);
        }
تکه کد CommandLineApplication.Execute آرگومان‌های ورودی را دریافت کرده و کامند مورد نظر را شناسایی میکند و همچنین مقدار عددی که از آن جهت return شدن استفاده می‌کند، همان عددهای صفر و غیر صفر میباشد که در بالا توضیح داده شده است.

نمونه استفاده از ابزار نهایی
PS D:\projects\Samples\globaltools> dotnet-notes new-note -t "sample1" -b "this is body"
the note is saved
PS D:\projects\Samples\globaltools> dotnet-notes new-note -t "test1" -b "this is body of another note"
the note is saved
PS D:\projects\Samples\globaltools> dotnet-notes list
sample1
test1
PS D:\projects\Samples\globaltools> dotnet-notes list -g sa
sample1
PS D:\projects\Samples\globaltools> dotnet-notes show -t sample1
this is body
در ابزار بالا کامند new-note به صورت جدا از هم با خط تیره مشخص شده‌است. دلیل این امر نیز جداشدن این کلمات در نام کلاس با حروف بزرگ است. در صورتیکه قصد ندارید نام کامندها با خط تیره از هم جدا شوند، باید نام کلاس را از NewNote به Newnote تغییر دهید.
مطالب
ASP.NET MVC #4

بررسی نحوه ارتباطات بین اجزای مختلف الگوی MVC در ASP.NET MVC

اینبار برخلاف قسمت قبل، قالب پروژه خالی ASP.NET MVC را در VS.NET انتخاب کرده (ASP.NET MVC 3 Web Application و بعد انتخاب قالب Empty نمایش داده شده) و سپس پروژه جدیدی را شروع می‌کنیم. View Engine را هم Razor در نظر خواهیم گرفت.
پس از ایجاد ساختار اولیه پروژه، بدون اعمال هیچ تغییری، برنامه را اجرا کنید. بلافاصله با پیغام The resource cannot be found یا 404 یافت نشد، مواجه خواهیم شد.
همانطور که در پایان قسمت دوم نیز ذکر شد، پردازش‌ها در ASP.NET MVC از کنترلرها شروع می‌شوند و نه از صفحات وب. بنابراین برای رفع این مشکل نیاز است تا یک کلاس کنترلر جدید را اضافه کنیم. به همین جهت بر روی پوشه استاندارد Controllers کلیک راست کرده، از منوی ظاهر شده قسمت Add، گزینه‌ی Controller را انتخاب کنید:


در صفحه بعدی که ظاهر می‌شود، نام HomeController را وارد کنید (با توجه به اینکه مطابق قراردادهای ASP.NET MVC، نام کنترلر باید به کلمه Controller ختم شود). البته لازم به ذکر است که این مراحل را به همان شکل متداول مراجعه به منوی Project و انتخاب Add Class و سپس ارث بری از کلاس Controller نیز می‌توان انجام داد و طی این مراحل الزامی نیست. کلاسی که به صورت خودکار از طریق منوی Add Controller یاد شده ایجاد می‌شود، به شکل زیر است:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace MvcApplication1.Controllers
{
public class HomeController : Controller
{
//
// GET: /Home/
public ActionResult Index()
{
return View();
}
}
}

سؤال:
در قسمت دوم عنوان شد که کنترلر باید کلاسی باشد که اینترفیس IController را پیاده سازی کرده است، اما در اینجا ارث بری از کلاس Controller را شاهد هستیم. جریان چیست؟!
سلسله مراتبی که بکارگرفته شده به صورت زیر است:
public abstract class ControllerBase : IController
public abstract class Controller : ControllerBase

ControllerBase، اینترفیس IController را پیاده سازی کرده و سپس کلاس Controller از کلاس ControllerBase مشتق شده است. شاید بپرسید که این همه پیچ و تاب برای چیست؟!
مشکلی که با اینترفیس خالص وجود دارد، عدم نگارش پذیری آن است. به این معنا که اگر متدی یا خاصیتی در نگارش بعدی به این اینترفیس اضافه شد، هیچکدام از پروژه‌های قدیمی دیگر کامپایل نخواهند شد و باید ابتدا این متد یا خاصیت جدید را نیز لحاظ کنند. اینجا است که کار کلاس‌های abstract شروع می‌شود. در یک کلاس abstract می‌توان پیاده سازی پیش فرضی را نیز ارائه داد. به این ترتیب مصرف کننده نهایی کلاس Controller متوجه این تغییرات نخواهد شد.
اگر برنامه نویس «من» باشم، شما رو وادار خواهم کرد که متدهای جدید اینترفیس تعریفی‌ام را پیاده سازی کنید! همینه که هست! اما اگر طراح مایکروسافت باشد، بلافاصله انبوهی از جماعت ایرادگیر که بالای 100 تا از کنترلر‌های اون‌ها الان فقط در یک پروژه از کار افتاده،‌ ممکن است جلوی دفتر مایکروسافت دست به خود سوزی بزنند! اینجا است که مایکروسافت مجبور است تا این پیچ و تاب‌ها را اعمال کند که اگر روزی متدی در اینترفیس IController بنابر نیازهای جدید درنظر گرفته شد، بتوان سریع یک پیاده سازی پیش فرض از آن‌را در کلاس‌های abstract یاد شده قرار داد (یکی از تفاوت‌های مهم کلاس‌های abstract با اینترفیس‌ها) تا جماعت ایرادگیر و نق‌زن متوجه تغییری نشوند و باز هم پروژه‌های قدیمی بدون مشکل کامپایل شوند. تابحال به فلسفه وجودی کلاس‌های abstract از این دیدگاه فکر کرده بودید؟!

بعد از این توضیحات و کارها، اگر اینبار برنامه را اجرا کنیم، خطای زیر نمایش داده می‌شود:

The view 'Index' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Views/Home/Index.aspx
~/Views/Home/Index.ascx
~/Views/Shared/Index.aspx
~/Views/Shared/Index.ascx
~/Views/Home/Index.cshtml
~/Views/Home/Index.vbhtml
~/Views/Shared/Index.cshtml
~/Views/Shared/Index.vbhtml

همانطور که ملاحظه می‌کنید چون نام کنترلر ما Home است، فریم ورک در پوشه استاندارد Views در زیر پوشه‌ای به همان نام Home، به دنبال یک سری فایل می‌گردد. فایل‌های aspx مربوط به View Engine ایی به همین نام بوده و فایل‌های cshtml و vbhtml مربوط به View Engine دیگری به نام Razor هستند.
بنابراین نیاز است تا یکی از این فایل‌ها را در مکان‌های یاد شده ایجاد کنیم. برای این منظور حداقل دو راه وجود دارد. یا دستی اینکار را انجام دهیم یا اینکه از ابزار توکار خود VS.NET برای ایجاد یک View جدید استفاده کنیم.
برای ایجاد View ایی مرتبط با متد Index (در ASP.NET MVC نام دیگر متدهای قرار گرفته در یک کنترلر، Action نیز می‌باشد)، روی خود متد کلیک راست کرده و گزینه Add View را انتخاب کنید:


در صفحه بعدی ظاهر شده، پیش فرض‌ها را پذیرفته و بر روی دکمه Add کلیک نمائید. اتفاقی که رخ خواهد داد شامل ایجاد فایل Index.cshtml، با محتوای زیر است (با توجه به اینکه زبان پروژه سی شارپ است و View Engine انتخابی Razor می‌باشد، cshtml تولید گردید و گرنه vbhtml ایجاد می‌شد):

@{
ViewBag.Title = "Index";
}
<h2>Index</h2>

مجددا برنامه را اجرا کنید. اینبار بدون خطایی کلمه Index را در صفحه تولیدی می‌توان مشاهده کرد. نکته جالب این فایل‌های View جدید، عدم مشاهده ویژگی‌های runat=server و سایر موارد مشابه است.

چند سؤال مهم:
در حین ایجاد اولین کنترلر جهت نمایش صفحه پیش فرض برنامه، نام HomeController انتخاب شد. چرا مثلا نام TestController وارد نشد؟ برنامه از کجا متوجه شد که باید حتما این کنترلر را پردازش کند. نقش متد Index چیست؟ آیا حتما باید Index باشد و در اینجا نام دیگری را نمی‌توان وارد کرد؟ «قرارداد» پردازشی این‌ها کجا تنظیم می‌شود؟ فریم ورک، این سیم کشی‌ها را چگونه انجام می‌دهد؟
پاسخ به تمام این سؤال‌ها، در ویژگی «مسیریابی یا Routing» نهفته است. فایل Global.asax.cs برنامه را باز کنید. تعاریف مرتبط با مسیریابی پیش فرض را در متد RegisterRoutes آن می‌توان مشاهده کرد:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}



پروسه هدایت یک درخواست HTTP به یک کنترلر، در اینجا مسیریابی یا Routing نامیده می‌شود. این قابلیت در فضای نام System.Web.Routing تعریف شده است و باید دقت داشت که جزو ASP.NET MVC نیست. این امکانات جزو ASP.NET Runtime است و به همراه دات نت 3.5 سرویس پک یک برای اولین بار ارائه شد. بنابراین جهت استفاده در ASP.NET Web forms نیز مهیا است. در ASP.NET MVC از این امکانات برای ارسال درخواست‌ها به کنترلرها استفاده می‌شود.
در متد routes.MapRoute فراخوانی شده‌ای که در کدهای بالا ملاحظه می‌کنید، کار نگاشت یک URL به Actionهای یک کنترلر صورت می‌گیرد (یا همان متدهای تعریف شده در کنترلرها). همچنین از این URLها پارامترهای این متدها یا اکشن‌ها نیز قابل استخراج است.
در متد MapRoute، اولین پارامتر تعریف شده، یک نام پیش فرض است و در ادامه اگر آدرسی را به فرم «یک چیزی اسلش یک چیزی اسلش یک چیزی» یافت، اولین قسمت آن‌را به عنوان نام کنترلر تفسیر خواهد کرد، دومین قسمت آن، نام متد عمومی موجود در کنترلر فرض شده و سومین قسمت به عنوان پارامتر ارسالی به این متد پردازش می‌شود.
برای مثال از آدرس زیر اینطور می‌توان دریافت که:
http://hostname/home/about

Home نام کنترلی است که فریم ورک به دنبال آن خواهد گشت تا این درخواست رسیده را پردازش کند و about نام متدی عمومی در این کلاس است که به صورت خودکار فراخوانی می‌گردد. در اینجا پارامتر id ایی هم وجود ندارد. در یک برنامه امکان تعریف چندین و چند مسیریابی وجود دارد.
پارامتر سوم متد routes.MapRoute، یک سری پیش فرض را تعریف می‌کند و این مورد همانجایی است که از اطلاعات آن جهت تعریف کنترلر پیش فرض استفاده کردیم. برای مثال به چهار آدرس زیر دقت نمائید:

http://localhost/
http://localhost/home
http://localhost/home/about
http://localhost/process/list

در حالت http://localhost/،‌ هر سه مقدار پیش فرض مورد استفاده قرار خواهند گرفت چون سه جزئی ({controller}/{action}/{id}) که موتور مسیریابی به دنبال آن‌ها می‌گردد، در این آدرس وجود خارجی ندارد. بنابراین نام کنترلر پیش فرض در این حالت همان Home مشخص شده در پارامتر سوم متد routes.MapRoute خواهد بود و متد پیش فرض نیز Index و پارامتری هم به آن ارسال نخواهد شد.
در حالت http://localhost/home نام کنترلر مشخص است اما دو جزء دیگر ذکر نشده‌اند، بنابراین مقادیر آن‌ها از پیش فرض‌های صریح ذکر شده در متد routes.MapRoute گرفته می‌شود. یعنی نام متد اکشن مورد پردازش، Index خواهد بود به همراه هیچ آرگومان خاصی.
به علاوه باید خاطرنشان کرد که این مقادیر case sensitive یا حساس به بزرگی و کوچکی حروف نیستند. بنابراین مهم نیست که http://localhost/HoMe باشد یا http://localhost/HOMe یا هر ترکیب دیگری.
یا اگر آدرس رسیده http://localhost/process/list باشد، این مسیریابی پیش فرض تعریف شده قادر به پردازش آن می‌باشد. به این معنا که کنترلری به نام Process وهله سازی و سپس متدی به نام List در آن فراخوانی خواهند شد.

یک نکته:
ترتیب routes.MapRouteهای تعریف شده در اینجا مهم است و اگر اولین URL رسیده با الگوی تعریف شده مطابقت داشت، کار را تمام خواهد کرد و به سایر تعاریف نخواهد رسید. مثلا اگر در اینجا یک مسیریابی دیگر را به نام Process تعریف کنیم:

public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

routes.MapRoute(
"Process", // Route name
"Process/{action}/{id}", // URL with parameters
new { controller = "Process", action = "List", id = UrlParameter.Optional } // Parameter defaults
);

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);
}

آدرسی به فرم http://localhost/Process، به صورت خودکار به کنترلر Process و متد عمومی List آن نگاشت خواهد شد و کار به مسیریابی بعدی نخواهد رسید.


مطالب
بهینه سازی سرعت یافت ویوها با سفارشی سازی Lookup Caching در Razor View Engine
 در این مقاله سعی داریم تا سرعت یافت و جستجوی View‌های متناظر با هر اکشن را در View Engine، با پیاده سازی قابلیت Caching نتیجه یافت آدرس فیزیکی view‌ها در درخواست‌های متوالی، افزایش دهیم تا عملا بازده سیستم را تا حدودی بهبود ببخشیم.

طی مطالعاتی که بنده بر روی سورس MVC داشتم، به صورت پیش فرض، در زمانیکه پروژه در حالت Release اجرا می‌شود، نتیجه حاصل از یافت آدرس فیزیکی ویو‌های متناظر با اکشن متدها در Application cache ذخیره می‌شود (HttpContext.Cache). این امر سبب اجتناب از عمل یافت چند باره بر روی آدرس فیزیکی ویو‌ها در درخواست‌های متوالی ارسال شده برای رندر یک ویو خواهد شد.

 نکته ای که وجود دارد این هست که علاوه بر مفید بودن این امر و بهبود سرعت در درخواست‌های متوالی برای اکشن متد‌ها، این عمل با توجه به مشاهدات بنده از سورس MVC علاوه بر مفید بودن، تا حدودی هزینه بر هم هست و هزینه‌ای که متوجه سیستم می‌شود شامل مسائل مدیریت توکار حافظه کش توسط MVC است که مسائلی مانند سیاستهای مدیریت زمان انقضاء مداخل موجود در حافظه‌ی کش اختصاص داده شده به Lookup Cahching و  مدیریت مسائل thread-safe و ... را شامل می‌شود.

همانطور که می‌دانید، معمولا تعداد ویو‌ها اینقدر زیاد نیست که Caching نتایج یافت مسیر فیزیکی view ها، حجم زیادی از حافظه Ram را اشغال کند پس با این وجود به نظر می‌رسد که اشغال کردن این میزان اندک از حافظه در مقابل بهبود سرعت، قابل چشم پوشی است و سیاست‌های توکار نامبرده فقط عملا تاثیر منفی در روند Lookup Caching پیشفرض MVC خواهند گذاشت. برای جلوگیری از تاثیرات منفی سیاست‌های نامبرده و عملا بهبود سرعت Caching نتایج Lookup آدرس فیزیکی ویو‌ها میتوانیم یک لایه Caching سطح بالاتر به View Engine اضافه کنیم .

خوشبختانه تمامی View Engine‌های MVC شامل Web Forms  و Razor از کلاس VirtualPathProviderViewEngine مشتق شده‌اند که نکته مثبت که توسعه Caching اختصاصی نامبرده را برای ما مقدور می‌کند. در اینجا خاصیت ( Property ) قابل تنظیم ViewLocationCache از نوع IViewLocationCache هست .

بنابراین ما یک کلاس جدید ایجاد کرده و از اینترفیس IViewLocationCache مشتق میکنیم تا به صورت دلخواه بتوانیم اعضای این اینترفیس را پیاده سازی کنیم .

خوب؛ بنابر این اوصاف، من کلاس یاد شده را به شکل زیر پیاده سازی کردم :
    public class CustomViewCache : IViewLocationCache
    {

        private readonly static string s_key = "_customLookupCach" + Guid.NewGuid().ToString();
        private readonly IViewLocationCache _cache;

        public CustomViewCache(IViewLocationCache cache)
        {
            _cache = cache;
        }

        private static IDictionary<string, string> GetRequestCache(HttpContextBase httpContext)
        {
            var d = httpContext.Cache[s_key] as IDictionary<string, string>;
            if (d == null)
            {
                d = new Dictionary<string, string>();
                httpContext.Cache.Insert(s_key, d, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, 15, 0));
            }
            return d;
        }

        public string GetViewLocation(HttpContextBase httpContext, string key)
        {
            var d = GetRequestCache(httpContext);
            string location;
            if (!d.TryGetValue(key, out location))
            {
                location = _cache.GetViewLocation(httpContext, key);
                d[key] = location;
            }
            return location;
        }

        public void InsertViewLocation(HttpContextBase httpContext, string key, string virtualPath)
        {
            _cache.InsertViewLocation(httpContext, key, virtualPath);
        }
    }
و به صورت زیر می‌توانید از آن استفاده کنید:
 protected void Application_Start() {
    ViewEngines.Engines.Clear();
    var ve = new RazorViewEngine();
    ve.ViewLocationCache = new CustomViewCache(ve.ViewLocationCache);
    ViewEngines.Engines.Add(ve);
    ... 
}

نکته: فقط به یاد داشته باشید که اگر View جدیدی اضافه کردید یا یک View را حذف کردید، برای جلوگیری از بروز مشکل، حتما و حتما اگر پروژه در مراحل توسعه بر روی IIS قرار دارد app domain را ری‌استارت کنید تا حافظه کش مربوط به یافت‌ها پاک شود (و به روز رسانی) تا عدم وجود آدرس فیزیکی View جدید در کش، شما را دچار مشکل نکند.
مطالب
کارهایی جهت بالابردن کارآیی Entity Framework #3

در قسمت‌های قبلی (^ و ^) راهکارهایی جهت بالا بردن کارآیی، ارائه شد. در ادامه، به آخرین قسمت این سری اشاره خواهم کرد.

فراخوانی متد شناسایی تغییرات

یادآوری: قبل از هر چیز با توجه به این مقاله دانستن این نکته الزامی است که فراخوانی برخی متدها مانند DbSet.Add سبب فراخوانی DataContext.ChangeTracker.DetectChanges خواهند شد.

فرض کنید قصد افزودن 2000 موجودیت دانش آموز را دارید:

for (int i = 0; i < 2000; i++)
{
    Pupil pupil = GetNewPupil();
    db.Pupils.Add(pupil);
}
db.SaveChanges();
در کد بالا بدلیل فراخوانی متد DbSet.Add و به دنبال آن فراخوانی متد DetectChanges در هر بار اجرای حلقه (2000بار) مدت زمان اجرای کد بالا بسیار زیاد است و اگر به پروفایلر نگاهی بیاندازید، اشغال CPU توسط کوئری بالا، بیش از حد متعارف است.

اگر به تصویر بالا دقت کنید بیش از 34 ثانیه (خط 193 قسمت سوم شکل) جهت افزودن 2000 موجودیت به کانتکست سپری شده است. در حالی که درج این 2000 موجودیت کمی بیش از 1 ثانیه (خط 195 قسمت سوم شکل) که 379 میلی ثانیه (قسمت دوم شکل) آن مربوط به اجرای کوئری اختصاص یافته  طول کشیده است.
بیشترین زمان صرف شده‌ی برای درج 2000 موجودیت، در کد برنامه سپری شده است که با بررسی بیشتر در پروفایلر، متوجه زمان بر بودن فراخوانی متد ()DetectChanges که در فضای نام Data.Entity.Core وجود دارد خواهید شد. این متد 2000 بار به تعداد موجودیت هایی که قصد داریم به بانک اطلاعاتی اضافه نماییم، فراخوانی شده است.

همانطور که در شکل بالا مشخص است همان 34 ثانیه جهت ردیابی تغییرات صرف شده است. EF ردیابی تغییرات را بصورت پیش فرض هر زمانی که قصد افزودن یا ویرایش موجودیتی را داشته باشید، انجام خواهد داد و هر چه موجودیت‌های بیشتری را بخواهید ویرایش یا اضافه نمایید، این زمان نیز بیشتر خواهد شد. در حقیقت زمان لازم برای الگوریتم ردیابی تغییرات بصورت نمایی با رشد موجودیت‌ها افزوده می‌شود. به عبارت دیگر اگر این تعداد موجودیت‌ها را به 4000 عدد برسانید، مدت زمان لازم بیش از 2 برابر افزوده خواهد شد.

راه حل اول:
استفاده از متد ()AddRange ارائه شده در EF6 که جهت درج دسته‌ای (Bulk Insert) ارائه شده است:
var list = new List<Pupil>();

for (int i = 0; i < 2000; i++)
{
    Pupil pupil = GetNewPupil();
    list.Add(pupil);
}

db.Pupils.AddRange(list);
db.SaveChanges();

راه حل دوم:

در سناریوهای پیچیده، مانند درج دسته‌ای چندین موجودیت، شاید مجبور به خاموش نمودن این قابلیت شوید:
db.Configuration.AutoDetectChangesEnabled = false;
توجه داشته باشید اگر قصد دارید از امکان ردیابی تغییرات مجددا بهره‌مند شوید، باید این قابلیت را نیز فعال نمایید. با خاموش نمودن ردیابی تغییرات بار دیگر کوئری ابتدایی را اجرا نمایید. مدت زمان لازم جهت افزودن 2000 موجودیت به کانتکست از بیش از 34 ثانیه به 85 میلی ثانیه کاهش یافته است؛ ولی اعمال تغییرات به بانک اطلاعاتی همانند مرتبه اول بیش از 1 ثانیه طول خواهد کشید.


ردیابی تغییرات

هنگامی که موجودیتی را از بانک اطلاعاتی دریافت نمایید، می‌توانید آن را ویرایش نمایید و مجددا به بانک اطلاعاتی اعمال نمایید. چون EF اطلاعی از قصد شما برای موجودیت نمی‌داند، مجبور است تغییرات شما را زیر نظر بگیرد که این زیر نظر گرفتن، هزینه و سربار دارد و این سربار و هزینه برای داده‌های زیاد، بیشتر خواهد شد. بنابراین اگر قصد دارید اطلاعاتی فقط خواندنی را از بانک اطلاعاتی دریافت نمایید، بهتر است صراحتا به EF بگویید این موجودیت را تحت ردیابی قرار ندهد.
string city = "New York";

List<School> schools = db.Schools
    .AsNoTracking()
    .Where(s => s.City == city)
    .Take(100)
    .ToList();

استفاده از متد AsNoTracking  در کد بالا سبب خواهد شد 100 مدرسه که در شهر نیویورک وجود دارد توسط EF، بدون تحت نظر گرفتن آن‌ها از بانک اطلاعاتی دریافت شوند و سرباری نیز تحمیل نشود.

ویوهای از قبل کامپایل شده

معمولا، هنگامی که از EF برای اولین بار استفاده می‌نمایید، ویوهایی ایجاد می‌گردد که برای ایجاد کوئری‌ها مورد استفاده قرار می‌گیرند. این ویوها در طول حیات برنامه فقط یکبار ایجاد می‌شوند. ولی همین یکبار هم زمانبر هستند. خوشبختانه راه‌هایی وجود دارد که ایجاد این ویوها را در زمان runtime انجام نداد و آن راه، استفاده از ویوهای از پیش کامپایل شده است. یکی از راههای ایجاد این ویوها استفاده از Entity Framework Power Tools است. بعد از نصب اکستنشن، بر روی فایل کانتکست راست کلیک کرده و سپس گزینه‌ی Generate Views را از منوی Entity Framework انتخاب کنید.

توجه داشته باشید که هر تغییری را بعد از ایجاد این ویوها بر روی کانتکست اعمال نمایید، باید آن‌ها را مجددا تولید کنید. برای آشنایی بیشتر با این ویوها به این لینک مراجعه کنید. هم چنین پکیج نیوگتی بنام EFInteractiveViews نیز برای این منظور تهیه و توزیع شده است.


حذف کوئری‌های ابتدایی غیر ضروری

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

با توجه به توضیحات، در صورتیکه اطلاعی از نسخه‌ی دیتابیس دارید، می‌توانید این کوئری ابتدایی را تحریف نمایید. برای اینکار می‌توان توسط متد ()ResolveManifestToken کلاسی که اینترفیس IManifestTokenResolver را پیاده سازی کرده است، نسخه‌ی دیتابیس را برگردانیم و از یک رفت و برگشت به دیتابیس جلوگیری نماییم.

 public class CustomManifestTokenResolver : IManifestTokenResolver
{
    public string ResolveManifestToken(DbConnection connection)
    {
        return "2014";
    }
}
و توسط کلاسی که از کلاس DbConfiguration ارث بری کرده است آن را استفاده نماییم.
 public class CustomDbConfiguration : DbConfiguration
{
    public CustomDbConfiguration()
    {
        SetManifestTokenResolver(new CustomManifestTokenResolver());
    }
}


تخریب کانتکست

تخریب و از بین بردن کانتکست هنگامی که به آن نیاز نداریم بسیار ضروری است. یکی از روشهای اصولی برای Disposing کانتکست، محصور کردن آن بوسیله دستور Using است (البته فرض بر این است که قرار نیست از الگوهای اشاره شده استفاده نماییم). در صورت عدم تخریب صحیح کانتکست باید منتظر آسیب جدی به کارایی Garbage Collector جهت آزاد سازی منابع مورد استفاده کانتکست و هم چنین باز نمودن اتصالات جدید به دیتابیس باشید.


پاسخگویی به چندین درخواست بر روی یک کانکشن

EF از قابلیتی بنام Multiple Result Sets می‌تواند بهره ببرد که این قابلیت باعث می‌شود بر روی یک کانکشن ایجاد شده، یک یا چند درخواست از دیتابیس ارسال و یا دریافت شود که سبب کاهش تعداد رفت و برگشت به دیتابیس می‌شود. کاربرد دیگر این قابلیت، زمانی است که تاخیر زیادی (latency) بین اپلیکیشن و دیتابیس وجود دارد.

برای فعالسازی کافی است مقدار زیر را در کانکشن استرینگ اضافه نمایید:

MultipleActiveResultSets=True;


استفاده از متدهای ناهمگام

در C#5 و EF6 پشتیبانی خوبی از متدهای ناهمگام فراهم شده است و اکثر متدهایی مانند ToListAsync, CountAsync, FirstAsync, SaveChangesAsync و غیره که باعث رفت و برگشت به دیتابیس می‌شوند امکان پشتیبانی ناهمگام را نیز دارند. البته این قابلیت برای برنامه‌های یک درخواست در یک زمان شاید مفید نباشد؛ ولی برای برنامه‌های وبی برعکس. در برنامه وب جهت پشتیبانی از بارگذاری همزمان (concurrent) قابلیت ناهمگام (Async) سبب خواهد شد منابع تا زمان اجرای کوئری به ThreadPool بازگردانده شود و برای سرویس دهی مهیا باشند که باعث افزایش scalability خواهد شد.

بررسی و آزمایش با داده‌های واقعی

در اکثر مواقع کارآیی با حجیم شدن داده‌ها کاهش پیدا می‌کند (البته در صورت عدم رعایت اصول استاندارد). بنابراین بررسی کارآیی در محیط هایی با حجم داده‌های بالا ضروری است. هیچ چیز بدتر از آن نیست که همه چیز در محیط توسعه خوب و بی نقص باشد ولی در محیط عملیاتی به شکست بیانجامد. به همین جهت سعی کنید از ابزارهای تولید داده (^ و ^ و ^) برای ایجاد داده‌های آزمایشی استفاده نمایید. سپس کارآیی کوئری خود را مورد بررسی و آزمایش قرار دهید.

مطالب
بازسازی کد: جایگزینی متغیر موقتی با پرس و جو (Replace temp with query)
زمانیکه متغیری برای نگهداری موقت نتیجه‌ی یک expression تعریف شده‌است، بهتر است expression مربوطه به متدی انتقال پیدا کرده و تمامی استفاده‌ها از متغیر موقتی با فراخوانی متد ایجاد شده جایگزین شوند. 
مشکل اصلی در ارتباط با متغیرهای محلی، ترویج ایجاد متدهای بلند توسط آنها است. مشخص است که این متغیرها در بدنه متد خود قابل استفاده هستند و تنها راه اشتراک مقدار آنها طولانی‌تر شدن متد است. اما زمانیکه این متغیرها با متد پرس و جوی مرتبط با آن جایگزین شوند، این مقدار توسط دیگر متدهای کلاس قابل دسترسی خواهد بود. این کار ایجاد متدهایی با اندازه مناسب را آسان‌تر می‌کند. 
این بازسازی کد بیشتر اوقات در کنار بازسازی استخراج متد استفاده می‌شود؛ به طوری که قبل از انجام استخراج متد، ابتدا تکلیف متغیرهای محلی مشخص می‌شود. یک نوع از متغیرهای محلی که نیاز به بررسی و تغییر خواهند داشت این دسته از متغیرهای محلی هستند.

مراحل انجام این بازسازی کد  

  • متغیر موقتی ای را که تنها یک بار مقداردهی شده است، پیدا کنید (در صورتیکه متغیر چندین بار مقداردهی شده باشد، باید بازسازی جداسازی متغیرهای موقتی را اعمال نمایید).
  • متغیر را به readonly تغییر دهید.
  • کد را کامپایل نمایید تا اطمینان حاصل کنید متغیر تنها یک بار مقداردهی شده‌است.
  • Expression سمت راست مقداردهی متغیر را به متد، منتقل نمایید. دو نکته در مورد متد تازه ایجاد شده:  
    • این متد را به صورت private تعریف نمایید. در صورتی در آینده مصرف دیگری برای آن پیدا کردید می‌توانید سطح دسترسی آن‌را آزادتر نمایید. 
    • اطمینان حاصل نمایید متد مورد نظر، شیء یا خصوصیتی را ویرایش نمی‌کند. در صورتیکه این کار را انجام می‌دهد، باید بخش ویرایش آن را از بخش پرس و جوی آن جدا نمایید (Separate query from modifier). 
  • کد را مجددا کامپایل نمایید.  
  • تمامی استفاده‌های متغیر را با فراخوانی متد ایجاد شده، تغییر دهید. 
مثال: تکه کد زیر را در نظر بگیرید  
public class OrderItem 
{ 
    private double quantity; 
    private double itemPrice; 
    public double CalculateTotal() 
    { 
        double basePrice = quantity * itemPrice; 

        if (basePrice > 1000) 
        { 
            return basePrice * 0.95; 
        } 
        else 
        { 
            return basePrice * 0.98; 
        } 
    } 
}

در این کلاس متدی برای محاسبه قیمت نهایی یک آیتم سفارش ایجاد شده‌است. با دقت در کد می‌توان تشخیص داد که متغیر basePrice یک متغیر محلی موقتی است. این تکه کد را می‌توان به صورت زیر بازسازی کرد   
public class OrderItem 
{ 
    private double quantity; 
    private double itemPrice; 
    public double CalculateTotal() 
    { 
        if (BasePrice() > 1000) 
        { 
            return BasePrice() * 0.95; 
        } 
        else 
        { 
            return BasePrice() * 0.98; 
        } 
    } 
    private double BasePrice() 
    { 
        return quantity * itemPrice; 
    } 
}
کد حاصل از بازسازی انجام شده، شامل یک متد به نام BasePrice است که مقدار قیمت پایه را بر می‌گرداند. این متد را به صورت private تعریف کردیم. اما می‌توان در صورت نیاز به override کردن آن در کلاس‌های مشتق شده‌ی احتمالی، سطح دستری آن را به مقدار مناسبی تغییر داد.  به نظر شما چه بازسازی کد دیگری را می‌توان بر روی متد CalculateTotal انجام داد؟ 

آیا این بازسازی کد تاثیر منفی بر کارآیی خواهد داشت؟ 

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