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

مزیت استفاده از یوزر کنترل‌ها، ماژولار کردن برنامه است. برای مثال اگر صفحه جاری شما قرار است از چهار قسمت اخبار، منوی پویا ، سخن روز و آمار کاربران تشکیل شود، می‌توان هر کدام را توسط یک یوزر کنترل پیاده سازی کرده و سپس صفحه اصلی را از کنار هم قرار دادن این یوزر کنترل‌ها تهیه نمود.
با این توضیحات اکنون می‌خواهیم یک یوزکنترل 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 استفاده شد.

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


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


نظرات مطالب
بررسی روش آپلود فایل‌ها در ASP.NET Core
دریک فرم blazor wasm از کدزیراستفاده کردیم.که فایلهای انتخابی کاربر رو تبدیل به آرایه ای ازجنس بایت در نظر میگیریم.ودر سمت سرور نیز همین آرایه رو پردازش و ذخیره میکنیم و ازسایرکلاینها هم فایلها رو درهمین قالب دریافت خواهیم کرد یعنی آرایه ای از جنس بایت.
 private async Task UploadFiles(InputFileChangeEventArgs e)
    {
        foreach (var file in e.GetMultipleFiles())
        {
            var fileData = new FileToBeSaveVM();
            var buffers = new byte[file.Size];
            await file.OpenReadStream().ReadAsync(buffers);

            fileData.FileName = file.Name;
            fileData.FileSize = file.Size;
            fileData.FileType = file.ContentType;
            fileData.Extension = Path.GetExtension(file.Name);
            fileData.ImageBytes = buffers;
            lstFileToBeSaves.fileToBeSaves.Add(fileData);
        }

    }
public class FileToBeSaveVM
{
    public byte[] ImageBytes { get; set; }
    public string FileName { get; set; }
    public string FileType { get; set; }
    public string Extension { get; set; }
    public long FileSize { get; set; }
}
کدهای سمت سرور:
public async Task UploadFileAsync(List<FileToBeSaveVM> files, string uploadFolder)
    {
        var folderDirectory=createUploadDir(uploadFolder);
        foreach (var file in files)
        {            
            string fileExtenstion = Path.GetExtension(file.FileName);
            string fileuniqName =$"{Guid.NewGuid()}{fileExtenstion}";
            string fileName = Path.Combine(folderDirectory, fileuniqName);
            string url=$"{uploadFolder}/{fileuniqName}";
            using (var fileStream = File.Create(fileName))
            {
                await fileStream.WriteAsync(file.ImageBytes);
           }
      }

پاسخ به بازخورد‌های پروژه‌ها
استفاده از Fluent Query در دیتا سورس PDFReporter
- با هاست دار تماس بگیرید و عنوان کنید که برنامه شما نیاز است دسترسی write روی پوشه pdf داشته باشد.
- یا از روش ذیل استفاده کنید:
.Generate(data =>
                {
                    fileName = HttpUtility.UrlEncode(fileName, Encoding.UTF8);
                    data.FlushInBrowser(fileName, FlushType.Inline);
                }); // creating an in-memory PDF file
نظرات اشتراک‌ها
معرفی کتابخانه EPPlus.Core
نحوه فرمت دادن به فیلد محاسباتی (مجموع ستون ) به چه صورت است؟
 var numberformat = "#,##0.00_);(#,##0.00)";
 var dataCellStyleName = "TableNumber";
 var numStyle = package.Workbook.Styles.CreateNamedStyle(dataCellStyleName);
  numStyle.Style.Numberformat.Format = numberformat;

          //Add Value ...

var tbl = worksheet.Tables.Add(new ExcelAddressBase(fromRow: 1, fromCol: 1, toRow: list.Count + 1, toColumn: 3), "Data");
 tbl.ShowHeader = false;
 tbl.TableStyle = TableStyles.Light1;
 tbl.ShowTotal = true;

 tbl.Columns[2].DataCellStyleName = dataCellStyleName;
 tbl.Columns[2].TotalsRowFunction = RowFunctions.Sum;

مطالب
طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت دوم
در مطلب «طراحی افزونه پذیر با ASP.NET MVC 4.x/5.x - قسمت اول» با ساختار کلی یک پروژه‌ی افزونه‌ی پذیر ASP.NET MVC آشنا شدیم. پس از راه اندازی آن و مدتی کار کردن با این نوع پروژه‌ها، این سؤال پیش خواهد آمد که ... خوب، اگر هر افزونه تصاویر یا فایل‌های CSS و JS اختصاصی خودش را بخواهد داشته باشد، چطور؟ موارد عمومی مانند بوت استرپ و جی‌کوئری را می‌توان در پروژه‌ی پایه قرار داد تا تمام افزونه‌ها به صورت یکسانی از آن‌ها استفاده کنند، اما هدف، ماژولار شدن برنامه است و جدا کردن فایل‌های ویژه‌ی هر پروژه، از پروژ‌ه‌ای دیگر و همچنین بالا بردن سهولت کار تیمی، با شکستن اجزای یک پروژه به صورت افزونه‌هایی مختلف، بین اعضای یک تیم. در این قسمت نحوه‌ی مدفون سازی انواع فایل‌های استاتیک افزونه‌ها را درون فایل‌های DLL آن‌ها بررسی خواهیم کرد. به این ترتیب دیگر نیازی به ارائه‌ی مجزای آن‌ها و یا کپی کردن آن‌ها در پوشه‌های پروژه‌ی اصلی نخواهد بود.


مدفون سازی فایل‌های CSS و JS هر افزونه درون فایل DLL آن

به solution جاری، یک class library جدید را به نام MvcPluginMasterApp.Common اضافه کنید. از آن جهت قرار دادن کلاس‌های عمومی و مشترک بین افزونه‌ها استفاده خواهیم کرد. برای مثال قصد نداریم کلاس‌های سفارشی و عمومی ذیل را هربار به صورت مستقیم در افزونه‌ای جدید کپی کنیم. کتابخانه‌ی Common، امکان استفاده‌ی مجدد از یک سری کدهای تکراری را در بین افزونه‌ها میسر می‌کند.
این پروژه برای کامپایل شدن نیاز به بسته‌ی نیوگت ذیل دارد:
 PM> install-package Microsoft.AspNet.Web.Optimization
همچنین باید به صورت دستی، در قسمت ارجاعات پروژه، ارجاعی را به اسمبلی استاندارد System.Web نیز به آن اضافه نمائید.
پس از این مقدمات، کلاس ذیل را به این پروژه‌ی class library جدید اضافه کنید:
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Web.Optimization;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceTransform : IBundleTransform
    {
        private readonly IList<string> _resourceFiles;
        private readonly string _contentType;
        private readonly Assembly _assembly;
 
        public EmbeddedResourceTransform(IList<string> resourceFiles, string contentType, Assembly assembly)
        {
            _resourceFiles = resourceFiles;
            _contentType = contentType;
            _assembly = assembly;
        }
 
        public void Process(BundleContext context, BundleResponse response)
        {
            var result = new StringBuilder();
 
            foreach (var resource in _resourceFiles)
            {
                using (var stream = _assembly.GetManifestResourceStream(resource))
                {
                    if (stream == null)
                    {
                        throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", resource, _assembly.FullName));
                    }
 
                    using (var reader = new StreamReader(stream))
                    {
                        result.Append(reader.ReadToEnd());
                    }
                }
            }
 
            response.ContentType = _contentType;
            response.Content = result.ToString();
        }
    }
}
اگر با سیستم bundling & minification کار کرده باشید، با تعاریفی مانند ("new Bundle("~/Plugin1/Scripts آشنا هستید. سازنده‌ی کلاس Bundle، پارامتر دومی را نیز می‌پذیرد که از نوع IBundleTransform است. با پیاده سازی اینترفیس IBundleTransform می‌توان محل ارائه‌ی فایل‌های استاتیک CSS و JS را بجای فایل سیستم متداول و پیش فرض، به منابع مدفون شده‌ی در اسمبلی جاری هدایت و تنظیم کرد.
کلاس فوق در اسمبلی معرفی شده به آن، توسط متد GetManifestResourceStream به دنبال فایل‌ها و منابع مدفون شده گشته و سپس محتوای آن‌ها را بازگشت می‌دهد.
اکنون برای استفاده‌ی از آن، به پروژه‌ی MvcPluginMasterApp.Plugin1 مراجعه کرده و ارجاعی را به پروژه‌ی MvcPluginMasterApp.Common فوق اضافه نمائید. سپس در فایل Plugin1.cs، متد RegisterBundles آن‌را به نحو ذیل تکمیل کنید:
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)
        {
            var executingAssembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var assemblyNameSpace = executingAssembly.GetName().Name;
            var scriptsBundle = new Bundle("~/Plugin1/Scripts",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Scripts.test1.js"
                }, "application/javascript", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                scriptsBundle.Transforms.Add(new JsMinify());
            }
            bundles.Add(scriptsBundle);
            var cssBundle = new Bundle("~/Plugin1/Content",
                new EmbeddedResourceTransform(new List<string>
                {
                    assemblyNameSpace + ".Content.test1.css"
                }, "text/css", executingAssembly));
            if (!HttpContext.Current.IsDebuggingEnabled)
            {
                cssBundle.Transforms.Add(new CssMinify());
            }
            bundles.Add(cssBundle);
            BundleTable.EnableOptimizations = true;
        }
 
        public void RegisterRoutes(RouteCollection routes)
        {
        }
 
        public void RegisterServices(IContainer container)
        {
        }
    }
}
در اینجا نحوه‌ی کار با کلاس سفارشی EmbeddedResourceTransform را مشاهده می‌کنید. ابتدا فایل‌های js و سپس فایل‌های css برنامه به سیستم Bundling برنامه اضافه شده‌اند.
این فایل‌ها به صورت ذیل در پروژه تعریف گردیده‌اند:


همانطور که مشاهده می‌کنید، باید به خواص هر کدام مراجعه کرد و سپس Build action آن‌ها را به embedded resource تغییر داد، تا در حین کامپایل، به صورت خودکار در قسمت منابع اسمبلی ذخیره شوند.

یک نکته‌ی مهم
اینبار برای مسیردهی منابع، باید بجای / فایل سیستم، از «نقطه» استفاده کرد. زیرا منابع با نام‌هایی مانند namespace.folder.name در قسمت resources یک اسمبلی ذخیره می‌شوند:



مدفون سازی تصاویر ثابت هر افزونه درون فایل DLL آن

مجددا به اسمبلی مشترک MvcPluginMasterApp.Common مراجعه کرده و اینبار کلاس جدید ذیل را به آن اضافه کنید:
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Web;
using System.Web.Routing;
 
namespace MvcPluginMasterApp.Common.WebToolkit
{
    public class EmbeddedResourceRouteHandler : IRouteHandler
    {
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceRouteHandler(Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
        {
            return new EmbeddedResourceHttpHandler(requestContext.RouteData, _assembly, _resourcePath, _cacheDuration);
        }
    }
 
    public class EmbeddedResourceHttpHandler : IHttpHandler
    {
        private readonly RouteData _routeData;
        private readonly Assembly _assembly;
        private readonly string _resourcePath;
        private readonly TimeSpan _cacheDuration;
 
        public EmbeddedResourceHttpHandler(
            RouteData routeData, Assembly assembly, string resourcePath, TimeSpan cacheDuration)
        {
            _routeData = routeData;
            _assembly = assembly;
            _resourcePath = resourcePath;
            _cacheDuration = cacheDuration;
        }
 
        public bool IsReusable
        {
            get { return false; }
        }
 
        public void ProcessRequest(HttpContext context)
        {
            var routeDataValues = _routeData.Values;
            var fileName = routeDataValues["file"].ToString();
            var fileExtension = routeDataValues["extension"].ToString();
 
            var manifestResourceName = string.Format("{0}.{1}.{2}", _resourcePath, fileName, fileExtension);
            var stream = _assembly.GetManifestResourceStream(manifestResourceName);
            if (stream == null)
            {
                throw new KeyNotFoundException(string.Format("Embedded resource key: '{0}' not found in the '{1}' assembly.", manifestResourceName, _assembly.FullName));
            }
 
            context.Response.Clear();
            context.Response.ContentType = "application/octet-stream";
            cacheIt(context.Response, _cacheDuration);
            stream.CopyTo(context.Response.OutputStream);
        }
 
        private static void cacheIt(HttpResponse response, TimeSpan duration)
        {
            var cache = response.Cache;
 
            var maxAgeField = cache.GetType().GetField("_maxAge", BindingFlags.Instance | BindingFlags.NonPublic);
            if (maxAgeField != null) maxAgeField.SetValue(cache, duration);
 
            cache.SetCacheability(HttpCacheability.Public);
            cache.SetExpires(DateTime.Now.Add(duration));
            cache.SetMaxAge(duration);
            cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
        }
    }
}
تصاویر پروژه‌ی افزونه نیز به صورت embedded resource در اسمبلی آن قرار خواهند گرفت. به همین جهت باید سیستم مسیریابی را پس درخواست رسیده‌ی جهت نمایش تصاویر، به منابع ذخیره شده‌ی در اسمبلی آن هدایت نمود. اینکار را با پیاده سازی یک IRouteHandler سفارشی، می‌توان به نحو فوق مدیریت کرد.
این IRouteHandler، نام و پسوند فایل را دریافت کرده و سپس به قسمت منابع اسمبلی رجوع، فایل مرتبط را استخراج و سپس بازگشت می‌دهد. همچنین برای کاهش سربار سیستم، امکان کش شدن منابع استاتیک نیز در آن درنظر گرفته شده‌است و هدرهای خاص caching را به صورت خودکار اضافه می‌کند.
سیستم bundling نیز هدرهای کش کردن را به صورت خودکار و توکار اضافه می‌کند.

اکنون به تعاریف Plugin1 مراجعه کنید و سپس این IRouteHandler سفارشی را به نحو ذیل به آن معرفی نمائید:
namespace MvcPluginMasterApp.Plugin1
{
    public class Plugin1 : IPlugin
    { 
        public void RegisterRoutes(RouteCollection routes)
        {
            //todo: add custom routes.
 
            var assembly = Assembly.GetExecutingAssembly();
            // Mostly the default namespace and assembly name are the same
            var nameSpace = assembly.GetName().Name;
            var resourcePath = string.Format("{0}.Images", nameSpace);
 
            routes.Insert(0,
                new Route("NewsArea/Images/{file}.{extension}",
                    new RouteValueDictionary(new { }),
                    new RouteValueDictionary(new { extension = "png|jpg" }),
                    new EmbeddedResourceRouteHandler(assembly, resourcePath, cacheDuration: TimeSpan.FromDays(30))
                ));
        } 
    }
}
در مسیریابی تعریف شده، تمام درخواست‌های رسیده‌ی به مسیر NewsArea/Images به EmbeddedResourceRouteHandler هدایت می‌شوند.
مطابق تعریف آن، file و extension به صورت خودکار جدا شده و توسط routeData.Values در متد ProcessRequest کلاس EmbeddedResourceHttpHandler قابل دسترسی خواهند شد.
پسوندهایی که توسط آن بررسی می‌شوند از نوع png یا jpg تعریف شده‌اند. همچنین مدت زمان کش کردن هر منبع استاتیک تصویری به یک ماه تنظیم شده‌است.


استفاده‌ی نهایی از تنظیمات فوق در یک View افزونه

پس از اینکه تصاویر و فایل‌های css و js را به صورت embedded resource تعریف کردیم و همچنین تنظیمات مسیریابی و bundling خاص آن‌ها را نیز مشخص نمودیم، اکنون نوبت به استفاده‌ی از آن‌ها در یک View است:
@{
    ViewBag.Title = "From Plugin 1";
}
@Styles.Render("~/Plugin1/Content")
 
<h2>@ViewBag.Message</h2>
 
<div class="row">
    Embedded image:
    <img src="@Url.Content("~/NewsArea/Images/chart.png")" alt="clock" />
</div>
 
@section scripts
{
    @Scripts.Render("~/Plugin1/Scripts")
}
در اینجا نحوه‌ی تعریف فایل‌های CSS و JS ارائه شده‌ی توسط سیستم Bundling را مشاهده می‌کنید.
همچنین مسیر تصویر مشخص شده‌ی در آن، اینبار یک NewsArea اضافه‌تر دارد. فایل اصلی تصویر، در مسیر Images/chart.png قرار گرفته‌است اما می‌خواهیم این درخواست‌ها را به مسیریابی جدید {NewsArea/Images/{file}.{extension هدایت کنیم. بنابراین نیاز است به این نکته نیز دقت داشت.

اینبار اگر برنامه را اجرا کنیم، می‌توان به سه نکته در آن دقت داشت:


الف) alert اجرا شده از فایل js مدفون شده خوانده شده‌است.
ب) رنگ قرمز متن (تگ h2) از فایل css مدفون شده، گرفته شده‌است.
ج) تصویر نمایش داده شده، همان تصویر مدفون شده‌ی در فایل DLL برنامه است.
و هیچکدام از این فایل‌ها، به پوشه‌های پروژه‌ی اصلی برنامه، کپی نشده‌اند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
 MvcPluginMasterApp-Part2.zip
مطالب
کنترل عمومی فایل‌های آپلودی در ASP.NET MVC
در مطلب «محدود کردن کاربر‌ها به آپلود فایل‌هایی خاص در ASP.NET MVC» تصمیم گیری بر اساس یک لیست سفید صورت می‌گیرد. برای مثال کاربران فقط قرار است تصویرهایی از نوع png یا jpg را ارسال کنند. اکنون نیاز است حالت کلی‌تری را درنظر بگیریم که کاربر قرار است هر نوع فایل دلخواهی را ارسال کند. در اینجا نباید امکان آپلود هر نوع فایلی، خصوصا فایل‌های اجرایی ASP.NET یا هر نوع موتور اجرایی دیگری که ممکن است روی سرور نصب باشد (مثلا PHP)، وجود داشته باشد. برای این منظور فیلتر دیگری به نام AllowUploadSafeFiles طراحی شده است که سورس آن‌را در ذیل مشاهده می‌کنید:
using System;
using System.Linq;
using System.Collections.Generic;
using System.IO;
using System.Web.Mvc;

namespace SecurityModule
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public sealed class AllowUploadSafeFilesAttribute : ActionFilterAttribute
    {
        static readonly IList<string> ExtToFilter = new List<string> { 
            ".aspx", ".asax", ".asp", ".ashx", ".asmx", ".axd", ".master", ".svc", ".php" ,        
            ".php3" , ".php4", ".ph3", ".ph4", ".php4", ".ph5", ".sphp", ".cfm", ".ps", ".stm",
            ".htaccess", ".htpasswd", ".php5", ".phtml", ".cgi", ".pl", ".plx", ".py", ".rb", ".sh", ".jsp",
            ".cshtml", ".vbhtml", ".swf" , ".xap", ".asptxt"
        };

        static readonly IList<string> NameToFilter = new List<string> { 
           "web.config" , "htaccess" , "htpasswd", "web~1.con"
        };

        static bool canUpload(string fileName)
        {
            if (string.IsNullOrWhiteSpace(fileName))
                return false;

            fileName = fileName.ToLowerInvariant();
            var name = Path.GetFileName(fileName);
            var ext = Path.GetExtension(fileName);

            if (string.IsNullOrWhiteSpace(name))
                throw new InvalidOperationException("Uploaded file should have a name.");

            return !ExtToFilter.Contains(ext) &&
                   !NameToFilter.Contains(name) &&
                   !NameToFilter.Contains(ext) &&
                   //for "file.asp;.jpg" files
                   ExtToFilter.All(item => !name.Contains(item));
        }

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var files = filterContext.HttpContext.Request.Files;
            foreach (string file in files)
            {
                var postedFile = files[file];
                if (postedFile == null || postedFile.ContentLength == 0) continue;

                if (!canUpload(postedFile.FileName))
                    throw new InvalidOperationException(string.Format("You are not allowed to upload {0} file.", Path.GetFileName(postedFile.FileName)));
            }

            base.OnActionExecuting(filterContext);
        }
    }
}
در این فیلتر، یک سری پسوند خطرناک مانند aspx، asp و امثال آن فیلتر می‌شوند و اجازه آپلود نخواهند یافت. همچنین فایل‌هایی مانند web.config یا نام داسی آن معادل web~1.con نیز فرصت آپلود نخواهد یافت.
استفاده از این فیلتر سفارشی به نحو زیر است:
[AllowUploadSafeFiles]
public ActionResult UploadFile(HttpPostedFileBase file)
مطالب
پارامترها در ES 6
Destructuring assignment این امکان را به ES 6 اضافه کرده‌است تا بتوان خواص یک شیء یا اعضای یک آرایه را با سهولت بیشتری به متغیرها نسبت داد و نگارش آن بسیار شبیه است به تعریف اشیاء یا آرایه‌ها در جاوا اسکریپت.

Destructuring Arrays

بدون استفاده از Destructuring assignment برای دسترسی به اعضای یک آرایه و انتساب آن‌ها به متغیرهای مختلف، روش متداول زیر مرسوم است:
var first = someArray[0];
var second = someArray[1];
var third = someArray[2];
اما با استفاده از Destructuring assignment این سه سطر، تبدیل به یک سطر می‌شوند:
 var [first, second, third] = someArray;
همانطور که ملاحظه می‌کنید، سمت چپ این انتساب، بسیار شبیه است به تعریف یک آرایه، اما در اینجا مفهوم Destructuring assignment را دارد و سه متغیر جدید را تعریف می‌کند.

یک مثال:
 let [one, two, three] = ['globin', 'ghoul', 'ghost', 'white walker'];
console.log(`one is ${one}, two is ${two}, three is ${three}`)
// => one is globin, two is ghoul, three is ghost
در اینجا ترکیبی از Destructuring assignment و بهبودهای کار با رشته‌ها را در ES 6، ملاحظه می‌کنید. سمت چپ انتساب، سه متغیر جدید را تعریف کرده‌است که این سه متغیر با سه عضو اول آرایه مقدار دهی می‌شوند.

همچنین در این مثال اگر علاقمند بودیم صرفا به اعضای اول و چهارم این آرایه دسترسی پیدا کنیم، می‌توان نوشت:
 let [firstMonster, , , fourthMonster] =  ['globin', 'ghoul', 'ghost', 'white walker'];
console.log(`the first monster is ${firstMonster}, the fourth is ${fourthMonster}`)
// => one is globin, two is ghoul, three is ghost
تعریف یک کامای خالی، سبب پرش به عضو بعدی خواهد شد و به معنای صرفنظر کردن از ایندکس مطرح شده‌است. برای مثال در اینجا از ایندکس‌های 2 و 3 صرفنظر شده‌است.

امکان دسترسی به اعضای تو در تو نیز با Destructuring assignment پیش بینی شده‌است:
 let nested = [1, [2, 3], 4];
let [a, [b], d] = nested;
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4
در مثال فوق، دومین عضو آرایه، خود نیز یک آرایه‌است. برای دسترسی به این آرایه‌ی دوم، دومین عضو Destructuring assignment نیز باید یک Destructuring assignment جدید باشد.

می‌توان از Destructuring assignment جهت جابجایی مقادیر متغیرها بدون انتساب به یک متغیر موقتی نیز استفاده کرد:
 let point = [1, 2];
let [xVal, yVal] = point;
[xVal, yVal] = [yVal, xVal];
console.log(xVal); // 2
console.log(yVal); // 1
در این مثال ابتدا یک آرایه با دو عضو تعریف شده‌است. سپس اعضای این آرایه به دو متغیر جدید xVal و yVal انتساب یافته‌اند. در ادامه در سطر سوم، مقادیر این دو متغیر با هم تعویض شده‌اند.


Destructuring Objects

امکانات Destructuring assignment، به کار با آرایه‌ها محدود نمی‌شود و از آن می‌توان برای کار با اشیاء نیز استفاده کرد. فرض کنید شیء pouch به صورت زیر تعریف شده‌است:
 let pouch = {coins: 10};
روش متداول دسترسی به خاصیت coins، به صورت pouch.coins است:
 let coins = pouch.coins;
اما با استفاده از Destructuring assignment می‌توان نوشت (در حالت کار با اشیاء، بجای [] از {} استفاده می‌شود):
 let {coins} = pouch;
در این مثال، خاصیت coins شیء pouch به متغیر جدید coins انتساب داده شده‌است. نکته‌ای که در اینجا باید به آن دقت داشت، همنامی متغیر جدید coins با خاصیت coins است. اگر بخواهیم این خاصیت را به یک متغیر غیرهمنام انتساب دهیم، باید به صورت زیر عمل کرد:
 let pouch = {coins: 10};
let {coins: newVar1 } = pouch;
console.log(newVar1); //10
در مثال فوق، مقدار خاصیت coins به متغیر جدیدی با نام newVar1 انتساب داده شده‌است.

در اینجا نیز امکان کار با اشیای تو در تو، پیش بینی شده‌است:
let point = {
    x: 1,
    y: 2,
    z: {
         one: 3,
         two: 4
    }
};
let { x: a, y: b, z: { one: c, two: d } } = point;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4
در این مثال، خاصیت z شیء point نیز خود یک شیء دیگر است. برای دسترسی به آن همانند کار با آرایه‌ها نیاز است از یک {} دیگر برای استخراج خواص one و two استفاده کرد.
در انتساب فوق، خاصیت x شیء point به متغیر جدید a، خاصیت y شیء point به متغیر جدید b و خاصیت one شیء منتسب به خاصیت z، به متغیر c و خاصیت two شیء منتسب به خاصیت z، به متغیر d انتساب یافته‌اند.


ترکیب Destructuring Objects و Destructuring Arrays

در مثال زیر، نمونه‌ای ترکیبی از Destructuring اشیاء و آرایه‌ها را با هم مشاهده می‌کنید:
let mixed = {
    one: 1,
    two: 2,
    values: [3, 4, 5]
};
let { one: a, two: b, values: [c, , e] } = mixed;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(e); // 5
در این مثال، خاصیت one شیء mixed به متغیر جدید a، خاصیت two آن به متغیر جدید b و اعضای اول و سوم آرایه‌ی values به متغیرهای جدید c و e انتساب داده شده‌اند. از ایندکس دوم آرایه‌ی values نیز با معرفی یک کاما، صرفنظر گردیده‌است.


Destructuring Function Arguments

از Destructuring در حین تعریف پارامترهای متدها نیز می‌توان استفاده کرد.
 function removeBreakpoint({ url, line, column }) {
  // ...
}
در این مثال، متد removeBreakpoint دارای سه پارامتر ورودی تعریف شده‌ی توسط Destructuring است. در این حالت این پارامترها به صورت خودکار از شیء ارسالی به این متد دریافت و مقدار دهی خواهند شد.

و یا برای مثال در زبان #C امکان تعریف named arguments (آرگومان‌های نامدار) و همچنین تعریف مقادیر پیش فرضی برای آن‌ها وجود دارد. در اینجا نیز می‌توان با استفاده از Destructuring به تعریفی مشابه آن برای ارائه‌ی آرگومان‌هایی با مقادیر پیش فرض رسید:
 function random ({ min=1, max=300 }) {
    return Math.floor(Math.random() * (max - min)) + min
}
console.log(random({}))
// <- 174
console.log(random({max: 24}))
// <- 18
در این مثال پارامترهای min و max تعریف شده‌ی با Destructuring، دارای یک مقدار پیش فرض هستند. اگر شیءایی خالی را به این متد ارسال کنیم، از مقادیر پیش فرض استفاده خواهد شد و یا اگر max را مقدار دهی کنیم، مقدار min، از مقدار پیش فرض آن دریافت می‌گردد.
و یا اینبار jQuery Ajax را می‌توان با پارامترهای پیش فرض آن به صورت ذیل خلاصه نویسی کرد:
 jQuery.ajax = function (url, {
  async = true,
  beforeSend = noop,
  cache = true,
  complete = noop,
  crossDomain = false,
  global = true,
  // ... more config
}) {
    // ... do stuff
};
همچنین اینبار امکان شبیه سازی دریافت چندین خروجی از متد، به نحو ساده‌تر و واضح‌تری میسر است:
 function returnMultipleValues() {
     return [1, 2];
}
var [foo, bar] = returnMultipleValues();
در ابتدا، متدی تعریف شده‌است که یک آرایه‌ی معمولی را بازگشت می‌دهد. اما با استفاده از Destructuring می‌توان چندین خروجی با معنا را در طی یک سطر، از آن دریافت کرد.
شبیه به همین مورد در حین کار با اشیاء نیز میسر است:
function returnMultipleValues() {
  return {
            foo: 1,
            bar: 2
     };
}
var { foo, bar } = returnMultipleValues();
متدی که یک شیء را بر می‌گرداند و با استفاده از Destructuring، خروجی آن به دو متغیر جدید، انتساب داده شده‌اند.


تعریف مقادیر پیش فرض در حین Destructuring

در انتساب ذیل، چون شیء سمت راست، دارای خاصیت foo نیست، مقدار این پارامتر جدید undefined خواهد بود. برای رفع این مشکل می‌توان به آن مقدار پیش فرضی را نیز نسبت داد:
var {foo=3} = { bar: 2 }
console.log(foo)
// <- 3
چند مثال دیگر:
اگر مقدار پیش فرض، ذکر شود و خاصیت متناظر با آن دارای مقدار باشد، از همان مقدار اصلی ذکر شده استفاده می‌شود:
var {foo=3} = { foo: 2 }
console.log(foo)
// <- 2
اما اگر این مقدار undefined باشد، به مقدار پیش فرض سوئیچ خواهد شد:
var {foo=3} = { foo: undefined }
console.log(foo)
// <- 3
این مورد در حین کار با آرایه‌ها نیز برقرار است:
var [b=10] = [undefined]
console.log(b)
// <- 10

var [c=10] = []
console.log(c)
// <- 10


ES6 — default + rest + spread

علاوه بر destructuring، سه قابلیت و بهبود دیگر نیز در زمینه‌ی کار با متغیرها و پارامترها به ES 6 اضافه شده‌اند:

1) امکان تعریف مقادیر پیش فرض پارامترها
function inc(number, increment) {
        increment = increment || 1;
        return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2)); // 3
در جاوا اسکریپت، الزامی برای فراخوانی و ذکر تمام پارامترهای یک متد وجود ندارد. برای نمونه در مثال فوق می‌توان متد inc را با یک و یا دو پارامتر فراخوانی کرد. در حالتیکه پارامتری ذکر نشود، مقدار آن تعریف نشده خواهد بود و روش برخورد با آن استفاده از عملگر || برای تعریف مقداری پیش فرض است. برای بهبود این وضعیت در ES 6، امکان تعریف مقدار پیش فرض پارامترها نیز درنظر گرفته شده‌است:
function inc(number, increment = 1) {
        return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2)); // 3
در ES 6 امکان تعریف پارامترهایی با مقادیر پیش فرض، پیش از پارامترهایی که دارای مقادیر پیش فرض نیستند نیز میسر است (برخلاف زبان سی‌شارپ که چنین اجازه‌ای را نمی‌دهد):
function sum(a, b = 2, c) {
     return a + b + c;
}
console.log(sum(1, 5, 10)); // 16 -> b === 5
console.log(sum(1, undefined, 10)); // 13 -> b as default
همچنین در حین تعریف این مقدار پیش فرض، می‌توان از مقادیر غیر ثابت هم استفاده کرد (باز هم برخلاف سی‌شارپ). برای نمونه در مثال ذیل، خروجی یک متد، به عنوان مقدار پیش فرض پارامتری تعریف شده‌است:
 function getDefaultIncrement() {
    return 1;
}
function inc(number, increment = getDefaultIncrement()) {
    return number + increment;
}
console.log(inc(2, 2)); // 4
console.log(inc(2)); // 3


2) Spread

متد جمع زیر را درنظر بگیرید:
function sum(a, b, c) {
   return a + b + c;
}
روش متداول فراخوانی آن، ذکر تک تک آرگومان‌های آن به ترتیب است. اما با استفاده از عملگر spread اضافه شده به ES 6 که با سه نقطه بیان می‌شود، می‌توان نوشت:
 var args = [1, 2, 3];
console.log(sum(…args)); // 6
عملگر spread اجازه‌ی بسط و پخش شدن اعضای یک آرایه را به پارامترهای متناظر با آن‌ها می‌دهد. به علاوه امکان ترکیب این روش، با روش متداول ذکر صریح آرگومان‌ها نیز وجود دارد:
var args = [1, 2];
console.log(sum(…args, 3)); // 6
در این مثال، آرایه‌ی مدنظر تنها دو عضو دارد و متد sum دارای سه پارامتر است. با استفاده از عملگر spread، دو پارامتر اول متد به صورت خودکار از آرایه واکشی شده و جایگزین می‌شوند. آرگومان سوم هم به صورت متداولی ذکر شده‌است.

مثال‌هایی از ساده سازی اعمال متداول در ES 5 (جاوا اسکریپت فعلی) با کمک ES 6:
الف) ترکیب spread و Destructuring
 a = list[0], rest = list.slice(1)
معادل Destructuring ذیل است:
 [a, ...rest] = list

ب) ساده سازی کار با concat
بجای
 [1, 2].concat(more)
می‌توان نوشت:
[1, 2, ...more]

ج) افزودن یک رنج به یک آرایه
بجای
 list.push.apply(list, [3, 4])
می‌توان نوشت:
 list.push(...[3, 4])


3) Rest

جاوا اسکریپت دارای شیءایی است به نام arguments که توسط آن می‌توان به لیست پارامترهای یک متد دسترسی یافت. برای نمونه مثال ذیل را درنظر بگیرید:
function sum() {
     var numbers = Array.prototype.slice.call(arguments),
     result = 0;
     numbers.forEach(function (number) {
          result += number;
    });
    return result;
}
در اینجا به ظاهر متد sum دارای پارامتری نیست. اما با استفاده از شیء arguments، می‌توان هر تعداد آرگومانی را برای آن متصور شد و فراخوانی‌ها ذیل کاملا مجاز هستند:
console.log(sum(1)); // 1
console.log(sum(1, 2, 3, 4, 5)); // 15
اما مشکل اینجا است که به ظاهر متد sum، هیچ پارامتری را قبول نمی‌کند و هدف از تعریف آن واضح نیست. برای رفع این مشکل، در ES 6 عملگر rest معرفی شده‌است که بسیار شبیه به عملگر spread است:
function sum(…numbers) {
      var result = 0;
      numbers.forEach(function (number) {
          result += number;
      });
      return result;
}
console.log(sum(1)); // 1
console.log(sum(1, 2, 3, 4, 5)); // 15
در اینجا عملگر سه نقطه‌ای rest که به عنوان پارامتر متد معرفی شده‌است، بیانگر امکان دریافت لیستی از آرگومان‌ها، توسط متد sum است. به این ترتیب، تعریف این متد که تعداد آرگومان‌های متغیری را می‌پذیرد، وضوح بیشتری پیدا کرده‌است.
در اینجا باید دقت داشت که پس از ذکر rest، دیگر نمی‌توان پارامتری را تعریف کرد:
 function sum(…numbers, last) { // causes a syntax error
مطالب
اعمال 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 را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
مطالب
دقت نوع داده‌ی decimal در SQL Server و EF Core
از نوع داده‌ا‌ی decimal در SQL Server، بیشتر برای انجام کارهای تجاری و ذخیره‌ی قیمت‌ها و مبالغ استفاده می‌شود؛ جائیکه اعداد و ارقام خیلی سریع بزرگ می‌شوند و گاهی از اوقات ممکن است به همراه اعشار هم باشد. اما ... کار با آن‌ها در SQL Server نیازمند نکات ویژه‌ای است که اگر ندید گرفته شوند، محاسبات نادرستی را سبب خواهند شد!


مفهوم تعریف نوع decimal پیش‌فرض در SQL Server

فرض کنید از EF پیش از EF Core استفاده می‌کنید که به صورت پیش‌فرض، نوع System.Decimal را در مدل‌های شما به همان decimal در SQL Server نگاشت می‌کند. فکر می‌کنید در این حالت خروجی کوئری‌های زیر چه چیزی خواهد بود؟
select '0.4400' as Expected , cast(0.4400 as decimal) as Actual
select '1.3200' as Expected, cast(1.3200 as decimal) as Actual
select '1.7600' as Expected, cast(1.7600 as decimal) as Actual
select '65.0000' as Expected, cast(65.0000 as decimal) as Actual
select '99.50' as Expected, cast(99.50 as decimal) as Actual

این خروجی را در تصویر ذیل مشاهده می‌کنید. در اینجا خصوصا به مورد صفر دقت کنید:


 علت اینجا است که از دیدگاه SQL Server، نوع decimal پیش‌فرض، دقیقا به معنای decimal(18,0) است که به آرگومان اول آن، precision و به آرگومان دوم آن، scale می‌گویند. یعنی حداکثر چه تعداد رقم دسیمال، پیش از ممیز و چه تعداد عدد دسیمال، پس از ممیز قرار است در این نوع داده ذخیره شوند.
بنابراین باتوجه به اینکه در حالت پیش‌فرض، مقدار scale و یا همان تعداد ارقام مجاز پس از ممیز، صفر است، عدد ارائه شده، به نزدیک‌ترین عدد صحیح ممکن، گرد خواهد شد.

به همین جهت برای رفع این مشکل، باید دقیقا مشخص کرد که scale نوع داده‌ای decimal مدنظر چیست. برای مثال می‌توان از decimal(10,4) استفاده کرد که در اینجا، نتایج صحیحی را ارائه می‌دهد:


همچنین به عنوان تمرین، مثال زیر را حل کنید!
 select iif(cast(0.1 + 0.2 as decimal) = 0, 'true', 'false')

بنابراین باید به‌خاطر داشت، اگر از EF 6x (پیش از EF Core) استفاده می‌شود، حتما نیاز است مقادیر precision و scale را دقیقا مشخص کنیم؛ وگرنه حالت پیش‌فرض آن decimal(18,0) است:
modelBuilder.Properties<decimal>().Configure(x => x.HasPrecision(18, 6));


رفتار EF Core با نوع داده‌ای decimal

رفتار EF Core با نوع داده‌ای decimal بهبود یافته و حالت پیش‌فرض آن، بدون هیچگونه تنظیمی، نگاشت به decimal(18,2) است. به علاوه اگر این پیش‌فرض را هم تغییر ندهیم، در حین اعمال Migration، پیام اخطاری را نمایش می‌دهد:
No store type was specified for the decimal property 'Price' on entity type 'Product'.
This will cause values to be silently truncated if they do not fit in the default precision and scale.
Explicitly specify the SQL server column type that can accommodate all the values in 'OnModelCreating'
using 'HasColumnType', specify precision and scale using 'HasPrecision',
or configure a value converter using 'HasConversion'.
اگر می‌خواهید دیگر این اخطار نمایش داده نشود، می‌توان از EF Core 6x به بعد، به صورت زیر و سراسری، تنظیم زیر را اعمال کرد:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<decimal>().HavePrecision(18, 6);
}
و یا روش دیگر تنظیم آن، استفاده از ویژگی جدید [Precision(18, 2)] است که می‌توان آن‌ها را بر روی خواص decimal قرار داد. اگر از نگارش‌های پیش‌از EF Core 6x استفاده می‌کنید، می‌توان از ویژگی [Column(TypeName = "decimal(5, 2)")] نیز استفاده کرد.




دو مطلب مرتبط
- از نوع‌های داده‌ا‌ی float و یا double در مدل‌های EF خود استفاده نکنید.
- همیشه مراقب بزرگ شدن اعداد و مبالغ و جمع نهایی آن‌ها باشید!
نظرات مطالب
پیاده سازی RabbitMQ
یک نکته تکمیلی:
تعیین مقدار durable برابر با true ساعت می‌شود که تعریف صف بصورت مانا باشد نه دیتای داخل صف.
برای اینکه دیتای داخل صف نیز بصورت مانا باشد باید مقدار ویژگی DevliveryMode اینترفیس IBasicProperties را برابر 2 قرار داد:
using var connection = _rabbitMqService.CreateChannel();
using var model = connection.CreateModel();
IBasicProperties props = model.CreateBasicProperties();
props.DeliveryMode = 2;
var body = Encoding.UTF8.GetBytes("Hi");
model.BasicPublish("UserExchange",
                             string.Empty,
                             basicProperties: props,
                             body: body);