نظرات مطالب
ASP.NET MVC #5
فرق
 public string FirstName { get; set; }
با
       private string firstName;

       public string FirstName
        {
            get { return firstName; }
            set { firstName = value; }
        }
چیه
آخه تو دانشگاه به ما یاد دادند شبیه دومی بنویسیم اما اولی راحتتره حالا کلا با هم چه فرقی دارند؟
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 18 - کار با ASP.NET Web API
با توجه به مطالب مطرح شده در متن فوق و  نحوه استفاده از ViewModel در ASP.NET MVC و همچنین توصیه‌هایی که در رابطه با آدرس‌دهی صحیح WebApiها وجود دارد (استفاده از اسم جمع، استفاده از اسم به جای فعل و ...)، در رابطه با آدرس‌دهی صحیح برای تامین اطلاعات مورد نیاز Viewها  (در حالت ویرایش یا افزودن)، در سمت کلاینت که استفاده کننده آن می‌تواند یک کامپوننت Angular یا هر نوع دیگری باشد آیا دوستان نظر و Best Practice دارند؟
به طور مثال برای ویرایش و افزودن یک محصول به صورت زیر عمل می‌کنیم:
   //ViewModels

    public class CustomListItem
    {
        public int Id { get; set; }
        public string Text { get; set; }
    }

    public class ProductAddGetViewModel
    {
        public IEnumerable<CustomListItem> Categories { get; set; }
        public IEnumerable<CustomListItem> Groups { get; set; }
    }

    public class ProductAddViewModel
    {
        public string Name { get; set; }
        public bool IsActive { get; set; }

        public int CategoryId { get; set; }
        public int GroupId { get; set; }
    }

    public class ProductEditGetViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public IEnumerable<CustomListItem> Categories { get; set; }
        public IEnumerable<CustomListItem> Groups { get; set; }
    }

    public class ProductEditViewModel
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public int CategoryId { get; set; }
        public int GroupId { get; set; }
    }
و
    // ProductsController - ApiControler

    // GET: api/products/views/add
    [HttpGet("views/add")]
    public async Task<IActionResult> GetAdd()
    {
        ProductAddGetViewModel model = await _productService.GetAddModelAsync();
        return Ok(model)
    }

    // POST: api/products
    [HttpPost]
    public async Task<IActionResult> Add(ProductAddViewModel model)
    {
        ...
    }


    // GET: api/products/5/views/edit
    [HttpGet("{id}/views/edit")]
    public async Task<IActionResult> GetEdit(int id)
    {
        ProductEditGetViewModel model = await _productService.GetEditModelAsync(id);
        return Ok(model)
    }

    // PUT: api/products/5
    [HttpPut("{id}")]
    public async Task<IActionResult> Edit(int id, ProductEditViewModel model)
    {
        ...
    }
با توجه به اینکه حالت فوق، احتمالاً دو متد از چند متد مورد استفاده می‌باشد، آیا دوستان درباره متدهای GetAdd , GetEdit و همچنین آدرس‌دهی صحیح این نوع متدها که قرار است از سمت کلاینت فراخوانی شود نظری دارند؟
پ.ن: درباره نامگذاری بهتر ViewModelها هم اگر نظری هست ممنون میشم بیان شود.
مطالب
آشنایی با ویژگی DebuggerTypeProxy در VS.Net
در مطالب قبلی، ویژگی DebuggerDisplay معرفی شده بود. ویژگی دیگری شبیه به این ویژگی وجود دارد به نام DebuggerTypeProxy که در ادامه به معرفی آن می‌پردازیم.

کلاس زیر را در نظر بگیرید:
public class Data
{
    public string Name { get; set; }
    public string ValueInHex { get; set; }
}  
پس از اجرای برنامه ، مقادیر کلاس ایجاد شده به این صورت خواهند بود :


در اینجا مقدار Hex برایمان قابل فهم نیست. سناریویی را در نظر بگیرید که مقادیر باید داخل دیتابیس به صورت Hex نگهداری شوند، اما می‌خواهیم هنگام دیباگ، مقدار پراپرتی HexValue به صورت قابل درک و decimal آن نمایش داده شود.

برای انجام اینکار میتوانیم از DebuggerTypeProxy استفاده کنیم. ابتدا کلاسی ایجاد میکنیم که بعنوان proxy، مقادیر را به شکلی که نیاز داریم نمایش دهد. این کلاس object اصلی را در Constructor دریافت کرده و مقادیر مورد نظرمان، از طریق property هایی که در آن تعریف می‌کنیم قابل دسترسی هستند:

public class DataDebugView
{
    private readonly Data _data;

    public DataDebugView(Data data)
    {
        _data = data;
    }

    public string DecimalValue
    {
        get
        {
            bool isValidHex = int.TryParse(_data.HexValue, System.Globalization.NumberStyles.HexNumber, null, out var value);
            return isValidHex ? value.ToString() : "INVALID HEX STRING";
        }
    }
}

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

[DebuggerTypeProxy(typeof(DataDebugView))]
public class Data
{
    public string Name { get; set; }

    public string HexValue { get; set; }
}

بعد از اعمال تغییرات و اجرای دوباره برنامه، نحوه نمایش مقادیر کلاس به این صورت تغییر خواهند یافت:

مطالب
بارگذاری یک یوزرکنترل با استفاده از جی‌کوئری

مزیت استفاده از یوزر کنترل‌ها، ماژولار کردن برنامه است. برای مثال اگر صفحه جاری شما قرار است از چهار قسمت اخبار، منوی پویا ، سخن روز و آمار کاربران تشکیل شود، می‌توان هر کدام را توسط یک یوزر کنترل پیاده سازی کرده و سپس صفحه اصلی را از کنار هم قرار دادن این یوزر کنترل‌ها تهیه نمود.
با این توضیحات اکنون می‌خواهیم یک یوزکنترل ASP.Net را توسط jQuery Ajax بارگذاری کرده و نمایش دهیم. حداقل دو مورد کاربرد را می‌توان برای آن متصور شد:
الف) در اولین باری که یک صفحه در حال بارگذاری است، قسمت‌های مختلف آن‌را بتوان از یوزر کنترل‌های مختلف خواند و تا زمان بارگذاری کامل هر کدام، یک عبارت لطفا منتظر بمانید را نمایش داد. نمونه‌ی آن‌را شاید در بعضی از CMS های جدید دیده باشید. صفحه به سرعت بارگذاری می‌شود. در حالیکه مشغول مرور صفحه جاری هستید، قسمت‌های مختلف صفحه پدیدار می‌شوند.
ب) بارگذاری یک قسمت دلخواه صفحه بر اساس درخواست کاربر. مثلا کلیک بر روی یک دکمه و امثال آن.

روش کلی کار:
1) تهیه یک متد وب سرویس که یوزر کنترل را بر روی سرور اجرا کرده و حاصل را تبدیل به یک رشته کند.
2) استفاده از متد Ajax جی‌کوئری برای فراخوانی این متد وب سرویس و افزودن رشته دریافت شده به صفحه.
بدیهی است زمانیکه متد Ajax فراخوانی می‌شود می‌توان عبارت یا تصویر منتظر بمانید را نمایش داد و پس از پایان کار این متد، عبارت (یا تصویر) را مخفی نمود.

پیاده سازی:
قسمت تبدیل یک یوزر کنترل به رشته را قبلا در مقاله "تهیه قالب برای ایمیل‌های ارسالی یک برنامه ASP.Net" مشاهده کرده‌اید. در این‌جا برای استفاده از این متد در یک وب سرویس نیاز به کمی تغییر وجود داشت (KeyValuePair ها درست سریالایز نمی‌شوند) که نتیجه نهایی به صورت زیر است. یک فایل Ajax.asmx را به برنامه اضافه کرده و سپس در صفحه Ajax.asmx.cs کد آن به صورت زیر می‌تواند باشد:

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Script.Services;
using System.Web.Services;
using System.Web.UI;
using System.Web.UI.HtmlControls;

namespace AjaxTest
{
public class KeyVal
{
public string Key { set; get; }
public object Value { set; get; }
}

/// <summary>
/// Summary description for Ajax
/// </summary>
[ScriptService]
[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class Ajax : WebService
{
/// <summary>
/// Removes Form tags using Regular Expression
/// </summary>
private static string cleanHtml(string html)
{
return Regex.Replace(html, @"<[/]?(form)[^>]*?>", string.Empty, RegexOptions.IgnoreCase);
}

/// <summary>
/// تبدیل یک یوزر کنترل به معادل اچ تی ام ال آن
/// </summary>
/// <param name="path">مسیر یوزر کنترل</param>
/// <param name="properties">لیست خواص به همراه مقادیر مورد نظر</param>
/// <returns></returns>
/// <exception cref="NotImplementedException"><c>NotImplementedException</c>.</exception>
[WebMethod(EnableSession = true)]
[ScriptMethod(ResponseFormat = ResponseFormat.Json)]
public string RenderUserControl(string path,
List<KeyVal> properties)
{
Page pageHolder = new Page();

UserControl viewControl =
(UserControl)pageHolder.LoadControl(path);

viewControl.EnableViewState = false;

Type viewControlType = viewControl.GetType();

if (properties != null)
foreach (var pair in properties)
{
if (pair.Key != null)
{
PropertyInfo property =
viewControlType.GetProperty(pair.Key);

if (property != null)
{
if (pair.Value != null) property.SetValue(viewControl, pair.Value, null);
}
else
{
throw new NotImplementedException(string.Format(
"UserControl: {0} does not have a public {1} property.",
path, pair.Key));
}
}
}

//Form control is mandatory on page control to process User Controls
HtmlForm form = new HtmlForm();

//Add user control to the form
form.Controls.Add(viewControl);

//Add form to the page
pageHolder.Controls.Add(form);

//Write the control Html to text writer
StringWriter textWriter = new StringWriter();

//execute page on server
HttpContext.Current.Server.Execute(pageHolder, textWriter, false);

// Clean up code and return html
return cleanHtml(textWriter.ToString());
}
}
}
تا این‌جا متد وب سرویسی را داریم که می‌تواند مسیر یک یوزر کنترل را به همراه خواص عمومی آن‌را دریافت کرده و سپس یوزر کنترل را رندر نموده و حاصل را به صورت HTML به شما تحویل دهد. با استفاده از reflection خواص عمومی یوزر کنترل یافت شده و مقادیر لازم به آن‌ها پاس می‌شوند.

چند نکته:
الف) وب کانفیگ برنامه ASP.Net شما اگر با VS 2008 ایجاد شده باشد مداخل لازم را برای استفاده از این وب سرویس توسط jQuery Ajax دارد در غیر اینصورت موفق به استفاده از آن نخواهید شد.
ب) هنگام بازگرداندن این اطلاعات با فرمت json = ResponseFormat.Json جهت استفاده در jQuery Ajax ، گاهی از اوقات بسته به حجم بازگردانده شده ممکن است خطایی حاصل شده و عملیات متوقف شد. این طول پیش فرض را (maxJsonLength) در وب کانفیگ به صورت زیر تنظیم کنید تا مشکل حل شود:

<system.web.extensions>
<scripting>
<webServices>
<jsonSerialization maxJsonLength="10000000"></jsonSerialization>
</webServices>
</scripting>
</system.web.extensions>

برای پیاده سازی قسمت Ajax آن برای اینکه کار کمی تمیزتر و با قابلیت استفاده مجدد شود یک پلاگین تهیه شده (فایلی با نام jquery.advloaduc.js) که سورس آن به صورت زیر است:

$.fn.advloaduc = function(options) {
var defaults = {
webServiceName: 'Ajax.asmx', //نام فایل وب سرویس ما
renderUCMethod: 'RenderUserControl', //متد وب سرویس
ucMethodJsonParams: '{path:\'\'}',//پارامترهایی که قرار است پاس شوند
completeHandler: null //پس از پایان کار وب سرویس این متد جاوا اسکریپتی فراخوانی می‌شود
};
var options = $.extend(defaults, options);

return this.each(function() {
var obj = $(this);
obj.prepend("<div align='center'> لطفا اندکی تامل بفرمائید... <img src=\"images/loading.gif\"/></div>");

$.ajax({
type: "POST",
url: options.webServiceName + "/" + options.renderUCMethod,
data: options.ucMethodJsonParams,
contentType: "application/json; charset=utf-8",
dataType: "json",
success:
function(msg) {
obj.html(msg.d);

// if specified make callback and pass element
if (options.completeHandler)
options.completeHandler(this);
},
error:
function(XMLHttpRequest, textStatus, errorThrown) {
obj.html("امکان اتصال به سرور در این لحظه مقدور نیست. لطفا مجددا سعی کنید.");
}
});
});
};
برای اینکه با کلیات این روش آشنا شوید می‌توان به مقاله "بررسی وجود نام کاربر با استفاده از jQuery Ajax در ASP.Net" مراجعه نمود که از ذکر مجدد آن‌ها خودداری می‌شود. همچنین در مورد نوشتن یک پلاگین جی‌کوئری در مقاله "افزونه جملات قصار jQuery" توضیحاتی داده شده است.
عمده کاری که در این پلاگین صورت می‌گیرد فراخوانی متد Ajax جی‌کوئری است. سپس به متد وب سرویس ما (که در اینجا نام آن به صورت پارامتر نیز قابل دریافت است)، پارامترهای لازم پاس شده و سپس نتیجه حاصل به یک شیء در صفحه اضافه می‌شود.
completeHandler آن اختیاری است و پس از پایان کار متد اجکس فراخوانی می‌شود. در صورتیکه به آن نیازی نداشتید یا مقدار آن را null قرار دهید یا اصلا آن‌را ذکر نکنید.

مثالی در مورد استفاده از این وب سرویس و همچنین پلاگین جی‌کوئری نوشته شده:

الف) یوزر کنترل ساده زیر را به پروژه اضافه کنید:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="part1.ascx.cs" Inherits="TestJQueryAjax.part1" %>
<asp:Label runat="server" ID="lblData" ></asp:Label>
بدیهی است یک یوزر کنترل می‌تواند به اندازه یک صفحه کامل پیچیده باشد به همراه انواع و اقسام ارتباطات با دیتابیس و غیره.

سپس کد آن‌را به صورت زیر تغییر دهید:

using System;
using System.Threading;

namespace TestJQueryAjax
{
public partial class part1 : System.Web.UI.UserControl
{
public string Text1 { set; get; }
public string Text2 { set; get; }

protected void Page_Load(object sender, EventArgs e)
{
Thread.Sleep(3000);
if (!string.IsNullOrEmpty(Text1) && !string.IsNullOrEmpty(Text2))
lblData.Text = Text1 + "<br/>" + Text2;
}
}
}
این یوزر کنترل دو خاصیت عمومی دارد که توسط وب سرویس مقدار دهی خواهد شد و نهایتا حاصل نهایی را در یک لیبل در دو سطر نمایش می‌دهد.
عمدا یک sleep سه ثانیه‌ای در اینجا در نظر گرفته شده تا اثر آن‌را بهتر بتوان مشاهده کرد.

ب) اکنون کد مربوط به صفحه‌ای که قرار است این یوزر کنترل را به صورت غیرهمزمان بارگذاری کند به صورت زیر خواهد بود (مهم‌ترین قسمت آن نحوه تشکیل پارامترها و مقدار دهی خواص یوزر کنترل است):

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="TestJQueryAjax._Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>

<script src="js/jquery.js" type="text/javascript"></script>
<script src="js/jquery.advloaduc.js" type="text/javascript"></script>
<script src="js/json2.js" type="text/javascript"></script>

<script type="text/javascript">
function showAlert() {
alert('finished!');
}

//تشکیل پارامترهای متد وب سرویس جهت ارسال به آن
var fileName = 'part1.ascx';
var props = [{ 'Key': 'Text1', 'Value': 'سطر یک' }, { 'Key': 'Text2', 'Value': 'سطر 2'}];
var jsonText = JSON.stringify({ path: fileName, properties: props });

$(document).ready(function() {
$("#loadMyUc").advloaduc({
webServiceName: 'Ajax.asmx',
renderUCMethod: 'RenderUserControl',
ucMethodJsonParams: jsonText,
completeHandler: showAlert
});
});

</script>

</head>
<body>
<form id="form1" runat="server">
<div id="loadMyUc">
</div>
</form>
</body>

</html>
نکته:
برای ارسال صحیح و امن اطلاعات json به سرور، از اسکریپت استاندارد json2.js استفاده شد.

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


دریافت مثال فوق


مطالب
نحوه‌ی استفاده از ViewComponent درون Controller
در ASP.NET Core یک View Component، در نهایت خلاصه‌ایی از قابلیت‌هایی را ارائه میدهد که قرار است توسط یک کنترلر مدیریت شوند؛ زیرا پارامترهای یک View Component از طریق یک HTTP Request تامین نمی‌شوند. یعنی به صورت مستقیم از طریق درخواست‌های HTTP قابل دسترسی نیستند. فرض کنید در یک برنامه می‌خواهیم لیست کاربران سایت را نمایش دهیم تا با کلیک بر روی نام کاربر، امکان ویرایش کاربر انتخاب شده را داشته باشیم. با کلیک بر روی لینک مورد نظر، اطلاعات درخواست، به کنترلر UserManagerController و سپس اکشن متد Edit ارسال خواهد شد. در حالت عادی باید یک ViewComponent برای لیست کاربران و همچنین یک UserManagerController، برای ویرایش کاربر درون پروژه داشته باشیم:
public class UserListViewComponent : ViewComponent
{
    private readonly UserRepository repository;

    public UserListViewComponent(UserRepository repository)
    {
        this.repository = repository;
    }
    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return View(model: users);
    }
}

ویووی کامپوننت فوق نیز به این صورت تعریف شده است:
@model IList<User>
@foreach (var user in Model)
{
    <li>
        <a asp-controller="UserManager" asp-action="Edit" asp-route-id="@user.Id">@user.Name</a>
    </li>
}
<a class="btn btn-info" asp-controller="UserManager" asp-action="Index">More...</a>

کنترلر UserManager نیز یک چنین تعریفی را دارد:
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }
}

در ادامه ویووهای تعریف شده‌ی برای کنترلر فوق را نیز مشاهده میکنید:
// Views/UserManager/Edit.cshtml
@model User
<div class="row">
    <div class="col-md-8">
        <form method="post">
            <input type="hidden" asp-for="Id" />
            <div class="form-group">
                <label asp-for="Name"></label>
                <input asp-for="Name" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="LastName"></label>
                <input asp-for="LastName" class="form-control"/>
            </div>
            <div class="form-group">
                <label asp-for="Age"></label>
                <input asp-for="Age" class="form-control"/>
            </div>
            <button type="submit" class="btn btn-primary">Save</button>
        </form>
    </div>
</div>

// Views/UserManager/Index.cshtml
@model IList<User>
<table class="table">
    <tr>
        <td>Id</td>
        <td>Name</td>
        <td>LastName</td>
        <td>Age</td>
    </tr>
    @foreach (var user in Model)
    {
         <tr>
            <td>@user.Id</td>
            <td>@user.Name</td>
            <td>@user.LastName</td>
            <td>@user.Age</td>
        </tr>
    }
</table>

همانطور که مشاهده می‌کنید، کنترلر UserManager و کامپوننت UserList، به ترتیب کار مدیریت و نمایش کاربران را انجام میدهند و منطقاً هر دو جزو قابلیت‌های User هستند. برای جلوگیری از تکرار کد، می‌توانیم کنترلر و ویو‌وکامپوننت فوق را با هم ادغام کنیم؛ در واقع می‌توانیم UserListViewComponent را درون UserManagerController تعریف کنیم. برای این کار کافی است فایل UserManagerController را اینگونه تغییر دهیم:
[ViewComponent(Name = "UserList")]
public class UserManagerController : Controller
{
    private readonly UserRepository repository;

    public UserManagerController(UserRepository repository)
    {
        this.repository = repository;
    }

    public ViewResult Index()
    {
        var users = repository.GetUsers().ToList();
        return View(users);
    }

    public ViewResult Edit(int id)
    {
        var user = repository.GetUsers().FirstOrDefault(u => u.Id == id);
        return View(user);
    }

    [HttpPost]
    public IActionResult Edit(User user)
    {
        repository.Edit(user);
        return RedirectToAction("Index", "Home");
    }

    public IViewComponentResult Invoke()
    {
        var users = repository.GetUsers().Take(10).ToList();
        return new ViewViewComponentResult
        {
            ViewData = new ViewDataDictionary<IList<User>>(ViewData, users)
        };
    }
}

  
توضیحات:
همانطور که پیش‌تر نیز بحث شده است، از ویژگی ViewComponent زمانی استفاده خواهد شد که کلاس موردنظر از کلاس پایه ViewComponent ارث‌بری نکرده باشد و همچنین نام کلاس به ViewComponent ختم نشده باشد. با تعیین پراپرتی Name، یک نام برای ViewComponent تعیین کرده‌ایم که در نهایت درون ویوو، توسط Component.Invoke@  قابل فراخوانی باشد. همچنین از آنجائیکه UserManagerController از کلاس پایه ViewComponent ارث‌بری نکرده است، در نتیجه به اشیاء IViewComponentResult دسترسی نداریم، از این جهت به صورت مستقیم ViewViewComponentResult را ایجاد کرده‌ایم و مدلی که قرار است که به ویوو کامپوننت پاس داده شود را مقداردهی کرده‌ایم.

محل تعریف Viewها
Viewهای کنترلر و همچنین ویووکامپوننت مانند روش فوق قابل ترکیب نیستند؛ در نتیجه نیازی به تغییر هیچکدام از ویووها نخواهیم داشت.
UserManagerController:
/Views/UserManager/Edit.cshtml
/Views/UserManager/Index.cshtml

UserListViewComponent:
/Views/Shared/Components/UserList/Default.cshtml

نکته: دقت داشته باشید که ویجت نمایش لیست کاربران که پیشتر به صورت مستقل از عملکرد یک اکشن متد کار می‌کرده، قرار نیست جایگزین لیست کاربران (اکشن متد Index درون کنترلر UserManager) شود؛ یعنی به صورت مستقل از آن عمل میکند. هدف بیشتر قرار دادن View Component موردنظر درون کنترلر UserManager است.
مطالب
انجام کارهای زمانبندی شده در برنامه‌های ASP.NET توسط DNT Scheduler
اگر به دو مطلب استفاده از Quartz.Net (^ و ^) و خصوصا نظرات آن دقت کرده باشید به این نتیجه خواهید رسید که ... این کتابخانه‌ی در اصل جاوایی گنگ طراحی شده‌است. در سایت جاری برای انجام کارهای زمانبندی شده (مانند ارسال ایمیل‌های روزانه خلاصه مطالب، تهیه خروجی PDF و XML سایت، تبدیل پیش نویس‌ها به مطالب، بازسازی ایندکس‌های جستجو و امثال آن) از یک Thread timer استفاده می‌شود که حجم نهایی کتابخانه‌ی محصور کننده و مدیریت کننده‌ی وظایف آن جمعا 8 کیلوبایت است؛ متشکل از ... سه کلاس. در ادامه کدهای کامل و نحوه‌ی استفاده از آن را بررسی خواهیم کرد.


دریافت کتابخانه DNT Scheduler و مثال آن

DNTScheduler 
در این بسته، کدهای کتابخانه‌ی DNT Scheduler و یک مثال وب فرم را، ملاحظه خواهید کرد. از این جهت که برای ثبت وظایف این کتابخانه، از فایل global.asax.cs استفاده می‌شود، اهمیتی ندارد که پروژه‌ی شما وب فرم است یا MVC. با هر دو حالت کار می‌کند.



نحوه‌ی تعریف یک وظیفه‌ی جدید

کار با تعریف یک کلاس و پیاده سازی ScheduledTaskTemplate شروع می‌شود:
 public class SendEmailsTask : ScheduledTaskTemplate
برای نمونه :
using System;

namespace DNTScheduler.TestWebApplication.WebTasks
{
    public class SendEmailsTask : ScheduledTaskTemplate
    {
        /// <summary>
        /// اگر چند جاب در یک زمان مشخص داشتید، این خاصیت ترتیب اجرای آن‌ها را مشخص خواهد کرد
        /// </summary>
        public override int Order
        {
            get { return 1; }
        }

        public override bool RunAt(DateTime utcNow)
        {
            if (this.IsShuttingDown || this.Pause)
                return false;

            var now = utcNow.AddHours(3.5);
            return now.Minute % 2 == 0 && now.Second == 1;
        }

        public override void Run()
        {
            if (this.IsShuttingDown || this.Pause)
                return;

            System.Diagnostics.Trace.WriteLine("Running Send Emails");
        }

        public override string Name
        {
            get { return "ارسال ایمیل"; }
        }
    }
}
- در اینجا Order، ترتیب اجرای وظیفه‌ی جاری را در مقایسه با سایر وظیفه‌هایی که قرار است در یک زمان مشخص اجرا شوند، مشخص می‌کند.
- متد RunAt ثانیه‌ای یکبار فراخوانی می‌شود (بنابراین بررسی now.Second را فراموش نکنید). زمان ارسالی به آن UTC است و اگر برای نمونه می‌خواهید بر اساس ساعت ایران کار کنید باید 3.5 ساعت به آن اضافه نمائید. این مساله برای سرورهایی که خارج از ایران قرار دارند مهم است. چون زمان محلی آن‌ها برای تصمیم گیری در مورد زمان اجرای کارها مفید نیست.
در متد RunAt فرصت خواهید داشت تا منطق زمان اجرای وظیفه‌ی جاری را مشخص کنید. برای نمونه در مثال فوق، این وظیفه هر دو دقیقه یکبار اجرا می‌شود. یا اگر خواستید اجرای آن فقط در سال 23 و 33 دقیقه هر روز باشد، تعریف آن به نحو ذیل خواهد بود:
        public override bool RunAt(DateTime utcNow)
        {
            if (this.IsShuttingDown || this.Pause)
                return false;

            var now = utcNow.AddHours(3.5);
            return now.Hour == 23 && now.Minute == 33 && now.Second == 1;
        }
- خاصیت IsShuttingDown موجود در کلاس پایه ScheduledTaskTemplate، توسط کتابخانه‌ی DNT Scheduler مقدار دهی می‌شود. این کتابخانه قادر است زمان خاموش شدن پروسه‌ی فعلی IIS را تشخیص داده و خاصیت IsShuttingDown را true کند. بنابراین در حین اجرای وظیفه‌ای مشخص، به مقدار IsShuttingDown دقت داشته باشید. اگر true شد، یعنی فقط 30 ثانیه وقت دارید تا کار را تمام کنید.
خاصیت Pause هر وظیفه را برنامه می‌تواند تغییر دهد. به این ترتیب در مورد توقف یا ادامه‌ی یک وظیفه می‌توان تصمیم گیری کرد. خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks، لیست وظایف تعریف شده را در اختیار شما قرار می‌دهد.
- در متد Run، منطق وظیفه‌ی تعریف شده را باید مشخص کرد. برای مثال ارسال ایمیل یا تهیه‌ی بک آپ.
- Name نیز نام وظیفه‌ی جاری است که می‌تواند در گزارشات مفید باشد.

همین مقدار برای تعریف یک وظیفه کافی است.


نحوه‌ی ثبت و راه اندازی وظایف تعریف شده

پس از اینکه چند وظیفه را تعریف کردیم، برای مدیریت بهتر آن‌ها می‌توان یک کلاس ثبت و معرفی کلی را مثلا به نام ScheduledTasksRegistry ایجاد کرد:
using System;
using System.Net;

namespace DNTScheduler.TestWebApplication.WebTasks
{
    public static class ScheduledTasksRegistry
    {
        public static void Init()
        {
            ScheduledTasksCoordinator.Current.AddScheduledTasks(
                new SendEmailsTask(),
                new DoBackupTask());

            ScheduledTasksCoordinator.Current.OnUnexpectedException = (exception, scheduledTask) =>
            {
                //todo: log the exception.
                System.Diagnostics.Trace.WriteLine(scheduledTask.Name + ":" + exception.Message);
            };

            ScheduledTasksCoordinator.Current.Start();
        }

        public static void End()
        {
            ScheduledTasksCoordinator.Current.Dispose();
        }

        public static void WakeUp(string pageUrl)
        {
            try
            {
                using (var client = new WebClient())
                {
                    client.Credentials = CredentialCache.DefaultNetworkCredentials;
                    client.Headers.Add("User-Agent", "ScheduledTasks 1.0");
                    client.DownloadData(pageUrl);
                }
            }
            catch (Exception ex)
            {
                //todo: log ex
                System.Diagnostics.Trace.WriteLine(ex.Message);
            }
        }
    }
}
- شیء ScheduledTasksCoordinator.Current، نمایانگر تنها وهله‌ی مدیریت وظایف برنامه است.
- توسط متد ScheduledTasksCoordinator.Current.AddScheduledTasks، تنها کافی است کلاس‌های وظایف مشتق شده از ScheduledTaskTemplate، معرفی شوند.
- به کمک متد ScheduledTasksCoordinator.Current.Start، کار Thread timer برنامه شروع می‌شود.
- اگر در حین اجرای متد Run، استثنایی رخ دهد، آن‌را توسط یک Action delegate به نام ScheduledTasksCoordinator.Current.OnUnexpectedException می‌توانید دریافت کنید. کتابخانه‌ی DNT Scheduler برای اجرای وظایف، از یک ترد با سطح تقدم Below normal استفاده می‌کند تا در حین اجرای وظایف، برنامه‌ی جاری با اخلال و کندی مواجه نشده و بتواند به درخواست‌های رسیده پاسخ دهد. در این بین اگر استثنایی رخ دهد، می‌تواند کل پروسه‌ی IIS را خاموش کند. به همین جهت این کتابخانه کار try/catch استثناهای متد Run را نیز انجام می‌دهد تا از این لحاظ مشکلی نباشد.
- متد ScheduledTasksCoordinator.Current.Dispose کار مدیر وظایف برنامه را خاتمه می‌دهد.
- از متد WakeUp تعریف شده می‌توان برای بیدار کردن مجدد برنامه استفاده کرد.


استفاده از کلاس ScheduledTasksRegistry تعریف شده

پس از اینکه کلاس ScheduledTasksRegistry را تعریف کردیم، نیاز است آن‌را به فایل استاندارد global.asax.cs برنامه به نحو ذیل معرفی کنیم:
using System;
using System.Configuration;
using DNTScheduler.TestWebApplication.WebTasks;

namespace DNTScheduler.TestWebApplication
{
    public class Global : System.Web.HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            ScheduledTasksRegistry.Init();
        }

        protected void Application_End()
        {
            ScheduledTasksRegistry.End();
            //نکته مهم این روش نیاز به سرویس پینگ سایت برای زنده نگه داشتن آن است
            ScheduledTasksRegistry.WakeUp(ConfigurationManager.AppSettings["SiteRootUrl"]);
        }
    }
}
- متد ScheduledTasksRegistry.Init در حین آغاز برنامه فراخوانی می‌شود.
- متد ScheduledTasksRegistry.End در پایان کار برنامه جهت پاکسازی منابع باید فراخوانی گردد.
همچنین در اینجا با فراخوانی ScheduledTasksRegistry.WakeUp، می‌توانید برنامه را مجددا زنده کنید! IIS مجاز است یک سایت ASP.NET را پس از مثلا 20 دقیقه عدم فعالیت (فعالیت به معنای درخواست‌های رسیده به سایت است و نه کارهای پس زمینه)، از حافظه خارج کند (این عدد در application pool برنامه قابل تنظیم است). در اینجا در فایل web.config برنامه می‌توانید آدرس یکی از صفحات سایت را برای فراخوانی مجدد تعریف کنید:
 <?xml version="1.0"?>
<configuration>
  <appSettings>
      <add key="SiteRootUrl" value="http://localhost:10189/Default.aspx" />
  </appSettings>
</configuration>
همینکه درخواست مجددی به این صفحه برسد، مجددا برنامه توسط IIS بارگذاری شده و اجرا می‌گردد. به این ترتیب وظایف تعریف شده، در طول یک روز بدون مشکل کار خواهند کرد.


گزارشگیری از وظایف تعریف شده

برای دسترسی به کلیه وظایف تعریف شده، از خاصیت ScheduledTasksCoordinator.Current.ScheduledTasks استفاده نمائید:
var jobsList = ScheduledTasksCoordinator.Current.ScheduledTasks.Select(x => new
{
   TaskName = x.Name,
   LastRunTime = x.LastRun,
   LastRunWasSuccessful = x.IsLastRunSuccessful,
   IsPaused = x.Pause,
}).ToList();
لیست حاصل را به سادگی می‌توان در یک Grid نمایش داد.
مطالب دوره‌ها
استفاده از IL Code Weaving برای تولید ویژگی‌های تکراری مورد نیاز در WCF
با استفاده از IL Code Weaving علاوه بر مدیریت اعمال تکراری پراکنده در سراسر برنامه مانند ثبت وقایع، مدیریت استثناءها، کش کردن داده‌ها و غیره، می‌توان قابلیتی را به کدهای موجود نیز افزود. برای مثال یک برنامه معمول WCF را درنظر بگیرید.
using System.Runtime.Serialization;

namespace AOP03.DataContracts
{
    [DataContract]
    public class User
    {
        [DataMember]
        public int Id { set; get; }

        [DataMember]
        public string Name { set; get; }
    }
}
نیاز است کلاس‌ها و خواص آن توسط ویژگی‌های DataContract و DataMember مزین شوند. در این بین نیز اگر یکی فراموش گردد، کار دیباگ برنامه مشکل خواهد شد و در کل حجم بالایی از کدهای تکراری در اینجا باید در مورد تمام کلاس‌های مورد نیاز انجام شود. در ادامه قصد داریم تولید این ویژگی‌ها را توسط PostSharp انجام دهیم. به عبارتی یک پوشه خاص به نام DataContracts را ایجاد کرده و کلاس‌های خود را به نحوی متداول و بدون اعمال ویژگی خاصی تعریف کنیم. در ادامه پس از کامپایل آن، به صورت خودکار با ویرایش کدهای IL توسط PostSharp، ویژگی‌های لازم را به اسمبلی نهایی اضافه نمائیم.


تهیه DataContractAspect جهت اعمال خودکار ویژگی‌های DataContract و DataMember

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using PostSharp.Aspects;
using PostSharp.Extensibility;
using PostSharp.Reflection;

namespace AOP03
{
    [Serializable]
    //این ویژگی تنها نیاز است به کلاس‌ها اعمال شود
    [MulticastAttributeUsage(MulticastTargets.Class)]
    public class DataContractAspect : TypeLevelAspect, IAspectProvider
    {
        public IEnumerable<AspectInstance> ProvideAspects(object targetElement)
        {
            var targetType = (Type)targetElement; //همان نوعی است که ویژگی جاری به آن اعمال خواهد شد

            //این سطر معادل است با درخواست تولید ویژگی دیتاکانترکت
            var introduceDataContractAspect = new CustomAttributeIntroductionAspect(
                new ObjectConstruction(typeof(DataContractAttribute).GetConstructor(Type.EmptyTypes)));

            //این سطر معادل است با درخواست تولید ویژگی دیتاممبر
            var introduceDataMemberAspect = new CustomAttributeIntroductionAspect(
                new ObjectConstruction(typeof(DataMemberAttribute).GetConstructor(Type.EmptyTypes)));

            //در اینجا کار اعمال ویژگی دیتاکانترکت به کلاسی که به عنوان پارامتر متد جاری
            //دریافت شده انجام خواهد شد
            yield return new AspectInstance(targetType, introduceDataContractAspect);

            //مرحله بعد کار اعمال ویژگی دیتاممبر به خواص کلاس است
            foreach (var property in targetType.GetProperties(BindingFlags.Public |
                                                          BindingFlags.DeclaredOnly |
                                                          BindingFlags.Instance))
            {
                if (property.CanWrite)
                    yield return new AspectInstance(property, introduceDataMemberAspect);
            }
        }
    }
}
توضیحات مرتبط با قسمت‌های مختلف این Aspect سفارشی، به صورت کامنت در کدهای فوق ارائه شده‌اند.
برای اعمال آن به سراسر برنامه تنها کافی است به فایل AssemblyInfo.cs پروژه مراجعه و سپس سطر زیر را به آن اضافه کنیم:
 [assembly: DataContractAspect(AttributeTargetTypes = "AOP03.DataContracts.*")]
به این ترتیب در زمان کامپایل پروژه، Aspect تعریف شده به تمام کلاس‌های موجود در فضای نام AOP03.DataContracts اعمال خواهند شد.

در این حالت اگر کلیه ویژگی‌های کلاس User فوق را حذف و برنامه را کامپایل کنیم، با مراجعه به برنامه ILSpy می‌توان صحت اعمال ویژگی‌ها را به کمک PostSharp بررسی کرد:
 

مطالب
BulkInsert در EF CodeFirst
یکی از مشکلات برنامه نویسان، نوشتن هزاران رکورد در دیتابیس در مدت زمان بسیار کوتاهی است که عموما این کار در هنگام خواندن اطلاعات از فایل‌های اکسل و گاها از فایل‌های text ای اتفاق می‌افتد. برای مثال در زمان نوشتن این اطلاعات، با Timeout مواجه شده و اگر هم Timeout ندهد بسیار کند عمل می‌کند.
در این پست قصد داریم روش نوشتن هزاران رکورد را در کسری از ثانیه توسط EF Code first مورد بررسی قرار دهیم و در نهایت مقایسه ای با AddRange در EntityFramework داشته باشیم.
خوب؛ در ابتدا مدلی را با نام Personel را به شکل زیر طراحی مینماییم .
public class Personel
    {
        [Key]
        public int PersonelID { get; set; }

        [MaxLength(15)]
        public string Name { get; set; }

        [MaxLength(25)]
        public string Family { get; set; }

        [MaxLength(10)]
        public string CodeMelli { get; set; }

    }
 سپس این مدل را در Context خود معرفی نمایید همانند کلاس زیر:
 public class PersonalContext : DbContext
    {
        public DbSet<Personel> Personel { get; set; }
       
        public override int SaveChanges()
        {
            return base.SaveChanges();
        }
    }
برای ساختن دیتابیس در Entityframework CodeFirst  میتوانید به سری آموزشی CodeFirst در سایت جاری مراجعه نمایید. اکنون همه چیز مهیا است برای انجام عملیات Bulk Insert .
در ابتدا پاورشل نیوگت را باز کرده و پکیج مورد نظر را با توجه به نسخه Ef استفاده شده، به پروژه اضافه نمایید. همانند دستور زیر :
Install-Package EntityFramework.BulkInsert-ef6
بعد از نصب پکیج مورد نظر، باید لیستی از موجودیت‌ها را از یک فایل اکسل خوانده و به BulkInsert EF ارسال نماییم. برای این کار مانند زیر عمل مینماییم.
public ActionResult Insert()
        {
            int Counter = 1000;
            List<Personel> Lst = new List<Personel>();
            // شبیه سازی خواندن رکورد‌ها از فایل اکسل
            for (int i = 0; i < Counter; i++)
            {
                Lst.Add(new Personel
                {
                    CodeMelli = "0000000000",
                    Family = "Karimi",
                    Name = "Mohammad"
                });
            }

            PersonalContext db = new PersonalContext();
            db.BulkInsert(Lst);
            db.SaveChanges();
            return View();
        }
تنها نکته‌ی استفاده از متد BulkInsert، اضافه نمودن ارجاعی از ;using EntityFramework.BulkInsert.Extensions به بالای کلاس جاری است.
در شکل زیر  میتوانید مقایسه ای بین bulkInsert  و AddRange را در تعداد رکورد‌های نوشته شده و مدت زمان صرف شده برای نوشتن در دیتابیس، مشاهده نمایید.

نظرات مطالب
C# 6 - The nameof Operator
یک نکته‌ی تکمیلی: تکامل اپراتور nameof در C# 12.0

همانطور که در این مطلب مشاهده کردید، اپراتور nameof، روشی بسیار مفید جهت دسترسی به نام متغیرها، نوع‌ها و یا اعضای یک کلاس است. در C# 12، این ویژگی اندکی بهبود یافته‌است و امکان دسترسی به اطلاعات اعضای یک کلاس را هم دارد:
public class NameofClass
{
    public string SomeProperty { get; set; }

    // Now legal with C# 12
    // would show "Length" on the console
    public const string NameOfSomePropertyLength = nameof(SomeProperty.Length); 
    
    public static int StaticField;
    public const string NameOfStaticFieldMinValue  = nameof(StaticField.MinValue);

    [Description($"String {nameof(SomeProperty.Length)}")]
    public int StringLength(string s)
    {
        return s.Length;
    }
}
در این مثال، اگر سعی کنیم مقدار NameOfSomePropertyLength را در کنسول نمایش دهیم، عبارت Length ظاهر خواهد شد. تا پیش از C# 12 برای دسترسی به یک چنین قابلیتی نیاز به نمونه سازی و تولید شیءای از کلاس NameofClass فوق وجود داشت تا بتوان اپراتور nameof را به خواص آن اعمال کرد. این محدودیت در C# 12 برطرف شده‌است.

همچنین همانطور که مشاهده می‌کنید، امکان دسترسی به اطلاعات فیلدهای استاتیک و یا بکارگیری این قابلیت در Attributes هم میسر شده‌است.
مطالب
ایجاد قابلیت قالب یا Theme در ASP.NET MVC
در این مقاله قصد داریم قابلیت ایجاد قالب را در پروژه‌های ASP.NET MVC، فراهم کنیم تا ظاهر سایت یا به اصطلاح  قالب سایت از طریق فایل کانفیگ تغییر کند. همانطور که می‌دانید معماری ASP.NET MVC براساس قراردادهای پیش فرض، قابل تعویض و تغییر طراحی شده است. یکی از این قراردادها، نحوه‌ی پیدا کردن یک view برای کنترلر و اکشن‌های آن است که به صورت زیر در ViewEngine تعریف شده‌است: 
ViewEngine.ViewLocationFormats= "~/Views/{controller}/{action}.cshtml"
در ادامه قصد داریم این مسیر یابی پیش فرض را طوری تغییر دهیم تا در پوشه‌ی themes پروژه و زیرپوشه‌ای با نام قالب که از فایل کانفیگ خوانده می‌شود نیز به دنبال view مرتبط با کنترلر و اکشن بگردد: 
"~/Themes/{ThemeName}/Views/{controller}/{action}.cshtml"
 برای راحتی کار، یک Extension Method برای اینترفیس پایه viewEngin  تعریف می‌کنیم: 
   public static void Themeable(this VirtualPathProviderViewEngine engine)
        {
            var ThemePath = "~/Themes";
            var ThemeName = WebConfigurationManager.AppSettings["MvcTheme"];
            if (string.IsNullOrEmpty(ThemeName))
                return;

            var themeFolder = HttpContext.Current.Server.MapPath(string.Format("{0}/{1}/", ThemePath, ThemeName));
            if (!Directory.Exists(themeFolder))
                throw new DirectoryNotFoundException(string.Format("Theme folder not exists: {0}/{1}}", ThemePath,
                    ThemeName));

            var newViewLocations = new[]
            {
                string.Format("{0}/{1}/Views/{2}/{3}.cshtml", ThemePath, ThemeName, "{1}", "{0}"),
                string.Format("{0}/{1}/Views/Shared/{2}.cshtml", ThemePath, ThemeName, "{0}"),
                 
                // vb.net :
                // string.Format("{0}/{1}/Views/{2}/{3}.vbhtml", ThemePath, ThemeName, "{1}", "{0}"),
                // string.Format("{0}/{1}/Views/Shared/{2}.vbhtml", ThemePath, ThemeName, "{0}"),
            };
            engine.ViewLocationFormats = newViewLocations;
            engine.PartialViewLocationFormats = newViewLocations;
        }
سپس در فایل کانفیگ، نام قالب را وارد کنید: 
  <appSettings>
 ...
    <add key="MvcTheme" value="Test1" />
  </appSettings>
و در فایل Global.asax، قابلیت فراخوانی قالب را فعال کنید:
ViewEngines.Engines.OfType<RazorViewEngine>().Single().Themeable();
حالا کافی است محتویات views اصلی را به پوشه‌ی views  قالب مورد نظر کپی کرده و فایل _ViewStart.cshtml را اصلاح کنید، بطوریکه که به فایل layout  داخل پوشه قالب اشاره کند: 
@{
    // Layout = "~/Views/Shared/_Layout.cshtml";
    Layout = "~/themes/test1/Views/Shared/_Layout.cshtml";
}
تا اینجای کار را اگر امتحان کنید، همه چیز درست است؛ مگر اینکه بخواهید از Bundling استفاده کنید. اگر بخواهید از css و اسکریپت‌های اصلی پروژه استفاده کنید، می‌توانید از همان  bundle‌های اصلی، در داخل layout و سایر viewهای قالب استفاده کنید. ولی اینکار نمی‌تواند کاربردی باشد؛ چون ساختار و اجزای هر قالب می‌تواند کاملا مجزا باشد. مثلا در یک قالب از  بوت استرپ استفاده می‌کنیم و در قالبی دیگر از UI Fabric مایکروسافت استفاده می‌کنیم. به همین دلیل، دست به کار می‌شویم و یک Bundling داینامیک را طراحی می‌کنیم:
ابتدا مدل زیر را تعریف کنید:
    public class ThemeBundle
    {
        public BundleType BundleType { get; set; }
        public string VirtualPath { get; set; }
        public string[] Urls { get; set; }
    }

    public enum BundleType
    {
        Style, Script
    }
در داخل هر پوشه‌ی قالب، به دنبال فایل ThemeBundle.json می‌گردیم تا تعاریف Bundling را از آن بخوانیم و در داخل پروژه استفاده کنیم.
توسط کد زیر bundleها را از محل پوشه‌ی قالب، فراخوانی می‌کنیم:
public static void RegisterThemeBundels(BundleCollection bundles)
        {
            var ThemePath = "~/Themes";
            var ThemeName = WebConfigurationManager.AppSettings["MvcTheme"];
            var ThemeBundleFileName = "ThemeBundle.json";

        List<ThemeBundle> list;

            try
            {
                JavaScriptSerializer jss = new JavaScriptSerializer();
                var jsonaddress =
                    System.Web.HttpContext.Current.Server.MapPath(string.Format("{0}/{1}/{2}", ThemePath, ThemeName, ThemeBundleFileName));
                var json = System.IO.File.ReadAllText(jsonaddress);
                  list = jss.Deserialize<List<ThemeBundle>>(json);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Cannot read {0}. see more error in inner exception.", ThemeBundleFileName), ex);
            }

            foreach (var themeBundle in list)
            {
                switch (themeBundle.BundleType)
                {
                    case BundleType.Script:
                        bundles.Add(new ScriptBundle(themeBundle.VirtualPath).Include(
                            themeBundle.Urls));
                        break;
                    case BundleType.Style:
                        bundles.Add(new StyleBundle(themeBundle.VirtualPath).Include(
                            themeBundle.Urls));
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(themeBundle.BundleType));
                }
            }
        }
دستور فراخوانی bundle‌ها در صورتیکه نام قالب در فایل کانفیگ تعریف شده باشد:
    public class BundleConfig
    {        
        public static void RegisterBundles(BundleCollection bundles)
        {
            if (MvcTheme.ThemeName != null)
            {
                MvcTheme.RegisterThemeBundels(bundles); 
                return;
            }

            bundles.Add(new ScriptBundle("~/bundles/jquery").Include(
                        "~/Scripts/jquery-{version}.js"));

            bundles.Add(new ScriptBundle("~/bundles/jqueryval").Include(
                        "~/Scripts/jquery.validate*"));
...
}
}
فایل ThemeBundle.json قالب test1 را بصورت زیر تعریف می‌کنیم:
[
  {
    "BundleType": "Script",
    "VirtualPath": "~/themes/test1/js/jquery",
    "Urls": [ "~/themes/test1/js/jquery-1.10.2.js" ]
  },
  {
    "BundleType": "Script",
    "VirtualPath": "~/themes/test1/js/jqueryval",
    "Urls": [ "~/themes/test1/js/jquery.validate.js",
               "~/themes/test1/js/jquery.validate.unobtrusive.js" ]
  },
  {
    "BundleType": "Script",
    "VirtualPath": "~/themes/test1/js/modernizr",
    "Urls": [ "~/themes/test1/js/modernizr-2.6.2.js" ]
  },
  {
    "BundleType": "Script",
    "VirtualPath": "~/themes/test1/js/bootstrap",
    "Urls": [ "~/themes/test1/js/bootstrap.js",
              "~/themes/test1/js/respond.js" ]
  },
  {
    "BundleType": "Style",
    "VirtualPath": "~/themes/test1/css/css",
    "Urls": [ "~/themes/test1/css/bootstrap.css",
              "~/themes/test1/css/site.css" ]
  }
]
در نهایت ساختار پوشه‌ی قالب به صورت زیر می‌باشد:
 Themes
├───Test1
│    │ThemeBundle.json
│    ├───Css
│    ├───Fonts
│    ├───Images
│    ├───Js
│    └───Views
├───Test2
│    │ThemeBundle.json
│    ├───Css
│    ├───Fonts
│    ├───Images
│    ├───Js
│    └───Views
 کد کامل و نهایی :
  public static class MvcTheme
    {
 
        public static string ThemeName { get; }
        public static string ThemePath { get; set; }
        private const string AppSettingName = "MvcTheme";
        private const string ThemeBundleFileName = "ThemeBundle.json";

        static MvcTheme()
        {
            ThemePath = "~/Themes";
            ThemeName = WebConfigurationManager.AppSettings[AppSettingName];
        }

        public static void Themeable(this VirtualPathProviderViewEngine engine)
        {
            if (string.IsNullOrEmpty(ThemeName))
                return;

            var themeFolder = HttpContext.Current.Server.MapPath(string.Format("{0}/{1}/", ThemePath, ThemeName));
            if (!Directory.Exists(themeFolder))
                throw new DirectoryNotFoundException(string.Format("Theme folder not exists: {0}/{1}}", ThemePath,
                    ThemeName));

            var newViewLocations = new[]
            { 
                string.Format("{0}/{1}/Views/{2}/{3}.cshtml", ThemePath, ThemeName, "{1}", "{0}"), 
                string.Format("{0}/{1}/Views/Shared/{2}.cshtml", ThemePath, ThemeName, "{0}"),
                              
                // vb.net :
                // string.Format("{0}/{1}/Views/{2}/{3}.vbhtml", ThemePath, ThemeName, "{1}", "{0}"),
                // string.Format("{0}/{1}/Views/Shared/{2}.vbhtml", ThemePath, ThemeName, "{0}"),

            };
            engine.ViewLocationFormats = newViewLocations;
            engine.PartialViewLocationFormats = newViewLocations; 
        }

        public static void RegisterThemeBundels(BundleCollection bundles)
        {
            if(ThemeName == null)
                return;

            var list = ReadThemeBundles();

            foreach (var themeBundle in list)
            {
                switch (themeBundle.BundleType)
                {
                    case BundleType.Script:
                        bundles.Add(new ScriptBundle(themeBundle.VirtualPath).Include(
                            themeBundle.Urls));
                        break;
                    case BundleType.Style:
                        bundles.Add(new StyleBundle(themeBundle.VirtualPath).Include(
                            themeBundle.Urls));
                        break;
                    default:
                        throw new ArgumentOutOfRangeException(nameof(themeBundle.BundleType));
                }
            }
        }

        public static List<ThemeBundle> ReadThemeBundles()
        {
            try
            {
                JavaScriptSerializer jss = new JavaScriptSerializer();
                var jsonaddress =
                    System.Web.HttpContext.Current.Server.MapPath(string.Format("{0}/{1}/{2}", ThemePath, ThemeName, ThemeBundleFileName));
                var json = System.IO.File.ReadAllText(jsonaddress);
                var list = jss.Deserialize<List<ThemeBundle>>(json);

                return list;
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("Cannot read {0}. see more error in inner exception.", ThemeBundleFileName), ex);
            }
        }
    }

    public class ThemeBundle
    {
        public BundleType BundleType { get; set; }
        public string VirtualPath { get; set; }
        public string[] Urls { get; set; }
    }

    public enum BundleType
    {
        Style, Script
    }
- نکته یک: با حذف مقدار نام قالب در فایل کانفیگ، به راحتی به حالت پیش فرض asp.net mvc بر میگردید.
- نکته دو: نام bundle را حتما هم عمق با آدرس قالب تعریف کنید تا وقتی فایل css  به پوشه‌ی images  یا فونت مجاور خود اشاره می‌کند، آدرس دهی معتبر باشد.
- نکته سه: اگر از RazorGenerator استفاده می‌کنید، در فایل RazorGeneratorMvcStart متد ()Themeable را بر روی engine آن صدا بزنید.