مطالب
نحوه اضافه کردن Auto-Complete به جستجوی لوسین در ASP.NET MVC و Web forms
پیشنیازها:
چگونه با استفاده از لوسین مطالب را ایندکس کنیم؟
چگونه از افزونه jQuery Auto-Complete استفاده کنیم؟
نحوه استفاده صحیح از لوسین در ASP.NET


اگر به جستجوی سایت دقت کرده باشید، قابلیت ارائه پیشنهاداتی به کاربر توسط یک Auto-Complete به آن اضافه شده‌است. در مطلب جاری به بررسی این مورد به همراه دو مثال Web forms و MVC پرداخته خواهد شد.


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

الف) دریافت لوسین
از طریق NuGet آخرین نگارش را دریافت و به پروژه خود اضافه کنید. همچنین Lucene.NET Contrib را نیز به همین نحو دریافت نمائید.

ب) ایجاد ایندکس
کدهای این قسمت با مطلب برجسته سازی قسمت‌های جستجو شده، یکی است:
using System.Collections.Generic;
using System.IO;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Documents;
using Lucene.Net.Index;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class CreateIndex
    {
        static readonly Lucene.Net.Util.Version _version = Lucene.Net.Util.Version.LUCENE_30;

        public static Document MapPostToDocument(Post post)
        {
            var postDocument = new Document();
            postDocument.Add(new Field("Id", post.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED));
            var titleField = new Field("Title", post.Title, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS);
            titleField.Boost = 3;
            postDocument.Add(titleField);
            postDocument.Add(new Field("Body", post.Body.RemoveHtmlTags(), Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.WITH_POSITIONS_OFFSETS));
            return postDocument;
        }

        public static void CreateFullTextIndex(IEnumerable<Post> dataList, string path)
        {
            var directory = FSDirectory.Open(new DirectoryInfo(path));
            var analyzer = new StandardAnalyzer(_version);
            using (var writer = new IndexWriter(directory, analyzer, create: true, mfl: IndexWriter.MaxFieldLength.UNLIMITED))
            {
                foreach (var post in dataList)
                {
                    writer.AddDocument(MapPostToDocument(post));
                }

                writer.Optimize();
                writer.Commit();
                writer.Close();
                directory.Close();
            }
        }
    }
}
تنها تفاوت آن اضافه شدن titleField.Boost = 3 می‌باشد. توسط Boost به لوسین خواهیم گفت که اهمیت عبارات ذکر شده در عناوین مطالب، بیشتر است از اهمیت متون آن‌ها.


ج) تهیه قسمت منبع داده Auto-Complete

namespace LuceneSearch.Core.Model
{
    public class SearchResult
    {
        public int Id { set; get; }
        public string Title { set; get; }
    }
}

using System.Collections.Generic;
using System.IO;
using Lucene.Net.Index;
using Lucene.Net.Search;
using Lucene.Net.Store;
using LuceneSearch.Core.Model;
using LuceneSearch.Core.Utils;

namespace LuceneSearch.Core
{
    public static class AutoComplete
    {
        private static IndexSearcher _searcher;

        /// <summary>
        /// Get terms starting with the given prefix
        /// </summary>
        /// <param name="prefix"></param>
        /// <param name="maxItems"></param>
        /// <returns></returns>
        public static IList<SearchResult> GetTermsScored(string indexPath, string prefix, int maxItems = 10)
        {
            if (_searcher == null)
                _searcher = new IndexSearcher(FSDirectory.Open(new DirectoryInfo(indexPath)), true);

            var resultsList = new List<SearchResult>();
            if (string.IsNullOrWhiteSpace(prefix))
                return resultsList;

            prefix = prefix.ApplyCorrectYeKe();

            var results = _searcher.Search(new PrefixQuery(new Term("Title", prefix)), null, maxItems);
            if (results.TotalHits == 0)
            {
                results = _searcher.Search(new PrefixQuery(new Term("Body", prefix)), null, maxItems);
            }

            foreach (var doc in results.ScoreDocs)
            {
                resultsList.Add(new SearchResult
                {
                    Title = _searcher.Doc(doc.Doc).Get("Title"),
                    Id = int.Parse(_searcher.Doc(doc.Doc).Get("Id"))
                });
            }

            return resultsList;
        }
    }
}
توضیحات:
برای نمایش Auto-Complete نیاز به منبع داده داریم که نحوه ایجاد آن‌را در کدهای فوق ملاحظه می‌کنید. در اینجا توسط جستجوی سریع لوسین و امکانات PrefixQuery آن، به تعدادی مشخص (maxItems)، رکوردهای یافت شده را بازگشت خواهیم داد. خروجی حاصل لیستی است از SearchResultها شامل عنوان مطلب و Id آن. عنوان را به کاربر نمایش خواهیم داد؛ از Id برای هدایت او به مطلبی مشخص استفاده خواهیم کرد.


د) نمایش Auto-Complete در ASP.NET MVC

using System.Text;
using System.Web.Mvc;
using LuceneSearch.Core;
using System.Web;

namespace LuceneSearch.Controllers
{
    public class HomeController : Controller
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public ActionResult Index(int? id)
        {
            if (id.HasValue)
            {
                //todo: do something
            }
            return View(); //Show the page
        }

        public virtual ActionResult ScoredTerms(string q)
        {
            if (string.IsNullOrWhiteSpace(q))
                return Content(string.Empty);

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = this.Url.Action(actionName: "Index", controllerName: "Home", routeValues: new { id = item.Id }, protocol: "http");
                result.AppendLine(item.Title + "|" + postUrl);
            }

            return Content(result.ToString());
        }
    }
}

@{
    ViewBag.Title = "جستجو";
    var scoredTermsUrl = Url.Action(actionName: "ScoredTerms", controllerName: "Home");
    var bulletImage = Url.Content("~/Content/Images/bullet_shape.png");
}
<h2>
    جستجو</h2>

<div align="center">
    @Html.TextBox("term", "", htmlAttributes: new { dir = "ltr" })
    <br />
    جهت آزمایش lu را وارد نمائید
</div>

@section scripts
{
    <script type="text/javascript">
        EnableSearchAutocomplete('@scoredTermsUrl', '@bulletImage');
    </script>
}

function EnableSearchAutocomplete(url, img) {
    var formatItem = function (row) {
        if (!row) return "";
        return "<img src='" + img + "' /> " + row[0];
    }

    $(document).ready(function () {
        $("#term").autocomplete(url, {
            dir: 'rtl', minChars: 2, delay: 5,
            mustMatch: false, max: 20, autoFill: false,
            matchContains: false, scroll: false, width: 300,
            formatItem: formatItem
        }).result(function (evt, row, formatted) {
            if (!row) return;
            window.location = row[1];
        });
    });
}
توضیحات:
- ابتدا ارجاعاتی را به jQuery، افزونه Auto-Complete و اسکریپت سفارشی تهیه شده، در فایل layout پروژه تعریف خواهیم کرد.
در اینجا سه قسمت را مشاهده می‌کنید: کدهای کنترلر، View متناظر و اسکریپتی که Auto-Complete را فعال خواهد ساخت.
- قسمت مهم کدهای کنترلر، دو سطر زیر هستند:
result.AppendLine(item.Title + "|" + postUrl);
return Content(result.ToString());
مطابق نیاز افزونه انتخاب شده در مثال جاری، فرمت خروجی مدنظر باید شامل سطرهایی حاوی متن قابل نمایش به همراه یک Id (یا در اینجا یک آدرس مشخص) باشد. البته ذکر این Id اختیاری بوده و در اینجا جهت تکمیل بحث ارائه شده است.
return Content هم سبب بازگشت این اطلاعات به افزونه خواهد شد.
- کدهای View متناظر بسیار ساده هستند. تنها نام TextBox تعریف شده مهم می‌باشد که در متد جاوا اسکریپتی EnableSearchAutocomplete استفاده شده است. به علاوه، نحوه مقدار دهی آدرس دسترسی به اکشن متد ScoredTerms نیز مهم می‌باشد.
- در متد EnableSearchAutocomplete نحوه فراخوانی افزونه autocomplete را ملاحظه می‌کنید.
جهت آن، به راست به چپ تنظیم شده است. با 2 کاراکتر ورودی فعال خواهد شد با وقفه‌ای کوتاه. نیازی نیست تا انتخاب کاربر از لیست ظاهر شده حتما با عبارت جستجو شده صد در صد یکی باشد. حداکثر 20 آیتم در لیست ظاهر خواهند شد. اسکرول بار لیست را حذف کرده‌ایم. عرض آن به 300 تنظیم شده است و نحوه فرمت دهی نمایشی آن‌را نیز ملاحظه می‌کنید. برای این منظور از متد formatItem استفاده شده است. آرایه row در اینجا در برگیرنده اعضای Title و Id ارسالی به افزونه است. اندیس صفر آن به عنوان دریافتی اشاره می‌کند.
همچنین نحوه نشان دادن عکس العمل به عنصر انتخابی را هم ملاحظه می‌کنید (در متد result مقدار دهی شده).  window.location را به عنصر دوم آرایه row هدایت خواهیم کرد. این عنصر دوم مطابق کدهای اکشن متد تهیه شده، به آدرس یک صفحه اشاره می‌کند.


ه) نمایش Auto-Complete در ASP.NET WebForms

قسمت عمده مطالب فوق با وب فرم‌ها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.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">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>جستجو</title>
    <link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script>
    <script src="Scripts/custom.js" type="text/javascript"></script>
</head>
<body dir="rtl">
    <h2>
        جستجو</h2>
    <form id="form1" runat="server">
    <div align="center">
        <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox>
        <br />
        جهت آزمایش lu را وارد نمائید
    </div>
    </form>
    <script type="text/javascript">
        EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png');
    </script>
</body>
</html>

using System.Text;
using System.Web;
using LuceneSearch.Core;

namespace LuceneSearch.WebForms
{
    public class Search : IHttpHandler
    {
        static string _indexPath = HttpRuntime.AppDomainAppPath + @"App_Data\idx";

        public void ProcessRequest(HttpContext context)
        {
            string q = context.Request.QueryString["q"];
            if (string.IsNullOrWhiteSpace(q))
            {
                context.Response.Write(string.Empty);
                context.Response.End();
            }

            var result = new StringBuilder();
            var items = AutoComplete.GetTermsScored(_indexPath, q);
            foreach (var item in items)
            {
                var postUrl = "Default.aspx?id=" + item.Id;
                result.AppendLine(item.Title + "|" + postUrl);
            }

            context.Response.ContentType = "text/plain";
            context.Response.Write(result.ToString());
            context.Response.End();
        }

        public bool IsReusable
        { get { return false; } }
    }
}

در اینجا بجای Controller از یک Generic handler استفاده شده است (Search.ashx).
result.AppendLine(item.Title + "|" + postUrl);
context.Response.Write(result.ToString());
در آن، عنوان مطالب یافت شده به همراه یک آدرس مشخص، تهیه و در Response نوشته خواهند شد.


کدهای کامل مثال فوق را از اینجا می‌توانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین می‌باشد که IIS Express 7.5 را نیز پیشتر نصب کرده‌اید.
کلمه عبور فایل: dotnettips91
 
مطالب
آموزش فایرباگ - #3 - JavaScript Development
توابع توسعه جاوا اسکریپت در فایرباگ به دو بخش تقسیم می‌شوند :
  • توابع خط فرمان - Command Line API
  • توابع کنسول - Console API
توابع خط فرمان توابعی هستند که فقط در خط فرمان قابل استفاده هستند و توابع کنسول هم توابعی هستند که خارج از محیط خط فرمان ( ، در بین کدهای جاوا اسکریپت برنامه ) هم قابل استفاده هستند .
در این قسمت توابع خط فرمان را بررسی خواهیم کرد و در قسمت بعدی با توابع کنسول آشنا خواهیم شد .

توابع خط فرمان - Command Line API :

این توابع حدود 14 تا هستند که بوسیله آنها می‌توانیم در حین اجرای برنامه تست‌های مختلفی انجام داده یا اطلاعاتی از قسمت‌های مختلف بدست آوریم .
توجه : برای همراه شدن با تست‌های انجام شده در این مقاله می‌توانید کد صفحه‌ی زیر را ذخیره کنید و برای اجرای کدها ، آن‌ها را در قسمت خط فرمان ( در تب کنسول ) قرار بدهید و دکمه‌ی Run ( یا Ctrl + Enter ) را بزنید .
<div id="first" class="content">Content1 with css class and id</div>
<div class="content">
    Content2 with css class
    <a class="links" href="#">Link1</a>
    <a href="#">Link2</a>
</div>
<div>
    Content3 without css class and id
</div>
<input type="button" onclick="myFunc()" value="Run myFunc" />
<input type="text" id="myInput" />

<script type="text/javascript">
    function myFunc() {
        loop(1000);
        loop(50000);
    }
    function loop(number) {
        for (var i = 0; i < number; i++) { }
    }
</script>
قبل از توضیح این توابع ، یک مثال ساده می‌زنیم :
فرض کنید می‌خواهید همه‌ی المنت هایی که با یک selector مطابقت دارند را مشاهده کنید . ( آرایه ای از المنت‌ها دریافت کنید )
$$("div.content");
نتیجه :

می خواهید یکی از توابع جاوا اسکریپت برنامه را اجرا و آن را از لحاظ سرعت ، تعداد فراخوانی شدن و ... بررسی کنید .
profile("myFunc Testing");
myFunc();
profileEnd();
نتیجه :

اکنون با همه‌ی توابع خط فرمان آشنا می‌شویم :

  • $(id)

    معادل دستور document.getElementById است که یک المنت با id داده شده بر می‌گرداند .
    $("first");
    نتیجه :

  • $$(selector)


    آرایه ای از المنت‌های مطابق با selector داده شده بر می‌گرداند .
    $$("div.content")
    نتیجه :

    به تفاوت دو دستور توجه کنید . خروجی دستور اول ، یک المنت است و خروجی دستور دوم یک آرایه از المنت که بین [ و ] قرار گرفته اند .

    برای آشنایی بیشتر با CSS Seletor‌ها به این لینک مراجعه کنید : http://www.w3.org/TR/css3-selectors

  • $x(xPathExpression)

    آرایه ای از المنت هایی را بر می‌گرداند که با xPath داده شده مطابقت داشته باشند .
    var objects = $x("html/body/div[2]/a")
    
    for(var i = 0; i < objects.length; i++) {
       console.log(objects[i]);
    }
    نتیجه :

    برای آشنایی بیشتر با عبارات xPath به این لینک مراجعه کنید : http://www.w3schools.com/xpath

  • dir(object)

    تمام خصوصیات شیء ارسال شده را لیست می‌کند .
    var objects = $x("html/body/div[2]/a")
    
    dir(objects);
    نتیجه :

  • dirxml(node)

    سورس یک المنت را بصورت درختواره ( tree ) پرینت می‌کند . همچنین با کلیک بروی هر node ، فایرباگ آن node را در تب html نمایش می‌دهد .
    var node = $("first");
    dirxml(node);
    نتیجه :

    توجه کنید که این دستور فقط یک node دریافت می‌کند . برای همین اگر از دستور $$("#first") استفاده می‌کنید ، چون این دستور یک آرایه بر می‌گرداند ، باید اولین عضو آرایه را دریافت و ارسال کنید .
    یعنی :
    var node = $$("#first")[0];
    dirxml(node);
  • clear()

    این دستور محیط console را خالی می‌کند . عملکرد این دستور معادل کلیک دکمه‌ی Clear ( در بالا - چپ تب کنسول ) است .

  • inspect(object[,tabName])

    توسط این دستور می‌توانید یک شیء را در مناسب‌ترین تب فایرباگ یا یکی از تب‌های مورد نظر خود ، Inspect کنید .
    var node = $("first");
    
    inspect(node); // inspect in html tab
    inspect(node,'dom'); // inspect in dom tab
  • keys(object)

    آرایه ای از "نام" تمام خصوصیات شیء ارسال شده بر می‌گرداند .
    var obj = $("first");
    keys(obj)
  • values(object)

    آرایه ای از "مقدار" تمام خصوصیات شیء ارسال شده بر می‌گرداند .
    var obj = $("first");
    values(obj)
  • debug(fn) and undebug(fn)

    این متدها یک BreakPoint در ابتدای تابع مشخص شده اضافه/حذف می‌کنند . ( در تب Script ) . به همین ترتیب هنگامی که تابع مورد نظر فراخوانی شود ، در نقطه ای که BreakPoint قرار داده شده توقف خواهد کرد .
    البته می‌شود BreakPoint را دستی هم قرار داد . در اصل این تابع ، این عملیات را ساده‌تر می‌کند .
    debug(myFunc);
    myFunc();
    undebug(myFunc);
  • monitor(fn) and unmonitor(fn)

    این متدها برای فعال/غیرفعال کردن Logging فراخوانی‌های یک تابع استفاده می‌شوند .
    در حالت عادی برای پی بردن به اینکه یک تابع اجرا می‌شود یا نه ، در تابع مورد نظر یک alert قرار می‌دهیم و تست می‌کنیم . که این روش در برنامه برنامه‌های بزرگ صحیح نیست . زیرا در این حالت باید بین حجم زیادی کد به دنبال تابع مورد نظر بگردیم  و سپس alert را قرار بدهیم و بعد اطمینان از صحت عملکرد تابع مجدد آن را حذف کرد ، که با اتلاف زمان و به خطر انداختن کدها همراه است .
    اما با استفاده از این متدها ، تنها نیاز به داشتن اسم تابع داریم ( و نه مکان تابع در کدهای برنامه ) .
    تست monitor :
    monitor(myFunc);
    // now click on "Run myFunc" button
    تست unmonitor :
    unmonitor(myFunc);
    // now click on "Run myFunc" button
  • monitorEvents(object[, types]) and unmonitorEvents(object[, types])

    این متدها عملیات Event Logging برای یک شیء را فعال/غیرفعال می‌کنند . در کنار شیء مورد نظر ، می‌توان نوع رویداد را هم به متد ارسال کرد . در این صورت عملیات Logging فقط برای همان گروه رویداد/رویداد ، فعال/غیرفعال می‌شود .
    منظور از گروه رویداد ، مجموعه رویداد‌های یک شیء است . مثلا mousemove , moseover , mousedown , ... در گروه mouse قرار می‌گیرند . یعنی می‌توانید با ارسال کلمه‌ی mouse فقط رویدادهای mouse را تحت نظر بگیرید یا اینکه فقط یک رویداد را مشخص کنید ، مثل mousedown .
    راه ساده‌تر فعال کردن Event Logging ، رفتن به تب Html ، راست کلیک کردن بروی المنت مورد نظر و فعال کردن گزینه‌ی Log Events می‌باشد .
    var obj = $("myInput");
    monitorEvents(obj,'keypress');

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

    توضیحات بیشتر : http://getfirebug.com/wiki/index.php/MonitorEvents

  • profile([title]) and profileEnd()

    این متدها ، JavaScript Profiler را فعال/غیرفعال می‌کنند . هنگام فعال کردن می‌توانید یک عنوان هم برای پروفایل مشخص کنید . در قسمت قبلی مقاله در مورد این قابلیت توضیحاتی ارائه شد .
    سه را برای اجرای Profiler وجود دارد :
    1 - کلیک بروی دکمه‌ی Profiler در بالای تب کنسول .
    2 - استفاده از کد console.profile("ProfileTitle") در کدهای جاوا اسکریپت .
    3 - استفاده از متد profile("Profile Title") در خط فرمان .

    profile("myFunc Testing");
    myFunc();
    profileEnd();
    نتیجه :



    ستون‌های Profiler :

    Function : نام تابع اجرا شده .
    Calls : تعداد دفعات فراخوانی تابع .
    Percent : زمان اجرای تابع در زمان کل ، به درصد .
    Own Time : زمان اجرای تابع به تنهایی . برای مثال در کد ما ، زمان اجرای تابع myFunc به تنهایی تقریبا صفر است زیرا عمیاتی در خود انجام نمی‌دهد و زمان صرف شده در این تابع ، برای اجرای 2 با تابع loop است . این زمان ( Own Time ) زمان اجرای تابع ، منهای زمان صرف شده برای فراخوانی توابع دیگر است .
    Time : زمان اجرای تابع از نقطه‌ی آغاز تا پایان . مجموع زمان اجرای خود تابع به همراه زمان اجرای توابع فراخوانی شده . در کد ما ، این زمان ، مجموع زمان اجرای خود تابع به همراه دو بار فراخوانی تابع loop است .
    Avg : میانگین زمان اجرای هربار تابع . فرمول : Avg = Time / Calls
    Min & Max : حداقل و حداکثر زمان اجرای تابع .
    File : نام فایل و شماره خطی که تابع در آن قرار دارد .

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

منابع :
Packtpub.Firebug.1.5.Editing.Debugging.and.Monitoring.Web.Pages.Apr.2010
مطالب
ASP.NET MVC #22

تهیه سایت‌های چند زبانه و بومی سازی نمایش اطلاعات در ASP.NET MVC

زمانیکه دات نت فریم ورک نیاز به انجام اعمال حساس به مسایل بومی را داشته باشد،‌ ابتدا به مقادیر تنظیم شده دو خاصیت زیر دقت می‌کند:
الف) System.Threading.Thread.CurrentThread.CurrentCulture
بر این اساس دات نت می‌تواند تشخیص دهد که برای مثال خروجی متد DateTime.Now.ToString در کانادا و آمریکا باید با هم تفاوت داشته باشند. مثلا در آمریکا ابتدا ماه، سپس روز و در آخر سال نمایش داده می‌شود و در کانادا ابتدا سال، بعد ماه و در آخر روز نمایش داده خواهد شد. یا نمونه‌ی دیگری از این دست می‌تواند نحوه نمایش علامت واحد پولی کشورها باشد.
ب) System.Threading.Thread.CurrentThread.CurrentUICulture
مقدار CurrentUICulture بر روی بارگذاری فایل‌های مخصوصی به نام Resource، تاثیر گذار است.

این خواص را یا به صورت دستی می‌توان تنظیم کرد و یا ASP.NET، این اطلاعات را از هدر Accept-Language دریافتی از مرورگر کاربر به صورت خودکار مقدار دهی می‌کند. البته برای این منظور نیاز است یک سطر زیر را به فایل وب کانفیگ برنامه اضافه کرد:

<system.web>
<globalization culture="auto" uiCulture="auto" />

یا اگر نیاز باشد تا برنامه را ملزم به نمایش اطلاعات Resource مرتبط با فرهنگ بومی خاصی کرد نیز می‌توان در همین قسمت مقادیر culture و uiCulture را دستی تنظیم نمود و یا اگر همانند برنامه‌هایی که چند لینک را بالای صفحه نمایش می‌دهند که برای مثال به نگارش‌های فارسی/عربی/انگلیسی اشاره می‌کند، اینکار را با کد نویسی نیز می‌توان انجام داد:

System.Threading.Thread.CurrentThread.CurrentCulture =
System.Globalization.CultureInfo.CreateSpecificCulture("fa");


جهت آزمایش این مطلب، ابتدا تنظیم globalization فوق را به فایل وب کانفیگ برنامه اضافه کنید. سپس به مسیر زیر در IE مراجعه کنید:

IE -> Tools -> Intenet options -> Genarl tab -> Languages

در اینجا می‌توان هدر Accept-Language را مقدار دهی کرد. برای نمونه اگر مقدار زبان پیش فرض را به فرانسه تنظیم کنیم (به عنوان اولین زبان تعریف شده در لیست) و سپس سعی در نمایش مقدار decimal زیر را داشته باشیم:

string.Format("{0:C}", 10.5M)

اگر زبان پیش فرض، انگلیسی آمریکایی باشد، $ نمایش داده خواهد شد و اگر زبان به فرانسه تنظیم شود، یورو در کنار عدد مبلغ نمایش داده می‌شود.
تا اینجا تنها با تنظیم culture=auto به این نتیجه رسیده‌ایم. اما سایر قسمت‌های صفحه چطور؟ برای مثال برچسب‌های نمایش داده شده را چگونه می‌توان به صورت خودکار بر اساس Accept-Language مرجح کاربر تنظیم کرد؟ خوشبختانه در دات نت، زیر ساخت مدیریت برنامه‌های چند زبانه به صورت توکار وجود دارد که در ادامه به بررسی آن خواهیم پرداخت.


آشنایی با ساختار فایل‌های Resource


فایل‌های Resource یا منبع، در حقیقت فایل‌هایی هستند مبتنی بر XML با پسوند resx و هدف آن‌ها ذخیره سازی رشته‌های متناظر با فرهنگ‌های مختلف می‌باشد و برای استفاده از آن‌ها حداقل یک فایل منبع پیش فرض باید تعریف شود. برای نمونه فایل mydata.resx را در نظر بگیرید. برای ایجاد فایل منبع اسپانیایی متناظر، باید فایلی را به نام mydata.es.resx تولید کرد. البته نوع فرهنگ مورد استفاده را کاملتر نیز می‌توان ذکر کرد برای مثال mydata.es-mex.resx جهت فرهنگ اسپانیایی مکزیکی بکارگرفته خواهد شد، یا mydata.fr-ca.resx به فرانسوی کانادایی اشاره می‌کند. سپس مدیریت منابع دات نت فریم ورک بر اساس مقدار CurrentUICulture جاری، اطلاعات فایل متناظری را بارگذاری خواهد کرد. اگر فایل متناظری وجود نداشت، از اطلاعات همان فایل پیش فرض استفاده می‌گردد.
حین تهیه برنامه‌ها نیازی نیست تا مستقیما با فایل‌های XML منابع کار کرد. زمانیکه اولین فایل منبع تولید می‌شود، به همراه آن یک فایل cs یا vb نیز ایجاد خواهد شد که امکان دسترسی به کلیدهای تعریف شده در فایل‌های XML را به صورت strongly typed میسر می‌کند. این فایل‌های خودکار، تنها برای فایل پیش فرض mydata.resx تولید می‌شوند،‌از این جهت که تعاریف اطلاعات سایر فرهنگ‌های متناظر نیز باید با همان کلیدهای فایل پیش فرض آغاز شوند. تنها «مقادیر» کلیدهای تعریف شده در کلاس‌های منبع متفاوت هستند.
اگر به خواص فایل‌های resx در VS.NET دقت کنیم، نوع Build action آن‌ها به embedded resource تنظیم شده است.


مثالی جهت بررسی استفاده از فایل‌های Resource

یک پروژه جدید خالی ASP.NET MVC را آغاز کنید. فایل وب کانفیگ آن‌را ویرایش کرده و تنظیمات globalization ابتدای بحث را به آن اضافه کنید. سپس مدل، کنترلر و View متناظر با متد Index آن‌را با محتوای زیر به پروژه اضافه نمائید:

namespace MvcApplication19.Models
{
public class Employee
{
public int Id { set; get; }
public string Name { set; get; }
}
}

using System.Web.Mvc;
using MvcApplication19.Models;

namespace MvcApplication19.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
var employee = new Employee { Name = "Name 1" };
return View(employee);
}
}
}

@model MvcApplication19.Models.Employee
@{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
<fieldset>
<legend>Employee</legend>
<div class="display-label">
Name
</div>
<div class="display-field">
@Html.DisplayFor(model => model.Name)
</div>
</fieldset>
<fieldset>
<legend>Employee Info</legend>
@Html.DisplayForModel()
</fieldset>

قصد داریم در View فوق بر اساس uiCulture کاربر مراجعه کننده به سایت، برچسب Name را مقدار دهی کنیم. اگر کاربری از ایران مراجعه کند، «نام کارمند» نمایش داده شود و سایر کاربران، «Employee Name» را مشاهده کنند. همچنین این تغییرات باید بر روی متد Html.DisplayForModel نیز تاثیرگذار باشد.
برای این منظور بر روی پوشه Views/Home که محل قرارگیری فایل Index.cshtml فوق است کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس در صفحه ظاهر شده، گزینه «Resources file» را انتخاب کرده و برای مثال نام Index_cshtml.resx را وارد کنید.
به این ترتیب اولین فایل منبع مرتبط با View جاری که فایل پیش فرض نیز می‌باشد ایجاد خواهد شد. این فایل، به همراه فایل Index_cshtml.Designer.cs تولید می‌شود. سپس همین مراحل را طی کنید، اما اینبار نام Index_cshtml.fa.resx را حین افزودن فایل منبع وارد نمائید که برای تعریف اطلاعات بومی ایران مورد استفاده قرار خواهد گرفت. فایل دومی که اضافه شده است، فاقد فایل cs همراه می‌باشد.
اکنون فایل Index_cshtml.resx را در VS.NET باز کنید. از بالای صفحه، به کمک گزینه Access modifier، سطح دسترسی متدهای فایل cs همراه آن‌را به public تغییر دهید. پیش فرض آن internal است که برای کار ما مفید نیست. از این جهت که امکان دسترسی به متدهای استاتیک تعریف شده در فایل خودکار Index_cshtml.Designer.cs را در View های برنامه، نخواهیم داشت. سپس دو جفت «نام-مقدار» را در فایل resx وارد کنید. مثلا نام را Name و مقدار آن‌را «Employee Name» و سپس نام دیگر را NameIsNotRight و مقدار آن‌را «Name is required» وارد نمائید.
در ادامه فایل Index_cshtml.fa.resx را باز کنید. در اینجا نیز دو جفت «نام-مقدار» متناظر با فایل پیش فرض منبع را باید وارد کرد. کلیدها یا نام‌ها یکی است اما قسمت مقدار اینبار باید فارسی وارد شود. مثلا نام را Name و مقدار آن‌را «نام کارمند» وارد نمائید. سپس کلید یا نام NameIsNotRight و مقدار «لطفا نام را وارد نمائید» را تنظیم نمائید.
تا اینجا کار تهیه فایل‌های منبع متناظر با View جاری به پایان می‌رسد.
در ادامه با کمک فایل Index_cshtml.Designer.cs که هربار پس از تغییر فایل resx متناظر آن به صورت خودکار توسط VS.NET تولید و به روز می‌شود، می‌توان به کلیدها یا نام‌هایی که تعریف کرده‌ایم، در قسمت‌های مختلف برنامه دست یافت. برای نمونه تعریف کلید Name در این فایل به نحو زیر است:

namespace MvcApplication19.Views.Home {
public class Index_cshtml {
public static string Name {
get {
return ResourceManager.GetString("Name", resourceCulture);
}
}
}
}

بنابراین برای استفاده از آن در هر View ایی تنها کافی است بنویسیم:

@MvcApplication19.Views.Home.Index_cshtml.Name

به این ترتیب بر اساس تنظیمات محلی کاربر، اطلاعات به صورت خودکار از فایل‌های Index_cshtml.fa.resx فارسی یا فایل پیش فرض Index_cshtml.resx، دریافت می‌گردد.
علاوه بر امکان دسترسی مستقیم به کلیدهای تعریف شده در فایل‌های منبع، امکان استفاده از آن‌ها توسط data annotations نیز میسر است. در این حالت می‌توان مثلا پیغام‌های اعتبار سنجی را بومی کرد یا حین استفاده از متد Html.DisplayForModel، بر روی برچسب نمایش داده شده خودکار، تاثیر گذار بود. برای اینکار باید اندکی مدل برنامه را ویرایش کرد:

using System.ComponentModel.DataAnnotations;

namespace MvcApplication19.Models
{
public class Employee
{
[ScaffoldColumn(false)]
public int Id { set; get; }

[Display(ResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
Name = "Name")]
[Required(ErrorMessageResourceType = typeof(MvcApplication19.Views.Home.Index_cshtml),
ErrorMessageResourceName = "NameIsNotRight")]
public string Name { set; get; }
}
}

همانطور که ملاحظه می‌کنید، حین تعریف ویژگی‌های Display یا Required، امکان تعریف نام کلاس متناظر با فایل resx خاصی وجود دارد. به علاوه ErrorMessageResourceName به نام یک کلید در این فایل و یا پارامتر Name ویژگی Display نیز به نام کلیدی در فایل منبع مشخص شده، اشاره می‌کنند. این اطلاعات توسط متدهای Html.DisplayForModel، Html.ValidationMessageFor، Html.LabelFor و امثال آن به صورت خودکار مورد استفاده قرار خواهند گرفت.


نکته‌ای در مورد کش کردن اطلاعات
در این مثال اگر فیلتر OutputCache را بر روی متد Index تعریف کنیم، حتما نیاز است به هدر Accept-Language نیز دقت داشت. در غیراینصورت تمام کاربران، صرفنظر از تنظیمات بومی آن‌ها، یک صفحه را مشاهده خواهند کرد:

[OutputCache(Duration = 60, VaryByHeader = "Accept-Language")]
public ActionResult Index()


مطالب
ASP.NET MVC #16

مدیریت خطاها در یک برنامه ASP.NET MVC


استفاده از فیلتر HandleError

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

using System;
using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class HomeController : Controller
{
[HandleError]
public ActionResult Index()
{
throw new InvalidOperationException();
return View();
}
}
}

در اینجا جهت آزمایش برنامه، به عمد یک استثنای دستی را صادر می‌کنیم. برای آزمایش برنامه هم نیاز است آن‌را خارج از دیباگر VS.NET اجرا کرد (آدرس برنامه را مستقیما خارج از VS.NET در یک مرورگر وارد کنید). همچنین یک سطر زیر را نیز لازم است به فایل web.config برنامه اضافه نمائید:

<system.web>
<customErrors mode="On" />

اکنون اگر برنامه را خارج از مرورگر اجرا کنید، با توجه به استفاده از ویژگی HandleError و همچنین بروز یک استثنا در متد Index، خودبخود صفحه Views\Shared\Error.cshtml به کاربر نمایش داده خواهد شد. در غیراینصورت صفحه زرد رنگ پیش فرض خطای ASP.NET به کاربر نمایش داده می‌شود که محتوای آن‌ها بیشتر برای برنامه نویس‌ها مناسب است و نه کاربران نهایی سیستم.
اگر علاقمند باشید که این ویژگی به صورت خودکار به تمام متدهای کنترلرهای برنامه اعمال شود، کافی است یک سطر زیر را به متد Application_Start فایل Global.asax.cs اضافه نمائید:

GlobalFilters.Filters.Add(new HandleErrorAttribute());

البته نیازی به انجام اینکار نیست زیرا اگر به متد RegisterGlobalFilters فایل Global.asax.cs دقت کنیم، اینکار پیشتر توسط قالب پیش فرض VS.NET انجام شده است. فقط برای فعال سازی آن نیاز است تگ customErrors در فایل وب کانفیگ برنامه مقدار دهی و تنظیم شود.



استفاده از صفحه خطای سفارشی دیگری بجای فایل Error.cshtml

امکان تنظیم نمایش صفحه خطای سفارشی دیگری نیز وجود دارد. برای مثال استفاده از فایل Views\Shared\CustomErrorView.cshtml :

[HandleError(View = "CustomErrorView")]



استفاده از صفحات خطای متفاوت به ازای استثناهای مختلف

می‌توان فیلتر HandleError را تنها به یک نوع استثنای خاص محدود کرد. همچنین امکان استفاده از چندین ویژگی HandleError برای یک متد نیز وجود دارد:

[HandleError(ExceptionType = typeof(NullReferenceException), View = "ErrorHandling")]



دسترسی به اطلاعات استثناء در صفحه نمایش خطاها

زمانیکه برنامه به صفحه خطا هدایت می‌شود، نوع Model آن System.Web.Mvc.HandleErrorInfo می‌باشد:

@model System.Web.Mvc.HandleErrorInfo

@{
ViewBag.Title = "DbError";
}

<h2>An Error Has Occurred</h2>

@if (Model != null)
{
<p>@Model.Exception.GetType().Name<br />
thrown in @Model.ControllerName @Model.ActionName</p>
}

البته این نکته را صرفا به عنوان اطلاعات عمومی در نظر داشته باشید. زیرا اگر قرار باشد مجددا اصل استثناء را نمایش دهیم، همان صفحه زرد رنگ ASP.NET شاید بهتر باشد.



استفاده از تگ customErrors در فایل Web.config برنامه

ویژگی حالت تگ customErrors در فایل web.config برنامه، سه مقدار را می‌تواند بپذیرد:
الف) Off : صفحه زرد رنگ معرفی خطای ASP.NET را به همراه تمام اطلاعات مرتبط با استثنای رخ داده نمایش می‌دهد.
ب) RemoteOnly : همان حالت الف است با این تفاوت که صفحه خطا را فقط در کامپیوتری که وب سرور بر روی آن نصب است نمایش خواهد داد.
ج) On : یک صفحه خطای سفارشی شده را نمایش می‌دهد.

بنابراین هیچگاه از حالت Off استفاده نکنید. زیرا خطاهای نمایش داده شده، علاوه بر برنامه نویس، برای مهاجم به یک سایت نیز بسیار دلپذیر است!
حالت RemoteOnly در زمان توسعه برنامه توصیه می‌شود.
حالت On حین توزیع برنامه باید بکارگرفته شود.



مدیریت خطاهای رخ داده خارج از MVC Pipeline

HandleErrorAttribute تنها استثناهای رخ داده داخل ASP.NET MVC Pipeline را مدیریت می‌کند (یا خطاهایی از نوع 500). اگر این نوع استثناها خارج از آن رخ دهند مثلا فایلی یافت نشود (خطای 404) و امثال آن، باید به روش زیر عمل کرد:

<customErrors mode="On" defaultRedirect="error">
<error statusCode="404" redirect="error/notfound" />
<error statusCode="403" redirect="error/forbidden" />
</customErrors>

در اینجا اگر فایلی یافت نشد، کاربر به کنترلری به نام error و متدی به نام notfound هدایت خواهد شد. بنابراین نیاز به کنترلر زیر وجود دارد؛ به علاوه به ازای هر متد هم یک View متناظر باید اضافه شود (کلیک راست روی نام متد و انتخاب گزینه افزودن View جدید).

using System.Web.Mvc;

namespace MvcApplication13.Controllers
{
public class ErrorController : Controller
{
public ActionResult Index()
{
return View();
}

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

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

برای آزمایش این قسمت، برنامه را اجرا کرده و سپس مثلا آدرس غیرموجود http://localhost/xyz را وارد کنید.



استفاده از فیلتر HandleError اجباری نیست

در همین قسمت قبل پس از افزودن customErrors و defaultRedirect آن که به نام یک کنترلر اشاره می‌کند، کلیه فیلترهای HandleError اضافه شده به برنامه را حذف کنید. سپس برنامه را خارج از محیط VS.NET اجرا کنید. باز هم متد Index کنترلر Error اجرا خواهد شد. به عبارتی الزاما نیازی به استفاده از فیلتر HandleError نیست و به کمک مقدار دهی صحیح تگ customErrors، کار نمایش خودکار صفحه سفارشی خطاها به کاربر انجام خواهد شد.
البته بدیهی است که گزینه‌های نمایش یک View خاص به ازای استثنایی ویژه، یکی از مزیت‌های استفاده از فیلتر HandleError می‌باشد که امکان تنظیم آن در فایل web.config وجود ندارد.



ثبت اطلاعات استثناهای رخ داده به کمک ELMAH

نمایش صفحه‌ی خطای سفارشی به کاربر، یکی از موارد ضروری تمام برنامه‌های ASP.NET است، اما کافی نیست. ثبت اطلاعات جزئیات استثناهای رخ داده در طول زمان می‌توانند به بالا بردن کیفیت برنامه به شدت کمک کنند. برای این منظور می‌توان همانند سابق از متد Application_Error قابل تعریف در فایل Global.asax.cs کمک گرفت؛ اما با وجود افزونه‌ای به نام ELMAH اینکار اتلاف وقت است و اصلا توصیه نمی‌شود. همچنین به کمک ELMAH می‌توان مشکلات را تبدیل به ایمیل‌های خودکار کرد یا از آن‌ها فید RSS درست نمود.
برای دریافت ELMAH یا به سایت اصلی آن مراجعه نمائید و یا به کمک NuGet هم به سادگی قابل دریافت است. پس از دریافت، ارجاعی را به اسمبلی آن (Elmah.dll) اضافه نمائید. در ادامه فایل web.config برنامه را گشوده و چند سطر زیر را به آن در قسمت configuration اضافه کنید:

<configuration>
<configSections>
<sectionGroup name="elmah">
<section name="security" requirePermission="false" type="Elmah.SecuritySectionHandler, Elmah"/>
<section name="errorLog" requirePermission="false" type="Elmah.ErrorLogSectionHandler, Elmah"/>
<section name="errorMail" requirePermission="false" type="Elmah.ErrorMailSectionHandler, Elmah"/>
<section name="errorFilter" requirePermission="false" type="Elmah.ErrorFilterSectionHandler, Elmah"/>
<section name="errorTweet" requirePermission="false" type="Elmah.ErrorTweetSectionHandler, Elmah"/>
</sectionGroup>
</configSections>

سپس ذیل قسمت appSettings، تنظیمات پروایدر ذخیره سازی اطلاعات آن‌را وارد نمائید. مثلا در اینجا از فایل‌های XML برای ذخیره سازی اطلاعات استفاده خواهد شد (که امن‌ترین حالت ممکن است؛ از این لحاظ که اگر بانک اطلاعاتی را انتخاب کنید، ممکن است مشکل اصلی از همانجا ناشی شده باشد. بنابراین خطایی ثبت نخواهد شد. همچنین در این حالت نیازی به سایر DLLهای همراه ELMAH هم نیست). در اینجا مسیر ذخیره سازی اطلاعات در پوشه app_data/errorslog تنظیم شده است:

<elmah>
<security allowRemoteAccess="1"/>
<errorLog type="Elmah.XmlFileErrorLog, Elmah" logPath="~/App_Data/ErrorsLog"/>
</elmah>

در ادامه در قسمت system.web، دو تعریف زیر را اضافه نمائید. به این ترتیب امکان دسترسی به آدرس http://server/elmah.axd مهیا می‌گردد:

<httpModules>
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</httpModules>
<httpHandlers>
<add verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</httpHandlers>

البته برای IIS7 تنظیمات ذیل نیز باید اضافه شوند:

<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<modules runAllManagedModulesForAllRequests="true">
<add name="ErrorLog" type="Elmah.ErrorLogModule, Elmah"/>
</modules>
<handlers>
<add name="Elmah" verb="POST,GET,HEAD" path="elmah.axd" type="Elmah.ErrorLogPageFactory, Elmah"/>
</handlers>
</system.webServer>

و به این ترتیب تنظیمات اولیه ELMAH به پایان می‌رسد (و با ASP.NET Web forms هیچ تفاوتی ندارد).
مرحله بعد، تنظیمات مسیریابی ASP.NET MVC است برای اینکه آدرس http://server/elmah.axd را وارد سیستم پردازشی خود نکند. البته اینکار پیشتر انجام شده است:

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

بنابراین همین تنظیمات، به همراه قالب پیش فرض یک پروژه جدید ASP.NET MVC برای استفاده از ELMAH کفایت می‌کند. اکنون پروژه جاری را یکبار دیگر خارج از VS.NET اجرا کرده و سپس به مسیر http://localhost/elmah.axd جهت مشاهده خطاهای لاگ شده به همراه جزئیات کامل آن‌ها مراجعه کنید.

مشکل: استثناهای برنامه توسط ELMAH لاگ نمی‌شوند!

فیلتر HandleError با ELMAH سازگار نیست. زیرا با استفاده از آن، متدهای کنترلرها به صورت خودکار داخل یک try/catch اجرا شده و به این ترتیب استثناهای رخ داده، مدیریت گردیده و به ELMAH هدایت نمی‌شوند. بنابراین نیاز است به متد RegisterGlobalFilters فایل Global.asax.cs مراجعه کرده و سطر زیر را حذف کنید:

filters.Add(new HandleErrorAttribute());

و یا اگر قصد نداشتید اینکار را انجام دهید، می‌توان به نحو زیر نیز مشکل را حل کرد:

using System.Web.Mvc;
using Elmah;

namespace MvcApplication13.CustomFilters
{
public class ElmahHandledErrorLoggerFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
if (context.ExceptionHandled)
ErrorSignal.FromCurrentContext().Raise(context.Exception);
// all other exceptions will be caught by ELMAH anyway
}
}
}

در اینجا یک فیلتر سفارشی به برنامه اضافه شده است تا خطاهای مدیریت شده برنامه (خطاهای مدیریت شده توسط فیلتر HandleError توکار) را به موتور ELMAH هدایت کند. سایر خطاهای مدیریت نشده به صورت خودکار توسط ELMAH ثبت خواهند شد و نیازی به انجام کار اضافی در این مورد نیست.
سپس این فیلتر جدید را به صورت سراسری تعریف کنید:

public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new ElmahHandledErrorLoggerFilter());
filters.Add(new HandleErrorAttribute());
}

ترتیب این‌ها هم مهم است. ابتدا باید ElmahHandledErrorLoggerFilter معرفی شود.


تذکر مهم!
حین استفاده از ELMAH یک نکته را فراموش نکنید:
اگر allowRemoteAccess آن‌را به عدد 1 تنظیم کرده‌اید، به هیچ عنوان از نام پیش فرض elmah.axd استفاده نکنید (هر نام اختیاری دیگری را که علاقمند بودید و به سادگی قابل حدس زدن نبود، در فایل web.config وارد کنید).


خلاصه بحث
1- در ASP.NET MVC نیازی نیست تا متدهای کنترلرها را با try/catch شلوغ کنید.
2- حتما قسمت customErrors فایل وب کانفیگ برنامه را دهی کنید (این مورد را به چک لیست اجباری تهیه یک برنامه ASP.NET MVC اضافه کنید).
3- استفاده از فیلتر HandleError اختیاری است. اگر از قابلیت فیلتر کردن استثناهای ویژه آن استفاده نمی‌کنید، مقدار دهی customErrors وب کانفیگ برنامه هم همان کار را انجام می‌دهد.
4- برای ثبت جزئیات دقیق استثناهای رخ داده در برنامه، از ELMAH استفاده کنید و بی‌جهت وقت خودتان را صرف بازنویسی این افزونه ارزشمند نکنید.

مطالب مشابه
معرفی ELMAH
ثبت استثناهای مدیریت شده توسط ELMAH

مطالب
مروری بر کدهای کلاس SqlHelper

قسمتی از یک پروژه به همراه کلاس SqlHelper آن در کامنت‌های مطلب «اهمیت Code review» توسط یکی از خوانندگان بلاگ جهت Code review مطرح شده که بهتر است در یک مطلب جدید و مجزا به آن پرداخته شود. قسمت مهم آن کلاس SqlHelper است و مابقی در اینجا ندید گرفته می‌شوند:

//It's only for code review purpose!  
using System.Data;
using System.Data.SqlClient;
using System.Web.Configuration;


public sealed class SqlHelper
{
private SqlHelper() { }


// Send Connection String
//---------------------------------------------------------------------------------------
public static string GetCntString()
{
return WebConfigurationManager.ConnectionStrings["db_ConnectionString"].ConnectionString;
}


// Connect to Data Base SqlServer
//---------------------------------------------------------------------------------------
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)
{
try
{
if (sqlCnt == null) sqlCnt = new SqlConnection();
sqlCnt.ConnectionString = cntString;
if (sqlCnt.State != ConnectionState.Open) sqlCnt.Open();
return sqlCnt;
}
catch (SqlException)
{
return null;
}
}


// Run ExecuteScalar Command
//---------------------------------------------------------------------------------------
public static string RunExecuteScalarCmd(ref SqlConnection sqlCnt, string strCmd, bool blnClose)
{
Connect2Db(ref sqlCnt, GetCntString());
using (sqlCnt)
{
using(SqlCommand sqlCmd = sqlCnt.CreateCommand())
{
sqlCmd.CommandText = strCmd;
object objResult = sqlCmd.ExecuteScalar();
if (blnClose) CloseCnt(ref sqlCnt, true);
return (objResult == null) ? string.Empty : objResult.ToString();
}
}
}

// Close SqlServer Connection
//---------------------------------------------------------------------------------------
public static bool CloseCnt(ref SqlConnection sqlCnt, bool nullSqlCnt)
{
try
{
if (sqlCnt == null) return true;
if (sqlCnt.State == ConnectionState.Open)
{
sqlCnt.Close();
sqlCnt.Dispose();
}
if (nullSqlCnt) sqlCnt = null;
return true;
}
catch (SqlException)
{
return false;
}
}
}


مثالی از نحوه استفاده ارائه شده:

protected void BtnTest_Click(object sender, EventArgs e)
{
SqlConnection sqlCnt = new SqlConnection();
string strQuery = "SELECT COUNT(UnitPrice) AS PriceCount FROM [Order Details]";


// در این مرحله پارامتر سوم یعنی کانکشن باز نگه داشته شود
string strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, false);



strQuery = "SELECT LastName + N'-' + FirstName AS FullName FROM Employees WHERE (EmployeeID = 9)";
// در این مرحله پارامتر سوم یعنی کانکشن بسته شود
strResult = SqlHelper.RunExecuteScalarCmd(ref sqlCnt, strQuery, true);
}


مروری بر این کد:

1) نحوه کامنت نوشتن
بین سی شارپ و زبان سی++ تفاوت وجود دارد. این نحوه کامنت نویسی بیشتر در سی++ متداول است. اگر از ویژوال استودیو استفاده می‌کنید، مکان نما را به سطر قبل از یک متد منتقل کرده و سه بار پشت سر هم forward slash را تایپ کنید. به صورت خودکار ساختار خالی زیر تشکیل خواهد شد:
/// <summary>
///
/// </summary>
/// <param name="sqlCnt"></param>
/// <param name="cntString"></param>
/// <returns></returns>
public static SqlConnection Connect2Db(ref SqlConnection sqlCnt, string cntString)

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

2) وجود سازنده private
احتمالا هدف این بوده که نه شخصی و نه حتی کامپایلر، وهله‌ای از این کلاس را ایجاد نکند. بنابراین بهتر است کلاسی را که تمام متدهای آن static است (که به این هم خواهیم رسید!) ، راسا static معرفی کنید. به این ترتیب نیازی به سازنده private نخواهد بود.

3) وجود try/catch
یک اصل کلی وجود دارد: اگر در حال طراحی یک کتابخانه پایه‌ای هستید، try/catch را در هیچ متدی از آن لحاظ نکنید. بله؛ درست خوندید! لطفا try/catch ننویسید! کرش کردن برنامه خوب است! لا‌یه‌های بالاتر برنامه که در حال استفاده از کدهای شما هستند متوجه خواهند شد که مشکلی رخ داده و این مشکل توسط کتابخانه مورد استفاده «خفه» نشده. برای مثال اگر هم اکنون SQL Server در دسترس نیست، لایه‌های بالاتر برنامه باید این مشکل را متوجه شوند. Exception اصلا چیز بدی نیست! کرش برنامه اصلا بد نیست!
فرض کنید که دچار بیماری شده‌اید. اگر مثلا تبی رخ ندهد، از کجا باید متوجه شد که نیاز به مراقبت پزشکی وجود دارد؟ اگر هیچ علامتی بروز داده نشود که تا الان نسل بشر منقرض شده بود!

4) وجود ref و out
دوستان گرامی! این ref و out فقط جهت سازگاری با زبان C در سی شارپ وجود دارد. لطفا تا حد ممکن از آن استفاده نکنید! مثلا استفاده از توابع API‌ ویندوز که با C نوشته شده‌اند.
یکی از مهم‌ترین کاربردهای pointers در زبان سی، دریافت بیش از یک خروجی از یک تابع است. برای مثال یک متد API ویندوز را فراخوانی می‌کنید؛ خروجی آن یک ساختار است که به کمک pointers به عنوان یکی از پارامترهای همان متد معرفی شده. این روش به وفور در طراحی ویندوز بکار رفته. ولی خوب در سی شارپ که از این نوع مشکلات وجود ندارد. یک کلاس ساده را طراحی کنید که چندین خاصیت دارد. هر کدام از این خاصیت‌ها می‌توانند نمایانگر یک خروجی باشند. خروجی متد را از نوع این کلاس تعریف کنید. یا برای مثال در دات نت 4، امکان دیگری به نام Tuples معرفی شده برای کسانی که سریع می‌خواهند چند خروجی از یک تابع دریافت کنند و نمی‌خواهند برای اینکار یک کلاس بنویسند.
ضمن اینکه برای مثال در متد Connect2Db، هم کانکشن یکبار به صورت ref معرفی شده و یکبار به صورت خروجی متد. اصلا نیازی به استفاده از ref در اینجا نبوده. حتی نیازی به خروجی کانکشن هم در این متد وجود نداشته. کلیه تغییرات شما در شیء کانکشنی که به عنوان پارامتر ارسال شده، در خارج از آن متد هم منعکس می‌شود (شبیه به همان بحث pointers در زبان سی). بنابراین وجود ref غیرضروری است؛ وجود خروجی متد هم به همین صورت.

5) استفاده از using در متد RunExecuteScalarCmd
استفاده از using خیلی خوب است؛ همیشه اینکار را انجام دهید!
اما اگر اینکار را انجام دادید، بدانید که شیء sqlCnt در پایان بدنه using ، توسط GC نابوده شده است. بنابراین اینجا bool blnClose دیگر چه کاربردی دارد؟! تصمیم شما دیگر اهمیتی نخواهد داشت؛ چون کار تخریبی پیشتر انجام شده.

6) متد CloseCnt
این متد زاید است؛ به دلیلی که در قسمت (5) عنوان شد. using های استفاده شده، کار را تمام کرده‌اند. بنابراین بستن اشیاء dispose شده معنا نخواهد داشت.

7) در مورد نحوه استفاده
اگر SqlHelper را در اینجا مثلا یک DAL ساده فرض کنیم (data access layer)، جای قسمت BLL (business logic layer) در اینجا خالی است. عموما هم چون توضیحات این موارد را خیلی بد ارائه داده‌اند، افراد از شنیدن اسم آن‌ها هم وحشت می‌کنند. BLL یعنی کمی دست به Refactoring بزنید و این پیاده سازی منطق تجاری ارائه شده در متد BtnTest_Click را به یک کلاس مجزا خارج از code behind پروژه منتقل کنید. Code behind فقط محل استفاده نهایی از آن باشد. همین! فعلا با همین مختصر شروع کنید.
مورد دیگری که در اینجا باز هم مشهود است، عدم استفاده از پارامتر در کوئری‌ها است. چون از پارامتر استفاده نکرده‌اید، SQL Server مجبور است برای حالت EmployeeID = 9 یکبار execution plan را محاسبه کند، برای کوئری بعدی مثلا EmployeeID = 19، اینکار را تکرار کند و الی آخر. این یعنی مصرف حافظه بالا و همچنین سرعت پایین انجام کوئری‌ها. بنابراین اینقدر در قید و بند باز نگه داشتن یک کانکشن نباشید؛ مشکل اصلی جای دیگری است!

8) برنامه وب و اطلاعات استاتیک!
این پروژه، یک پروژه ASP.NET است. دیدن تعاریف استاتیک در این نوع پروژه‌ها یک علامت خطر است! در این مورد قبلا مطلب نوشتم:
متغیرهای استاتیک و برنامه‌های ASP.NET


یک درخواست عمومی!
لطف کنید در پروژ‌های «جدید» خودتون این نوع کلاس‌های SqlHelper رو «دور بریزید». یاد گرفتن کار با یک ORM جدید اصلا سخت نیست. مثلا طراحی Entity framework مایکروسافت به حدی ساده است که هر شخصی با داشتن بهره هوشی در حد یک عنکبوت آبی یا حتی جلبک دریایی هم می‌تونه با اون کار کنه! فقط NHibernate هست که کمی مرد افکن است و گرنه مابقی به عمد ساده طراحی شده‌اند.
مزایای کار کردن با ORM ها این است:
- کوئری‌های حاصل از آن‌ها «پارامتری» است؛ که این دو مزیت عمده را به همراه دارد:
امنیت: مقاومت در برابر SQL Injection
سرعت و همچنین مصرف حافظه کمتر: با کوئری‌های پارامتری در SQL Server همانند رویه‌های ذخیره شده رفتار می‌شود.
- عدم نیاز به نوشتن DAL شخصی پر از باگ. چون ORM یعنی همان DAL که توسط یک سری حرفه‌ای طراحی شده.
- یک دست شدن کدها در یک تیم. چون همه بر اساس یک اینترفیس مشخص کار خواهند کرد.
- امکان استفاده از امکانات جدید زبان‌های دات نتی مانند LINQ و نوشتن کوئری‌های strongly typed تحت کنترل کامپایلر.
- پایین آوردن هزینه‌های آموزشی افراد در یک تیم. مثلا EF را می‌شود به عنوان یک پیشنیاز در نظر گرفت؛ عمومی است و همه گیر. کسی هم از شنیدن نام آن تعجب نخواهد کرد. کتاب(های) آموزشی هم در مورد آن زیاد هست.
و ...


مطالب
جایگزین کردن jQuery با JavaScript خالص - قسمت پنجم - درخواست‌های Ajax
AJAX و یا «Asynchronous JavaScript and XML» قابلیتی است که توسط web API جاوا اسکریپتی مرورگرها برای دریافت و یا به روز رسانی اطلاعات، بدون بارگذاری مجدد و کامل صفحه، ارائه می‌شود. این قابلیت اولین بار در سال 1999 توسط مایکروسافت با ارائه‌ی مرورگر IE 5.0 و معرفی شیء XMLHTTP که توسط یک ActiveX control ارائه می‌شد، میسر گردید و این روزها توسط استاندارد XMLHttpRequest در تمام مرورگرها قابل استفاده است. این استاندارد نیز مدتی است که توسط Fetch API مرورگرهای جدید جایگزین شده‌است (پس از 15 سال از ارائه‌ی استاندارد XMLHttpRequest) و در این لحظه در تمام مرورگرهای غالب وب پشتیبانی می‌شود.
در ادامه روش‌های مختلف ارسال درخواست‌های Ajax را توسط jQuery و همچنین معادل‌های XMLHttpRequest و Fetch API آن‌ها بررسی می‌کنیم.


ارسال درخواست‌های GET

توسط استاندارد جدید Fetch API 
  توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/my/name').then(function(response) {
  if (response.ok) {
   return response.text();
  }
  else {
   throw new Error();
  }
 }).then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );
 var xhr = new XMLHttpRequest();
  xhr.open('GET', '/my/name');
 xhr.onload = function() {
  if (xhr.status >= 400) {
   console.error('Name request failed!');
  }
  else {
   console.log('my name is ' + xhr.responseText);
  }
 };
 xhr.onerror = function() {
  console.error('Name request failed!');
 };
 xhr.send();
 $.get('/my/name').then(
  function success(name) {
  console.log('my name is ' + name);
  },
  function failure() {
  console.error('Name request failed!');
  }
 );

jQuery برای پشتیبانی از درخواست‌های Ajax، متد ویژه‌ای را به نام ()ajax ارائه می‌کند که برای ارسال درخواست‌هایی از هر نوع، مانندGET، POST و غیره کاربرد دارد. همچنین برای بعضی از نوع‌ها، متدهای کوتاه‌تری را مانند ()get و ()post نیز در اختیار برنامه نویس قرار می‌دهد. جاوا اسکریپت خالص و Web API مرورگرها نیز دو شیء XMLHttpRequest و fetch را برای ارسال درخواست‌های غیرهمزمان، ارائه می‌کند. XMLHttpRequest در تمام مرورگرهای قدیمی و جدید پشتیبانی می‌شود، اما fetch API مدتی است که در غالب مرورگرهای امروزی در دسترس است. در جدول فوق روش ارسال درخواست‌های Ajax از نوع GET را توسط این سه روش مشاهده می‌کنید.
در این مثال‌ها درخواستی به آدرس my/name سمت سرور ارسال شده و انتظار می‌رود که یک plaintext حاوی متن کاربر بازگشت داده شود که در نهایت در console لاگ می‌شود.
- در حالت استفاده‌ی از jQuery در صورت بازگشت موفقیت آمیز پاسخی از طرف سرور، متد success و در غیراینصورت متد failure اجرا می‌شود. باید درنظر داشت که متد ajax جی‌کوئری، چیزی بیشتر از یک محصور کننده‌ی اشیاء XMLHttpRequest نیست.
- در حالت کار با XMLHttpRequest باید اندکی بیشتر تایپ کرد؛ اما اصول کار یکی است. در اینجا onload زمانی فراخوانی می‌شود که پاسخی از سرور دریافت شده و عملیات خاتمه یافته‌است؛ هرچند این پاسخ می‌تواند یک خطا نیز باشد. به همین جهت باید status آن‌را بررسی کرد. اگر رخ‌داد onerror فراخوانی شد، یعنی درخواست، در سطوح بسیار پایین آن مانند بروز یک خطای CORS با شکست مواجه شده‌است.
همانطور که مشاهده می‌کنید در حالت کار با XMLHttpRequest جاوا اسکریپت از اشیاء Promise پشتیبانی نمی‌کند که این کمبود با معرفی fetch API برطرف شده‌است که نمونه‌ای از آن‌را با متد then متصل به fetch مشاهده می‌کنید؛ دقیقا مشابه متد ajax جی‌کوئری که آن نیز یک Promise را بازگشت می‌دهد.
تفاوت Promise جی‌کوئری با fetch API در این است که جی‌کوئری در صورتیکه یک status code بیانگر خطا را دریافت کند، قسمت failure را اجرا می‌کند؛ اما fetch API مانند اشیاء XMLHttpRequest تنها در صورت بروز خطاهای سطح پایین درخواست، این متد را فراخوانی خواهد کرد. هرچند اگر در اینجا response.ok نبود، می‌توان با صدور یک استثناء به رفتاری شبیه به jQuery رسید و قسمت then failure را به صورت خودکار اجرا کرد.


ارسال درخواست‌های Ajax از نوع POST ، PUT و DELETE

در اینجا اطلاعاتی با MIME type از نوع plaintext به سمت سرور ارسال می‌شود. جهت سهولت توضیح و تمرکز بر روی قسمت‌های مهم آن، بخش مدیریت پاسخ آن حذف شده‌است و این مورد دقیقا با مثال قبلی که در مورد درخواست‌های از نوع GET بود، یکی است.
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
توسط jQuery 
 fetch('/user/name', {
  method: 'POST',
  body: 'some data'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', '/user/name');
 xhr.send('some data');
 $.ajax({
  method: 'POST',
  url: '/user/name',
  contentType: 'text/plain',
  data: 'some data'
 });
در حالت ارسال اطلاعات به سرور با متد POST، نیاز است contentType متد ajax جی‌کوئری حتما ذکر شود. در غیراینصورت آن‌را به application/x-www-form-urlencoded تنظیم می‌کند که ممکن است الزاما مقداری نباشد که مدنظر ما است. در اینجا بدنه‌ی درخواست به خاصیت data انتساب داده می‌شود.
اگر از شیء XMLHttpRequest استفاده شود، Content-Type آن به صورت پیش‌فرض به text/plain تنظیم شده‌است. در اینجا بدنه‌ی درخواست به متد send ارسال می‌شود.
متد fetch نیز همانند شیء XMLHttpRequest دارای Content-Type پیش‌فرض از نوع text/plain است. در اینجا بدنه‌ی درخواست به خاصیت body انتساب داده می‌شود.

درخواست‌های از نوع POST عموما برای ایجاد رکوردی جدید در سمت سرور مورد استفاده قرار می‌گیرند و از درخواست‌های PUT بیشتر برای به روز رسانی مقادیر موجود یک رکورد کمک گرفته می‌شود. درخواست‌های از نوع PUT نیز دقیقا مانند درخواست‌های از نوع POST در اینجا مدیریت می‌شوند و در هر سه حالت، متد ارسال اطلاعات، به مقدار PUT تنظیم خواهد شد:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('/user/1', {
  method: 'PUT',
  body: //record including new mobile number 
 });
 var xhr = new XMLHttpRequest();
 xhr.open('PUT', '/user/1');
 xhr.send(/* record including new data */);
 $.ajax({
  method: 'PUT',
  url: '/user/1',
  contentType: 'text/plain',
  data: //record including new data
 });
درخواست‌های از نوع DELETE نیز مانند قبل بوده و تنها تفاوت آن، نداشتن بدنه‌ی درخواست است:
 توسط استاندارد جدید Fetch API   توسط XMLHttpRequest استاندارد  توسط jQuery 
 fetch('/user/1', {method: 'DELETE'});
 var xhr = new XMLHttpRequest();
 xhr.open('DELETE', '/user/1');
 xhr.send();
 $.ajax('/user/1', {method: 'DELETE'});


مدیریت Encoding درخواست‌های Ajax

در مثال‌های قبل، اطلاعاتی از نوع text/plain را به سمت سرور ارسال کردیم که به آن encoding type نیز گفته می‌شود. برای تکمیل بحث می‌توان حالت‌های دیگری مانند application/x-www-form-urlencoded، application/json و یا multipart/form-data را که در برنامه‌های کاربردی زیاد مورد استفاده قرار می‌گیرند، بررسی کرد.

کار با URL Encoding

عموما URL encoding در دو قسمت آدرس درخواستی به سرور و یا حتی بدنه‌ی درخواست ارسالی تنظیم می‌شود. MIME type آن نیز application/x-www-form-urlencoded است و اطلاعات آن شامل یکسری جفت کلید/مقدار است. برای متدهای ارسال از نوع GET و DELETE، اطلاعات آن در انتهای آدرس درخواستی و برای سایر حالات در بدنه‌ی درخواست ذکر می‌شوند.
در جی‌کوئری با استفاده از متد param آن می‌توان یک شیء جاوا اسکریپتی را به معادل URL-encoded string آن‌ها تبدیل کرد:
$.param({
  key1: 'some value',
  'key 2': 'another value'
});
با این خروجی encode شده:
 key1=some+value&key+2=another+value
البته باید دقت داشت زمانیکه از متد ajax جی‌کوئری استفاده می‌شود، دیگر نیازی به استفاده‌ی مستقیم از متد param نیست:
 $.ajax({
  method: 'POST',
  url: '/user',
  data: {
  name: 'VahidN',
  address: 'Address 1',
  phone: '555-555-5555'
  }
 });
در اینجا یک شیء جاوا اسکریپتی معمولی به خاصیت data آن نسبت داده شده‌است که در پشت صحنه در حین ارسال به سرور، چون Content-Type پیش‌فرض (و ذکر نشده‌ی در اینجا) دقیقا همان application/x-www-form-urlencoded است، به صورت خودکار تبدیل به یک URL-encoded string می‌شود.

برخلاف جی‌کوئری در حین کار با روش‌های جاوا اسکریپتی خالص این encoding باید به صورت دستی و صریحی انجام شود. برای این منظور دو متد استاندارد encodeURI و encodeURIComponent در جاوا اسکریپت مورد استفاده قرار می‌گیرند. هدف متد encodeURI اعمال آن بر روی یک URL کامل است و یا کلید/مقدارهای جدا شده‌ی توسط & مانند first=Vahid&last=N. اما متد encodeURIComponent صرفا جهت اعمال بر روی یک تک مقدار طراحی شده‌است.
به این ترتیب معادل جاوا اسکریپتی قطعه کد جی‌کوئری فوق به صورت زیر است:
 var xhr = new XMLHttpRequest();
 var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
 xhr.send(data);
که در آن data توسط encodeURI تبدیل به یک رشته‌ی URL-encoded شده و سپس با ذکر صریح Content-Type به سمت سرور ارسال می‌شود.
روش انجام اینکار توسط fetch API به صورت زیر است:
   var data = encodeURI('name=VahidN&address=Address 1&phone=555-555-5555');
  fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/x-www-form-urlencoded'},
  body: data
 });
معادل متد param جی‌کوئری با جاوا اسکریپت خالص به صورت زیر است؛ برای تبدیل یک شیء جاوا اسکریپتی به معادل URL-encoded string آن:
function param(object) {
    var encodedString = '';
    for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
            if (encodedString.length > 0) {
                encodedString += '&';
            }
            encodedString += encodeURI(prop + '=' + object[prop]);
        }
    }
    return encodedString;
}


کار با JSON Encoding

در عمل JSON نمایش رشته‌ای یک شیء جاوا اسکریپتی است و هدف آن سهولت نقل و انتقالات این اشیاء به سرور و برعکس است. برخلاف حالت application/x-www-form-urlencoded که اطلاعات آن مسطح است، حالت application/json امکان ارسال اطلاعات سلسله مراتبی را نیز میسر می‌کند (مانند مثال زیر که phone آن دیگر مسطح نیست و خود آن نیز یک شیء جاوا اسکریپتی است).
 در جی‌کوئری برای ارسال اشیاء جاوا اسکریپتی JSON Encoded به سمت سرور از روش زیر استفاده می‌شود:
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: 'application/json',
  data: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
در اینجا نسبت به مثال قبلی، ذکر Content-Type ضروری بوده و همچنین data نیز باید به صورت دستی encode شود. برای این منظور می‌توان از متد استاندارد JSON.stringify استفاده کرد که از زمان IE 8.0 به بعد در تمام مرورگرها پشتیبانی می‌شود.
پیاده سازی همین مثال با جاوا اسکریپت خالص و XMLHttpRequest استاندارد به صورت زیر است:
  var xhr = new XMLHttpRequest();
 var data = JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
    home: '555-555-5555',
    mobile: '444-444-4444'
   }
   });
 xhr.open('POST', '/user');
 xhr.setRequestHeader('Content-Type', 'application/json');
 xhr.send(data);
که در اینجا نیز Content-Type به صورت صریحی ذکر و از متد JSON.stringify برای encode دستی اطلاعات کمک گرفته شده‌است.
در این حالت اگر خروجی سرور نیز JSON باشد، روش دریافت و پردازش آن به صورت زیر است:
 var xhr = new XMLHttpRequest();
 xhr.open('GET', '/user/1');
 xhr.onload = function() {
  var user = JSON.parse(xhr.responseText);
  // do something with this user JavaScript object 
 };
 xhr.send();
رخ‌داد onload، پس از پایان درخواست فراخوانی می‌شود. در اینجا برای دسترسی به response body می‌توان از خاصیت responseText استفاده کرد و سپس توسط متد JSON.parse این رشته را تبدیل به یک شیء جاوا اسکریپتی نمود.
اگر از مرورگر IE صرفنظر کنیم، تمام مرورگرهای دیگر دارای خاصیتی به نام xhr.response نیز هستند که نیاز به تبدیل و Parse دستی رشته‌ی دریافتی را حذف می‌کند؛ از این جهت که این خاصیت حاوی شیء جاوا اسکریپتی معادل بدنه‌ی response دریافتی از سمت سرور است. البته با این شرط که سرور، Content-Type مساوی application/json را برای response تنظیم کرده باشد.
و روش انجام این عملیات توسط fetch API به صورت زیر است:
 fetch('/user', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
   name: 'VahidN',
   address: 'Address 1',
   phone: {
   home: '555-555-5555',
   mobile: '444-444-4444'
  }
  });
 });
که در اینجا نیز هدر Content-Type تنظیم و همچنین از متد JSON.stringify برای تبدیل شیء جاوا اسکریپتی به رشته‌ی معادل، استفاده شده‌است.
و یا اگر بخواهیم اطلاعات JSON دریافتی از سمت سرور را در اینجا پردازش کنیم، روش کار به صورت زیر است:
 fetch('/user/1').then(function(response) {
  return response.json();
  }).then(function(userRecord) {
   // do something with this user JavaScript object 
 });
Fetch API به صورت یک Promise، امکان دسترسی به شیء response را مهیا می‌کند. چون می‌دانیم خروجی آن json است، از متد ()json آن که یک Promise را بازگشت می‌دهد استفاده خواهیم کرد. پس از پایان موفقیت آمیز درخواست، then دوم تعریف شده، اجرا و userRecord ارسالی به آن، همان شیء جاوا اسکریپتی دریافتی از سمت سرور است.
همین مثال را اگر بخواهیم توسط ECMAScript 2016 و Arrow functions آن بازنویسی کنیم، به قطعه کد زیر می‌رسیم:
 fetch('/user/1')
  .then(response => response.json()) // Transform the data into json 
  .then(userRecord => {
  // do something with this userRecord object
  });


کار با Multipart Encoding

نوع دیگری از encoding که بیشتر با فرم‌های HTML بکار می‌رود، multipart/form-data نام دارد:
 <form action="my/server" method="POST" enctype="multipart/form-data">
  <label>First Name:
   <input name="first">
  </label>
 
  <label>Last Name:
   <input name="last">
  </label>
 
  <button>Submit</button>
 </form>
با فعال بودن این نوع encoding، اطلاعات نمونه‌ی فرم فوق به شکل زیر به سمت سرور ارسال می‌شوند:
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="first"

 Vahid
 -----------------------------1686536745986416462127721994
 Content-Disposition: form-data; name="last"

 N
 -----------------------------1686536745986416462127721994--
از این روش نه فقط برای ارسال اطلاعات کلید/مقدارها و اشیاء جاوا اسکریپتی استفاده می‌شود، بلکه از آن برای ارسال اطلاعات فایل‌های باینری نیز کمک گرفته می‌شود.
روش ارسال اطلاعات با این نوع encoding خاص به سمت سرور توسط متد ajax جی‌کوئری به صورت زیر است:
  var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');
 
 $.ajax({
  method: 'POST',
  url: '/user',
  contentType: false,
  processData: false,
  data: formData
 });
همانطور که ملاحظه می‌کنید jQuery روش توکاری را برای انجام اینکار نداشته و باید از FormData جاوا اسکریپت یا همان web API مرورگرها به همراه متد ajax آن استفاده کرد. در این حالت اطلاعات به صورت کلید/مقدارها به شیء استاندارد FormData اضافه شده و سپس به سمت سرور ارسال می‌شوند. باید دقت داشت FormData از نگارش‌های پس از IE 9.0 در دسترس است.
در اینجا ذکر processData: false ضروری است. در غیراینصورت jQuery این اطلاعات را به یک URL-encoded string تبدیل می‌کند. همچنین با اعلام contentType: false، جی‌کوئری در کار مرورگر دخالت نمی‌کند. از این جهت که هدر ویژه‌ی این نوع درخواست‌ها توسط خود مرورگر تنظیم می‌شود و برای مثال یک چنین شکلی را دارد:
 multipart/form-data; boundary=—————————1686536745986416462127721994
این عدد انتهای هدر یک unique ID است که جزئی از اطلاعات multipart بوده و توسط مرورگر به انتهای هدر اصلی multipart/form-data اضافه می‌شود.

روش انجام اینکار با XMLHttpRequest و همچنین fetch API به صورت زیر است:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 var formData = new FormData();
 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 fetch('/user', {
  method: 'POST',
  body: formData
 });
 var formData = new FormData(),
  xhr = new XMLHttpRequest();

 formData.append('name', 'VahidN');
 formData.append('address', 'Address 1');
 formData.append('phone', '555-555-5555');

 xhr.open('POST', '/user');
 xhr.send(formData);
همانطور که مشاهده می‌کنید قسمت استفاده‌ی از FormData استاندارد در اینجا یکسان است و همچنین نیازی به ذکر هدر و یا اطلاعات اضافه‌تری نیست.


آپلود فایل‌ها توسط درخواست‌های Ajax ایی

تنها راه آپلود فایل‌ها در مرورگرهای قدیمی که شامل IE 9.0 هم می‌شود، تعریف المان <"input type="file> در داخل المان <form> و سپس submit مستقیم آن فرم است. برای رفع این مشکل در مرورگرهای پس از IE 9.0 و پشتیبانی از Ajax، جهت آپلود فایل‌ها، استاندارد XMLHttpRequest Level 2 معرفی شده‌است. در این حالت اگر المان <input type=file> در صفحه وجود داشته باشد، روش ارسال Ajax ایی آن به سمت سرور به صورت زیر است:
توسط استاندارد جدید Fetch API 
 توسط XMLHttpRequest استاندارد  توسط jQuery 
var file = document.querySelector(
      'INPUT[type="file"]').files[0];
fetch('/uploads', {
  method: 'POST',
  body: file
  });
 }
var file = document.querySelector(
    'INPUT[type="file"]').files[0],
xhr = new XMLHttpRequest();
xhr.open('POST', '/uploads');
xhr.send(file);

var file = $('INPUT[type="file"]')[0].files[0]; 
$.ajax({
   method: 'POST',
   url: '/uploads',
   contentType: false,
   processData: false,
   data: file
  });
در اینجا روش آپلود یک تک فایل را به سرور، توسط درخواست‌های Ajax ایی مشاهده می‌کنید و توضیحات contentType: false و processData: false آن مانند قبل است تا jQuery این اطلاعات multipart را پیش از ارسال به سرور دستکاری نکند.
یک نکته: اگر نیاز به آپلود بیش از یک فایل را داشتید و همچنین در اینجا نیاز به اطلاعات دیگری مانند سایر فیلدهای فرم نیز وجود داشت، از همان روش تعریف  new FormData و افزودن اطلاعات مورد نیاز به آن استفاده کنید. امکان افزودن شیء file نیز به FormData پیش بینی شده‌است.


دانلود فایل‌ها توسط درخواست‌های Ajax ایی

پیشتر در حین بررسی JSON encoding توسط fetch API از متد ()json برای تبدیل اطلاعات دریافتی از سرور به json و بازگشت آن به صورت یک Promise استفاده کردیم:
  fetch(url) 
  .then((resp) => resp.json()) // Transform the data into json 
  .then(function(data) { 
    // use data object
    }) 
  })
در اینجا علاوه بر ()json، متدهای استاندارد دیگری نیز پیش بینی شده‌اند که همگی یک Promise را بازگشت می‌دهند:
- ()clone یک کپی از response را تهیه می‌کند.
- ()redirect یک response جدید را با URL دیگری ایجاد می‌کند.
- ()arrayBuffer یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء ArrayBuffer تبدیل می‌کند.
- ()formData یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء FormData تبدیل می‌کند.
- ()blob یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء Blob تبدیل می‌کند.
- ()text یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به string تبدیل می‌کند.
- ()json یک Promise را بازگشت می‌دهد که پس از پایان درخواست، response را به یک شیء جاوا اسکریپتی تبدیل می‌کند.

در اینجا متدی که می‌تواند برای تبدیل یک byte array بازگشتی از سرور به فایل قابل دریافت در سمت کلاینت مورد استفاده قرار گیرد، متد blob است:
function downloadBlob()
{
       fetch('/Home/InMemoryReport')
   .then(function(response) {
         return response.blob();
   })
   .then(function(xlsxBlob) {
             var a = document.createElement("a");
             document.body.appendChild(a);
             a.style = "display: none";
             let url = window.URL.createObjectURL(xlsxBlob);
             a.href = url;
             a.download = "report.xlsx";
             a.click();
             window.URL.revokeObjectURL(url);
   });
}
فرض کنید مسیر Home/InMemoryReport یک گزارش PDF و یا اکسل را به صورت byte array بازگشت می‌دهد. اولین then نوشته شده، درخواست تبدیل این byte array را پس از پایان response به یک شیء Blob می‌دهد. پس از پایان درخواست، این Promise اجرا شده و نتیجه‌ی آن به صورت خودکار در اختیار then دوم قرار می‌گیرد. در اینجا همانطور که در قسمت قبل نیز بررسی کردیم، یک المان جدید anchor مخفی را ایجاد کرده و به صفحه اضافه می‌کنیم. سپس url آن‌را به این شیء Blob، توسط متد استاندارد window.URL.createObjectURL تنظیم می‌کنیم. با استفاده از متد URL.createObjectURL می‌توان آدرس موقت محلی را برای دسترسی به آن تولید کرد و یک چنین آدرس‌هایی به صورت blob:http تولید می‌شوند. نام این فایل را هم توسط ویژگی download این شیء می‌توان تنظیم نمود. در نهایت بر روی آن، متد click را فراخوانی می‌کنیم. با اینکار مرورگر این فایل را به صورت یک فایل دریافت شده‌ی متداول در لیست فایل‌های آن قرار می‌دهد. این روش در مورد تدارک دکمه‌ی دریافت تمام blobهای دریافتی از سرور کاربرد دارد.


ارسال درخواست‌های Ajax به دومین‌های دیگر (CORS)

گاهی از اوقات نیاز است اطلاعاتی را توسط درخواست‌های Ajax، به سروری دیگر در دومینی دیگر ارسال و یا دریافت کرد. هرچند انجام اینکار به صورت مستقیم و خارج از مرورگر بدون مشکل قابل انجام است، اما مرورگرها برای درخواست‌های جاوا اسکریپتی محدودیت «same-origin policy» را اعمال می‌کنند. به این معنا که XMLHttpRequest بین دومین‌ها به صورت پیش‌فرض ممنوع است. برای ارسال درخواست‌های مجاز و از پیش مشخص شده‌ی Ajax بین دومین‌ها، تاکنون دو روش پیش بینی شده‌است:
الف) روش JSONP
«same-origin policy» از شروع ارسال درخواستی به خارج از دومین جاری، جلوگیری می‌کند. هرچند این مورد به درخواست‌های XMLHttpRequest اعمال می‌شود، اما در مورد المان‌هایی از نوع <a>، <img>  و  <script> صادق نیست و آن‌ها محدود به این سیاست امنیتی نیستند. روش «JavaScript Object Notation with Padding» و یا به اختصار JSONP از یکی از همین استثناءها جهت ارسال درخواست‌هایی به سایر دومین‌ها استفاده می‌کند. البته نام این روش کمی غلط انداز است؛ از این جهت که در این فرآیند اصلا JSON ایی مورد استفاده قرار نمی‌گیرد؛ خروجی سرور در این حالت یک تابع جاوا اسکریپتی است و نه JSON.
روش انجام این نوع درخواست‌ها را توسط جی‌کوئری در ذیل مشاهده می‌کنید:
 $.ajax('http://jsonp-aware-endpoint.com/user/1', {
  jsonp: 'callback',
  dataType: 'jsonp'
 }).then(function(response) {
  // handle user info from server 
 });
در این حالت jQuery پس از اجرای تابع دریافتی از سرور، نتیجه‌ی آن‌را در قسمت then، در اختیار مصرف کننده قرار می‌دهد.
انجام اینکار بدون jQuery و در حقیقت کاری که jQuery در پشت صحنه برای ایجاد تگ script انجام می‌دهد، چنین چیزی است:
 window.myJsonpCallback = function(data) {
  // handle user info from server 
 };

 var scriptEl = document.createElement('script');
 scriptEl.setAttribute('src',
  'http://jsonp-aware-endpoint.com/user/1?callback=myJsonpCallback');
 document.body.appendChild(scriptEl);
هرچند این روش هنوز هم در بعضی از سایت‌ها مورد استفاده‌است، در کل بهتر است از آن دوری کنید؛ چون ممکن است به مشکلات امنیتی دیگری ختم شود. این روش بیشتر در روزهای آغازین معرفی محدودیت «same-origin policy» بکار گرفته می‌شد (در زمان IE 7.0).


ب) روش CORS

CORS  و یا Cross Origin Resource Sharing روش مدرن و پذیرفته شده‌ی ارسال درخواست‌های Ajax در بین دومین‌ها است و دارای دو نوع ساده و غیرساده است. نوع ساده‌ی آن به همراه هدر مخصوص Origin است که جهت بیان دومین ارسال کننده‌ی درخواست بکار می‌رود و تنها از encodingهای “text/plain”  و  “application/x-www-form-urlencoded”  پشتیبانی می‌کند. نوع غیرساده‌ی آن که این روزها بیشتر بکار می‌رود، از نوع «preflight» است. Preflight در اینجا به این معنا است که زمانیکه درخواست Ajax ایی را به دومین دیگری ارسال کردید، پیش از ارسال، مرورگر یک درخواست از نوع OPTIONS را به سمت سرور مقصد ارسال می‌کند. در این حالت اگر سرور مجوز مناسبی را صادر کرد، آنگاه مرورگر اصل درخواست را به سمت آن سرور ارسال می‌کند. به همین جهت در این حالت به ازای هر درخواستی که در برنامه ارسال می‌شود، در برگه‌ی network مرورگر، دو درخواست را مشاهده خواهید کرد. درخواست preflight از نوع OPTIONS به صورت خودکار توسط مرورگر مدیریت می‌شود و نیازی به کدنویسی خاصی ندارد.


مدیریت کوکی‌ها در درخواست‌های Ajax

اگر درخواست Ajax ایی را به دومین دیگری ارسال کنید، به صورت پیش‌فرض به همراه کوکی‌های مرتبط نخواهد بود. برای رفع این مشکل نیاز است خاصیت withCredentials را به true تنظیم کنید:
توسط استاندارد جدید Fetch API 
توسط XMLHttpRequest استاندارد 
 توسط jQuery 
 fetch('http://someotherdomain.com', {
  method: 'POST',
  headers: {
  'Content-Type': 'text/plain'
  },
  credentials: 'include'
 });
 var xhr = new XMLHttpRequest();
 xhr.open('POST', 'http://someotherdomain.com');
 xhr.withCredentials = true;
 xhr.setRequestHeader('Content-Type', 'text/plain');
 xhr.send('sometext');
 $.ajax('http://someotherdomain.com', {
  method: 'POST',
  contentType: 'text/plain',
  data: 'sometext',
  beforeSend: function(xmlHttpRequest) {
  xmlHttpRequest.withCredentials = true;
  }
 });
 
یک نکته‌ی مهم: در fetch API حتی برای درخواست‌های ساده نیز کوکی‌ها ارسال نمی‌شوند. در این حالت برای کار با دومین جاری و ارسال کوکی‌های کاربر به سمت سرور، باید از تنظیم 'credentials: 'same-origin استفاده کرد؛ زیرا مقدار پیش‌فرض آن omit است.
مطالب
بررسی بارگذاری داده ها در انبار های داده و معرفی الگوهای بکار رفته در آن

مقدمه

در لینکی که چندی پیش به اشتراک گذاشته بودم؛ به مطلبی تحت این عنوان اشاره شده بود: "آیا از KPI باید به انباره داده و هوش تجاری رسید؟" (بر گرفته از وبلاگ آقای جام سحر) که در آن به موانع پیش روی انجام پروژه‌های BI در ایران پرداخته شده است.
این مقاله بر گرفته از فصل سوم یکی از White Paper‌های ماکروسافت با عنوان Microsoft EDW Architecture, Guidance and Deployment Best Practices می‌باشد. که به شرح عملیات Loading در فاز ETL می‌پردازد. از آنجا که به منظور پیاده سازی این نوع پروژه‌ها معمولاً در ایران برون سپاری صورت می‌گیرد و مدیران شرکت‌ها بیشتر درگیر سیستم‌های OLTP هستند و مجری پروژه (شرکت پیمانکار) معمولاً کوتاهترین مسیر را جهت انجام پروژه انتخاب می‌کند(و امروزه نیک میدانیم که "انتخاب مسیرهای کوتاه در زمان کم می‌تواند به پیچیدگی‌های بسیار جدی در دراز مدت منجر شود!") و همچنین از آنجا که متاسفانه به دلیل عدم ثبات مدیریت در ایران معمولاً "مدیریت برای تحویل پروژه تحت فشار است و نه برای مسائل پشتیبانی " و مسائل دیگری از این دست؛ چنانچه در تحویل گیری محصول به درستی تست نرم افزار صورت نگیرد، در نظر گرفتن موارد زیر:
Verification: Are we building the product right? ~ Software correctly implements a specific function
  Validation: Are we building the right product? ~  Software is traceable to customer requirements
پروژه با شکست مواجه می‌شود و انتظارات مدیران بهره بردار را برآورده نمی‌کند. به هر روی در این مقاله به ترجمه مطالب زیر پرداخته می‌شود، توصیه میکنم در صورتی که با خواندن متن انگلیسی مشکلی ندارید، اصل مقاله مذکور خوانده شود.
1- Full Load vs Incremental Load
2- Detecting Net Changes
2-1- Pulling Net Changes – Last Change Column
2-2- Pulling Net Changes – No Last Change Column
2-3- Pushing Net Changes
3- ETL Patterns
3-1- Destination load Patterns
3-2- Versioned Insert Pattern
3-3- Update Pattern
3-4- Versioned Insert: Net Changes 
4- Data Integration Best Practices
4-1- Basic Data Flow Patterns
4-1-1- Update Pattern
4-1-2- Update Pattern – ETL Framework
4-1-3- Versioned Insert Pattern
4-1-4- Update vs. Versioned Insert
4-2- Dimension Patterns
4-3- Fact Table Patterns
4-3-1- Managing Inferred Members

1- Full Load vs Incremental Load

نسل‌های اولیه DW (اختصار Data Warehouse) به شکل Full Loads پیاده سازی می‌شدند، به این طریق که هر بار عملیات بارگذاری صورت می‌گرفت، DW از نو دوباره ساخته می‌شد. شکل زیر مراحل مختلف انجام شده در این روش را نمایش می‌دهد:

پروسه Full Load شامل مراحل زیر بود:

  1. Drop Indexes: از آنجا که Index‌ها زمان بارگذاری را افزایش می‌دادند، این عمل صورت می‌پذیرفت.
  2. Truncate Tables: تمامی رکوردهای موجود در جداول حذف می‌شدند.
  3. Bulk Copy
  4. Load Data
  5. Post Process: شامل عملیاتی نظیر شاخص گذاری روی داده هایی است که اخیراً بارگذاری شده اند و....

روی  هم رفته Full Load مسئله ای مشکل ساز بود، زیرا نیاز به زمانی برای بارگذاری مجدد داده‌ها داشت و مسئله‌ی مهم‌تر نداشتن امکان دستیابی به گزارشاتی تاریخچه ای با ماهیت زمان برای مشتریان کسب وکار بود. به این دلیل که همواره یک کپی از آخرین داده‌های موجود در سیستم عملیاتی درون DW قرار می‌گرفت؛ که با بکارگیری Full Load اغلب قادر به ارائه‌ی این نوع از گزارشات نبودیم، بدین ترتیب سازمان‌ها به نسل دوم روی آورند که در این دیدگاه از مفهوم Incremental Load استفاده می‌شود. اشکال زیر مراحلی که در این روش انجام می‌شود را نمایان می‌سازد:

Incremental Load with an Extract In area

Incremental Load without an Extract In area

مراحل Incremental Load شامل:

  1. بارگذاری تغییرات نسبت به آخرین فرآیند بارگذاری انجام شده
  2. درج / بروزرسانی تغییرات درون Production area
  3. درج / بروزرسانی Consumption area نسبت به Production area


تفاوت‌های اصلی میان Full Load و Incremental Load در این است که در Incremental Load:

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

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

2- Detecting Net Changes

فرآیند لود افزایشی ETL، بایست قادر به شناسائی رکورد‌های تغییریافته در مبداء باشد، که این عمل با استفاده از هر یک از تکنیک‌های Push یا Pull انجام می‌شود.

  • در تکنیک Pull، فرآیند ETL رکوردهای تغییریافته در مبداء را انتخاب می‌کند:
  • ایده‌آل وجود داشتن یک ستون Last Changed در سیستم مبداء است؛ که از آن می‌توان جهت انتخاب رکوردهای تغییر یافته استفاده نمود.
  • چنانچه ستون Last Changed وجود نداشته باشد، تمامی رکوردهای مبداء باید با رکورد‌های مقصد مقایسه شود.
  • در تکنیک Push، مبداء تغییرات را شناسائی می‌کند و آنها را به سمت مقصد Push می‌کند؛ این درخواست می‌تواند توسط فرآیند ETL انجام شود.
از آنجایی که پردازش ETL معمولاً در زمان هایی که Peak کاری وجود ندارد، اجرا می‌شود، استفاده از مکانیسم Pull برای شناسایی تغییرات نسبت به مکانسیم Push ارجحیت دارد.


2-1- Pulling Net Changes – Last Change Column

بیشتر جداول در سیستم‌های مبداء حاوی ستون هایی هستند که زمان ایجاد و یا اصلاح رکوردها را ثبت می‌کنند. در نوع دیگری از سیستم‌های مبداء ستونی با مقدار عددی وجود دارد، که هر زمان رکوردی تغییر یافت به آن ستون مقداری اضافه می‌شود. هر دوی این تکنیک‌ها به فرآیند ETL اجازه می‌دهند، بطور کارآمدی رکوردهای تغییریافته را انتخاب کند. (با مقایسه، بیشترین مقدار قرار گرفته در آن ستون؛ که در طول آخرین اجرای فرآیند ETL بدست آمده است). نمونه ای از جداول سیستم مبداء که دارای تغییرات زمانی است در شکل زیر نمایش داده می‌شود.

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

2-2- Pulling Net Changes – No Last Change Column

شکل زیر گردش فرآیند را هنگامی که ستون Last Change وجود ندارد؛ نمایش می‌دهد.


این گردش فرآیند شامل:
  1. Join میان مبداء و مقصد با استفاده از یک دستور Left Outer Join است.
  2. تمامی رکورد‌های مبداء که در مقصد وجود ندارند، پردازش می‌شوند.
  3. زمانی که رکوردی در مقصد وجود داشته باشد مقادیر داده‌های مبداء و مقصد مقایسه می‌شوند.
  4. تمامی رکوردهای مبداء که تغییر یافته اند پردازش می‌شوند.
از آنجایی که تمامی رکورد‌ها پردازش می‌شوند، این روش بویژه برای جداول حجیم؛ روش کارآمدی نیست.

2-3- Pushing Net Changes

دو متد متداول Push وجود دارد که در تصویر زیر نمایش داده  شده است.

تفاوت این دو روش به شرح زیر است:

  1. در سناریو اول (شکل سمت چپ)؛ بانک اطلاعاتی رابطه ای سیستم مبداء Transaction Log را مرتب مانیتور می‌کند تا تغییرات را شناسائی کرده و در ادامه تمامی این تغییرات را در جدولی در مقصد درج می‌کند.
  2. در سناریو دوم؛ توسعه دهندگان Trigger هایی ایجاد می‌کنند تا هر زمان که رکوردی تغییر یافت، تغییرات در جدولی که در مقصد وجود دارد درج گردد.

مسئله ای که در هر دو مورد وجود دارد Load اضافه ای است؛ که روی سیستم مبداء وجود دارد و می‌تواند Performance سیستم‌های OLTP را تحت تاثیر قرار دهد. به هر روی سناریو نخست معمولاً کاراتر از سناریویی است که از Trigger استفاده می‌کند.

3- ETL Patterns

پس از شناسائی رکوردهایی که در مبداء تغییر یافته اند، نیاز داریم تا این تغییرات در مقصد اعمال شود. در این قسمت به معرفی الگوهایی که برای اعمال این تغییرات وجود دارد می‌پردازیم.

3-1- Destination load Patterns

تشخیص چگونگی اضافه نمودن تغییرات در مقصد تابع دو عامل زیر است:

  • آیا رکورد هم اینک در مقصد وجود دارد؟
  • الگوی استفاده شده برای جدول مقصد به کدام شکل است؟ (Update یا Versioned Insert)

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

3-2- Versioned Insert Pattern

Kimball Type II Slowly Changing Dimension نمونه ای از الگوی Versioned Insert است؛ که در آن نمونه ای از یک موجودیت دارای ورژن‌های متعددی است. مطابق تصویر زیر؛ این الگو به ستون‌های اضافه ای نیاز دارند که وضعیت نمونه ای از یک رکورد را نمایش دهد.


این ستون‌ها به شرح زیر هستند:

  • Start Date: زمانی که وضعیت آن نمونه از رکورد فعال می‌شود.
  • End Date: زمانی که وضعیت آن نمونه از رکورد غیر فعال می‌شود.
  • Record Status: وضعیت‌های یک رکورد را نشان می‌دهد، که حداقل به شکل Active یا Inactive است.
  • # Version: این ستون که اختیاری می‌باشد، ورژن آن نمونه از رکورد را ثبت می‌کند.


برای مثال شکل زیر؛ بیانگر وضعیت اولیه رکوردی در این الگو است:


فرض کنید که این رکورد در تاریخ March 2 , 2010 در سیستم مبداء تغییر می‌کند. فرآیند ETL این تغییر را شناسائی می‌کند و همانند تصویر زیر؛ به شکل نمونه ای ثانویه از این رکورد، اقدام به درج آن می‌کند.

توجه داشته باشید زمانی که رکورد دوم در جدول درج می‌شود، به منظور بازتاب این تغییر؛ رکورد اول به شکل زیر بروزرسانی می‌گردد:

  • End Date: تا این زمان وضعیت این رکورد فعال بوده است.
  • Record Status:که Active به Inactive تغییر پیدا می‌کند.


در برخی از پیاده سازی‌های DW عمدتاً از الگوی Versioned Insert استفاده می‌شود و هرگز از الگوی Update استفاده نمی‌شود. مزیت این استراتژی در این است که تمامی تاریخچه تغییرات ردیابی و ثبت می‌شود. به هر روی غالباً هزینه ثبت کردن این تغییرات منجر به ایجاد نسخه‌های زیادی از تغییرات می‌شود. تیم DW برای مواردی که تغییرات متاثر از گزارشات تاریخچه ای نیستند، می‌توانند الگوی Update را در نظر گیرند.

3-3- Update Pattern

الگوی Update روی رکورد موجود، تغییرات سیستم مبداء را بروزرسانی می‌کند. مزیت این روش در این است که همواره یک رکورد وجود دارد و در نتیجه باعث ایجاد Query‌های کارآمدتر می‌شود. تصویر زیر بیانگر ستون هایی است که برای پشتیبانی از الگوی Update بایست ایجاد کرد.


این ستون‌ها به شرح زیر هستند:

  • Record Status: وضعیت‌های یک رکورد را نشان می‌دهد که حداقل به شکل Active یا Inactive است.
  • # Version: این ستون که اختیاری می‌باشد، ورژن آن نمونه از رکورد را ثبت می‌کند.


موارد اصلی الگوی Update عبارتند از:

  • تاریخ ثبت نمی‌شود. ابزاری ارزشمند برای نظارت بر داده ها، تغییرات تاریخی است و زمانی که ممیزی داده رخ می‌دهد؛ می‌تواند مفید واقع شود.
  • بروزرسانی‌ها یک الگوی مبتنی بر مجموعه هستند. استفاده از بروزرسانی هر بار یک رکورد در ابزار ETL خیلی کارآمد (موجه) نیست.


یک روش دیگر برای در نظر گرفتن موارد فوق؛ اضافه کردن یک جدول برای درج ورژن‌ها به الگوی Update است که در شکل زیر نشان داده شده است.


اضافه نمودن یک جدول تاریخچه، که تمامی تغییرات سیستم مبداء را ثبت  می‌کند؛ نظارت و ممیزی داده‌ها را نیز فراهم می‌کند و همچنین بروزرسانی‌های کارآمد مبتنی بر مجموعه را برای جداول DW به ارمغان می‌آورد.

3-4- Versioned Insert: Net Changes 

این الگو غالباً در جداول حجیم Fact که بروزرسانی آنها پر هزینه است استفاده می‌شود. شکل زیر منطق استفاده شده در این الگو را نشان می‌دهد.

توجه داشته باشید در این الگو:
  • مقادیر مالی و عددی محاسبه شده؛ به عنوان یک Net Change از نمونه قبلی رکورد در جدول Fact ذخیره می‌شود.
  • هیچ گونه فعالیت Post Processing صورت نمی‌گیرد (از قبیل بروزرسانی جداول Fact پس از کامل شدن Data Flow). هدف استفاده از این الگو اجتناب از بروزرسانی روی جداول بسیار حجیم می‌باشد.
  • عدم بروزرسانی و همچنین اندازه جدول Fact زمینه ای را فراهم می‌کند که منطق شناسائی رکوردهای تغییریافته پیچیده تر  می‌شود. این پیچیدگی از آنجا ناشی می‌شود که نیاز به مقایسه رکوردهای جدول Fact آتی با جدول Fact موجود می‌باشد.

4- Data Integration Best Practices

هم اکنون پس از آشنایی با مفاهیم و الگو‌های توزیع داده‌ها به ارائه تعدادی نمونه می‌پردازیم؛ که بتوان این ایده‌ها و الگوها را در عمل پوشش داد.

4-1- Basic Data Flow Patterns

هر یک از الگوهای Update Pattern و Versioned Insert Pattern می‌توانند برای انواعی از جداول بکار روند که معروفترین آن‌ها توسط Kimball ساخته شده اند.

  • (Slowly Changing Dimension Type I (SCD I: از Update Pattern استفاده می‌کند.
  • (Slowly Changing Dimension Type II (SCD II: از Versioned Insert Pattern استفاده می‌کند.
  • Fact Table: نوع الگویی که استفاده می‌کند به نوع جدول Fact ای که Load خواهد شد بستگی دارد.

4-1-1- Update Pattern 

مطابق تصویر زیر جدولی که تنها حاوی ورژن فعلی رکورد هاست؛ از Update Dataflow Pattern استفاده می‌کند.


مواردی که در مورد این گردش کاری باید در نظر داشت به شرح زیر است:

  • این Data Flow فقط سطرهایی را به یک مقصد اضافه خواهد کرد. SSIS دارای گزینه “Table or view fast load” می‌باشد که بارگذاری‌های انبوه و سریع را پشتیبانی می‌کند.
  • درون یک Data Flow بروزرسانی  رکورد‌ها را می‌توان با استفاده از تبدیل OLE DB Command انجام داد. توجه داشته باشید خروجی‌های این تبدیل در یک دستور Update به ازای هر رکورد بکار می‌رود؛ مفهوم بروزرسانی انبوه در این Data Flow وجود ندارد. بدین ترتیب الگوی فعلی ارائه شده؛ تنها رکوردها را درج می‌کند و هرگز در این Data Flow رکوردها Update نمی‌شوند.
  • هر جدول دارای یک جدول تاریخچه است که برای ذخیره همه فعالیت‌های مرتبط با آن بکار می‌رود. یک رکورد در جدول تاریخچه زمانی درج خواهد شد؛ که رکورد مبداء در مقصد وجود داشته باشد ولی دارای مقداری متفاوت باشد.
  • راه دیگر فرستادن تغییرات رکوردها به یک جدول کاری است که پس از پایان یافتن فرآیند Update ، خالی (Truncate) می‌شود.
  • مزیت نگهداری تمامی رکوردها در یک جدول تاریخچه؛ ایجاد یک دنباله ممیزی است که می‌تواند برای نظارت بر داده‌ها به منظور نمایان ساختن موارد مطرح شده توسط مصرف کننده‌های کسب و کار استفاده شود.
  • گزینه‌های متفاوتی برای تشخیص تغییرات رکوردها وجود دارد که در ادامه به شرح آنها می‌پردازیم.


شکل زیر نمایش دهنده چگونگی پیاده سازی Update Dataflow Pattern در یک SSIS می‌باشد:


این SSIS شامل عناصر زیر است:

  • Destination table lookup:

به منظور تشخیص اینکه رکورد در جدول مقصد وجود دارد از “lkpPersonContact” استفاده می‌کنیم.

  • Change detection logic:

با استفاده از “DidRecordChange” مبداء و مقصد مقایسه می‌شوند. اگر تفاوتی بین مبداء و مقصد وجود نداشت؛ رکورد نادیده گرفته می‌شود. چنانچه بین مبداء و مقصد تفاوت وجود داشت؛ رکورد در جدول تاریخچه درج خواهد شد.

  • Detection Inserts:

رکوردها در جدول مقصد درج خواهند شد در صورتیکه در آن وجود نداشته باشند.

  • Destination History Inserts:

رکوردها در جدول تاریخچه مقصد درج خواهند شد، در صورتیکه (در مقصد) وجود داشته باشند.

پس از اتمام Data Flow یک روال Post-processing مسئولیت بروزرسانی رکوردهای جدول اصلی و رکوردهای ذخیره شده در جدول تاریخچه را بر عهده دارد که می‌تواند مطابق تصویر زیر با استفاده از یک Execute Process Task پیاده سازی شود.


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

  • بروزرسانی رکوردهای جداول با استفاده از رکوردهای درج شده در جدول تاریخچه.
  • درج تمامی رکوردهای جدید (نسخه اولیه و در درون جدول تاریخچه). کلید اصلی جداولی که ستون  آنها IDENTITY است مقدار نامشخصی دارد؛ تا زمانی که درج صورت گیرد، این به معنای آن است که پیش از انتقال آنها به جدول تاریخچه نیاز است منتظر درج شدن آنها باشیم.

4-1-2- Update Pattern – ETL Framework

تصویر زیر بیانگر انجام این عملیات با استفاده از ابزارهای ETL است.
در نگاه نخستین ممکن است Data Flow از نوع اصلی خود پیچیده‌تر به نظر آید؛ که در واقع این گونه نیز هست، زیرا در فاز توسعه بیشتر Framework‌ها جهت پیاده سازی به یک زمان اضافه‌تری نیاز دارند. به هر روی این زمان جهت اجتناب از هزینه روزانه تطبیق داده‌ها گرفته خواهد شد.
مزایای حاصل شده از افزودن این منطق اضافی عبارت است از:

  • پشتیبانی از ستون هایی که کارهای ممیزی و نظارت بر داده‌ها را آسانتر می‌کنند.
  • تعداد سطرها شاخص مناسبی است که می‌تواند بهبود آن Data Flow خاص را فراهم کند. ناظر اطلاعات با استفاده از تعداد رکوردها می‌تواند ناهنجاری‌ها را شناسائی کند.

بهره برداران ETL و ناظران اطلاعات می‌توانند با استفاده از خلاصه تعداد رکوردها درک بیشتری درباره فعالیت‌های آن کسب کنند. پس از آنکه تعداد رکوردها، مشکوک به نظر آمد؛ تحقیقات بیشتری می‌تواند اتفاق افتد. (با عمیق‌تر شدن در جزئیات گزارشات)
 

4-1-3- Versioned Insert Pattern

جدولی که به صورت Versioned Insert پر شده است می‌تواند از Versioned Insert Dataflow Pattern استفاده کند. همانند شکل زیر که گردش کار در آن برای کارآئی بیشتر بازنگری شده است.


توجه داشته باشید Data Flow در این روش شامل:

  • تمامی رکوردهای جدید و تغییر یافته در جدول Versioned Insert قرار می‌گیرند.
  • این روش دارای Data Flow ساده‌تری نسبت به الگوی Update می‌باشد.

شکل زیر SSIS versioned insert data flow pattern را نشان می‌دهد:
 

تعدادی نکته در Data Flow فوق وجود دارد که عبارتند از:

  • در شیء “lkpDimGeography” گزینه “Redirect rows to no match output” با مقدار “Ignore Failures” تنظیم شده است.
  • شیء “DidRecordChange” بررسی می‌کند چنانچه ستون‌های مبداء و مقصد یکسان باشند، آیا کلید اصلی جدول مقصد Not Null است. اگر این عبارت True ارزیابی شود، رکورد نادیده گرفته می‌شود.
  • منطق شناسائی تغییرات دربردارنده تغییرات ستون داده ای در مبداء نمی‌باشد.
  • ستون و تعداد رکوردها مشابه با Data Flow قبلی (ETL Framework) می‌باشد.

4-1-4- Update vs. Versioned Insert

الگوی Versioned Insert نسبت الگوی Update دارای پیاده سازی ساده‌تر و فعالیت‌های I/O کمتری است. از منظر دیگر، جدولی که از الگوی Update استفاده می‌کند، دارای تعداد رکوردهای کمتری است که می‌تواند به معنای Performance بهتر نیز تعبیر شود. ممکن است سوالی مطرح شود، اینکه چرا برای انجام کار به جدول تاریخچه نیاز است؛ این جدول را که نمی‌توان Truncate نمود، پس چرا به منظور بروزرسانی از جدول اصلی استفاده می‌شود؟ پاسخ این پرسش در این است که جدول تاریخچه، ناظر اطلاعات و ممیزین داده را قادر می‌سازد، تغییرات در طول زمان را پیگیری نمایند.
 

4-2- Dimension Patterns

بروزرسانی Dimension موارد زیر را شامل می‌شود:

  • پیگیری تاریخچه
  • انجام بروزرسانی
  • تشخیص رکوردهای جدید
  • مدیریت surrogate keys

چنانچه با یک Dimension کوچک مواجه هستید (با مقدار هزاران رکورد یا کمتر، که با صدها هزار رکورد یا بیشتر ضدیت دارد)،  می‌توانید از تبدیل “Slowly Changing Dimension” که بصورت Built-in در SSIS موجود است، استفاده نمائید. به هر روی با آنکه این تبدیل چندین ویژگی محدودکننده Performance دارد، اغلب کارآمدتر از پروسسه هایی که توسط خودتان ایجاد می‌شود. در واقع فرآیند بارگذاری در جداول Dimension با مقایسه داده‌ها بین مبداء و مقصد انجام می‌شود. به طور معمول مقایسه روی یک ورژن جدید و یا مجموعه ای از سطرهای جدید یک جدول با مجموعه داده‌های موجود در جدول متناظرش صورت می‌گیرد. پس از تشخیص چگونگی تغییر در داده ها، یک سری عملیات درج و بروزرسانی انجام می‌شود. شکل زیر نمونه ای از پردازش سریع در Dimension را نمایش می‌دهد؛ که شامل مراحل اساسی زیر است:

  • منبع فوقانی سمت چپ، رکوردها را در یک SSIS از یک سیستم مبداء (یا یک سیستم میانی) به شکل Pull دریافت می‌کند. منبع فوقانی سمت راست، داده‌ها را از خود جدول Dimension به شکل Pull دریافت می‌کند.
  • با استفاده از Merge Join رکوردها از طریق Source Key شان مقایسه می‌شوند. (در شکل بعدی جزئیات این مقایسه نمایش داده شده است.)
  • با استفاده از یک Conditional Spilt داده‌ها ارزیابی می‌شوند؛ سطرها یا مستقیماً در جدول Dimension درج می‌شوند (منبع تحتانی سمت چپ) و یا در یک جدول عملیاتی (منبع تحتانی سمت راست) جهت انجام بروزرسانی درج می‌شوند.
  • در گام پایانی (که نمایش داده نشده) مجموعه ای از بروزرسانی بین جدول عملیاتی و جدول Dimension صورت می‌گیرد.

 

با Merge Join ارتباطی بین رکوردهای مبداء و رکوردهای مقصد برقرار می‌شود. (در این مثال “CustomerAlternateKey”). هنگامی که از این دیدگاه استفاده می‌کنید، خاطر جمع شوید که نوع Join با مقدار “Left outer join” تنظیم شده است؛ بدین ترتیب قادر هستید تا رکوردهای جدید را از مبداء تشخیص دهید؛ از آنجا که هنوز در جدول Dimension قرار نگرفته اند.


گام پایانی به منظور تشخیص اینکه آیا رکورد، جدید یا تغییر یافته است (یا بلاتکلیف است)، مقایسه داده هاست. شکل زیر نمایش می‌دهد چگونه این ارزیابی با استفاده از تبدیل “Conditional Spilt” صورت می‌گیرد.


Conditional Spilt مستقیماً با استفاده از یک Adapter تعریف شده روی مقصد یا یک جدول کاری بروزرسانی که از یک Adapter تعریف شده روی مقصد استفاده می‌کند؛ توسط مجموعه دستور Update زیر، رکوردها را در جدول Dimension قرار می‌دهد. دستور Update زیر مستقیماً با استفاده از روش Join روی جدول Dimension و جدول کاری، مجموعه ای را بصورت انبوه بروزرسانی می‌کند.

UPDATE AdventureWorksDW2008R2.dbo.DimCustomer
    SET AddressLine1 = stgDimCustomerUpdates.AddressLine1
    , AddressLine2 = stgDimCustomerUpdates.AddressLine2
    , BirthDate = stgDimCustomerUpdates.BirthDate
    , CommuteDistance = stgDimCustomerUpdates.CommuteDistance
    , DateFirstPurchase = stgDimCustomerUpdates.DateFirstPurchase
    , EmailAddress = stgDimCustomerUpdates.EmailAddress
    , EnglishEducation = stgDimCustomerUpdates.EnglishEducation
    , EnglishOccupation = stgDimCustomerUpdates.EnglishOccupation
    , FirstName = stgDimCustomerUpdates.FirstName
    , Gender = stgDimCustomerUpdates.Gender
    , GeographyKey = stgDimCustomerUpdates.GeographyKey
    , HouseOwnerFlag = stgDimCustomerUpdates.HouseOwnerFlag
    , LastName = stgDimCustomerUpdates.LastName
    , MaritalStatus = stgDimCustomerUpdates.MaritalStatus
    , MiddleName = stgDimCustomerUpdates.MiddleName
    , NumberCarsOwned = stgDimCustomerUpdates.NumberCarsOwned
    , NumberChildrenAtHome = stgDimCustomerUpdates.NumberChildrenAtHome
    , Phone = stgDimCustomerUpdates.Phone
    , Suffix = stgDimCustomerUpdates.Suffix
    , Title = stgDimCustomerUpdates.Title
    , TotalChildren = stgDimCustomerUpdates.TotalChildren
FROM AdventureWorksDW2008.dbo.DimCustomer DimCustomer
  INNER JOIN dbo.stgDimCustomerUpdates ON
DimCustomer.CustomerAlternateKey = stgDimCustomerUpdates.CustomerAlternateKey

4-3- Fact Table Patterns

جداول Fact به پردازش‌های منحصر به فردی نیازمند هستند، نخست به کلیدهای Surrogate جدول Dimension نیاز دارند تا Measure‌های محاسبه شدنی را بدست آورند. این اعمال از طریق تبدیلات Lookup، Merge Join و Derived Column صورت می‌گیرد. با بروزرسانی ها، تفاضل رکورد‌ها و یا Snapshot بیشتر این فرآیندهای دشوار انجام می‌شوند.

4-3-1- Inserts

روی اغلب جداول Fact عمل درج صورت می‌گیرد؛ که کار متداولی در جدول Fact می‌باشد. شاید ساده‌ترین کار که در فرآیند ساخت ETL صورت می‌گیرد، عملیات درج روی تنها تعدادی از جدول Fact می‌باشد. درج کردن در صورت لزوم بارگذاری انبوه داده ها، مدیریت شاخص‌ها و مدیریت پارتیشن‌ها را شامل می‌شود.

4-3-2- Updates

بروزرسانی روی جداول Fact معمولاً به یکی از سه طریق زیر انجام می‌گیرد:

  • از طریق یک تغییر یا بروزرسانی رکورد
  • از طریق یک دستور Insert خنثی کننده (Via an Insert of a compensating transaction)
  • با استفاده از یک SQL MERGE


در موردی که تغییرات با فرکانس کمی روی جدول Fact صورت می‌گیرد و یا فرآیند بروزرسانی قابل مدیریت است؛ ساده‌ترین روش انجام یک دستور Update روی جدول Fact می‌باشد. نکته  مهمی که هنگام انجام بروزرسانی باید به خاطر داشته باشید، استفاده از روش بروزرسانی مبتنی بر مجموعه است؛ به همان طریق که در قسمت الگوهای Dimension ذکر آن رفت.
در طریقی دیگر (درج compensating) می‌توان اقدام به درج رکورد تغییر یافته نمود، تا ترجیحاً بروزرسانی روی آن صورت گیرد. این استراتژی به سادگی داده‌های جدول Fact میان سیستم مبداء و مقصد را که تغییر یافته اند، به صورت یک رکورد جدید درج خواهد کرد. تصویر زیر مثالی از اجرای موارد فوق را نمایش می‌دهد.
 

در آخرین روش از یک دستور SQL MERGE استفاده می‌شود که در آن با استفاده از ادغام و مقایسه، تمامی داده‌های جدید و تغییر یافته جدول Fact، درج و یا بروزرسانی می‌شوند. نمونه ای از استفاده دستور Merge به شرح زیر است:

MERGE dbo.FactSalesQuota AS T
USING SSIS_PDS.dbo.stgFactSalesQuota AS S
ON T.EmployeeKey = S.EmployeeKey
AND T.DateKey = S.DateKey
WHEN MATCHED AND BY target
THEN INSERT(EmployeeKey, DateKey, CalendarYear, CalendarQuarter, SalesAmountQuota)
VALUES(S.EmployeeKey, S.DateKey, S.CalendarYear, S.CalendarQuarter, S.SalesAmountQuota)
WHEN MATCHED AND T.SalesAmountQuota != S.SalesAmountQuota
THEN UPDATE SET T.SalesAmountQuota = S.SalesAmountQuota
;
اشکال این روش Performance است؛ گرچه این دستور به سادگی عملیات درج و بروزرسانی را انجام می‌دهد ولی به صورت سطر به سطر عملیات انجام می‌شود (در هر زمان یک سطر). در موقعیت هایی که با مقدار زیادی داده مواجه هستید، اغلب بهتر است به صورت انبوه عملیات درج و به صورت مجموعه عملیات بروزرسانی انجام گیرد.

4-3-3- Managing Inferred Members

زمانیکه یک ارجاع در جدول Fact به یک عضو Dimension که هنوز بارگذاری نشده‌است بوجود  آید؛ یک Inferred Member تعبیر می‌شود. به سه طریق می‌توان این Inferred Member‌ها را مدیریت نمود:

  • رکوردهای جدول Fact پیش از درج اسکن شوند؛ ایجاد هر Inferred Member در Dimension و سپس بارگذاری رکوردها در جدول Fact
  • در طول عملیات بارگذاری روی Fact؛ هر رکورد مفقوده شده به یک جدول موقتی ارسال شود، رکوردهای مفقوده شده به Dimension اضافه شود، در ادامه مجدداً آن رکوردهای Fact در جدول Fact بارگذاری شوند.
  • در یک Data Flow زمانی که یک رکورد مفقود شده، بلاتکلیف تعبیر می‌شود؛ آن زمان یک رکورد به Dimension اضافه شود و Surrogate Key بدست آمده را برگردانیم؛ سپس Dimension بارگذاری شود.


شکل زیر این موارد را نمایش می‌دهد:

مطالب
ASP.NET MVC #11

بررسی نکات تکمیلی Model binder در ASP.NET MVC

یک برنامه خالی جدید ASP.NET MVC را شروع کنید و سپس مدل زیر را به پوشه Models آن اضافه نمائید:

using System;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

از این مدل چند مقصود ذیل دنبال می‌شوند:
استفاده از Id به عنوان primary key برای edit و update رکوردها. استفاده از DateTime برای اینکه اگر کاربری اطلاعات بی ربطی را وارد کرد چگونه باید این مشکل را در حالت model binding خودکار تشخیص داد و استفاده از IsAdmin برای یادآوری یک نکته امنیتی بسیار مهم که اگر حین model binding خودکار به آن توجه نشود، سایت را با مشکلات حاد امنیتی مواجه خواهد کرد. سیستم پیشرفته است. می‌تواند به صورت خودکار ورودی‌های کاربر را تبدیل به یک شیء حاضر و آماده کند ... اما باید حین استفاده از این قابلیت دلپذیر به یک سری نکات امنیتی هم دقت داشت تا سایت ما به نحو دلپذیری هک نشود!

در ادامه یک کنترلر جدید به نام UserController را به پوشه کنترلرهای پروژه اضافه نمائید. همچنین نام کنترلر پیش فرض تعریف شده در قسمت مسیریابی فایل Global.asax.cs را هم به User تغییر دهید تا در هربار اجرای برنامه در VS.NET، نیازی به تایپ آدرس‌های مرتبط با UserController نداشته باشیم.
یک منبع داده تشکیل شده در حافظه را هم برای نمایش لیستی از کاربران، به نحو زیر به پروژه اضافه خواهیم کرد:

using System;
using System.Collections.Generic;

namespace MvcApplication7.Models
{
public class Users
{
public IList<User> CreateInMemoryDataSource()
{
return new[]
{
new User { Id = 1, Name = "User1", Password = "123", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 2, Name = "User2", Password = "456", IsAdmin = false, AddDate = DateTime.Now },
new User { Id = 3, Name = "User3", Password = "789", IsAdmin = true, AddDate = DateTime.Now }
};
}
}
}

در اینجا فعلا هدف آشنایی با زیر ساخت‌های ASP.NET MVC است و درک صحیح نحوه کارکرد آن. مهم نیست از EF استفاده می‌کنید یا NH یا حتی ADO.NET کلاسیک و یا از Micro ORMهایی که پس از ارائه دات نت 4 مرسوم شده‌اند. تهیه یک ToList یا Insert و Update با این فریم ورک‌ها خارج از بحث جاری هستند.

سورس کامل کنترلر User به شرح زیر است:

using System;
using System.Linq;
using System.Web.Mvc;
using MvcApplication7.Models;

namespace MvcApplication7.Controllers
{
public class UserController : Controller
{
[HttpGet]
public ActionResult Index()
{
var usersList = new Users().CreateInMemoryDataSource();
return View(usersList); // Shows the Index view.
}

[HttpGet]
public ActionResult Details(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Details view.
}

[HttpGet]
public ActionResult Create()
{
var user = new User { AddDate = DateTime.Now };
return View(user); // Shows the Create view.
}

[HttpPost]
public ActionResult Create(User user)
{
if (this.ModelState.IsValid)
{
// todo: Add record
return RedirectToAction("Index");
}
return View(user); // Shows the Create view again.
}

[HttpGet]
public ActionResult Edit(int id)
{
var user = new Users().CreateInMemoryDataSource().FirstOrDefault(x => x.Id == id);
if (user == null)
return View("Error");
return View(user); // Shows the Edit view.
}

[HttpPost]
public ActionResult Edit(User user)
{
if (this.ModelState.IsValid)
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}

[HttpPost]
public ActionResult Delete(int id)
{
// todo: Delete record
return RedirectToAction("Index");
}
}
}

توضیحات:

ایجاد خودکار فرم‌های ورود اطلاعات

در قسمت قبل برای توضیح دادن نحوه ایجاد فرم‌ها در ASP.NET MVC و همچنین نحوه نگاشت اطلاعات آن‌ها به اکشن متدهای کنترلرها، فرم‌های مورد نظر را دستی ایجاد کردیم.
اما باید درنظر داشت که برای ایجاد Viewها می‌توان از ابزار توکار خود VS.NET نیز استفاده کرد و سپس اطلاعات و فرم‌های تولیدی را سفارشی نمود. این سریع‌ترین راه ممکن است زمانیکه مدل مورد استفاده کاملا مشخص است و می‌خواهیم Strongly typed views را ایجاد کنیم.
برای نمونه بر روی متد Index کلیک راست کرده و گزینه Add view را انتخاب کنید. در اینجا گزینه‌ی create a strongly typed view را انتخاب کرده و سپس از لیست مدل‌ها، User را انتخاب نمائید. Scaffold template را هم بر روی حالت List قرار دهید.
برای متد Details هم به همین نحو عمل نمائید.
برای ایجاد View متناظر با متد Create در حالت HttpGet، تمام مراحل یکی است. فقط Scaffold template انتخابی را بر روی Create قرار دهید تا فرم ورود اطلاعات، به صورت خودکار تولید شود.
متد Create در حالت HttpPost نیازی به View اضافی ندارد. چون صرفا قرار است اطلاعاتی را از سرور دریافت و ثبت کند.
برای ایجاد View متناظر با متد Edit در حالت HttpGet، باز هم مراحل مانند قبل است با این تفاوت که Scaffold template انتخابی را بر روی گزینه Edit قرار دهید تا فرم ویرایش اطلاعات کاربر به صورت خودکار به پروژه اضافه شود.
متد Edit در حالت HttpPost نیازی به View اضافی ندارد و کارش تنها دریافت اطلاعات از سرور و به روز رسانی بانک اطلاعاتی است.
به همین ترتیب متد Delete نیز، نیازی به View خاصی ندارد. در اینجا بر اساس primary key دریافتی، می‌توان یک کاربر را یافته و حذف کرد.



سفارشی سازی Viewهای خودکار تولیدی

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

@Html.ActionLink("Delete", "Delete", new { id=item.Id }

در قسمت قبل هم عنوان شد که اعمال حذف باید بر اساس HttpPost محدود شوند تا بتوان میزان امنیت برنامه را بهبود داد. متد Delete هم در کنترلر فوق تنها به حالت HttpPost محدود شده است. بنابراین ActionLink پیش فرض را حذف کرده و بجای آن فرم و دکمه زیر را قرار می‌دهیم تا اطلاعات به سرور Post شوند:

@using (Html.BeginForm(actionName: "Delete", controllerName: "User", routeValues: new { id = item.Id }))
{
<input type="submit" value="Delete"
onclick="return confirm ('Do you want to delete this record?');" />
}

در اینجا نحوه ایجاد یک فرم، که id رکورد متناظر را به سرور ارسال می‌کند، مشاهده می‌کنید.



علت وجود دو متد، به ازای هر Edit یا Create

به ازای هر کدام از متدهای Edit و Create دو متد HttpGet و HttpPost را ایجاد کرده‌ایم. کار متدهای HttpGet نمایش View‌های متناظر به کاربر هستند. بنابراین وجود آن‌ها ضروری است. در این حالت چون از دو Verb متفاوت استفاده شده، می‌توان متدهای هم نامی را بدون مشکل استفاده کرد. به هر کدام از افعال Get و Post و امثال آن، یک Http Verb گفته می‌شود.



بررسی معتبر بودن اطلاعات دریافتی

کلاس پایه Controller که کنترلرهای برنامه از آن مشتق می‌شوند، شامل یک سری خواص و متدهای توکار نیز هست. برای مثال توسط خاصیت this.ModelState.IsValid می‌توان بررسی کرد که آیا Model دریافتی معتبر است یا خیر. برای بررسی این مورد، یک breakpoint را بر روی سطر this.ModelState.IsValid در متد Create قرار دهید. سپس به صفحه ایجاد کاربر جدید مراجعه کرده و مثلا بجای تاریخ روز، abcd را وارد کنید. سپس فرم را به سرور ارسال نمائید. در این حالت مقدار خاصیت this.ModelState.IsValid مساوی false می‌باشد که حتما باید به آن پیش از ثبت اطلاعات دقت داشت.



شبیه سازی عملکرد ViewState در ASP.NET MVC

در متدهای Create و Edit در حالت Post، اگر اطلاعات Model معتبر نباشند، مجددا شیء User دریافتی، به View بازگشت داده می‌شود. چرا؟
صفحات وب، زمانیکه به سرور ارسال می‌شوند، تمام اطلاعات کنترل‌های خود را از دست خواهد داد (صفحه پاک می‌شود، چون مجددا یک صفحه خالی از سرور دریافت خواهد شد). برای رفع این مشکل در ASP.NET Web forms، از مفهومی به نام ViewState کمک می‌گیرند. کار ViewState ذخیره موقت اطلاعات فرم جاری است برای استفاده مجدد پس از Postback. به این معنا که پس از ارسال فرم به سرور، اگر کاربری در textbox اول مقدار abc را وارد کرده بود، پس از نمایش مجدد فرم، مقدار abc را در همان textbox مشاهده خواهد کرد (شبیه سازی برنامه‌های دسکتاپ در محیط وب). بدیهی است وجود ViewState برای ذخیره سازی این نوع اطلاعات، حجم صفحه را بالا می‌برد (بسته به پیچیدگی صفحه ممکن است به چند صد کیلوبایت هم برسد).
در ASP.NET MVC بجای استفاده از ترفندی به نام ViewState، مجددا اطلاعات همان مدل متناظر با View را بازگشت می‌دهند. در این حالت پس از ارسال صفحه به سرور و نمایش مجدد صفحه ورود اطلاعات، تمام کنترل‌ها با همان مقادیر قبلی وارد شده توسط کاربر قابل مشاهده خواهند بود (مدل مشخص است، View ما هم از نوع strongly typed می‌باشد. در این حالت فریم ورک می‌داند که اطلاعات را چگونه به کنترل‌های قرار گرفته در صفحه نگاشت کند).
در مثال فوق، اگر اطلاعات وارد شده صحیح باشند، کاربر به صفحه Index هدایت خواهد شد. در غیراینصورت مجددا همان View جاری با همان اطلاعات model قبلی که کاربر تکمیل کرده است به او برای تصحیح، نمایش داده می‌شود. این مساله هم جهت بالا بردن سهولت کاربری برنامه بسیار مهم است. تصور کنید که یک فرم خالی با پیغام «تاریخ وارد شده معتبر نیست» مجدا به کاربر نمایش داده شود و از او درخواست کنیم که تمام اطلاعات دیگر را نیز از صفر وارد کند چون اطلاعات صفحه پس از ارسال به سرور پاک شده‌اند؛ که ... اصلا قابل قبول نیست و فوق‌العاده برنامه را غیرحرفه‌ای نمایش می‌دهد.



خطاهای نمایش داده شده به کاربر

به صورت پیش فرض خطایی که به کاربر نمایش داده می‌شود، استثنایی است که توسط فریم ورک صادر شده است. برای مثال نتوانسته است abcd را به یک تاریخ معتبر تبدیل کند. می‌توان توسط this.ModelState.AddModelError خطایی را نیز در اینجا اضافه کرد و پیغام بهتری را به کاربر نمایش داد. یا توسط یک سری data annotations هم کار اعتبار سنجی را سفارشی کرد که بحث آن به صورت جداگانه در یک قسمت مستقل بررسی خواهد شد.
ولی به صورت خلاصه اگر به فرم‌های تولید شده توسط VS.NET دقت کنید، در ابتدای هر فرم داریم:

@Html.ValidationSummary(true)

در اینجا خطاهای عمومی در سطح مدل نمایش داده می‌شوند. برای اضافه کردن این نوع خطاها، در متد AddModelError، مقدار key را خالی وارد کنید:

ModelState.AddModelError(string.Empty, "There is something wrong with model.");

همچنین در این فرم‌ها داریم:
@Html.EditorFor(model => model.AddDate)
@Html.ValidationMessageFor(model => model.AddDate)

EditorFor سعی می‌کند اندکی هوش به خرج دهد. یعنی اگر خاصیت دریافتی مثلا از نوع bool بود، خودش یک checkbox را در صفحه نمایش می‌دهد. همچنین بر اساس متادیتا یک خاصیت نیز می‌تواند تصمیم گیری را انجام دهد. این متادیتا منظور attributes و data annotations ایی است که به خواص یک مدل اعمال می‌شود. مثلا اگر ویژگی HiddenInput را به یک خاصیت اعمال کنیم، به شکل یک فیلد مخفی در صفحه ظاهر خواهد شد.
یا متد Html.DisplayFor، اطلاعات را به صورت فقط خواندنی نمایش می‌دهد. اصطلاحا به این نوع متدها، Templated Helpers هم گفته می‌شود. بحث بیشتر درباره‌ای این موارد به قسمتی مجزا و مستقل موکول می‌گردد. برای نمونه کل فرم ادیت برنامه را حذف کنید و بجای آن بنویسید Html.EditorForModel و سپس برنامه را اجرا کنید. یک فرم کامل خودکار ویرایش اطلاعات را مشاهده خواهید کرد (و البته نکات سفارشی سازی آن به یک قسمت کامل نیاز دارند).
در اینجا متد ValidationMessageFor کار نمایش خطاهای اعتبارسنجی مرتبط با یک خاصیت مشخص را انجام می‌دهد. بنابراین اگر قصد ارائه خطایی سفارشی و مخصوص یک فیلد مشخص را داشتید، در متد AddModelError، مقدار پارامتر اول یا همان key را مساوی نام خاصیت مورد نظر قرار دهید.


مقابله با مشکل امنیتی Mass Assignment در حین کار با Model binders

استفاده از Model binders بسیار لذت بخش است. یک شیء را به عنوان پارامتر اکشن متد خود معرفی می‌کنیم. فریم ورک هم در ادامه سعی می‌کند تا اطلاعات فرم را به خواص این شیء نگاشت کند. بدیهی است این روش نسبت به روش ASP.NET Web forms که باید به ازای تک تک کنترل‌های موجود در صفحه یکبار کار دریافت اطلاعات و مقدار دهی خواص یک شیء را انجام داد، بسیار ساده‌تر و سریعتر است.
اما اگر همین سیستم پیشرفته جدید ناآگاهانه مورد استفاده قرار گیرد می‌تواند منشاء حملات ناگواری شود که به نام «Mass Assignment» شهرت یافته‌اند.
همان صفحه ویرایش اطلاعات را درنظر بگیرید. چک باکس IsAdmin قرار است در قسمت مدیریتی برنامه تنظیم شود. اگر کاربری نیاز داشته باشد اطلاعات خودش را ویرایش کند، مثلا پسوردش را تغییر دهد، با یک صفحه ساده کلمه عبور قبلی را وارد کنید و دوبار کلمه عبور جدید را نیز وارد نمائید، مواجه خواهد شد. خوب ... اگر همین کاربر صفحه را جعل کند و فیلد چک باکس IsAdmin را به صفحه اضافه کند چه اتفاقی خواهد افتاد؟ بله ... مشکل هم همینجا است. در اینصورت کاربر عادی می‌تواند دسترسی خودش را تا سطح ادمین بالا ببرد، چون model binder اطلاعات IsAdmin را از کاربر دریافت کرده و به صورت خودکار به model ارائه شده، نگاشت کرده است.
برای مقابله با این نوع حملات چندین روش وجود دارند:
الف) ایجاد لیست سفید
به کمک ویژگی Bind می‌توان لیستی از خواص را جهت به روز رسانی به model binder معرفی کرد. مابقی ندید گرفته خواهند شد:

public ActionResult Edit([Bind(Include = "Name, Password")] User user)

در اینجا تنها خواص Name و Password توسط model binder به خواص شیء User نگاشت می‌شوند.
به علاوه همانطور که در قسمت قبل نیز ذکر شد، متد edit را به شکل زیر نیز می‌توان بازنویسی کرد. در اینجا متدهای توکار UpdateModel و TryUpdateModel نیز لیست سفید خواص مورد نظر را می‌پذیرند (اعمال دستی model binding):

[HttpPost]
public ActionResult Edit()
{
var user = new User();
if(TryUpdateModel(user, includeProperties: new[] { "Name", "Password" }))
{
// todo: Edit record
return RedirectToAction("Index");
}
return View(user); // Shows the Edit view again.
}


ب) ایجاد لیست سیاه
به همین ترتیب می‌توان تنها خواصی را معرفی کرد که باید صرفنظر شوند:
public ActionResult Edit([Bind(Exclude = "IsAdmin")] User user)

در اینجا از خاصیت IsAdmin صرف نظر گردیده و از مقدار ارسالی آن توسط کاربر استفاده نخواهد شد.
و یا می‌توان پارامتر excludeProperties متد TryUpdateModel را نیز مقدار دهی کرد.

لازم به ذکر است که ویژگی Bind را به کل یک کلاس هم می‌توان اعمال کرد. برای مثال:

using System;
using System.Web.Mvc;

namespace MvcApplication7.Models
{
[Bind(Exclude = "IsAdmin")]
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }
public bool IsAdmin { set; get; }
}
}

این مورد اثر سراسری داشته و قابل بازنویسی نیست. به عبارتی حتی اگر در متدی خاصیت IsAdmin را مجددا الحاق کنیم، تاثیری نخواهد داشت.
یا می‌توان از ویژگی ReadOnly هم استفاده کرد:
using System;
using System.ComponentModel;

namespace MvcApplication7.Models
{
public class User
{
public int Id { set; get; }
public string Name { set; get; }
public string Password { set; get; }
public DateTime AddDate { set; get; }

[ReadOnly(true)]
public bool IsAdmin { set; get; }
}
}

در این حالت هم خاصیت IsAdmin هیچگاه توسط model binder به روز و مقدار دهی نخواهد شد.

ج) استفاده از ViewModels
این راه حلی است که بیشتر مورد توجه معماران نرم افزار است و البته کسانی که پیشتر با الگوی MVVM کار کرده باشند این نام برایشان آشنا است؛ اما در اینجا مفهوم متفاوتی دارد. در الگوی MVVM، کلاس‌های ViewModel شبیه به کنترلرها در MVC هستند یا به عبارتی همانند رهبر یک اکستر عمل می‌کنند. اما در الگوی MVC خیر. در اینجا فقط مدل یک View هستند و نه بیشتر. هدف هم این است که بین Domain Model و View Model تفاوت قائل شد.
کار View model در الگوی MVC، شکل دادن به چندین domain model و همچنین اطلاعات اضافی دیگری که نیاز هستند، جهت استفاده نهایی توسط یک View می‌باشد. به این ترتیب View با یک شیء سر و کار خواهد داشت و همچنین منطق شکل دهی به اطلاعات مورد نیازش هم از داخل View حذف شده و به خواص View model در زمان تشکیل آن منتقل می‌شود.
مشخصات یک View model خوب به شرح زیر است:
الف) رابطه بین یک View و View model آن، رابطه‌ای یک به یک است. به ازای هر View، بهتر است یک کلاس View model وجود داشته باشد.
ب) View ساختار View model را دیکته می‌کند و نه کنترلر.
ج) View modelها صرفا یک سری کلاس POCO (کلاس‌هایی تشکیل شده از خاصیت، خاصیت، خاصیت ....) هستند که هیچ منطقی در آن‌ها قرار نمی‌گیرد.
د) View model باید حاوی تمام اطلاعاتی باشد که View جهت رندر نیاز دارد و نه بیشتر و الزامی هم ندارد که این اطلاعات مستقیما به domain models مرتبط شوند. برای مثال اگر قرار است firstName+LastName در View نمایش داده شود، کار این جمع زدن باید حین تهیه View Model انجام شود و نه داخل View. یا اگر قرار است اطلاعات عددی با سه رقم جدا کننده به کاربر نمایش داده شوند، وظیفه View Model است که یک خاصیت اضافی را برای تهیه این مورد تدارک ببیند. یا مثلا اگر یک فرم ثبت نام داریم و در این فرم لیستی وجود دارد که تنها Id عنصر انتخابی آن در Model اصلی مورد استفاده قرار می‌گیرد، تهیه اطلاعات این لیست هم کار ViewModel است و نه اینکه مدام به Model اصلی بخواهیم خاصیت اضافه کنیم.

ViewModel چگونه پیاده سازی می‌شود؟
اکثر مقالات را که مطالعه کنید، این روش را توصیه می‌کنند:

public class MyViewModel
{
    public SomeDomainModel1 Model1 { get; set; }
    public SomeDomainModel2 Model2 { get; set; }
    ...
}

یعنی اینکه View ما به اطلاعات مثلا دو Model نیاز دارد. این‌ها را به این شکل محصور و کپسوله می‌کنیم. اگر View، واقعا به تمام فیلدهای این کلاس‌ها نیاز داشته باشد، این روش صحیح است. در غیر اینصورت، این روش نادرست است (و متاسفانه همه جا هم دقیقا به این شکل تبلیغ می‌شود).
ViewModel محصور کننده یک یا چند مدل نیست. در اینجا حس غلط کار کردن با یک ViewModel را داریم. ViewModel فقط باید ارائه کننده اطلاعاتی باشد که یک View نیاز دارد و نه بیشتر و نه تمام خواص تمام کلاس‌های تعریف شده. به عبارتی این نوع تعریف صحیح است:

public class MyViewModel
{
    public string SomeExtraField1 { get; set; }
    public string SomeExtraField2 { get; set; }
public IEnumerable<SelectListItem> StateSelectList { get; set; }
// ...
    public string PersonFullName { set; set; }
}

در اینجا، View متناظری، قرار است نام کامل یک شخص را به علاوه یک سری اطلاعات اضافی که در domain model نیست، نمایش دهد. مثلا نمایش نام استان‌ها که نهایتا Id انتخابی آن قرار است در برنامه استفاده شود.
خلاصه علت وجودی ViewModel این موارد است:
الف) Model برنامه را مستقیما در معرض استفاده قرار ندهیم (عدم رعایت این نکته به مشکلات امنیتی حادی هم حین به روز رسانی اطلاعات ممکن است ختم ‌شود که پیشتر توضیح داده شد).
ب) فیلدهای نمایشی اضافی مورد نیاز یک View را داخل Model برنامه تعریف نکنیم (مثلا تعاریف عناصر یک دراپ داون لیست، جایش اینجا نیست. مدل فقط نیاز به Id عنصر انتخابی آن دارد).

با این توضیحات، اگر View به روز رسانی اطلاعات کلمه عبور کاربر، تنها به اطلاعات id آن کاربر و کلمه عبور او نیاز دارد، فقط باید همین اطلاعات را در اختیار View قرار داد و نه بیشتر:

namespace MvcApplication7.Models
{
public class UserViewModel
{
public int Id { set; get; }
public string Password { set; get; }
}
}

به این ترتیب دیگر خاصیت IsAdming اضافه‌ای وجود ندارد که بخواهد مورد حمله واقع شود.



استفاده از model binding برای آپلود فایل به سرور

برای آپلود فایل به سرور تنها کافی است یک اکشن متد به شکل زیر را تعریف کنیم. HttpPostedFileBase نیز یکی دیگر از model binderهای توکار ASP.NET MVC است:

[HttpGet]
public ActionResult Upload()
{
return View(); // Shows the upload page
}

[HttpPost]
public ActionResult Upload(System.Web.HttpPostedFileBase file)
{
string filename = Server.MapPath("~/files/somename.ext");
file.SaveAs(filename);
return RedirectToAction("Index");
}

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

@{
ViewBag.Title = "Upload";
}
<h2>
Upload</h2>
@using (Html.BeginForm(actionName: "Upload", controllerName: "User",
method: FormMethod.Post,
htmlAttributes: new { enctype = "multipart/form-data" }))
{
<text>Upload a photo:</text> <input type="file" name="photo" />
<input type="submit" value="Upload" />
}

اگر دقت کرده باشید در طراحی ASP.NET MVC از anonymously typed objects زیاد استفاده می‌شود. در اینجا هم برای معرفی enctype فرم آپلود، مورد استفاده قرار گرفته است. به عبارتی هر جایی که مشخص نبوده چه تعداد ویژگی یا کلا چه ویژگی‌ها و خاصیت‌هایی را می‌توان تنظیم کرد، اجازه تعریف آن‌ها را به صورت anonymously typed objects میسر کرده‌اند. یک نمونه دیگر آن در متد routes.MapRoute فایل Global.asax.cs است که پارامتر سوم دریافت مقدار پیش فرض‌ها نیز anonymously typed object است. یا نمونه دیگر آن‌را در همین قسمت در جایی که لینک delete را به فرم تبدیل کردیم مشاهده نمودید. مقدار routeValues هم یک anonymously typed object معرفی شد.



سفارشی سازی model binder پیش فرض ASP.NET MVC

در همین مثال فرض کنید تاریخ را به صورت شمسی از کاربر دریافت می‌کنیم. خاصیت تعریف شده هم DateTime میلادی است. به عبارتی model binder حین تبدیل رشته تاریخ شمسی دریافتی به تاریخ میلادی با شکست مواجه شده و نهایتا خاصیت this.ModelState.IsValid مقدارش false خواهد بود. برای حل این مشکل چکار باید کرد؟
برای این منظور باید نحوه پردازش یک نوع خاص را سفارشی کرد. ابتدا با پیاده سازی اینترفیس IModelBinder شروع می‌کنیم. توسط bindingContext.ValueProvider می‌توان به مقداری که کاربر وارد کرده در میانه راه دسترسی یافت. آن‌را تبدیل کرده و نمونه صحیح را بازگشت داد.
نمونه‌ای از این پیاده سازی را در ادامه ملاحظه می‌کنید:

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

namespace MvcApplication7.Binders
{
public class PersianDateModelBinder : IModelBinder
{

public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
var modelState = new ModelState { Value = valueResult };
object actualValue = null;
try
{
var parts = valueResult.AttemptedValue.Split('/'); //ex. 1391/1/19
if (parts.Length != 3) return null;
int year = int.Parse(parts[0]);
int month = int.Parse(parts[1]);
int day = int.Parse(parts[2]);
actualValue = new DateTime(year, month, day, new PersianCalendar());
}
catch (FormatException e)
{
modelState.Errors.Add(e);
}

bindingContext.ModelState.Add(bindingContext.ModelName, modelState);
return actualValue;
}
}
}

سپس برای معرفی PersianDateModelBinder جدید تنها کافی است سطر زیر را

ModelBinders.Binders.Add(typeof(DateTime), new PersianDateModelBinder());

به متد Application_Start قرار گرفته در فایل Global.asax.cs برنامه اضافه کرد. از این پس کاربران می‌توانند تاریخ‌ها را در برنامه شمسی وارد کنند و model binder بدون مشکل خواهد توانست اطلاعات ورودی را به معادل DateTime میلادی آن تبدیل کند و استفاده نماید.
تعریف مدل بایندر سفارشی در فایل Global.asax.cs آن‌را به صورت سراسری در تمام مدل‌ها و اکشن‌متدها فعال خواهد کرد. اگر نیاز بود تنها یک اکشن متد خاص از این مدل بایندر سفارشی استفاده کند می‌توان به روش زیر عمل کرد:

public ActionResult Create([ModelBinder(typeof(PersianDateModelBinder))] User user)

همچنین ویژگی ModelBinder را به یک کلاس هم می‌توان اعمال کرد:

[ModelBinder(typeof(PersianDateModelBinder))]
public class User
{


مطالب دوره‌ها
لیست ها و آرایه ها در #F
برای تعریف لیست در #F فقط کافیست از [] و برای جداسازی آیتم‌های موجود در لیست از عملگر :: (بخوانید cons) استفاده کنید. #F از لیست‌های خالی نیز پشتیبانی می‌کند. به مثال هایی از این دست توجه کنید

#1 let emptyList = []
#2 let oneItem = "one " :: []
#3 let twoItem = "one " :: "two " :: []
#1 تعریف یک لیست خالی
#2 تعریف یک لیست به همراه یک آیتم
#3 تعریف یک لیست به همراه دو آیتم
قبول دارم که دستورالعمل بالا برای مقدار دهی اولیه به لیست کمی طولانی و سخت است. برای همین می‌تونید از روش زیر هم استفاده کنید.
let shortHand = ["apples "; "pears"]
*کد بالا یک لیست با دو آیتم که از نوع رشته ای هستند تولید خواهد کرد.
می‌تونید از عملگر @ برای پیوستن دو لیست به هم نیز استفاده کنید.
let twoLists = ["one, "; "two, "] @ ["buckle "; "my "; "shoe "]

نکته : تمام آیتم‌های موجود در لیست باید از یک نوع باشند. بعنی امکان تعریف لیستی که دارای آیتم هایی با datatype‌های متفاوت باشد باعث تولید خطای کامپایلری می‌شود. اما اگر نیاز به لیستی دارید که باید چند datatype رو هم پوشش دهد می‌تونید از object‌ها استفاده کنید.
let objList = [box 1; box 2.0; box "three"]
در بالا یک لیست از object‌ها رو تعریف کرده ایم. فقط دقت کنید برای اینکه آیتم‌های موجود در لیست رو تبدیل به object کنیم از دستور box قبل از هر آیتم استفاده کردیم.

 در هنگام استفاده از عملگر‌ها @ و :: مقدار لیست تغییر نمی‌کند بلکه یک لیست جدید تولید خواهد شد.
#1 let one = ["one "]
#2 let two = "two " :: one
#3 let three = "three " :: two
#4 let rightWayRound = List.rev three

#5 let main() =
printfn "%A" one
printfn "%A" two
printfn "%A" three
printfn "%A" rightWayRound
#1 تعریف لیستی که دارای یک آیتم است.
#2 تعریف لیستی که دارای دو آیتم است(آیتم دوم لیست خود از نوع لیست است)
#3 تعریف لیستی که دارای سه آیتم است(ایتم دوم لیست خود از نوع لیستی است که دارای دو آیتم است)
# از تابع List.rev برای معکوس کردن آیتم‌های لیست three استفاده کردیم و مقادیر در لیستی به نام  rightWayRound قرار گرفت.
#5 تابع main برای چاپ اطلاعات لیست ها
بعد از اجرا خروجی زیر مشاهده می‌شود.
["one "]
["two "; "one "]
["three "; "two "; "one "]
["one "; "two "; "three "]
تفاوت بین لیست‌ها در #F و لیست و آرایه در دات نت(System.Collection.Generic)

 F#List
Net Array
 Net List
 #1 امکان تغییر در عناصر لیست
 NoYes
 Yes
 #2 امکان اضافه کردن عنصر جدید
 NoNo
 Yes
#3 جستجو
On
O1
O1
#1 در #F بعد از ساختن یک لیست امکان تغییر در مقادیر عناصر آن وجود ندارد.
#2 در #F بعد از ساختن یک لیست دیگه نمی‌تونید یک عنصر جدید به لیست اضافه کنید.
#3 جستجوی در لیست‌های #F به نسبت لیست‌ها و آرایه‌های در دات نت کند‌تر عمل می‌کند.

استفاده از عبارات در لیست ها
برای تعریف محدوده در لیست می‌تونیم به راحتی از روش زیر استفاده کنیم
let rangeList = [1..99]
برای ساخت لیست‌ها به صورت داینامیک استفاده از حلقه‌های تکرار در لیست مجاز است.
let dynamicList = [for x in 1..99 -> x*x]
کد بالا معادل کد زیر در #C  است.
for(int x=0;x<99 ; x++)
{
   myList.Add(x*x);
}
لیست‌ها و الگوی Matching
روش عادی برای کار با لیست‌ها در #F استفاده از الگوی Matching و توابع بازگشتی است.
let listOfList = [[2; 3; 5]; [7; 11; 13]; [17; 19; 23; 29]]

let rec concatList l =
match l with
| head :: tail -> head @ (concatList tail)
| [] -> []

let primes = concatList listOfList

printfn "%A" primes
در مثال بالا ابتدا یک لیست تعریف کردیم که دارای 3 آیتم است و هر آیتم آن خود یک لیست با سه آیتم است.(تمام آیتم‌ها از نوع داده عددی هستند). یک تابع بازگشتی برای پیمایش تمام آیتم‌های لیست نوشتم که در اون از الگوی Matching استفاده کردیم.
خروجی :
[2; 3; 5; 7; 11; 13; 17; 19; 23; 29]
ماژول لیست
در جدول زیر تعدادی از توابع ماژول لیست رو مشاهده می‌کنید.

 نام تابع
 توضیحات
 List.length  تابعی که طول لیست را برمی گرداند
 List.head  تابعی برای برگشت عنصر اول لیست
 List.tail  تمام عناصر لیست را بر میگرداند به جز عنصر اول
 List.init  یک لیست با توجه به تعداد آیتم ایجاد می‌کند و یم تابع را بر روی تک تک عناصر لیست ایجاد می‌کند.
 List.append  یک لیست را به عنوان ورودی دریافت می‌کند و به لیست مورد نظر اضافه می‌کند و مجموع دو لیست را برگشت می‌دهد
 List.filter  فقط عناصری را برگشت می‌دهد که شرط  مورد نظر بر روی آن‌ها مقدار true را برگشت دهد
 List.map  یک تابع مورد نظر را بر روی تک تک عناصر لیست اجرا می‌کند و لیست جدید را برگشت می‌دهد
 List.iter  یک تابع مورد نظر را بر روی تک تک عناصر لیست اجرا می‌کند  
 List.zip 
 مقادیر دو لیست را با هم تجمیع می‌کند و لیست جدید را برگشت می‌دهد. اگر طول 2 لیست ورودی یکی نباشد خطا رخ خواهدداد 
 List.unzip درست برعکس تابع بالا عمل می‌کند
 List.toArray  لیست را تبدیل به آرایه می‌کند
 List.ofArray آرایه را تبدیل به لیست می‌کند
مثال هایی از توابع بالا
 List.head [5; 4; 3]

List.tail [5; 4; 3]

List.map (fun x -> x*x) [1; 2; 3]

List.filter (fun x -> x % 3 = 0) [2; 3; 5; 7; 9]

Sequence Collection

seq در #F یک توالی از عناصری است که هم نوع باشند. عموما از sequence‌ها زمانی استفاده میکنیم که یک مجموعه از داده‌ها با تعداد زیاد و مرتب شده داشته باشیم ولی نیاز به استفاده از تمام عناصر آن نیست. کارایی sequence  در مجموعه‌های با تعداد زیاد از list‌ها به مراتب بهتر است. sequence‌‌ها را با تابع  seq می‌شناسند که معادل IEnumerable در دات نت است. بنابر این هر مجمو عه ای که IEnumerable رو در دات نت پیاده سازی کرده باشد در #F با seq قابل استفاده است.

مثال هایی از نحوه استفاده seq

#1 seq بامحدوده 1 تا 100 و توالی 10 
seq { 0 .. 10 .. 100 }
#2 استفاده از حلقه‌های تکرار برای تعریف محدوده و توالی در seq
seq { for i in 1 .. 10 do yield i * i }
#3 استفاده از <- به جای yield
seq { for i in 1 .. 10 -> i * i }
#4 استفاده از حلقه for به همراه شرط برای فیلتر کردن
let isprime n =
    let rec check i =
        i > n/2 || (n % i <> 0 && check (i + 1))
    check 2

let aSequence = seq { for n in 1..100 do if isprime n then yield n }
چگونگی استفاده از توابع seq
در این بخش به ارائه مثال هایی کاربردی‌تر از چگونگی استفاده از seq در #F می‌پردازیم. برای شروع نحوه ساخت یک seq خالی یا empty رو خواهم گفت.
let seqEmpty = Seq.empty
روش ساخت یک seq که فقط یک عنصر را برگشت می‌دهد.
let seqOne = Seq.singleton 10
برای ساختن یک seq همانند لیست‌ها می‌تونیم از seq.init استفاده کنیم. عدد 5 که بلافاصله بعد از تابع seq.init آمده است نشان دهنده تعداد آیتم‌ها موجود در seq خواهد بود. seq.iter هم یک تابع مورد نظر رو بر روی تک تک عناصر seq اجرا خواهد کرد.(همانند list.iter)
let seqFirst5MultiplesOf10 = Seq.init 5 (fun n -> n * 10)
Seq.iter (fun elem -> printf "%d " elem) seqFirst5MultiplesOf10
خروجی مثال بالا
0 10 20 30 40
با استفاده از توابع seq.ofArray , seq.ofList می‌تونیم seq مورد نظر خود را از لیست یا آرایه مورد نظر بسازیم.
let seqFromArray2 = [| 1 .. 10 |] |> Seq.ofArray 
البته این نکته رو هم یادآور بشم که به کمک عملیات تبدیل نوع(type casting) هم می‌تونیم آرایه رو به seq تبدیل کنیم. به صورت زیر
let seqFromArray1 = [| 1 .. 10 |] :> seq<int>
برای مشخص کردن اینکه آیا یک آیتم در seq موجود است یا نه می‌تونیم از seq.exists به صورت زیر استفاده کنیم.
let containsNumber number seq1 = Seq.exists (fun elem -> elem = number) seq1
let seq0to3 = seq {0 .. 3}
printfn "For sequence %A, contains zero is %b" seq0to3 (containsNumber 0 seq0to3)
اگر seq پاس داده شده به تابع exists خالی باشد یا یک ArgumentNullException متوقف خواهید شد.

برای جستجو و پیدا کردن یک آیتم در seq می‌تونیم از seq.find استفاده کنیم.
let isDivisibleBy number elem = elem % number = 0
let result = Seq.find (isDivisibleBy 5) [ 1 .. 100 ]
printfn "%d " result
دقت کنید که اگر هیچ آیتمی در sequence با  predicate مورد نظر پیدا نشود یک KeyNotFoundException رخ خواهد داد. در صورتی که مایل نباشید که استثنا رخ دهد می‌توانید از تابع seq.tryFind استفاده کنید. هم چنین خالی بودن sequence ورودی باعث ArgumentNullExceptionخواهد شد.

استفاده از lambda expression در توابع
lamdaExpressoion از توانایی‌ها مورد علاقه برنامه نویسان دات نت است و کمتر کسی است حاضر به استفاده از آن در کوئری‌های linq نباشد. در #F نیز می‌توانید از lambda Expression استفاده کنید. در ادامه به بررسی مثال هایی از این دست خواهیم پرداخت.

تابع skipWhile
همانند skipWhile در linq عمل می‌کند. یعنی یک predicate مورد نظر را بر روی تک تک عناصر یک لیست اجرا می‌کند و آیتم هایی که شرط برای آن‌ها true باشد نادیده گرفته میشوند و مابقی آیتم‌ها برگشت داده می‌شوند.
let mySeq = seq { for i in 1 .. 10 -> i*i }
let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn "" 
let mySeqSkipWhileLessThan10 = Seq.skipWhile (fun elem -> elem < 10) mySeq
mySeqSkipWhileLessThan10 |> printSeq
می‌بینید که predicate مورد نظر برای تابع skipWhile به صورت lambda expression است که با رنگ متفاوت نمایش داده شده است.(استفاده از کلمه fun).
 خروجی به صورت زیر است:
16 25 36 49 64 81 100
 برای بازگرداندن یک تعداد مشخص از آیتم‌های seq می‌تونید از توابع seq.take یا seq.truncate استفاده کنید. ابتدا باید تعداد مورد نظر و بعد لیست مورد نظر را به عنوان پارامتر مقدار دهی کنید.
مثال:
let mySeq = seq { for i in 1 .. 10 -> i*i }
let truncatedSeq = Seq.truncate 5 mySeq
let takenSeq = Seq.take 5 mySeq

let printSeq seq1 = Seq.iter (printf "%A ") seq1; printfn "" 
#1 truncatedSeq |> printSeq #3 takenSeq |> printSeq
خروجی
1 4 9 16 25 //truncate
1 4 9 16 25 //take

Tuples
tuples در #F به گروهی از مقادیر بی نام ولی مرتب شده که می‌توانند انواع متفاوت هم داشته باشند گفته می‌شود. ساختار کلی آن به صورت ( element , ... , element )  است که هر element خود می‌تواند یک عبارت نیز باشد.(مشابه کلاس Tuple در #C که به صورت generic استفاده می‌کنیم)
// Tuple of two integers.
( 1, 2 ) 

// Triple of strings.
( "one", "two", "three" ) 

// Tuple of unknown types.
( a, b ) 

// Tuple that has mixed types.
( "one", 1, 2.0 ) 

// Tuple of integer expressions.
( a + 1, b + 1)
نکات استفاده از tuple
#1 می‌تونیم از الگوی Matching برای دسترسی به عناصر tuple استفاده کنیم.
let print tuple1 =
   match tuple1 with
    | (a, b) -> printfn "Pair %A %A" a b
#2 میتونیم از let  برای تعربف الگوی tuple استفاده کنیم.
let (a, b) = (1, 2)
#3 توابع fst و snd مقادیر اول و دوم هر tuple رو بازگشت می‌دهند
let c = fst (1, 2) // return 1
let d = snd (1, 2)// return 2
#4 تابعی برای بازگشت عنصر سوم یک tuple وجود ندارد ولی این تابع رو با هم می‌نویسیم:
let third (_, _, c) = c
کاربرد tuple در کجاست
زمانی که یک تابع باید بیش از یک مقدار را بازگشت دهد از tuple‌ها استفاده می‌کنیم. برای مثال
let divRem a b = 
   let x = a / b
   let y = a % b
   (x, y)
خروجی تابع divRem از نوع tuple که دارای 2 مقدار است می‌باشد.
مطالب
C# 12.0 - Primary Constructors
قابلیتی تحت عنوان Primary Constructors به C# 12 اضافه شده‌است که ... البته جدید نیست! این قابلیت از زمان C# 9، با ارائه‌ی رکوردها، به زبان #C اضافه شد و در طی چند نگارش بعدی، توسعه و تکامل یافت (برای مثال اضافه شدن records for structs به C# 10) تا در C# 12، به کلاس‌های معمولی نیز تعمیم پیدا کرد. این ویژگی را در ادامه با جزئیات بیشتری بررسی می‌کنیم.


Primary Constructors چیست؟

Primary Constructors، قابلیتی است که به C# 12 اضافه شده‌است تا توسط آن بتوان خواص را مستقیما توسط پارامترهای سازنده‌ی یک کلاس تعریف و همچنین مقدار دهی کرد. هدف از آن، کاهش قابل ملاحظه‌ی یکسری کدهای تکراری و مشخص است تا به کلاس‌هایی زیباتر، کم‌حجم‌تر و خواناتر برسیم. برای مثال کلاس متداول زیر را درنظر بگیرید:
public class Employee
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public DateTime HireDate { get; set; }
    public decimal Salary { get; set; }

    public Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
    {
        FirstName = firstName;
        LastName = lastName;
        HireDate = hireDate;
        Salary = salary;
    }
}
در زبان ‍#C، سازنده، متد ویژه‌ای است که در حین ساخت نمونه‌ای از یک کلاس، فراخوانی می‌شود. هدف از آن‌، آغاز و مقدار دهی حالت شیء ایجاد شده‌است که عموما با مقدار دهی خواص آن شیء، انجام می‌شود.
اکنون اگر بخواهیم همین کلاس را با استفاده از ویژگی Primary Constructor اضافه شده به C# 12.0 بازنویسی کنیم، به قطعه کد زیر می‌رسیم:
public class Employee(string firstName, string lastName, DateTime hireDate, decimal salary)
{
    public string FirstName { get; set; } = firstName;
    public string LastName { get; set; } = lastName;
    public DateTime HireDate { get; set; } = hireDate;
    public decimal Salary { get; set; } = salary;
}
و نحوه‌ی نمونه سازی از آن به صورت زیر است:
var employee = new Employee("John", "Doe", new DateTime(2020, 1, 1), 50000);

یک نکته: اگر از Rider و یا ReSharper استفاده می‌کنید، یک چنین Refactoring توکاری جهت سهولت کار، به آن‌ها اضافه شده‌است و به سرعت می‌توان این تبدیلات را توسط آن‌ها انجام داد.




توضیحات:
- متد سازنده در این حالت، به ظاهر حذف شده و به قسمت تعریف کلاس منتقل شده‌است.
- تمام مقدار دهی‌های آغازین موجود در متد سازنده‌ی پیشین نیز حذف شده‌اند و مستقیما به قسمت تعریف خواص، منتقل شده‌اند.
در نتیجه از یک کلاس 15 سطری، به کلاسی 7 سطری رسیده‌ایم که کاهش حجم قابل ملاحظه‌ای را پیدا کرده‌است.

نکته 1: هیچ ضرورتی وجود ندارد که به همراه یک primary constructor، خواصی هم مانند مثال فوق ارائه شوند؛ چون پارامترهای آن در تمام اعضای این کلاس، به همین شکل، قابل دسترسی هستند. در این مثال صرفا جهت بازسازی کد قبلی، این خواص اضافی را مشاهده می‌کنید. یعنی اگر تنها قرار بود، کار تزریق وابستگی‌ها صورت گیرد که عموما به همراه تعریف فیلدهایی جهت انتساب پارامترهای متد سازنده به آن‌ها است، استفاده از یک primary constructor، کدهای فوق را بیش از این هم فشرده‌تر می‌کرد و ... یک سطری می‌شد.

نکته 2: استفاده از پارامترهای سازنده‌ی اولیه، صرفا جهت مقدار دهی خواص عمومی یک کلاس، یک code smell هستند! چون می‌توان یک چنین کارهایی را به نحو شکیل‌تری توسط required properties معرفی شده‌ی در C# 11، پیاده سازی کرد.


بررسی تاریخچه‌ی primary constructors

همانطور که در مقدمه‌ی بحث نیز عنوان شد، primary constructors قابلیت جدیدی نیست و برای نمونه به همراه C# 9 و مفهوم جدید رکوردهای آن، ارائه شد:
public record class Book(string Title, string Publisher);
مثال فوق که به positional syntax هم معروف است، به همراه بکارگیری primary constructors است. در اینجا کامپایلر به صورت خودکار، کار تولید کدهای خواص متناظر را که از نوع get و init دار هستند، انجام می‌دهد. در این حالت به علت استفاده از init accessors، پس از نمونه سازی شیءای از آن، دیگر نمی‌توان مقدار خواص متناظر را تغییر داد.
پس از آن در C# 10، این توسعه ادامه یافت و به امکان تعریف record structها، بسط یافت که در اینجا هم قابلیت تعریف primary constructors وجود دارد:
public record struct Color(int R, int G, int B);
که البته در این حالت برخلاف record classها، کامپایلر، کدی را که برای خواص تولید می‌کند، get و set دار است. در اینجا اگر نیاز است به همان حالت خواص get و init دار رسید، می‌توان یک readonly record struct را تعریف کرد.

پس از این مقدمات، اکنون در C# 12 نیز می‌توان primary constructors را به تمام کلاس‌ها و structهای معمولی هم اعمال کرد؛ با این تفاوت که در اینجا برخلاف رکوردها، کدهای خواص‌های متناظر، به صورت خودکار تولید نمی‌شوند و اگر به آن‌ها نیاز دارید، باید آن‌ها را همانند مثال ابتدای بحث، خودتان به صورت دستی تعریف کنید.


primary constructors کلاس‌ها و structهای معمولی، با primary constructors رکوردها یکی نیست

در C# 12 و به همراه معرفی primary constructors مخصوص کلاس‌ها و structهای معمولی آن، از روش متفاوتی برای دسترسی به پارامترهای تعریف شده، استفاده می‌کند که به آن capturing semantics هم می‌گویند. در این حالت پارامترهای تعریف شده‌ی در یک primary constructor، توسط هر عضوی از آن کلاس قابل استفاده‌است که یکی از کاربردهای آن، ساده کردن تعاریف تزریق وابستگی‌ها است. در این حالت دیگر نیازی نیست تا ابتدا یک فیلد را برای انتساب به پارامتر تزریق شده تعریف کرد و سپس از آن فیلد، استفاده نمود؛ مستقیما می‌توان با همان پارامتر تعریف شده، در متدها و اعضای کلاس، کار کرد.
برای مثال سرویس زیر را که از تزریق وابستگی‌ها، در سازنده‌ی خود استفاده می‌کند، درنظر بگیرید:
public class MyService
{
    private readonly IDepedent _dependent;
  
    public MyService(IDependent dependent)
    {
        _dependent = dependent;
    }
  
    public void Do() 
    {
        _dependent.DoWork();
    }
}
این کلاس در C# 12 به صورت زیر خلاصه شده و پارامتر dependent تعریف شده‌ی در سازنده‌ی اولیه‌ی آن، به همان شکل و بدون نیاز به کد اضافی، در سایر متدهای این کلاس قابل استفاده‌است:
public class MyService(IDependent dependent)
{
    public void Do() 
    {
        dependent.DoWork();
    }
}

البته مفهوم Captures هم در زبان #C جدید نیست و در ابتدا به همراه anonymous methods و بعدها به همراه lambda expressions، معرفی و بکار گرفته شد. برای مثال درون یک lambda expression، اگر از متغیری خارج از آن lambda expressions استفاده شود، کامپایلر یک capture از آن متغیر را تهیه کرده و استفاده می‌کند.

بنابراین به صورت خلاصه primary constructors در رکوردها، با هدف تعریف خواص عمومی فقط خواندنی، ارائه شدند؛ اما primary constructors ارائه شده‌ی در C# 12 که اینبار قابل اعمال به کلاس‌ها و structs معمولی است، بیشتر هدف ساده سازی تعریف کدهای تکراری private fields را دنبال می‌کند. برای نمونه این کدی است که کامپایلر برای primary constructor مثال ابتدای بحث تولید می‌کند و در اینجا نحوه‌ی تولید خودکار این فیلدهای خصوصی را مشاهده می‌کنید:
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace CS8Tests
{
  [NullableContext(1)]
  [Nullable(0)]
  public class Employee
  {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <FirstName>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string <LastName>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private DateTime <HireDate>k__BackingField;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private Decimal <Salary>k__BackingField;

    public Employee(string firstName, string lastName, DateTime hireDate, Decimal salary)
    {
      this.<FirstName>k__BackingField = firstName;
      this.<LastName>k__BackingField = lastName;
      this.<HireDate>k__BackingField = hireDate;
      this.<Salary>k__BackingField = salary;
      base..ctor();
    }

    public string FirstName
    {
      [CompilerGenerated] get
      {
        return this.<FirstName>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<FirstName>k__BackingField = value;
      }
    }

    public string LastName
    {
      [CompilerGenerated] get
      {
        return this.<LastName>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<LastName>k__BackingField = value;
      }
    }

    public DateTime HireDate
    {
      [CompilerGenerated] get
      {
        return this.<HireDate>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<HireDate>k__BackingField = value;
      }
    }

    public Decimal Salary
    {
      [CompilerGenerated] get
      {
        return this.<Salary>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<Salary>k__BackingField = value;
      }
    }
  }
}
بنابراین آیا پارامترهای سازنده‌ی اولیه، به صورت خواص تعریف می‌شوند و قابلیت تغییر میدان دید آن‌ها میسر است؟ پاسخ: خیر. این پارامترها توسط کامپایلر، به صورت فیلدهای خصوصی در سطح کلاس، تعریف و استفاده می‌شوند. یعنی تمام اعضای کلاس، البته منهای سازنده‌های ثانویه، به این پارامترها دسترسی دارند. همچنین، این تولید کد هم بهینه‌است و صرفا برای پارامترهایی انجام می‌شود که واقعا در کلاس استفاده شده باشند؛ درغیر اینصورت، فیلد خصوصی متناظری برای آن‌ها تولید نخواهد شد.

یک نکته: برای مشاهده‌ی یک چنین کدهایی می‌توانید از منوی Tools->IL Viewer برنامه‌ی Rider استفاده کرده و در برگه‌ی ظاهر شده، گزینه‌ی #Low-Level C آن‌را انتخاب نمائید.


امکان تعریف سازنده‌های دیگر، به همراه سازنده‌ی اولیه

اگر به کدهای #Low-Level C تولیدی فوق دقت کنید، این کلاس، به همراه یک سازنده‌ی خالی بدون پارامتر (parameter less constructor) نیست و سازنده‌ی پیش‌فرضی (default constructor) برای آن درنظر گرفته نشده‌است ... اما اگر کلاسی به همراه یک primary constructor تعریف شد، می‌توان با استفاده از واژه‌ی کلیدی this، سازنده‌ی ثانویه‌ای را هم برای آن تعریف کرد:
public class Person(string firstName, string lastName) 
{
    public Person() : this("John", "Smith") { }
    public Person(string firstName) : this(firstName, "Smith") { }
    public string FullName => $"{firstName} {lastName}";
}
در اینجا نحوه‌ی تعریف یک Default constructor بدون پارامتر را هم ملاحظه می‌کنید.


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

مثال زیر را درنظر بگیرید که در آن کلاس مشتق شده‌ی از کلاس User، یک سازنده‌ی اولیه را تعریف کرده:
public class User
{
    public User(string firstName, string lastName) { }
}

public class Editor(string firstName, string lastName) : User
{
}
در این حالت برنامه با خطای «Base class 'CS8Tests.User' does not contain parameterless constructor» کامپایل نمی‌شود. عنوان می‌کند که اگر کلاس مشتق شده می‌خواهد سازنده‌ی اولیه‌ای داشته باشد، باید کلاس پایه را به همراه یک سازنده‌ی پیش‌فرض بدون پارامتر تعریف کنید.
البته این محدودیت با structها وجود ندارد؛ چون structها، value type هستند و همواره به صورت پیش‌فرض، به همراه یک سازنده‌ی پیش فرض بدون پارامتر، تولید می‌شوند.
یک مثال: قطعه کد متداول ارث‌بری زیر را درنظر بگیرید که در آن، کلاس مشتق شده به کمک واژه‌ی کلید base، امکان تعریف سازنده‌ی جدیدی را یافته و یکی از پارامترهای سازنده‌ی کلاس پایه را مقدار دهی می‌کند:
public class Automobile
{
    public Automobile(int wheels, int seats)
    {
        Wheels = wheels;
        Seats = seats;
    }

    public int Wheels { get; }
    public int Seats { get; }
}

public class Car : Automobile
{
    public Car(int seats) : base(4, seats)
    {
    }
}
این تعاریف در C# 12 به صورت زیر خلاصه می‌شوند:
public class Automobile(int wheels, int seats)
{
    public int Wheels { get; } = wheels;
    public int Seats { get; } = seats;
}

public class Car(int seats) : Automobile(4, seats);

و یا یک نمونه مثال دیگر آن به صورت زیر است که در آن، ذکر بدنه‌ی کلاس در C# 12، الزامی ندارد:
public class MyBaseClass(string s); // no body required

public class Derived(int i, string s, bool b) : MyBaseClass(s)
{
    public int I { get; set; } = i;
    public string B => b.ToString();
}


توصیه به پرهیز از double capturing

با مفهوم capture در این مطلب آشنا شدیم. در مثال زیر دوبار از پارامتر سازنده‌ی age، در دو قسمت عمومی شده، استفاده شده‌است:
public class Human(int age)
{
    // initialization
    public int Age { get; set; } = age;

    // capture
    public string Bio => $"My age is {age}!";
}
در این حالت ممکن است استفاده کننده در طول برنامه، با وضعیت ناخواسته‌ی زیر مواجه شود:
var p = new Human(42);
Console.WriteLine(p.Age); // Output: 42
Console.WriteLine(p.Bio); // Output: My age is 42!

p.Age++;
Console.WriteLine(p.Age); // Output: 43
Console.WriteLine(p.Bio); // Output: My age is 42! // !
در اینجا پس از افزودن مقداری به خاصیت عمومی Age، زمانیکه به مقدار عبارت Bio مراجعه می‌شود، خروجی قبلی را دریافت می‌کنیم!
درک بهتر آن، نیاز به #Low-Level C کلاس Human را دارد:
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace CS8Tests
{
  [NullableContext(1)]
  [Nullable(0)]
  public class Human
  {
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <age>P;
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private int <Age>k__BackingField;

    public Human(int age)
    {
      this.<age>P = age;
      this.<Age>k__BackingField = this.<age>P;
      base..ctor();
    }

    public int Age
    {
      [CompilerGenerated] get
      {
        return this.<Age>k__BackingField;
      }
      [CompilerGenerated] set
      {
        this.<Age>k__BackingField = value;
      }
    }

    public string Bio
    {
      get
      {
        DefaultInterpolatedStringHandler interpolatedStringHandler = new DefaultInterpolatedStringHandler(11, 1);
        interpolatedStringHandler.AppendLiteral("My age is ");
        interpolatedStringHandler.AppendFormatted<int>(this.<age>P);
        interpolatedStringHandler.AppendLiteral("!");
        return interpolatedStringHandler.ToStringAndClear();
      }
    }
  }
}
همانطور که مشاهده می‌کنید، کامپایلر، پارامتر age را دوبار، جداگانه capture کرده‌است:
public Human(int age)
{
   this.<age>P = age;
   this.<Age>k__BackingField = this.<age>P;
   base..ctor();
}
به همین جهت است که ++p.Age، فقط بر روی یکی از فیلدهای capture شده تاثیر داشته و بر روی دیگری خیر. به این مورد، double capturing گفته می‌شود و توصیه شده از آن پرهیز کنید و بجای استفاده‌ی دوباره از پارامتر age، از خود خاصیت Age استفاده نمائید.