مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول
در طی چند قسمت، نحوه‌ی طراحی یک سیستم افزونه پذیر را با ASP.NET MVC بررسی خواهیم کرد. عناوین مواردی که در این سری پیاده سازی خواهند شد به ترتیب ذیل هستند:
1- چگونه Area‌های استاندارد را تبدیل به یک افزونه‌ی مجزا و منتقل شده‌ی به یک اسمبلی دیگر کنیم.
2- چگونه ساختار پایه‌ای را جهت تامین نیازهای هر افزونه جهت تزریق وابستگی‌ها تا ثبت مسیریابی‌ها و امثال آن تدارک ببینیم.
3- چگونه فایل‌های CSS ، JS و همچنین تصاویر ثابت هر افزونه را داخل اسمبلی آن قرار دهیم تا دیگر نیازی به ارائه‌ی مجزای آ‌ن‌ها نباشد.
4- چگونه Entity Framework Code-First را با این طراحی یکپارچه کرده و از آن جهت یافتن خودکار مدل‌ها و موجودیت‌های خاص هر افزونه استفاده کنیم؛ به همراه مباحث Migrations خودکار و همچنین پیاده سازی الگوی واحد کار.


در مطلب جاری، موارد اول و دوم بررسی خواهند شد. پیشنیازهای آن مطالب ذیل هستند:
الف) منظور از یک Area چیست؟
ب) توزیع پروژه‌های ASP.NET MVC بدون ارائه فایل‌های View آن
ج) آشنایی با تزریق وابستگی‌ها در ASP.NET MVC و همچنین اصول طراحی یک سیستم افزونه پذیر به کمک StructureMap
د) آشنایی با رخدادهای Build


تبدیل یک Area به یک افزونه‌ی مستقل

روش‌های زیادی برای خارج کردن Areaهای استاندارد ASP.NET MVC از یک پروژه و قرار دادن آن‌ها در اسمبلی‌های دیگر وجود دارند؛ اما در حال حاضر تنها روشی که نگهداری می‌شود و همچنین اعضای آن همان اعضای تیم نیوگت و ASP.NET MVC هستند، همان روش استفاده از Razor Generator است.
بنابراین ساختار ابتدایی پروژه‌ی افزونه پذیر ما به صورت ذیل خواهد بود:
1) ابتدا افزونه‌ی Razor Generator را نصب کنید.
2) سپس یک پروژه‌ی معمولی ASP.NET MVC را آغاز کنید. در این سری نام MvcPluginMasterApp برای آن در نظر گرفته شده‌است.
3) در ادامه یک پروژه‌ی معمولی دیگر ASP.NET MVC را نیز به پروژه‌ی جاری اضافه کنید. برای مثال نام آن در اینجا MvcPluginMasterApp.Plugin1 تنظیم شده‌است.
4) به پروژه‌ی MvcPluginMasterApp.Plugin1 یک Area جدید و معمولی را به نام NewsArea اضافه کنید.
5) از پروژه‌ی افزونه، تمام پوشه‌های غیر Area را حذف کنید. پوشه‌های Controllers و Models و Views حذف خواهند شد. همچنین فایل global.asax آن‌را نیز حذف کنید. هر افزونه، کنترلرها و Viewهای خود را از طریق Area مرتبط دریافت می‌کند و در این حالت دیگر نیازی به پوشه‌های Controllers و Models و Views واقع شده در ریشه‌ی اصلی پروژه‌ی افزونه نیست.
6) در ادامه کنسول پاور شل نیوگت را باز کرده و دستور ذیل را صادر کنید:
  PM> Install-Package RazorGenerator.Mvc
این دستور را باید یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه، اجرا کنید.


همانطور که در تصویر نیز مشخص شده‌است، برای اجرای دستور نصب RazorGenerator.Mvc نیاز است هربار پروژه‌ی پیش فرض را تغییر دهید.
7) اکنون پس از نصب RazorGenerator.Mvc، نوبت به اجرای آن بر روی هر دو پروژه‌ی اصلی و افزونه است:
  PM> Enable-RazorGenerator
بدیهی است این دستور را نیز باید همانند تصویر فوق، یکبار بر روی پروژه‌ی اصلی و یکبار بر روی پروژه‌ی افزونه اجرا کنید.
همچنین هربار که View جدیدی اضافه می‌شود نیز باید این‌کار را تکرار کنید یا اینکه مطابق شکل زیر، به خواص View جدید مراجعه کرده و Custom tool آن‌را به صورت دستی به RazorGenerator تنظیم نمائید. دستور Enable-RazorGenerator این‌کار را به صورت خودکار انجام می‌دهد.


تا اینجا موفق شدیم Viewهای افزونه را داخل فایل dll آن مدفون کنیم. به این ترتیب با کپی کردن افزونه به پوشه‌ی bin پروژه‌ی اصلی، دیگر نیازی به ارائه‌ی فایل‌های View آن نیست و تمام اطلاعات کنترلرها، مدل‌ها و Viewها به صورت یکجا از فایل dll افزونه‌ی ارائه شده خوانده می‌شوند.


کپی کردن خودکار افزونه به پوشه‌ی Bin پروژه‌ی اصلی

پس از اینکه ساختار اصلی کار شکل گرفت، هربار پس از کامپایل افزونه (یا افزونه‌ها)، نیاز است فایل‌های پوشه‌ی bin آن‌را به پوشه‌ی bin پروژه‌ی اصلی کپی کنیم (پروژه‌ی اصلی در این حالت هیچ ارجاع مستقیمی را به افزونه‌ی جدید نخواهد داشت). برای خودکار سازی این کار، به خواص پروژه‌ی افزونه مراجعه کرده و قسمت Build events آن‌را به نحو ذیل تنظیم کنید:


در اینجا دستور ذیل در قسمت Post-build event نوشته شده است:
 Copy "$(ProjectDir)$(OutDir)$(TargetName).*" "$(SolutionDir)MvcPluginMasterApp\bin\"
و سبب خواهد شد تا پس از هر کامپایل موفق، فایل‌های اسمبلی افزونه به پوشه‌ی bin پروژه‌ی MvcPluginMasterApp به صورت خودکار کپی شوند.


تنظیم فضاهای نام کلیه مسیریابی‌های پروژه

در همین حالت اگر پروژه را اجرا کنید، موتور ASP.NET MVC به صورت خودکار اطلاعات افزونه‌ی کپی شده به پوشه‌ی bin را دریافت و به Application domain جاری اعمال می‌کند؛ برای اینکار نیازی به کد نویسی اضافه‌تری نیست و خودکار است. برای آزمایش آن فقط کافی است یک break point را داخل کلاس RazorGeneratorMvcStart افزونه قرار دهید.
اما ... پس از اجرا، بلافاصله پیام تداخل فضاهای نام را دریافت می‌کنید. خطاهای حاصل عنوان می‌کند که در App domain جاری، دو کنترلر Home وجود دارند؛ یکی در پروژه‌ی اصلی و دیگری در پروژه‌ی افزونه و مشخص نیست که مسیریابی‌ها باید به کدامیک ختم شوند.
برای رفع این مشکل، به فایل NewsAreaAreaRegistration.cs پروژه‌ی افزونه مراجعه کرده و مسیریابی آن‌را به نحو ذیل تکمیل کنید تا فضای نام اختصاصی این Area صریحا مشخص گردد.
using System.Web.Mvc;
 
namespace MvcPluginMasterApp.Plugin1.Areas.NewsArea
{
    public class NewsAreaAreaRegistration : AreaRegistration
    {
        public override string AreaName
        {
            get
            {
                return "NewsArea";
            }
        }
 
        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.MapRoute(
                "NewsArea_default",
                "NewsArea/{controller}/{action}/{id}",
                // تکمیل نام کنترلر پیش فرض
                new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", this.GetType().Namespace) }
            );
        }
    }
}
همینکار را باید در پروژه‌ی اصلی و هر پروژه‌ی افزونه‌ی جدیدی نیز تکرار کرد. برای مثال به فایل RouteConfig.cs پروژه‌ی اصلی مراجعه کرده و تنظیم ذیل را اعمال نمائید:
using System.Web.Mvc;
using System.Web.Routing;
 
namespace MvcPluginMasterApp
{
    public class RouteConfig
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
                // مشخص کردن فضای نام مرتبط جهت جلوگیری از تداخل با سایر قسمت‌های برنامه
                namespaces: new[] { string.Format("{0}.Controllers", typeof(RouteConfig).Namespace) }
            );
        }
    }
}
بدون تنظیم فضاهای نام هر مسیریابی، امکان استفاده‌ی بهینه و بدون خطا از Areaها وجود نخواهد داشت.


طراحی قرارداد پایه افزونه‌ها

تا اینجا با نحوه‌ی تشکیل ساختار هر پروژه‌ی افزونه آشنا شدیم. اما هر افزونه در آینده نیاز به مواردی مانند منوی اختصاصی در منوی اصلی سایت، تنظیمات مسیریابی اختصاصی، تنظیمات EF و  امثال آن نیز خواهد داشت. به همین منظور، یک پروژه‌ی class library جدید را به نام MvcPluginMasterApp.PluginsBase آغاز کنید.
سپس قرار داد IPlugin را به نحو ذیل به آن اضافه نمائید:
using System;
using System.Reflection;
using System.Web.Optimization;
using System.Web.Routing;
using StructureMap;
 
namespace MvcPluginMasterApp.PluginsBase
{
    public interface IPlugin
    {
        EfBootstrapper GetEfBootstrapper();
        MenuItem GetMenuItem(RequestContext requestContext);
        void RegisterBundles(BundleCollection bundles);
        void RegisterRoutes(RouteCollection routes);
        void RegisterServices(IContainer container);
    }
 
    public class EfBootstrapper
    {
        /// <summary>
        /// Assemblies containing EntityTypeConfiguration classes.
        /// </summary>
        public Assembly[] ConfigurationsAssemblies { get; set; }
 
        /// <summary>
        /// Domain classes.
        /// </summary>
        public Type[] DomainEntities { get; set; }
 
        /// <summary>
        /// Custom Seed method.
        /// </summary>
        //public Action<IUnitOfWork> DatabaseSeeder { get; set; }
    }
 
    public class MenuItem
    {
        public string Name { set; get; }
        public string Url { set; get; }
    }
}
پروژه‌ی این قرارداد برای کامپایل شدن، نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package Microsoft.AspNet.Web.Optimization
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.


توضیحات قرار داد IPlugin

از این پس هر افزونه باید دارای کلاسی باشد که از اینترفیس IPlugin مشتق می‌شود. برای مثال فعلا کلاس ذیل را به افزونه‌ی پروژه اضافه نمائید:
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
 
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    {
        public EfBootstrapper GetEfBootstrapper()
        {
            return null;
        }
 
        public MenuItem GetMenuItem(RequestContext requestContext)
        {
            return new MenuItem
            {
                Name = "Plugin 1",
                Url = new UrlHelper(requestContext).Action("Index", "Home", new { area = "NewsArea" })
            };
        }
 
        public void RegisterBundles(BundleCollection bundles)
        {
            //todo: ...
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
        }
 
        public void RegisterServices(IContainer container)
        {
            // todo: add custom services.
 
            container.Configure(cfg =>
            {
                //cfg.For<INewsService>().Use<EfNewsService>();
            });
        }
    }
}
در قسمت جاری فقط از متد GetMenuItem آن استفاده خواهیم کرد. در قسمت‌های بعد، تنظیمات EF، تنظیمات مسیریابی‌ها و Bundling و همچنین ثبت سرویس‌های افزونه را نیز بررسی خواهیم کرد.
برای اینکه هر افزونه در منوی اصلی ظاهر شود، نیاز به یک نام، به همراه آدرسی به صفحه‌ی اصلی آن خواهد داشت. به همین جهت در متد GetMenuItem نحوه‌ی ساخت آدرسی را به اکشن متد Index کنترلر Home واقع در Area‌ایی به نام NewsArea، مشاهده می‌کنید.


بارگذاری و تشخیص خودکار افزونه‌ها

پس از اینکه هر افزونه دارای کلاسی مشتق شده از قرارداد IPlugin شد، نیاز است آن‌ها را به صورت خودکار یافته و سپس پردازش کنیم. این‌کار را به کتابخانه‌ی StructureMap واگذار خواهیم کرد. برای این منظور پروژه‌ی جدیدی را به نام MvcPluginMasterApp.IoCConfig آغاز کرده و سپس تنظیمات آن‌را به نحو ذیل تغییر دهید:
using System;
using System.IO;
using System.Threading;
using System.Web;
using MvcPluginMasterApp.PluginsBase;
using StructureMap;
using StructureMap.Graph;
 
namespace MvcPluginMasterApp.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        private static Container defaultContainer()
        {
            return new Container(cfg =>
            {
                cfg.Scan(scanner =>
                {
                    scanner.AssembliesFromPath(
                        path: Path.Combine(HttpRuntime.AppDomainAppPath, "bin"),
                            // یک اسمبلی نباید دوبار بارگذاری شود
                        assemblyFilter: assembly =>
                        {
                            return !assembly.FullName.Equals(typeof(IPlugin).Assembly.FullName);
                        });
 
                    scanner.WithDefaultConventions(); //Connects 'IName' interface to 'Name' class automatically.
                    scanner.AddAllTypesOf<IPlugin>().NameBy(item => item.FullName);
                });
            });
        }
    }
}
این پروژه‌ی class library جدید برای کامپایل شدن نیاز به بسته‌های نیوگت ذیل دارد:
PM> install-package EntityFramework
PM> install-package structuremap.web
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.

کاری که در کلاس SmObjectFactory انجام شده، بسیار ساده است. مسیر پوشه‌ی Bin پروژه‌ی اصلی به structuremap معرفی شده‌است. سپس به آن گفته‌ایم که تنها اسمبلی‌هایی را که دارای اینترفیس IPlugin هستند، به صورت خودکار بارگذاری کن. در ادامه تمام نوع‌های IPlugin را نیز به صورت خودکار یافته و در مخزن تنظیمات خود، اضافه کن.


تامین نیازهای مسیریابی و Bundling هر افزونه به صورت خودکار

در ادامه به پروژه‌ی اصلی مراجعه کرده و در پوشه‌ی App_Start آن کلاس ذیل را اضافه کنید:
using System.Linq;
using System.Web.Optimization;
using System.Web.Routing;
using MvcPluginMasterApp;
using MvcPluginMasterApp.IoCConfig;
using MvcPluginMasterApp.PluginsBase;
 
[assembly: WebActivatorEx.PostApplicationStartMethod(typeof(PluginsStart), "Start")]
 
namespace MvcPluginMasterApp
{
    public static class PluginsStart
    {
        public static void Start()
        {
            var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
            foreach (var plugin in plugins)
            {
                plugin.RegisterServices(SmObjectFactory.Container);
                plugin.RegisterRoutes(RouteTable.Routes);
                plugin.RegisterBundles(BundleTable.Bundles);
            }
        }
    }
}
بدیهی است در این حالت نیاز است ارجاعی را به پروژه‌ی MvcPluginMasterApp.PluginsBase به پروژه‌ی اصلی اضافه کنیم.
دراینجا با استفاده از کتابخانه‌ای به نام WebActivatorEx (که باز هم توسط نویسندگان اصلی Razor Generator تهیه شده‌است)، یک متد PostApplicationStartMethod سفارشی را تعریف کرده‌ایم.
مزیت استفاده از اینکار این است که فایل Global.asax.cs برنامه شلوغ نخواهد شد. در غیر اینصورت باید تمام این کدها را در انتهای متد Application_Start قرار می‌دادیم.
در اینجا با استفاده از structuremap، تمام افزونه‌های موجود به صورت خودکار بررسی شده و سپس پیشنیازهای مسیریابی و Bundling و همچنین تنظیمات IoC Container مورد نیاز آن‌ها به هر افزونه به صورت مستقل، تزریق خواهد شد.


اضافه کردن منو‌های خودکار افزونه‌ها به پروژه‌ی اصلی

پس از اینکه کار پردازش اولیه‌ی IPluginها به پایان رسید، اکنون نوبت به نمایش آدرس اختصاصی هر افزونه در منوی اصلی سایت است. برای این منظور فایل جدیدی را به نام PluginsMenu.cshtml_، در پوشه‌ی shared پروژه‌ی اصلی اضافه کنید؛ با این محتوا:
@using MvcPluginMasterApp.IoCConfig
@using MvcPluginMasterApp.PluginsBase
@{
    var plugins = SmObjectFactory.Container.GetAllInstances<IPlugin>().ToList();
}
 
@foreach (var plugin in plugins)
{
    var menuItem = plugin.GetMenuItem(this.Request.RequestContext);
    <li>
        <a href="@menuItem.Url">@menuItem.Name</a>
    </li>
}
در اینجا تمام افزونه‌ها به کمک structuremap یافت شده و سپس آیتم‌های منوی آن‌ها به صورت خودکار دریافت و اضافه می‌شوند.
سپس به فایل Layout.cshtml_ پروژه‌ی اصلی مراجعه و توسط فراخوانی Html.RenderPartial، آن‌را در بین سایر آیتم‌های منوی اصلی اضافه می‌کنیم:
<div class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
                <span class="icon-bar"></span>
            </button>
            @Html.ActionLink("MvcPlugin Master App", "Index", "Home", new { area = "" }, new { @class = "navbar-brand" })
        </div>
        <div class="navbar-collapse collapse">
            <ul class="nav navbar-nav">
                <li>@Html.ActionLink("Master App/Home", "Index", "Home", new {area = ""}, null)</li>
                @{ Html.RenderPartial("_PluginsMenu"); }
            </ul>
        </div>
    </div>
</div>
اکنون اگر پروژه را اجرا کنیم، یک چنین شکلی را خواهد داشت:



بنابراین به صورت خلاصه

1) هر افزونه، یک پروژه‌ی کامل ASP.NET MVC است که پوشه‌های ریشه‌ی اصلی آن حذف شده‌اند و اطلاعات آن توسط یک Area جدید تامین می‌شوند.
2) تنظیم فضای نام مسیریابی‌های تمام پروژه‌ها را فراموش نکنید. در غیر اینصورت شاهد تداخل پردازش کنترلرهای هم نام خواهید بود.
3) جهت سهولت کار، می‌توان فایل‌های bin هر افزونه را توسط رخداد post-build، به پوشه‌ی bin پروژه‌ی اصلی کپی کرد.
4) Viewهای هر افزونه توسط Razor Generator در فایل dll آن مدفون خواهند شد.
5) هر افزونه باید دارای کلاسی باشد که اینترفیس IPlugin را پیاده سازی می‌کند. از این اینترفیس برای ثبت اطلاعات هر افزونه یا دریافت اطلاعات سفارشی از آن کمک می‌گیریم.
6) با استفاده از استراکچرمپ و قرارداد IPlugin، منوهای هر افزونه را به صورت خودکار یافته و سپس به فایل layout اصلی اضافه می‌کنیم.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
MvcPluginMasterApp-Part1.zip
مطالب
چرا توسعه چابک (Agile Development)؟

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

همین که با شکستی مواجه می­شویم از تکرار چنین پروژه هایی اجتناب می­کنیم. ترس ما باعث می­شود تا فرآیندی بسازیم که فعالیت­های ما را محدود نموده و ایجاد آرتیفکت­ها[۱] را الزامی کند. در پروژه­ جدید از  چیزهایی که در پروژه‌های قبلی به خوبی کار کرده­اند، استفاده می­کنیم. انتظار ما این است که آنها برای پروژه جدید نیز به همان خوبی کار کند.

اما پروژه‌­ها آنقدر ساده نیستند که تعدادی محدودیت و آرتیفکت­ ما را از خطاها ایمن سازند. با بروز خطاهای جدید ما آنها را شناسایی و رفع می­کنیم. برای اینکه در آینده با این خطاها روبرو نشویم آنها را در محدودیت­ها و آرتیفکت­های جدیدی قرار می­دهیم. بعد از انجام پروژه‌های زیاد با فرآیندهای حجیم و پر زحمتی روبرو هستیم که توانایی تیم را کم کرده و باعث کاهش کیفیت تولید می­شوند.

فرآیندهای بزرگ و حجیم می­تواند مشکلات زیادی را ایجاد کند. متاسفانه این مشکلات باعث می­شود که خیلی از افراد فکر کنند که علت مشکلات، نبود فرآیندهای کافی است. بنابراین فرآیندها را حجیم­تر و پیچیده­تر می­کنند. این مسئله منجر به تورم فرآیندها می­گردد که در محدوده سال ۲۰۰۰ گریبان بسیاری از شرکت­های نرم افزاری را گرفت.

اتحاد چابک

در وضعیتی که تیم­های نرم افزاری در بسیاری از شرکت­ها خود را در مردابی از فرآیندهای زیاد شونده می­دیدند، تعدادی از خبره‌­های این صنعت که خود را اتحاد چابک[۲] نامیدند در اویل سال ۲۰۰۱ یکدیگر را ملاقات کرده و ارزش هایی را معرفی کردند تا تیم­های نرم افزاری سریعتر نرم افزار را توسعه داده و زودتر به تغییرات پاسخ دهند. چند ماه بعد، این گروه ارزش­هایی تعریف شده را تحت مانیفست اتحاد چابک در سایت http://agilemanifesto.org منتشر کردند.

مانیفست اتحاد چابک

ما با توسعه نرم افزار و کمک به دیگران در انجام آن، در حال کشف راه‌های بهتری برای توسعه نرم افزار هستیم. از این کار به ارزش‌های زیر می­رسیم :

۱- افراد و تعاملات بالاتر از فرآیندها و ابزارها

۲- نرم افزار کار کننده بالاتر از  مستندات جامع

۳- مشارکت مشتری بالاتر از قرارداد کاری

۴- پاسخگویی به تغییرات بالاتر از پیروی از یک برنامه

با آنکه موارد سمت چپ ارزشمند هستند ولی ما برای موارد سمت راست ارزش بیشتری قائل هستیم.

افراد و تعاملات بالاتر از فرآیندها و ابزارها

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

یک نیروی قوی لازم نیست که برنامه نویسی عالی باشد، بلکه کافیست که یک برنامه نویسی معمولی با قابلیت همکاری مناسب با سایر اعضای تیم باشد. کار کردن با دیگران، تعامل درست و سازنده با سایر اعضای تیم خیلی مهمتر از این که یک برنامه نویس با هوش باشد. برنامه نویسان معمولی که تعامل درستی با یکدیگر دارند به مراتب موفقتر هستند از تعداد برنامه نویسی عالی که قدرت تعامل مناسب با یکدیگر را ندارند.

در انتخاب ابزارها آنقدر وقت نگذارید که کار اصلی و تیم را فراموش کنید. به عنوان مثال می­توانید در شروع به جای بانک اطلاعاتی از فایل استفاده کنید، به جای ابزار کنترل کد گرانقیمت از برنامه رایگان کد باز استفاده کنید. باید به هیچ ابزاری عادت نکنید و صرفا به آنها به عنوان امکانی جهت تسهیل فرآیندها نگاه کنید.

نرم افزار کار کننده بالاتر از  مستندات جامع

نرم افزار بدون مستندات، فاجعه است. کد برنامه ابزار مناسبی برای تشریح سیستم نرم افزاری نیست. تیم باید مستندات قابل فهم مشتری بسازد تا ابعاد سیستم از تجزیه تحلیل تا طراحی و پیاده سازی آن را تشریح نماید.

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

بهتر است که همیشه مستندات کم حجمی از منطق و ساختار برنامه داشته باشید و آن را به روز نماید. البته آنها باید کوتاه و برجسته باشند. کوتاه به این معنی که ۱۰ تا ۲۰ صفحه بیشتر نباشد و برجسته به این معنی که طراحی کلی و ساختار سطح بالای سیستم را بیان نماید.

اگر فقط مستندات کوتاه از ساختار و منطق سیستم داشته باشیم چگونه می‌توانیم اعضای جدید تیم را آموزش دهیم؟ پاسخ کار نزدیک شدن به آنها است. ما دانش خود را با نشستن در کنار آنها و کمک کردن به آنها انتقال می­دهیم. ما آنها را بخشی از تیم می­کنیم و با تعامل نزدیک و رو در رو به آنها آموزش می­دهیم.

مشارکت مشتری بالاتر از قرارداد کاری

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

این قابل تصور است که مدیران شرکت به اعضای تیم توسعه بگویند که نیازهای آنها چیست، سپس اعضای تیم بروند و بعد از مدتی برگردند و یک سیستمی که نیازهای آنها را برآورده می‌کند، بسازند. اما این تعامل به کیفیت پایین نرم افزار و در نهایت شکست آن می‌انجامد. پروژه‌های موفق بر اساس دریافت بازخورد مشتری در بازه‌های زمانی کوتاه و مداوم است. به جای وابستگی به قرارداد یا دستور کار، مشتری به طور تنگاتنگ با تیم توسعه کار کرده و مرتبا اعمال نظر می­کند.

قراردادی که مشخص کننده نیازمندیها، زمانبندی و قیمت پروژه است، اساسا نقص دارد. بهترین قرارداد این است که تیم توسعه و مشتری با یکدیگر کار کنند.

پاسخگویی به تغییرات بالاتر از پیروی از یک برنامه

توانایی پاسخ به تغییرات اغلب تعیین کننده موفقیت یا شکست یک پروژه نرم افزاری است. وقتی که طرحی را می­ریزیم باید مطمئن شویم که به اندازه کافی انعطاف پذیر است و آمادگی پذیرش تغییرات در سطح بیزنس و تکنولوژی را دارد.

مسیر یک پروژه نرم افزاری نمی­تواند برای بازه زمانی طولانی برنامه ریزی شود. اولا احتمالا محیط تغییر می­کند و باعث تغییر در نیازمندی‌ها می­شود. ثانیا همین که سیستم شروع به کار کند مشتریان نیازمندی­های خود را تغییر می‌دهند. بنابراین اگر بدانیم که نیازها چیست و مطمئن شویم که تغییر نمی­کنند، قادر به برآورد مناسب خواهیم بود، که این شرایط بعید است.

یک استراتژی خوب برای برنامه ریزی این است که یک برنامه ریزی دقیق برای یک هفته بعد داشته باشیم و یک برنامه ریزی کلی برای سه ماه بعد.

اصول چابک

۱- بالاترین اولویت ما عبارت است از راضی کردن مشتری با تحویل سریع و مداوم نرم افزار با ارزش. تحویل نرم افزار با کارکردهای کم در زود هنگام بسیار مهم است چون هم مشتری چشم اندازی از محصول نهایی خواهد داشت و هم مسیر کمتر به بیراهه می‌رود.

۲- خوش آمدگویی به تغییرات حتی در انتهای توسعه. اعضای تیم چابک، تغییرات را چیز خوبی می‌بینند زیرا تغییرات به این معنی است که تیم بیشتر یاد گرفته است که چه چیزی مشتری را راضی می‌کند.

۳- تحویل نرم افزار قابل استفاده از چند هفته تا چند ماه با تقدم بر تحویل در دوره زمانی کوتاهتر. ما مجموعه از مستندات و طرحها را به مشتری نمی‌دهیم.

۴- افراد مسلط به بیزنس و توسعه دهندگان باید روزانه با یکدیگر روی پروژه کار کنند. یک پروژه نرم افزاری نیاز به هدایت مداوم دارد.

۵- ساخت پروژه را بر توان افراد با انگیزه بگذارید و به آنها محیط و ابزار را داده و اعتماد کنید.  مهمترین فاکتور موفقیت افراد هستند، هر چیز دیگر مانند فرآیند، محیط و مدیریت  فاکتورهای بعدی محسوب می­شوند که اگر تاثیر بدی روی افراد می­گذارند، باید تغییر کنند.

۶- بهترین و موثر‌ترین روش کسب اطلاعات در تیم توسعه، ارتباط چهره به چهره است. در تیم چابک افراد با یکدیگر صحبت می‌کنند. نامه نگاری و مستند سازی فقط زمانی که نیاز است باید صورت گیرد.

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

۸- فرآیندهای چابک توسعه با آهنگ ثابت را ترویج می‌دهد. حامیان، توسعه دهندگان و کاربران باید یک آهنگ توسعه ثابت را حفظ کنند که بیشتر شبیه به دو  ماراتون است یا دوی ۱۰۰ متر. آنها با سرعتی کار می‌کنند که بالاترین کیفیت را ارائه دهند.

۹- توجه مداوم به برتری تکنیکی و طراحی خوب منجر به چابکی می­گردد. کیفیت بالاتر کلیدی برای سرعت بالا است. راه سریعتر رفتن این است که نرم افزار تا جایی که ممکن است پاک و قوی نگهداریم. بنابراین همه اعضای تیم چابک تلاش می­کنند که با کیفیت­ترین کار ممکن را انجام دهند. آنها هر آشفتگی را به محض ایجاد برطرف می‌کنند.

۱۰-  سادگی هنر بیشینه کردن مقدار کاری که لازم نیست انجام شود، است. تیم چابک همیشه ساده‌ترین مسیر که با هدف آنها سازگار است را در پیش می­گیرند. آنها وقت زیادی روی مشکلاتی که ممکن است فردا رخ دهد، نمی­گذارند.  آنها کار امروز را با کیفیت انجام داده و مطمئن می­شوند که تغییر آن در صورت بروز مشکلات در فردا، آسان خواهد بود.

۱۱-  بهترین معماری و طراحی از تیم‌های خود سازمان ده بیرون می‌آید. مدیران، مسئولیت‌ها را به یک فردی  خاصی در تیم نمی‌دهند بلکه بر عکس با تیم به صورت یک نیروی واحد برخورد می­کنند. خود تیم تصمیم می­گیرد که هر مسئولیت را چه کسی انجام دهد. تیم چابک با هم روی کل جنبه‌های پروژه کار می­کنند. یعنی یک فرد خاص مسئول معماری، برنامه نویسی، تست و غیره نیستند. تیم، مسئولیتها را به اشتراک گذاشته و هر فرد بر کل کار تاثیر دارد.

۱۲-  در بازهای زمانی مناسب تیم در می­یابد که چگونه می­تواند کاراتر باشد و رفتار خود را متناسب با آن تغییر دهد. تیم می­داند که محیط دائما در حال تغییر است، بنابراین خود را با محیط تغییر می­دهد تا چابک بماند.

ضرورت توسعه چابک

امروزه صنعت نرم افزار دارای سابقه بدی در تحویل به موقع و با کیفیت نرم افزار است. گزارشات بسیاری تایید می­کنند که بیش از ۸۰ درصد از پروژه‌های نرم افزاری با شکست مواجه می­شوند؛ در سال ۲۰۰۵ موسسه IEEE  برآورد زده است که بیش از ۶۰ بیلیون دلار صرف پروژه‌های نرم افزاری شکست خورده شده است. عجب فاجعه­ای؟

شش دلیل اصلی شکست پروژه‌های نرم افزاری

وقتی که از مدیران و کارکنان سوال می­شود که چرا پروژه‌های نرم افزاری با شکست مواجه می­شوند، آنها به موضوعات گسترده ای اشاره می­کنند. اما شش دلیل زیر بارها و بارها تکرار شده است که به عنوان دلایل اصلی شکست نرم افزار معرفی می­شوند:

۱- درگیر نشدن  مشتری

۲- عدم درک درست نیازمندها

۳- زمان بندی غیر واقعی

۴- عدم پذیریش و مدیریت تغییرات

۵- کمبود تست نرم افزار

۶- فرآیندهای غیر منعطف و باد دار

چگونه چابکی این مشکلات را رفع می­کند؟

با آنکه Agile برای هر مشکلی راه حل ندارد ولی برای مسائل فوق بدین صورت کمک می­کند:

مشتری پادشاه است!

برای رفع مشکل عدم همکاری کاربر نهایی یا مشتری، Agile مشتری را عضوی از تیم توسعه می­کند. به عنوان عضوی از تیم، مشتری با تیم توسعه کار می­کند تا مطمئن شود که نیازمندها به درستی برآورده می­شوند. مشتری همکاری می­کند در شناسایی نیازمندی­ها، تایید می­کند نتیجه نهایی را و حرف آخر را در اینکه کدام ویژگی به نرم افزار اضافه شود، حذف شود و یا تغییر کند، را می­زند.

نیازمندی‌ها به صورت تست­های پذیرش[۳] نوشته می­شوند

برای مقابله با مشکل عدم درک درست نیازمندی­ها، Agile تاکید دارد که نیازمندیهای کسب شده باید به صورت ویژگی­هایی تعریف شوند که بر اساس معیارهای مشخصی قابل پذیرش باشند. این معیارهای پذیرش برای نوشتن تست­های پذیرش به کار می­روند. به این ترتیب قبل از اینکه کدی نوشته شود، ابتدا تست پذیرش نوشته می­شود. این بدین معنی است که هر کسی باید اول فکر کند که چه می­خواهد، قبل از اینکه از کسی بخواهد آن را انجام دهد. این راهکار فرایند کسب نیازمندی­ها را از بنیاد تغییر می­دهد و به صورت چشم گیری کیفیت برآورد و زمان بندی را بهبود می­دهد.

زمانبندی با مذاکره بین تیم توسعه و سفارش دهنده تنظیم می­شود

برای حل مشکل زمان بندی غیر واقعی، Agile زمان بندی را به صورت یک فرآیند مشترک بین تیم توسعه و سفارش دهنده تعریف می­کند. در شروع هر نسخه از  نرم افزار، سفارش دهنده ویژگی‌های مورد انتظار را به تیم توسعه می­گوید. تیم توسعه تاریخ تحویل را بر اساس ویژگی­ها برآورد می­زد و در اختیار سفارش دهنده قرار می­دهد. این تعامل تا رسیدن به یک دیدگاه مشترک ادامه می­یابد.

هیچ چیزی روی سنگ حک نشده است، مگر تاریخ تحویل

برای رفع مشکل ضعف در مدیریت تغییرات، Agile اصرار دارد که هر کسی باید تغییرات را بپذیرد و نسبت به آنها واقع بین باشد. یک اصل مهم Agile  می­گوید که هر چیزی می­تواند تغییر کند مگر تاریخ تحویل! به عبارت دیگر همین که محصول به سمت تولید شدن حرکت می­کند، مشتری (در تیم محصول) می­تواند بر اساس اولویت­ها و ارزش­های خود ویژگی­های محصول را کم یا زیاد کرده و یا تغییر دهد. به هر حال او باید واقع بین باشد. اگر او یک ویژگی جدید اضافه کنید، باید تاریخ تحویل را تغییر دهد. به این ترتیب همیشه تاریخ تحویل رعایت می­گردد.

تست­ها قبل از کد نوشته می­شوند و کاملا خودکار هستند

برای رفع مشکل کمبود تست، Agile تاکید می­کند که ابتدا باید تست­ها نوشته شوند و همواره ارزیابی گردند. هر برنامه نویس باید اول تست­ را بنویسد، سپس کد لازم برای پاس شدن آن را. همین که کد تغییر می­کند باید تست­ها دوباره اجرا شوند. در این راهکار، هر برنامه نویس مسئول تست­های خود است تا درستی برنامه از ابتدا تضمین گردد.

مدیریت پروژه یک فعالیت جداگانه نیست

برای رفع مشکل فرآیندهای غیر منعطف و باددار، Agile مدیریت پروژه را درون فرآیند توسعه می­گنجاند. وظایف مدیریت پروژه بین اعضای تیم توسعه تقسیم می­شود. برای مثال هر ۷ نفر در تیم توسعه نرم افزار (متدلوژی اسکرام) زمان تحویل را با مذاکره تعیین می­کنند. همچنین کد برنامه به صورت خودکار اطلاعات وضعیت پروژه را تولید می­کند. به عنوان مثال  نمودار burndown ، تست­های انجام نشده، پاس شده و رد شده به صورت خودکار تولید می­شوند.

به کار گیری توسعه چابک

یکی از مشکلات توسعه چابک این است که شما اول باید به خوبی آن را درک کنید تا قادر به پیاده سازی درست آن باشید. این درک هم باید کلی باشد (مانند Scrum و XP) و هم جزئی (مانند TDD و جلسات روازنه). اما چگونه باید به این درک برسیم؟ کتاب­ها و مقالات انگلیسی زیادی برای یادگیری توسعه چابک و پیاده سازی آن در سازمان وجود دارند، ولی متاسفانه منابع فارسی کمی در این زمینه است. هدف این کتاب رفع این کمبود و آموزش عملی توسعه چابک و ابزارهای پیاده سازی آن است.

برای این یک توسعه دهنده چابک شوید، باید به مهارت­های فردی و تیمی چابک برسید. در ادامه این مهارت­ها معرفی می­شوند.

مهارت­های فردی

قبل از هر چیز شما باید یک برنامه نویس باشید و مقدمات برنامه نویسی مانند الگوریتم و فلوچارت، دستورات برنامه نویسی، کار با متغیرها، توابع و آرایه­‌ها را بلد باشید. پس از تسلط به مقدمات برنامه نویسی می­توانید مهارت­های برنامه نویسی چابک را فرا بگیرید که عبارتند از:

- برنامه نویسی شیءگرا

- توسعه تست محور

- الگوهای طراحی

در ادامه نحوه کسب این مهارت­ها بیان می­شوند.

برنامه نویسی شیءگرا

اساس طراحی چابک بر تفکر شیءگرا استوار است. بنابراین تسلط به مفاهیم و طراحی شیءگرا ضروری است. 

توسعه تست محور

مهمترین و انقلابی‌ترین سبک برنامه نویسی از دهه گذشته تا به امروز، توسعه یا برنامه نویسی تست محور است. این سبک بسیاری از ارزش‌های توسعه چابک را فراهم می­کند و یادگیری آن برای هر توسعه دهنده چابک ضروری است.

الگوهای طراحی

الگوهای طراحی راه حل­های انتزاعی سطح بالا هستند. این الگوها بهترین تکنیک­های[۴] طراحی نرم افزار هستند و بسیاری از مشکلاتی که در طراحی نرم افزار رخ می­دهند با استفاده از این الگوها قابل حل هستند.

مهارت­های تیمی

انجام پروژه نرم افزاری یک کار تیمی است. شما پس از یادگیری مهارت­های فردی باید خود را آماده حضور در تیم توسعه چابک کنید. برای این منظور باید با مهارت تیمی مانند آشنایی با گردشکار تولید نرم افزار، حضور موثر در جلسات، قبول مسئولیت­ها و غیره آشنا شوید.

اسکرام

تمامی مهارت­های تیمی توسعه چابک توسط اسکرام آموزش داده می­شوند. اسکرام فریم ورکی برای توسعه چابک است که با تعریف فرآیندها، نقش­ها و آرتیفکت­های مشخص به تیم­های نرم افزاری کمک می­کند تا چابک شوند.


[۱] Artifact : خروجی یک فرآیند  است. مثلا خروجی طراحی شیءگرا، نمودارهای UML است.

[۲] Agile Alliance

[3] Acceptance Tests

[4] Best Practice

--------------------------------

اطلاعات بیشتر در http://AgileDevelopment.ir

مطالب
ASP.NET MVC #19

مروری بر امکانات Caching اطلاعات در ASP.NET MVC

در برنامه‌های وب، بالاترین حد کارآیی برنامه‌ها از طریق بهینه سازی الگوریتم‌ها حاصل نمی‌شود، بلکه با بکارگیری امکانات Caching سبب خواهیم شد تا اصلا کدی اجرا نشود. در ASP.NET MVC این هدف از طریق بکارگیری فیلتری به نام OutputCache میسر می‌گردد:

using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
return View();
}
}
}

همانطور که ملاحظه می‌کنید، OutputCache را به یک اکشن متد یا حتی به یک کنترلر نیز می‌توان اعمال کرد. به این ترتیب HTML نهایی حاصل از View متناظر با اکشن متد جاری فراخوانی شده، Cache خواهد شد. سپس زمانیکه درخواست بعدی به سرور ارسال می‌شود، نتیجه دریافت شده، همان اطلاعات Cache شده قبلی است و عملا در سمت سرور کدی اجرا نخواهد شد. در اینجا توسط پارامتر Duration، مدت زمان معتبر بودن کش حاصل، برحسب ثانیه مشخص می‌شود. VaryByParam مشخص می‌کند که اگر متدی پارامتری را دریافت می‌کند، آیا باید به ازای هر مقدار دریافتی، مقادیر کش شده متفاوتی ذخیره شوند یا خیر. در اینجا چون متد Index پارامتری ندارد، از مقدار none استفاده شده است.


مثال یک
یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. سپس کنترلر جدید Home را نیز به آن اضافه نمائید:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index()
{
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}

همچنین کدهای View متد Index را نیز به نحو زیر تغییر دهید:

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>
<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

در اینجا نمایش دو زمان دریافتی از کنترلر و زمان محاسبه شده در View را مشاهده می‌کنید. هدف این است که بررسی کنیم آیا فیلتر OutputCache بر روی این دو مقدار تاثیری دارد یا خیر.
برنامه را اجرا نمائید. سپس چند بار صفحه را Refresh کنید. مشاهده خواهید کرد که هر دو زمان یاد شده تا 60 ثانیه، تغییری نخواهند کرد و حاصل نهایی از Cache خواهنده می‌شود.
کاربرد یک چنین حالتی برای مثال نمایش اطلاعات بازدیدهای یک سایت است. نباید به ازای هر کاربر وارد شده به سایت، یکبار به بانک اطلاعاتی مراجعه کرد و آمار جدیدی را تهیه نمود. یا برای نمونه اگر جایی قرار است اطلاعات وضعیت آب و هوا نمایش داده شود، بهتر است این اطلاعات، مثلا هر نیم ساعت یکبار به روز شود و نه به ازای هر بازدید جدید از سایت، توسط صدها بازدید کننده همزمان. یا برای مثال کش کردن خروجی فید RSS یک بلاگ به مدت چند ساعت نیز ایده خوبی است. از این لحاظ که اگر اطلاعات بلاگ شما روزی یکبار به روز می‌شود، نیازی نیست تا به ازای هر برنامه فیدخوان، یکبار اطلاعات از بانک اطلاعاتی دریافت شده و پروسه رندر نهایی فید صورت گیرد. منوهای پویای یک سایت نیز در همین رده قرار می‌گیرند. دریافت اطلاعات منوهای پویای سایت به ازای هر درخواست رسیده کاربری جدید، کار اشتباهی است. این اطلاعات نیز باید کش شوند تا بار سرور کاهش یابد. البته تمام این‌ها زمانی میسر خواهند شد که اطلاعات سمت سرور کش شوند.


مثال دو
همان مثال قبلی را در اینجا جهت بررسی پارامتر VaryByParam به نحو زیر تغییر می‌دهیم:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Controllers
{
public class HomeController : Controller
{
[OutputCache(Duration = 60, VaryByParam = "none")]
public ActionResult Index(string parameter)
{
ViewBag.Msg = parameter ?? string.Empty;
ViewBag.ControllerTime = DateTime.Now;
return View();
}
}
}


در اینجا یک پارامتر به متد Index اضافه شده است. مقدار آن به ViewBag.Msg انتساب داده شده و سپس در View ، در بین تگ‌های h2 نمایش داده خواهد شد. همچنین یک فرم ساده هم جهت ارسال parameter به متد Index اضافه شده است:

@{
ViewBag.Title = "Index";
}

<h2>@ViewBag.Msg</h2>

<p>@ViewBag.ControllerTime</p>
<p>@DateTime.Now</p>

@using (Html.BeginForm())
{
@Html.TextBox("parameter")
<input type="submit" />
}

اکنون برنامه را اجرا کنید. در TextBox نمایش داده شده یکبار مثلا بنویسید Test1 و فرم را به سرور ارسال نمائید. سپس مقدار Test2 را وارد کرده و ارسال نمائید. در بار دوم، خروجی صفحه همانند زمانی است که مقدار Test1 ارسال شده است. علت این است که مقدار VaryByParam به none تنظیم شده است و صرفنظر از ورودی کاربر، همان اطلاعات کش شده قبلی بازگشت داده خواهد شد. برای رفع این مشکل، متد Index را به نحو زیر تغییر دهید، به طوریکه مقدار VaryByParam به نام پارامتر متد جاری اشاره کند:

[OutputCache(Duration = 60, VaryByParam = "parameter")]
public ActionResult Index(string parameter)

در ادامه مجددا برنامه را اجرا کنید. اکنون یکبار مقدار Test1 را به سرور ارسال کنید. سپس مقدار Test2 را ارسال نمائید. مجددا همین دو مرحله را با مقادیر Test1 و Test2 تکرار کنید. مشاهده خواهید کرد که اینبار اطلاعات بر اساس مقدار پارامتر ارسالی کش شده است.



تنظیمات متفاوت OutputCache

الف) VaryByParam : اگر مساوی none قرار گیرد، همواره همان مقدار کش شده قبلی نمایش داده می‌شود. اگر مقدار آن به نام پارامتر خاصی تنظیم شود، اطلاعات کش شده بر اساس مقادیر متفاوت پارامتر دریافتی، متفاوت خواهند بود. در اینجا پارامترهای متفاوت را با یک «,» می‌توان از هم جدا ساخت. اگر تعداد پارامترها زیاد است می‌توان مقدار VaryByParam را مساوی با * قرار داد. در این حالت به ازای مقادیر متفاوت دریافتی پارامترهای مختلف، اطلاعات مجزایی در کش قرار خواهد گرفت. این روش آخر آنچنان توصیه نمی‌شود چون سربار بالایی دارد و حجم بالایی از اطلاعات بر اساس پارامترهای مختلف، باید در کش قرار گیرند.
ب) Location : مکان قرارگیری اطلاعات کش شده را مشخص می‌کند. مقدار آن نیز بر اساس یک enum به نام OutputCacheLocation مشخص می‌گردد. در این حالت برای مثال می‌توان مکان‌های Server، Client و ServerAndClient را مقدار دهی نمود. مقدار Downstream به معنای کش شدن اطلاعات بر روی پروکسی سرورهای بین راه و یا مرورگرها است. پیش فرض آن Any است که ترکیبی از Server و Downstream می‌باشد.
اگر قرار است اطلاعات یکسانی به تمام کاربران نمایش داده شود، مثلا محتوای لیست یک منوی پویا،‌ محل قرارگیری اطلاعات کش باید سمت سرور باشد. اگر نیاز است به ازای هر کاربر محتوای اطلاعات کش شده متفاوت باشد، بهتر است محل سمت کلاینت را مقدار دهی نمود.
ج) VaryByHeader : اطلاعات، بر اساس هدرهای مشخص شده، کش می‌شوند. برای مثال مرسوم است که از Accept-Language در اینجا استفاده شود تا اطلاعات مثلا فرانسوی کش شده، به کاربر آلمانی تحویل داده نشود.
د) VaryByCustom :‌ در این حالت نام یک متد استاتیک تعریف شده در فایل global.asax.cs باید مشخص گردد. توسط این متد کلید رشته‌ای اطلاعاتی که قرار است کش شود، بازگشت داده خواهد شد.
ه) SqlDependency : در این حالت اطلاعات تا زمانیکه تغییری در جداول بانک اطلاعاتی SQL Server صورت نگیرد، کش خواهد شد.
و) Nostore : به پروکسی سرورهای بین راه و همچنین مرورگرها اطلاع می‌دهد که اطلاعات را نباید کش کنند. اگر قسمت اعتبار سنجی این سری را به خاطر داشته باشید، چنین تعریفی در قسمت Remote validation بکارگرفته شد:

[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]  

و یا می‌توان برای اینکار یک فیلتر سفارشی را نیز تهیه کرد:

using System;
using System.Web.Mvc;

namespace MvcApplication16.Helper
{
/// <summary>
/// Adds "Cache-Control: private, max-age=0" header,
/// ensuring that the responses are not cached by the user's browser.
/// </summary>
public class NoCachingAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);
filterContext.HttpContext.Response.CacheControl = "private";
filterContext.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
}
}
}

کار این فیلتر اضافه کردن هدر «Cache-Control: private, max-age=0» به Response است.


استفاده از فایل Web.Config برای معرفی تنظیمات Caching

یکی دیگر از تنظیمات ویژگی OutputCache، پارامتر CacheProfile است که امکان تنظیم آن در فایل web.config نیز وجود دارد. برای نمونه تنظیمات زیر را به قسمت system.web فایل وب کانفیگ برنامه اضافه کنید:


<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="Aggressive" location="ServerAndClient" duration="300"/>
<add name="Mild" duration="100" location="Server" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>

سپس مثلا برای استفاده از پروفایلی به نام Aggressive، خواهیم داشت:

[OutputCache(CacheProfile = "Aggressive", VaryByParam = "parameter")]
public ActionResult Index(string parameter)


استفاده از ویژگی به نام donut caching

تا اینجا به این نتیجه رسیدیم که OutputCache، کل خروجی یک View را بر اساس پارامترهای مختلفی که دریافت می‌کند، کش خواهد کرد. در این بین اگر بخواهیم تنها قسمت کوچکی از صفحه کش نشود چه باید کرد؟ برای حل این مشکل قابلیتی به نام cache substitution که به donut caching هم معروف است (چون آن‌را می‌توان به شکل یک donut تصور کرد!) در ASP.NET MVC قابل استفاده است.

@{ Response.WriteSubstitution(ctx => DateTime.Now.ToShortTimeString()); }

همانطور که ملاحظه می‌کنید برای تعریف یک چنین اطلاعاتی باید از متد Response.WriteSubstitution در یک view استفاده کرد. در این مثال، نمایش زمان جاری معرفی شده، صرف نظر از وضعیت کش صفحه جاری، کش نخواهد شد.

عکس آن هم ممکن است. فرض کنید که صفحه جاری شما از سه partial view تشکیل شده است. هر کدام از این partial viewها نیز مزین به OutpuCache هستند. اما صفحه اصلی درج کننده اطلاعات این سه partial view فاقد ویژگی Output کش است. در این حالت تنها اطلاعات این partial viewها کش خواهند شد و سایر قسمت‌های صفحه با هر بار درخواست از سرور، مجددا بر اساس اطلاعات جدید به روز خواهند شد. حالت توصیه شده نیز همین مورد است و متد Response.WriteSubstitution را صرفا جهت اطلاعات عمومی درنظر داشته باشید.


استفاده از امکانات Data Caching به صورت مستقیم

مطالبی که تا اینجا عنوان شدند به کش کردن اطلاعات Response اختصاص داشتند. اما امکانات Caching موجود، به این مورد خلاصه نشده و می‌توان اطلاعات و اشیاء را نیز کش کرد. برای مثال اطلاعات «با سطح دسترسی عمومی» دریافتی از بانک اطلاعاتی توسط یک کوئری را نیز می‌توان کش کرد. جهت انجام اینکار می‌توان از متدهای HttpRuntime.Cache.Insert و یا HttpContext.Cache.Insert استفاده کرد. استفاده از HttpContext.Cache.Insert حین نوشتن Unit tests دردسر کمتری دارد و mocking آن ساده است؛ از این جهت که بر اساس HttpContextBase تعریف شده‌است.
در ادامه یک کلاس کمکی نوشتن اطلاعات در cache و سپس بازیابی آن‌را ملاحظه می‌کنید:

using System;
using System.Web;
using System.Web.Caching;

namespace MvcApplication16.Helper
{
public static class CacheManager
{
public static void CacheInsert(this HttpContextBase httpContext, string key, object data, int durationMinutes)
{
if (data == null) return;
httpContext.Cache.Add(
key,
data,
null,
DateTime.Now.AddMinutes(durationMinutes),
TimeSpan.Zero,
CacheItemPriority.AboveNormal,
null);
}

public static T CacheRead<T>(this HttpContextBase httpContext, string key)
{
var data = httpContext.Cache[key];
if (data != null)
return (T)data;
return default(T);
}

public static void InvalidateCache(this HttpContextBase httpContext, string key)
{
httpContext.Cache.Remove(key);
}
}
}

و برای استفاده از آن در یک اکشن متد، ابتدا نیاز است فضای نام این کلاس تعریف شود و سپس برای نمونه متد HttpContext.CacheInsert در دسترس خواهد بود. HttpContext یکی از خواص تعریف شده در شیء کنترلر است که با ارث بری کنترلرها از آن، همواره در دسترس می‌باشد.
در اینجا برای نمونه اطلاعات یک لیست جنریک دریافتی از بانک اطلاعاتی را مثلا 10 دقیقه (بسته به پارامتر durationMinutes آن) می‌توان کش کرد و سپس توسط متد CacheRead آن‌را دریافت نمود. اگر متد CacheRead نال برگرداند به معنای خالی بودن کش است. بنابراین یکبار اطلاعات را از بانک اطلاعاتی دریافت نموده و سپس آن‌را کش خواهیم کردیم.
البته هستند ORMهایی که یک چنین کارهایی را به صورت توکار پشتیبانی کنند. به مکانیزم آن، Second level cache هم گفته می‌شود؛ به علاوه امکان استفاده از پروایدرهای دیگری را بجز کش IIS برای ذخیره سازی موقتی اطلاعات نیز فراهم می‌کنند.
همچنین باید دقت داشت این اعداد مدت زمان، هیچگونه ضمانتی ندارند. اگر IIS احساس کند که با کمبود منابع مواجه شده است، به سادگی شروع به حذف اطلاعات موجود در کش خواهد کرد.


نکته امنیتی مهم!
به هیچ عنوان از OutputCache در صفحاتی که نیاز به اعتبار سنجی دارند، استفاده نکنید و به همین جهت در قسمت کش کردن اطلاعات، بر روی «اطلاعاتی با سطح دسترسی عمومی» تاکید شد.
فرض کنید کارمندی به صفحه مشاهده فیش حقوقی خودش مراجعه کرده است. این ماه هم اضافه حقوق آنچنانی داشته است. شما هم این صفحه را به مدت سه ساعت کش کرده‌اید. آیا می‌توانید تصور کنید اگر همین گزارش کش شده با این اطلاعات، به سایر کارمندان نمایش داده شود چه قشقرقی به پا خواهد شد؟!
بنابراین هیچگاه اطلاعات مخصوص به یک کاربر اعتبار سنجی شده را کش نکنید و «تنها» اطلاعاتی نیاز به کش شدن دارند که عمومی باشند. برای مثال لیست آخرین اخبار سایت؛ لیست آخرین مدخل‌های فید RSS سایت؛ لیست اطلاعات منوی عمومی سایت؛ لیست تعداد کاربران مراجعه کننده به سایت در طول یک روز؛ گزارش آب و هوا و کلیه اطلاعاتی با سطح دسترسی عمومی که کش شدن آن‌ها مشکل ساز نباشد.
به صورت خلاصه هیچگاه در کدهای شما چنین تعریفی نباید مشاهده شود:
[Authorize]
[OutputCache(Duration = 60)]
public ActionResult Index()




بازخوردهای دوره
ایجاد یک کلاس جدید پویا و وهله‌ای از آن در زمان اجرا توسط Reflection.Emit
- در متن فوق جایی عنوان نشده که تنها اگر تعداد فیلدها از قبل مشخص بود، اینکار قابل انجام است. همچنین اگر به مثال بحث دقت کنید، پارامتر name رشته‌ای است. یعنی هر نام خاصیت دلخواهی قابل تعریف است. نوع آن نیز قابل مقدار دهی و تغییر است.
- در حلقه‌ای که نوشتید، کدهای «افزودن فیلد خصوصی» مثال بحث، «تعریف خاصیت رشته‌ای نام» ، «اتصال گت متد به خاصیت رشته‌ای نام» و «تعریف بدنه گت متد» باید به ازای هر خاصیت، تکرار شوند (پارامتر name را با نام خاصیت‌ها جایگزین کنید؛ نوع آن هم قابل تغییر است). اگر set هم دارد، علاوه بر متد گت، متد set_XYZ هم باید اضافه شود و روش کار یکی است.
نظرات اشتراک‌ها
چرا از آنگولار به ری اکت + ری داکس سوئیچ کردم!
ضرورتی به تکرار اصل مشکل که «تفرقه پراکنی» و «ترویج نفرت» هست، نیست. اینکه ما اینجا وقت گذاشتیم در مورد TypeScript و همچنین Angular مطلب تهیه کردیم، بعد شخصی اینجا تمام توانش را جهت ترویج «تئوری توطئه» در مورد این محصولات صرف کند، قابل تحمل نیست. شما اگر در جمعی قرار گرفتید که برای مثال در مورد TypeScript و همچنین Angular به صورت منظم مطلب تهیه می‌کنند، اخلاق حکم می‌کند که با آن‌ها همکاری کنید یا آن سایت را ترک کنید، بجای سمپاشی در این مورد؛ بجای سرد کرد و گرفتن انگیزه‌ی مثبت از این افراد؛ بجای کثیف خواندن این پروژه‌های سورس باز با کیفیت، چون توسط گروه و قبیله‌ی شما ارائه نشدند و نام چند شرکت بزرگ پشت آن‌ها است.

نتیجه گیری: یک جمع بر اساس «هم‌دلی» و «همراهی» می‌توانند رشد کنند و یا ... «جمع» نامیده نمی‌شوند.
نظرات اشتراک‌ها
لیست شهرهای ایران به صورت فایل xml
ممنون جناب نصیری اما چندتا سوال:
<cities>
  <state>&nbsp;</state> 
  <city>&nbsp;</city> 
  <province>&nbsp;</province> 
  </cities>
فلسفه وجود این نودها را میگید چیه؟ منکه ذهنم یاری نکرد!
خودتون تستش کردید؟ فک کنم یه مشکلات کوچیکی داره:
مثلا وقتی لیست استانها را بصورت Distinct میگیرم اصفهان، بوشهر، اذربایجان شرقی رو 2بار تکرار میکنه
ممنون
اشتراک‌ها
برنامه‌ای جهت حذف فایلها و فولدرهای قدیمی پوشه Setup ویژوال استادیو 2017

همانطور که می‌دانیم برای دانلود و نصب آفلاین ویژوال استادیو 2017 باید از کامند مخصوص آن به صورت ذیل استفاده نمود.

vs_enterprise.exe --layout C:\vs2017offline --lang en-US
حال در آینده نیز با تکرار همین کامند می‌توان پکیج‌های داخل آن را آپدیت نمود و یک پوشه نصب به روز در اختیار داشت ولی مشکلی که وجود دارد این است که پکیج‌های قدیمی نیز باقی می‌مانند و باعث می‌شود رفته رفته حجم پوشه نصب افزوده شود.
با استفاده از نرم افزار کاربردی معرفی شده می‌توان پکیج‌های قدیمی پوشه نصب را پیدا کرده و حذف نمود.

برنامه‌ای جهت حذف فایلها و فولدرهای قدیمی پوشه Setup ویژوال استادیو 2017
نظرات مطالب
Blazor 5x - قسمت دهم - مبانی Blazor - بخش 7 - مسیریابی

یک نکته‌ی تکمیلی: امکان تعریف مسیریابی صفحات، با استفاده از ویژگی Route

عموما مسیریابی‌های صفحات Blazor، به صورت زیر تعریف می‌شوند:

@page "/counter"

و اگر نیاز باشد تا این مسیر را در قسمت‌های دیگری هم ذکر کنیم (برای مثال در لینک‌ها و یا متد NavigateTo)، باید دقیقا همین مسیر و عبارت را در چندین قسمت برنامه تکرار کنیم. برای رفع این مشکل، با استفاده از ویژگی Route می‌توان مسیریابی فوق را به صورت زیر بازنویسی کرد:

@attribute [Route(Constants.CounterRoute)]

در این حالت می‌توان از مزیت تعریف مسیر مدنظر به صورت یک ثابت، به صورت زیر استفاده کرد:

public static class Constants
{
    public const string CounterRoute = "/counter";
}

و اگر در قسمت دیگری از برنامه نیاز به ارجاعی به آن بود، می‌توان همین رشته‌ی ثابت را مجددا مورد استفاده قرار داد:

NavigationManager.NavigateTo(Constants.CounterRoute);

نظرات مطالب
از سرگیری مجدد، لغو درخواست و سعی مجدد دریافت فایل‌های حجیم توسط HttpClient
یکی از صفحات وب سایتم که با mvc5  نوشته شده است مخصوص دانلود فایل اپلیکیشن هست و حدودا روزانه یک میلیون بازدید دارد کانفیگ سرور عالی ولی متاسفانه بعضی از مواقع هنگامی که کاربر دکمه دانلود فایل را کلیک میکند فایل شروع به دانلود و سپس متوقف میشود و مجددا پس از چند ثانیه  فایل جدید شروع به دانلود مشود و متوقف میشود و این مسئله چند بار تکرار میشود و در نهایت یک فایل دانلود میشود.یکی از راه حلهایی که به ذهنمان رسید ایجاد سرورهای دانلود فایل  بود که متاسفانه در حال حاضر برایمان مقدور نیست.ایا راه حلی وجود دارد که    مشکل را حل کند؟
نظرات مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
ضمن قدردانی از انتشار این دوره، در یک پروژه‌ی Blazor Server با در نظر گرفتن اینکه :

- از Asp.net Identity استفاده نمیکنیم.
- دسترسی به دیتا فقط از طریق API بوده و API‌ها هم نیاز به احراز هویت از طریق JWT دارند.
- از cookie authentication برای احراز هویت در سمت اپلیکیشن Blazor Server استفاده شده. 
موارد زیر باید چگونه هندل شوند؟

- توکن JWT که از API لاگین دریافت کرده ایم بهتر است کجا ذخیره شود؟ با توجه به اینکه در سمت سرور دسترسی به Local Storage مرورگر نداریم.
- چگونه میتوان DefaultRequestHeaders.Authorization را برای ارسال توکن jwt به نحوی مقدار دهی کرد که نیاز به تکرار آن در تمام فراخوانی‌های HttpClient نباشد؟
- آیا امکان پیاده سازی jwt authentication در Blazor Server هم وجود دارد؟
لینک سورس پروژه  بر روی گیت هاب.