مطالب
ایجاد Drop Down List های آبشاری توسط Kendo UI
پیشتر مطلبی را در مورد ایجاد Drop Down List‌های به هم پیوسته توسط jQuery Ajax در این سایت مطالعه کرده بودید. شبیه به همان مطلب را اینبار قصد داریم توسط Kendo UI پیاده سازی کنیم.


مدل‌های برنامه

در اینجا قصد داریم لیست گروه‌ها را به همراه محصولات مرتبط با آن‌ها، توسط دو drop down list نمایش دهیم:
public class Category
{
    public int CategoryId { set; get; }
    public string CategoryName { set; get; }
 
    [JsonIgnore]
    public IList<Product> Products { set; get; }
}


public class Product
{
    public int ProductId { set; get; }
    public string ProductName { set; get; }
}
از ویژگی JsonIgnore جهت عدم درج لیست محصولات، در خروجی JSON نهایی تولیدی گروه‌ها، استفاده شده‌است.


منبع داده JSON سمت سرور

پس از مشخص شدن مدل‌های برنامه، اکنون توسط دو اکشن متد، لیست گروه‌ها و همچنین لیست محصولات یک گروه خاص را با فرمت JSON بازگشت می‌دهیم:
using System.Linq;
using System.Text;
using System.Web.Mvc;
using KendoUI12.Models;
using Newtonsoft.Json;
 
namespace KendoUI12.Controllers
{
 
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View(); // shows the page.
        }
 
        [HttpGet]
        public ActionResult GetCategories()
        {
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(CategoriesDataSource.Items),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
 
        [HttpGet]
        public ActionResult GetProducts(int categoryId)
        {
 
            var products = CategoriesDataSource.Items
                            .Where(category => category.CategoryId == categoryId)
                            .SelectMany(category => category.Products)
                            .ToList();
 
            return new ContentResult
            {
                Content = JsonConvert.SerializeObject(products),
                ContentType = "application/json",
                ContentEncoding = Encoding.UTF8
            };
        }
    }
}
بار اولی که صفحه بارگذاری می‌شود، توسط یک درخواست Ajax ایی، لیست گروه‌ها دریافت خواهد شد. سپس با انتخاب یک گروه، اکشن متد GetProducts جهت بازگرداندن لیست محصولات آن گروه، فراخوانی می‌گردد.
در اینجا به عمد از JsonConvert.SerializeObject استفاده شده‌است تا ویژگی JsonIgnore کلاس گروه‌ها، توسط کتابخانه‌ی JSON.NET مورد استفاده قرار گیرد (ASP.NET MVC برخلاف ASP.NET Web API به صورت پیش فرض از JSON.NET استفاده نمی‌کند).


کدهای سمت کاربر برنامه

کدهای جاوا اسکریپتی Kendo UI را جهت تعریف دو drop down list به هم مرتبط و آبشاری، در ادامه ملاحظه می‌کنید:
<!--نحوه‌ی راست به چپ سازی -->
<div class="k-rtl k-header demo-section">
    <label for="categories">گروه‌ها: </label><input id="categories" style="width: 270px" />
    <label for="products">محصولات: </label><input id="products" disabled="disabled" style="width: 270px" />
</div>
 
@section JavaScript
{
    <script type="text/javascript">
        $(function () {
            $("#categories").kendoDropDownList({
                optionLabel: "انتخاب گروه...",
                dataTextField: "CategoryName",
                dataValueField: "CategoryId",
                dataSource: {
                    transport: {
                        read: {
                            url: "@Url.Action("GetCategories", "Home")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET'
                        }
                    }
                }
            });
 
            $("#products").kendoDropDownList({
                autoBind: false, // won’t try and read from the DataSource when it first loads
                cascadeFrom: "categories", // the id of the DropDown you want to cascade from
                optionLabel: "انتخاب محصول...",
                dataTextField: "ProductName",
                dataValueField: "ProductId",
                dataSource: {
                    // When the serverFiltering is disabled, then the combobox will not make any additional requests to the server.
                    serverFiltering: true, // the DataSource will send filter values to the server
                    transport: {
                        read: {
                            url: "@Url.Action("GetProducts", "Home")",
                            dataType: "json",
                            contentType: 'application/json; charset=utf-8',
                            type: 'GET',
                            data: function () {
                                return { categoryId: $("#categories").val() };
                            }
                        }
                    }
                }
            });
        });
    </script>
 
    <style scoped>
        .demo-section {
            width: 100%;
            height: 100px;
        }
    </style>
}
دراپ داون اول، به صورت متداولی تعریف شده‌است. ابتدا فیلدهای Text و Value هر ردیف آن مشخص و سپس منبع داده آن به اکشن متد GetCategories تنظیم گردیده‌است. به این ترتیب با اولین بار مشاهده‌ی صفحه، این دراپ داون پر خواهد شد.
سپس دراپ دوم که وابسته‌است به دراپ داون اول، با این نکات طراحی شده‌است:
الف) خاصیت autoBind آن به false تنظیم شده‌است. به این ترتیب این دراپ داون در اولین بار نمایش صفحه، به سرور جهت دریافت اطلاعات مراجعه نخواهد کرد.
ب) خاصیت cascadeFrom آن به id دراپ داون اول تنظیم شده‌است.
ج) در منبع داده‌ی آن دو تغییر مهم وجود دارند:
- خاصیت serverFiltering به true تنظیم شده‌است. این مورد سبب خواهد شد تا آیتم گروه انتخاب شده، به سرور ارسال شود.
- خاصیت data نیز تنظیم شده‌است. این مورد پارامتر categoryId اکشن متد GetProducts را تامین می‌کند و مقدار آن از مقدار انتخاب شده‌ی دراپ داون اول دریافت می‌گردد.

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


سپس با انتخاب یک گروه، لیست محصولات مرتبط با آن در دراپ داون دوم ظاهر می‌گردند:



کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
یک متد الحاقی لاگ ELMAH را ایجاد کنید:
using System;
using System.Text;
using Elmah;

namespace Common.WebToolkit
{
    public static class ElmahLogEx
    {
        public static void LogException(this string ex)
        {
            if (string.IsNullOrWhiteSpace(ex))
                return;
            LogException(new Exception(ex));
        }

        public static void LogException(this Exception ex)
        {
            if (ex == null) return;
            try
            {
                ErrorSignal.FromCurrentContext().Raise(ex);
            }
            catch
            {
                ErrorLog.GetDefault(null).Log(new Error(ex));
            }
        }
    }
}
سپس
- در کتابخانه‌ی فوق به قسمت ScheduledTasksCoordinator.Current.OnUnexpectedException هم دقت داشته باشد؛ مطابق مثال ارائه شده. این موارد را هم لاگ کنید:
 ScheduledTasksCoordinator.Current.OnUnexpectedException =
                    (exception, scheduledTask) => (scheduledTask.Name + ":" + exception).LogException();
- ابتدا و انتهای هر Task را لاگ کنید (متد الحاقی فوق را به صورت معمولی و با پیام‌هایی مشخص، در ابتدا و انتهای هر Task فراخوانی کنید؛ تا در لاگ‌های ELMAH ظاهر شوند).
- شروع به کار برنامه و خاتمه‌ی آن‌را لاگ کنید (متد الحاقی فوق را با پیام‌هایی مشخص، در متدهای Application_Start و Application_End فایل Global.asax.cs فراخوانی کنید تا مشخص شود که آیا برنامه خاتمه یافته‌است یا خیر).
نظرات مطالب
وی‍‍ژگی های پیشرفته ی AutoMapper - قسمت دوم
attribute‌های مدل مانند Display را چرا وقتی Map میکنیم نمیاره؟

به طور مثال در صورتی میاره که به شکل زیر باشه
public class Customer
  {
    public Customer()
    {
      Orders = new List<Order>();
    }
    [StringLength(10)]
    public string Title { get; set; }

    [Display(Name = "نام")]
    public string FirstName { get; set; }

    [Display(Name = "نام خانوادگی")]
    public string LastName { get; set; }
    public ICollection<Order> Orders { get; set; }
}

public class CustomerViewModel
{
    public Customer Customer{ get; set; }
}


مطالب
بهبود SEO در ASP.NET MVC
گوگل خلاصه نتایج Indexing یک سایت را توسط ابزاری به نام Google webmaster tools در اختیار علاقمندان قرار می‌دهد. Bing نیز چنین ابزاری را تدارک دیده است.
به آمارهای خطای حاصل از سایت جاری که دقت می‌کردم یک نکته آن جالب بود: «محتوای تکراری»


mydomain.com/Home/Index
mydomain.com/home/index
mydomain.com/Home/index
mydomain.com/home/Index
همانطور که ملاحظه می‌کنید، گوگل به کوچکی و بزرگی حروف بکار رفته در لینک‌ها حساس است. هرچند 4 لینک فوق به یک صفحه اشاره می‌کنند، اما گوگل 4 بار آن‌ها را ایندکس خواهد کرد و نهایتا به صورت یک خطای «محتوای تکراری» در گزارشات SEO آن ظاهر خواهد شد (به همراه کاهش رتبه SEO سایت).

راه حل

برای حل این مساله دو نکته باید درنظر گرفته شود:
الف) هدایت دائمی (Redirect permanent) صفحات قدیمی به صفحاتی جدید، با آدرس lowercase

using System.Globalization;
using System.Web;
using System.Web.Mvc;

namespace WebToolkit
{
    public class ForceWww : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            modifyUrlAndRedirectPermanent(filterContext);
            base.OnActionExecuting(filterContext);
        }

        private static void modifyUrlAndRedirectPermanent(ActionExecutingContext filterContext)
        {
            if (canIgnoreRequest(filterContext))
                return;

            var absoluteUrl = HttpUtility.UrlDecode(filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.ToString(CultureInfo.InvariantCulture));
            var absoluteUrlToLower = absoluteUrl.ToLowerInvariant();

            absoluteUrlToLower = forceWwwAndLowercase(filterContext, absoluteUrlToLower);
            absoluteUrlToLower = avoidTrailingSlashes(filterContext, absoluteUrlToLower);

            if (!absoluteUrl.Equals(absoluteUrlToLower))
            {
                filterContext.Result = new RedirectResult(absoluteUrlToLower, permanent: true);
            }
        }

        private static string avoidTrailingSlashes(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (!isRootRequest(filterContext) && absoluteUrlToLower.EndsWith("/"))
                return absoluteUrlToLower.TrimEnd(new[] { '/' });

            return absoluteUrlToLower;
        }

        private static bool isRootRequest(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.Url.AbsolutePath == "/";
        }

        private static bool canIgnoreRequest(ActionExecutingContext filterContext)
        {
            return filterContext.IsChildAction || 
                   filterContext.HttpContext.Request.IsAjaxRequest() ||
                   filterContext.RequestContext.HttpContext.Request.Url.AbsoluteUri.Contains("?");
        }

        private static string forceWwwAndLowercase(ActionExecutingContext filterContext, string absoluteUrlToLower)
        {
            if (isLocalRequet(filterContext))
                return absoluteUrlToLower;

            if (absoluteUrlToLower.Contains("www"))
                return absoluteUrlToLower;

            return absoluteUrlToLower.Replace("http://", "http://www.")
                                     .Replace("https://", "https://www.");
        }

        private static bool isLocalRequet(ActionExecutingContext filterContext)
        {
            return filterContext.RequestContext.HttpContext.Request.IsLocal;
        }
    }
}
کلاس فوق، نگارش تکمیل شده ForceWww که پیشتر در این سایت دیده‌اید. توسط آن سه بررسی مختلف بر روی لینک جاری در حال پردازش صورت خواهد گرفت:
- تمام آدرس‌های سایت باید www داشته باشند؛ تا آدرس‌های آن یکنواخت شده و خصوصا مشکلات لاگین و نوشته شدن کوکی‌ها به ازای آدرس‌های مختلف و سر درگمی کاربران کاهش یابد.
- اگر آدرس جاری lowercase نباشد، تبدیل به نمونه lowercase شده و درخواست کننده، به آدرس جدید هدایت می‌شود. این مورد خصوصا جهت موتورهای جستجو برای تصحیح نتایج آن‌ها بسیار مفید است.
- اسلش انتهای لینک‌ها در صورت وجود حذف خواهد شد. این مورد نیز در کاهش تعداد خطاهای «محتوای تکراری» مؤثر است.
- اگر آدرسی، کوئری استرینگ داشته باشد از آن صرفنظر خواهد شد؛ زیرا ممکن است اطلاعات موجود در آن به کوچکی و بزرگی حروف حساس باشند.


ب) کاهش بار سایت توسط تولید خودکار Urlهایی که در بدو امر lowercase هستند

برای پیاده سازی این مطلب می‌توان از پروژه سورس باز «LowercaseRoutesMVC» استفاده کرد. سه فایل cs دارد که می‌توانید به پروژه خود اضافه کنید. پس از آن، هرجایی در پروژه خود routes.MapRoute دارید تبدیل کنید به routes.MapRouteLowercase .
به این ترتیب به صورت خودکار تمام Urlهای تولید شده توسط HTML helpers توکار ASP.NET MVC (و نه Urlهایی که دستی نوشته شده‌اند)، در حین درج در صفحه به صورت lowercase ظاهر خواهند شد (صرفنظر از اینکه نام‌های کنترلرها و یا اکشن متدهای تعریف شده camel case هستند یا خیر). مزیت این مساله کاهش یک مرحله Redirect است که در قسمت الف ذکر شد. در این کتابخانه کمکی نیز از آدرس‌هایی که دارای کوئری استرینگ باشند، صرفنظر می‌شود.
مطالب
AOP با استفاده از Microsoft Unity
چند روز پیش فرصتی پیش آمد تا بتوانم مروری بر مطلب منتشر شده درباره AOP داشته باشم. به حق مطلب مورد نظر، بسیار خوب و مناسب شرح داده شده بود و همانند سایر مقالات جناب نصیری چیزی کم نداشت. اما امروز قصد پیاده سازی یک مثال AOP، با استفاده از Microsoft Unity Application Block را به عنوان IOC Container دارم. اگر شما هم، مانند من از UnityContainer به عنوان IOC Container در پروژه‌های خود استفاده می‌کنید نگران نباشید. این کتابخانه به خوبی از مباحث Interception پشتیبانی می‌کند. در ادامه طی یک مقاله این مورد را با هم بررسی می‌کنیم.
برای دوستانی که با AOP آشنایی ندارند پیشنهاد می‌شود ابتدا مطلب مورد نظر را یک بار مطالعه نمایند.
برای شروع یک پروژه در VS.Net بسازید و ارجاع به اسمبلی‌های زیر را در پروژه فراموش نکنید:
»Microsoft.Practices.EnterpriseLibrary.Common
»Microsoft.Practices.Unity
»Microsoft.Practices.Unity.Configuration
»Microsoft.Practices.Unity.Interception
»Microsoft.Practices.Unity.Interception.Configuration

یک اینترفیس به نام IMyOperation بسازید:
    public interface IMyOperation
    {      
        void DoIt();
    }

کلاسی می‌سازیم که اینترفیس بالا را پیاده سازی نماید:
 public void DoIt()
  {
     Console.WriteLine( "this is main block of code" );
  }
قصد داریم با استفاده از AOP یک سری کد مورد نظر خود(در این مثال کد لاگ کردن عملیات در یک فایل مد نظر است) را به کد‌های متد‌های مورد نظر تزریق کنیم. یعنی با فراخوانی این متد کد‌های لاگ عملیات در یک فایل ذخیره شود بدون تکرار یا فراخوانی دستی متد لاگ.
ابتدا یک کلاس برای لاگ عملیات می‌سازیم:
public class Logger
    {
        const string path = @"D:\Log.txt";

        public static void WriteToFile( string methodName )
        {
            object lockObject = new object();
            if ( !File.Exists( path ) )
            {
                File.Create( path );
            }
            lock ( lockObject )
            {
                using ( TextWriter writer = new StreamWriter( path , true ) )
                {
                    writer.WriteLine( string.Format( "{0} at {1}" , methodName , DateTime.Now ) );                
                }
            }
        }
    }
حال نیاز به یک Handler برای مدیریت فراخوانی کد‌های تزریق شده داریم. برای این کار یک کلاس می‌سازیم که اینترفیس ICallHandler را پیاده سازی نماید.
public class LogHandler : ICallHandler
    {
        public IMethodReturn Invoke( IMethodInvocation input , GetNextHandlerDelegate getNext )
        {
            Logger.WriteToFile( input.MethodBase.Name );

            var methodReturn = getNext()( input , getNext );         

            return methodReturn;
        }

        public int Order { get; set; }
    }
کلاس بالا یک متد به نام Invoke دارد که فراخوانی متد‌های مورد نظر برای تزریق کد را در دست خواهد گرفت. در این متد ابتدا عملیات لاگ در فایل مورد نظر ثبت می‌شود(با استفاده از Logger.WriteToFile). سپس با استفاده از getNext که از نوع GetNextHandlerDelegate است، اجرا را به کد‌های اصلی برنامه منتقل می‌کنیم.
 var methodReturn = getNext()( input , getNext );
برای مدیریت بهتر عملیات لاگ یک Attribute می‌سازیم که فقط متد هایی که نیاز به لاگ کردن دارند را مشخص کنیم. به صورت زیر:
 public class LogAttribute : HandlerAttribute
    {
        public override ICallHandler CreateHandler( Microsoft.Practices.Unity.IUnityContainer container )
        {
            return new LogHandler();
        }
    }
فقط دقت داشته باشید که کلاس مورد نظر به جای ارث بری از کلاس Attribute باید از کلاس HandlerAttribute که در فضای نام Microsoft.Practices.Unity.InterceptionExtension  تعبیه شده است ارث ببرد(خود این کلاس از کلاس Attribute ارث برده است).  کافیست در متد CreateHandler آن که Override شده است یک نمونه از کلاس LogHandler را برگشت دهیم.
برای آماده سازی Ms Unity جهت عملیات Interception باید کد‌های زیر در ابتدا برنامه قرار داده شود:
var  unityContainer = new UnityContainer();

 unityContainer.AddNewExtension<Interception>();

  unityContainer.Configure<Interception>().SetDefaultInterceptorFor<IMyOperation>( new InterfaceInterceptor() );
            
  unityContainer.RegisterType<IMyOperation, MyOperation>();

توضیح چند مطلب:
بعد از نمونه سازی از کلاس UnityContainer باید Interception به عنوان یک Extension به این Container اضافه شود. سپس با استفاده از متد Configure برای اینترفیس IMyOperation یک Interceptor پیش فرض تعیین می‌کنیم. در پایان هم به وسیله متد RegisterType کلاس MyOperation  به اینترفیس IMyOperation رجیستر می‌شود. از این پس هر گاه درخواستی برای اینترفیس IMyOperation از unityContainer شود یک نمونه از کلاس MyOperation در اختیار خواهیم داشت.
به عنوان نکته آخر متد DoIt در اینترفیس بالا باید دارای LogAttribute باشد تا عملیات مزین سازی با کد‌های لاگ به درستی انجام شود.

یک نکته تکمیلی:
در هنگام مزین سازی متد  set خاصیت ها، به دلیل اینکه اینترفیسی برای این کار وجود ندارد باید مستقیما عملیات AOP به خود کلاس اعمال شود. برای این کار باید به صورت زیر عمل نمود:

var container = new UnityContainer();
container.RegisterType<Book , Book>();

container.AddNewExtension<Interception>();

 var policy = container.Configure<Interception>().SetDefaultInterceptorFor<Book>( new VirtualMethodInterceptor() ).AddPolicy( "MyPolicy" );

  policy.AddMatchingRule( new PropertyMatchingRule( "*" , PropertyMatchingOption.Set ) );
  policy.AddCallHandler<Handler.NotifyChangedHandler>();
همان طور که مشاهده می‌کنید عملیات Interception مستقیما برای کلاس پیکر بندی می‌شود و به جای InterfaceInterceptor از VirtualMethodInterceptor برای تزریق کد به بدنه متد‌ها استفاده شده است. در پایان نیز با تعریف یک Policy می‌توانیم به راحتی(با استفاده از "*") متد Set  تمام خواص کلاس را به NotifyChangedHandler مزین نماییم.

سورس کامل مثال بالا
بازخوردهای دوره
استفاده از StructureMap به عنوان یک IoC Container
هر زمانیکه ObjectFactory حذف شد، آن‌را با پیاده سازی زیر جایگزین کنید. کار کردن با آن هم از طریق ObjectFactory.Container خواهد بود.
public static class ObjectFactory
    {
        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(x =>
            {
                // تنظیمات در اینجا
            });
        }
    }
پرسش‌ها
آیا امکان استفاده از Extension Method در زمان Select وجود دارد؟

سلام

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

var result = await query.Select(x => new Models.Output.Piping.LineJoints.LineJoint2
{
    Id = x.Id,
    JointNo = x.JointNo
})
.ToListAsync();

حالا برای اینکه از تکرار جلوگیری کنم، یک Extension Method نوشتم که کار تبدیل رو انجام بده:

public static class Ext
{
    public static Models.Output.Piping.LineJoints.LineJoint2 ToModel(
        this Domain.Entities.Piping.LineJoints.LineJoint domain)
    {
        return new Models.Output.Piping.LineJoints.LineJoint2
        {
            Id = domain.Id,
            JointNo = domain.JointNo
        };
    }
}

در نهایت Query را بصورت زیر تغییر دادم:

var result = await query.Select(x => x.ToModel()).ToListAsync();

سوال:

در حالت اول که تمام ستون ها را تعریف میکنم، بانک اطلاعاتی دقیقا همان ستون ها را بر میگرداند ولی در حالتی که از Extension Methodاستفاده کردم، بانک اطلاعاتی تمامی ستون ها را بر می گرداند و در سمت Client تبدیل انجام می شود. آیا راهی وجود داره که بتونم از نوشتن نام تمام ستون ها همانند نمونه اولیه جلوگیری کنم و از نام یک کلاس و یا چیزی شبیه Extension استفاده کنم؟ تشکر

مطالب
اعمال SEO بر روی AngularJS
در این بخش قصد داریم سئو را بر روی یک برنامه‌ی نوشته شده با آنگلولار و Asp.net Mvc اعمال نماییم. انگولار جی‌اس، صفحات را با  استفاده از جاوااسکریپت رندر میکند، ولی اکثر کرالر‌ها نمیتوانند جاوااسکریپت را اجرا کنند و موقع اجرای صفحات سایت ما  فقط یک div خالی را میبینند.
کاری که سرویس Prerender یا فیلتر سفارشی AjaxCrawlable برای ما انجام میدهد، درخواست‌هایی را که از طرف کرالرها آمده‌است را شناسایی میکند و مانند یک مرورگر، با استفاده از phantomjs آنرا اجرا میکند و نتیجه‌ی کامل صفحات ما را به صورت اچ تی ام ال استاتیک برمی‌گرداند.
فانتوم جی اس، موتور اختصاصی برای شبیه سازی مرورگر مبتنی بر Webkit می‌باشد. فانتوم جی اس را میتوانید بر روی ویندوز، لینوکس و مک نصب نمایید. فانتوم جی اس یک Console در اختیار برنامه نویس قرار می‌دهد که می‌توان توسط آن، برنامه‌های جاوااسکریپت را اجرا نمود. همچنین فانتوم جی اس میتواند اسکرین شاتی را نیز از محتوای وب سایت ما فراهم نماید.
 برای اینکه صفحات انگولار جی اس،ایندکس شوند سه مرحله وجود دارند:
1- به کرالر اطلاع دهیم که رندر کردن سایت، توسط جاوااسکریپت انجام میگردد؛ با اضافه کردن متاتگ زیر در اچ تی ام ال سایت (البته در حالت استفاده HTML5 push state ) :
<meta name="fragment" content="!">
<base href="/">
2- بعد از اضافه کردن متاتگ بالا، کرالر درخواست‌های خود را به صورت زیر به سایت ما ارسال میکند:
http://www.example.com/?_escaped_fragment_=
ما در این مثال از  HTML5 push state  استفاده میکنیم. بنابراین لینکی مانند http://www.example.com/user/123 توسط کرالر به صورت زیر دیده میشود: 
http://www.example.com/user/123?_escaped_fragment_=
3- اچ تی ام ال کاملا رندر شده توسط سایت ما به کرالر ارسال گردد.
برای رندر کردن  اچ تی ام ال صفحات، چندین روش وجود دارد:
روش اول: میتوانیم از سرویس‌های آماده‌ای همچون Prerender.io   استفاده کنیم که سرویسهایی را برای زبانهای مختلف ارائه کرده‌اند. باتوجه به توضیحات نمونه استفاده از آن در Asp.Net Mvc کافیست در سایت Prerender.io  ثبت نام کرده، Token را دریافت کنیم و در کانفیگ برنامه قرار دهیم و در کلاس PreStart قطعه کد زیر را قرار دهیم:
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
مثال استفاده از Prerender.io را میتوانید از این آدرس Simple_Demo_Prerender.zip دانلود نمایید.
 
یکی از ابزارهای مناسب تست کردن اینکه صفحات توسط کرالر ایندکس میشوند یا خیر، برنامه screamingfrog میباشد.
در پنل Ajax آن، صفحات ایندکس شده ما نمایش داده میشوند. لینکی مشابه زیر را در مرورگر اجرا کرده، با ViewPage Source کردن آن میتوانید نتیجه اچ تی ام ال کاملا رندر شده را مشاهده نمایید.
http://www.example.com/user/123?_escaped_fragment_=
نسخه رایگان سرویس Prerender.io تا 250 صفحه را پوشش میدهد.

روش دوم: فیلتر سفارشی AjaxCrawlable. در اولین قدم نیاز به نصب فانتوم جی اس داریم:
<package id="PhantomJS" version="1.9.2" targetFramework="net452" />
<package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
فایل phantomjs.exe را از پوشه packages\PhantomJS.1.9.2\tools\phantomjs\phantomjs.exe یافته و در پوشه bin برنامه قرار دهید. با Attribute زیر هر درخواستی که توسط کرالر ارسال گردد به اکشن returnHTML منتقل میگردد.
برای اینکه خطای معروف A potentially dangerous Request.Form value was detected from the client را دریافت نکنیم، کافیست قسمتهایی از آدرس را که شامل کاراکترهای خاصی مانند :// میباشند، از url حذف کنیم و در اکشن returnHtml قسمتهای حذف شده را  به url  اضافه نماییم.
کرالرها  با مشاهده تگ fragment، تمام لینکها را به همراه کوئری استرینگ _escaped_fragment_  میفرستند، که ما در سرور باید آنرا  با رشته خالی جایگزین نماییم.
 public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;
            var url = request.Url.ToString();
            if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML"))
            {
                url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty);
                url = url.Split(':')[1];
                filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
Route‌های پیشفرض را با کدهای زیر جایگزین میکنیم:
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
             name: "HtmlSnapshot",
             url: "HtmlSnapshot/returnHTML/{*url}",
             defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional });

            routes.MapRoute(
            name: "SPA",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index" })
        }
 اضافه کردن این فیلتر به فیلترهای Asp.net Mvc 
 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
ایجاد کنترلر HtmlSnapshot و متد returnHTML :
Url را به عنوان آرگومان به تابع page.open فایل جاوااسکریپتی فانتوم میدهیم و بعد از اجرای کامل، خروجی را درViewData قرار میدهیم 
public ActionResult returnHTML(string url)
        {
            var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":";
            url = prefix+url;
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
            var startInfo = new ProcessStartInfo
            {
                Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output1 = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output1.Replace("<!-- ngView:  -->", "").Replace("ng-view=\"\"", "");
            return View();
        }
در فایل renderHtml.cshtml
@{ 
    Layout = null;
}
@Html.Raw(ViewBag.result)
ایجاد فایل seo.js  در پوشه Scripts سایت :
در این بخش webpage  را ایجاد میکنیم و آدرس صفحه را از[system.args[1  دریافت کرده و عملیات کپچر کردن را آغاز میکنیم و بعد از تکمیل اطلاعات در سرور، کد زیر اجرا میشود:
console.log(page.content)

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();;
page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () {

});

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        console.log(page.content);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
صفحه Layout.Cshtml
<!DOCTYPE html>
<html ng-app="appOne">
<head>
    <meta name="fragment" content="!">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    <base href="/">
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <script src="~/Scripts/angular/angular.js"></script>
    <script src="~/Scripts/angular/angular-route.js"></script>
    <script src="~/Scripts/angular/angular-animate.js"></script>
    <script>
        angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) {
            $routeProvider.when('/one', {
                template: "<div>one</div>", controller: function ($scope) {
                }
            })
            .when('/two', {
                template: "<div>two</div>", controller: function ($scope) {
                }
            }).when('/', {
                template: "<div>home</div>", controller: function ($scope) {
                }
            });
            $locationProvider.html5Mode({
                enabled: true
            });
        });
    </script>
</head>
<body>
    <div id="body">
        <section ng-view></section>
        @RenderBody()
    </div>
    <div id="footer">
        <ul class='xoxo blogroll'>
            <li><a href="one">one</a></li>
            <li><a href="two">two</a></li>
        </ul>
    </div>
</body>
</html>

چند نکته تکمیلی:
* فانتوم جی اس قادر به اجرای لینکهای فارسی (utf-8) نمیباشد.
 * اگر خطای syntax error را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
نظرات مطالب
نوشتن TagHelperهای سفارشی برای ASP.NET Core
یک نکته: برای تبدیل محتوای یک TagBuilder به رشته می‌توان از متد زیر استفاده کرد:
using System.IO;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;

namespace Sample
{
    /// <summary>
    /// Html Helper Extensions
    /// </summary>
    public static class HtmlHelperExtensions
    {
        /// <summary>
        /// Convert IHtmlContent/TagBuilder to string
        /// </summary>
        public static string GetString(this IHtmlContent content)
        {
            using (var writer = new StringWriter())
            {
                content.WriteTo(writer, HtmlEncoder.Default);
                return writer.ToString();
            }
        }
    }
}
نظرات مطالب
مسیریابی (Routing) در ASP.NET MVC 5.x
ممنون از توجه شما
 با توجه به راهنمایی شما به صورت زیر عمل نمودم و درست هم جواب میدهد:
public static void RegisterRoutes(RouteCollection routes)
        {

            using (routes.GetWriteLock())
            {
                var pages = Task.Run(async () => { return await _pageService.FindAllAsync(); }).Result;

                routes.Clear();

                routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

                pages.ToList().ForEach(page => routes.IgnoreRoute(url: page.Url));

                routes.MapRoute(name: "Default",
                    url: "{controller}/{action}/{id}",
                    defaults: new
                    {
                        controller = MVC.Home.Name,
                        action = MVC.Home.ActionNames.Index,
                        id = UrlParameter.Optional
                    },
                    namespaces: new[] { $"{typeof(RouteConfig).Namespace}.Controllers" });

                var defaultRoute = routes.Last();
                routes.Remove(defaultRoute);

                //  routes.MapRoute(yadayada);

                routes.Add(defaultRoute);
            }
        }
ولی مشکلی که وجود داره این هست که ممکنه url هایی که از دیتابیس بازیابی شده اند بعد از مدتی از حالت Ignore خارج کنیم، به نظر شما چه راه حلی وجود داره که بعد از بالا آمدن برنامه، بتوانیم Route Collection را بروزرسانی نماییم؟