مطالب
کار با اسکنر در برنامه های تحت وب (قسمت دوم و آخر)

در قسمت قبل « کار با اسکنر در برنامه‌های تحت وب (قسمت اول) » دیدی از کاری که قرار است انجام دهیم، رسیدیم. حالا سراغ یک پروژه‌ی عملی و پیاده سازی مطالب مطرح شده می‌رویم.

ابتدا پروژه‌ی   WCF را شروع می‌کنیم. ویژوال استودیو را باز کرده و از قسمت New Project > Visual C# > WCF یک پروژه‌ی WCF Service Application جدید را مثلا با نام "WcfServiceScanner" ایجاد نمایید. پس از ایجاد، دو فایل IService1.cs و Service1.scv موجود را به IScannerService و ScannerService تغییر نام دهید. سپس ابتدا محتویات کلاس اینترفیس IScannerService را به صورت زیر تعریف نمایید :

    [ServiceContract]
    public interface IScannerService
    {
        [OperationContract]
        [WebInvoke(Method = "GET",
           BodyStyle = WebMessageBodyStyle.Wrapped,
           RequestFormat = WebMessageFormat.Json,
           ResponseFormat = WebMessageFormat.Json,
           UriTemplate = "GetScan")]
        string GetScan();
    }
در اینجا ما فقط اعلان متدهای مورد نیاز خود را ایجاد کرده‌ایم. علت استفاده از Attribute ایی با نام WebInvoke ، مشخص نمودن نوع خروجی به صورت Json است و همچنین عنوان آدرس مناسبی برای صدا زدن متد. پس از آن کلاس ScannerService را مطابق کدهای زیر تغییر دهید:
    public class ScannerService : IScannerService
    {
        public string GetScan()
        {
            // TODO Add code here
        }
    }
تا اینجا فقط یک WCF Service معمولی ساخته‌ایم .در ادامه به سراغ کلاس WIA برای ارتباط با اسکنر می‌رویم.
بر روی پروژه‌ی خود راست کلیک کرده و Add Reference را انتخاب نموده و سپس در قسمت COM، گزینه‌ی Microsoft Windows Image Acquisition Library v2.0 را به پروژه‌ی خود اضافه نمایید.
با اضافه شدن این ارجاع به پروژه، دسترسی به فضای نام WIA برای ما امکان پذیر می‌شود که ارجاعی از آن را در کلاس ScannerService قرار می‌دهیم.
using WIA;
اکنون متد GetScan را مطابق زیر اصلاح می‌نماییم:
        public string GetScan()
        {
            var imgResult = String.Empty;
            var dialog = new CommonDialogClass();
            try
            {
                // نمایش فرم پیشفرض اسکنر
                var image = dialog.ShowAcquireImage(WiaDeviceType.ScannerDeviceType);
                
                // ذخیره تصویر در یک فایل موقت
                var filename = Path.GetTempFileName();
                image.SaveFile(filename);
                var img = Image.FromFile(filename);

                // img جهت ارسال سمت کاربر و نمایش در تگ Base64 تبدیل تصویر به 
                imgResult = ImageHelper.ImageToBase64(img, ImageFormat.Jpeg);
            }
            catch
            {
                // از آنجاییه که امکان نمایش خطا وجود ندارد در صورت بروز خطا رشته خالی 
                // بازگردانده می‌شود که به معنای نبود تصویر می‌باشد
            }

            return imgResult;
        }
دقت داشته باشید که کدها را در زمان توسعه بین Try..Catch قرار ندهید چون ممکن‌است در این زمان به خطاهایی برخورد کنید که نیاز باشد در مرورگر آنها را دیده و رفع خطا نمایید.
CommonDialogClass  کلاس اصلی در اینجا جهت نمایش فرم کار با اسکنر می‌باشد و متد‌های مختلفی را جهت ارتباط با اسکنر در اختیار ما قرار می‌دهد که بسته به نیاز خود می‌توانید از آنها استفاده کنید. برای نمونه در مثال ما نیز متد اصلی که مورد استفاده قرار گرفته، ShowAcquireImage می‌باشد که این متد، فرم پیش فرض دریافت اسکنر را به کاربر نمایش می‌دهد و کاربر از طریق آن می‌تواند قبل از شروع اسکن، یکسری تنظیمات را انجام دهد.
این متد ابتدا به صورت خودکار فرم تعیین دستگاه اسکنر ورودی را نمایش داده :


و سپس فرم پیش فرض اسکنر‌های TWAIN را جهت تعیین تنظیمات اسکن نمایش می‌دهد که این امکان نیز در این فرم فراهم است تا دستگاه‌های Feeder یا Flated انتخاب گردند.

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

کدهای کلاس کمکی ImageHelper:

        public static string ImageToBase64(Image image, System.Drawing.Imaging.ImageFormat format)
        {
            if (image != null)
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    // Convert Image to byte[]
                    image.Save(ms, format);
                    byte[] imageBytes = ms.ToArray();

                    // Convert byte[] to Base64 String
                    string base64String = Convert.ToBase64String(imageBytes);
                    return base64String;
                }
            }
            return String.Empty;
        }
توجه داشته باشید که خروجی این متد قرار است توسط callBack یک متد جاوا اسکریپتی مورد استفاده قرار گرفته و احیانا عکس مورد نظر در صفحه نمایش داده شود. پس بهتر است که از قالب تصویر به شکل Base64 استفاده گردد. ضمن اینکه پلاگین‌های Jquery مرتبط با ویرایش تصویر هم از این قالب پشتیبانی می‌کنند. (اینجا )

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

کار ما تا اینجا در پروژه‌ی WCF Service تقریبا تمام است. اگر پروژه را یکبار Build نمایید برای اولین بار احتمالا پیغام خطاهای زیر ظاهر خواهند شد:


جهت رفع این خطا، در قسمت Reference‌های پروژه خود، WIA را انتخاب نموده و از Properties‌های آن خصوصیت Embed Interop Types را به False تغییر دهید؛ مشکل حل می‌شود.

به سراغ پروژه‌ی ویندوز فرم جهت هاست کردن این WCF سرویس می‌رویم. می‌توانید این سرویس را بر روی یک Console App یا Windows Service هم هاست کنید که در اینجا برای سادگی مثال، از WinForm استفاده می‌کنیم.
یک پروژه‌ی WinForm جدید را ایجاد کنید و سپس از قسمت Add Reference > Solution به مسیر پروژه‌ی قبلی رفته و dll‌‌های آن را به پروژه خود اضافه نمایید.
Form1.cs  را باز کرده و ابتدا دو متغیر زیر را در آن به صورت عمومی تعریف نمایید:
        private readonly Uri _baseAddress = new Uri("http://localhost:6019");
        private ServiceHost _host;
برای استفاده از کلاس ServiceHost لازم است تا ارجاعی به فضای نام System.ServiceModel داده شود. متغیر baseAddress_ نگه دارنده‌ی آدرس ثابت سرویس اسکنر در سمت کلاینت می‌باشد و به این ترتیب ما دقیقا می‌دانیم باید سرویس را با کدام آدرس در کدهای جاوا اسکریپتی خود فراخوانی نماییم.
حال در رویداد Form_Load برنامه، کدهای زیر را جهت هاست کردن سرویس اضافه می‌نماییم:
        private void Form1_Load(object sender, EventArgs e)
        {
            _host = new ServiceHost(typeof(WcfServiceScanner.ScannerService), _baseAddress);
            _host.Open();
        }

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            _host.Close();
        }
همین چند خط برای هاست کردن سرویس روی آدرس localhost و پورت 8010 کامپیوتر کلاینت کافی است. اما یکسری تنظیمات مربوط به خود سرویس هم وجود دارد که باید در زمان پیاده سازی سرویس، در خود پروژه‌ی سرویس، ایجاد می‌گردید. اما از آنجا که ما قرار است سرویس را در یک پروژه‌ی دیگر هاست کنیم، بنابراین این تنظیمات را باید در همین پروژه‌ی WinForm قرار دهیم.
فایل App.Config پروژه‌ی WinForm را باز کرده و کدهای آنرا مطابق زیر تغییر دهید:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup> 
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>

  <system.serviceModel>
    <behaviors>
      <serviceBehaviors>
        <behavior name="BehaviourMetaData">
          <serviceMetadata httpGetEnabled="true" />
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <services>
      <service name="WcfServiceScanner.ScannerService"
               behaviorConfiguration="BehaviourMetaData">
        <endpoint address=""
                  binding="basicHttpBinding"
                  contract="WcfServiceScanner.IScannerService" />
      </service>
    </services>
  </system.serviceModel>

</configuration>
اکنون پروژه‌ی هاست آماده اجرا می‌باشد. اگر آنرا اجرا کنید و در مرورگر خود آدرس ذکر شده را وارد کنید، صفحه‌ی زیر را مشاهده خواهد کرد که به معنی صحت اجرای سرویس اسکنر می‌باشد.

اگر موفق به اجرا نشدید و احیانا با خطای زیر مواجه شدید، اطمینان حاصل کنید که ویژوال استودیو Run as Administrator باشد. مشکل حل خواهد شد.


به سراغ پروژه‌ی بعدی، یعنی وب سایت خود می‌رویم. یک پروژه‌ی MVC جدید ایجاد نمایید و در View مورد نظر خود، کدهای زیر را جهت صدا زدن متد GetScan اضافه می‌کنیم.
( از آنجا که کدها به صورت جاوا اسکریپت می‌باشد، پس مهم نیست که حتما پروژه MVC باشد؛ یک صفحه‌ی HTML ساده هم کافی است).
<a href="#" id="get-scan">Get Scan</a>
<img src="" id="img-scanned" />
<script>
    $("#get-scan").click(function () {
        var url = 'http://localhost:6019/';
        $.get(url, function (data) {
            $("#img-scanned").attr("src","data:image/Jpeg;base64,  "+ data.GetScanResult);
        });
    });
</script>
دقت کنید در هنگام دریافت اطلاعات از سرویس، نتیجه به شکل GetScanResult خواهد بود. الان اگر پروژه را اجرا نمایید و روی لینک کلیک کنید، اسکنر شروع به دریافت اسکن خواهد کرد اما نتیجه‌ای بازگشت داده نخواهد شد و علت هم مشکل امنیتی CORS می‌باشد که به دلیل دریافت اطلاعات از یک دامین دیگر رخ می‌دهد و اگر با Firebug درخواست را بررسی کنید متوجه خطا به شکل زیر خواهید شد.


راه حل‌های زیادی برای این مشکل ارائه شده است، و متاسفانه بسیاری از آنها در شرایط پروژه‌ی ما جوابگو نمی‌باشد (به دلیل هاست روی یک پروژه ویندوزی). تنها راه حل مطمئن (تست شده) استفاده از یک کلاس سفارشی در پروژه‌ی WCF Service  می‌باشد که مثال آن در اینجا آورده شده است.
برای رفع مشکل به پروژه WcfServiceScanner بازگشته و کلاس جدیدی را به نام CORSSupport ایجاد کرده و کدهای زیر را به آن اضافه کنید:
    public class CORSSupport : IDispatchMessageInspector
    {
        Dictionary<string, string> requiredHeaders;
        public CORSSupport(Dictionary<string, string> headers)
        {
            requiredHeaders = headers ?? new Dictionary<string, string>();
        }

        public object AfterReceiveRequest(ref System.ServiceModel.Channels.Message request, System.ServiceModel.IClientChannel channel, System.ServiceModel.InstanceContext instanceContext)
        {
            var httpRequest = request.Properties["httpRequest"] as HttpRequestMessageProperty;
            if (httpRequest.Method.ToLower() == "options")
                instanceContext.Abort();
            return httpRequest;
        }

        public void BeforeSendReply(ref System.ServiceModel.Channels.Message reply, object correlationState)
        {
            var httpResponse = reply.Properties["httpResponse"] as HttpResponseMessageProperty;
            var httpRequest = correlationState as HttpRequestMessageProperty;

            foreach (var item in requiredHeaders)
            {
                httpResponse.Headers.Add(item.Key, item.Value);
            }
            var origin = httpRequest.Headers["origin"];
            if (origin != null)
                httpResponse.Headers.Add("Access-Control-Allow-Origin", origin);

            var method = httpRequest.Method;
            if (method.ToLower() == "options")
                httpResponse.StatusCode = System.Net.HttpStatusCode.NoContent;

        }
    }

    // Simply apply this attribute to a DataService-derived class to get
    // CORS support in that service
    [AttributeUsage(AttributeTargets.Class)]
    public class CORSSupportBehaviorAttribute : Attribute, IServiceBehavior
    {
        #region IServiceBehavior Members

        void IServiceBehavior.AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, BindingParameterCollection bindingParameters)
        {
        }

        void IServiceBehavior.ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            var requiredHeaders = new Dictionary<string, string>();

            //Chrome doesn't accept wildcards when authorization flag is true
            //requiredHeaders.Add("Access-Control-Allow-Origin", "*");
            requiredHeaders.Add("Access-Control-Request-Method", "POST,GET,PUT,DELETE,OPTIONS");
            requiredHeaders.Add("Access-Control-Allow-Headers", "Accept, Origin, Authorization, X-Requested-With,Content-Type");
            requiredHeaders.Add("Access-Control-Allow-Credentials", "true");
            foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
            {
                foreach (EndpointDispatcher ed in cd.Endpoints)
                {
                    ed.DispatchRuntime.MessageInspectors.Add(new CORSSupport(requiredHeaders));
                }
            }
        }

        void IServiceBehavior.Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
        }

        #endregion
    }
فضاهای نام لازم برای این کلاس به شرح زیر می‌باشد:
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.ServiceModel.Description;
using System.ServiceModel.Dispatcher;
کلاس ScannerService را باز کرده و آنرا به ویژگی
    [CORSSupportBehavior]
    public class ScannerService : IScannerService
    {
مزین نمایید.

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

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

طراحی مدل دیتابیس

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

مزایا:

  1. پیاده سازی آسان

معایب:

  1. در این روش با زیاد شدن هر زبان، تعداد ستون‌ها افزایش می‌یابد که باعث می‌شود طراحی مناسبی نداشته باشد.
  2. در ضمن این مورد باید توسط برنامه نویس مرتبا اضافه گردد یا اینکه برنامه نویس این امکان را در سیستم قرار دهد که مدیر سایت بتواند در پشت صحنه کوئری افزودن ستون را ایجاد کند که باید جدول مرتبا مورد alter گرفتن قرار بگیرد.
  3. ممکن است همیشه برای هر زبانی مطلبی قرار نگیرد و این مورد باعث می‌شود بی جهت فضایی برای آن در نظر گرفته شود.

پی نوشت: با اینکه امروزه بحث فیلدهای sparse Column وجود دارد ولی این فیلد‌ها در هر شرایطی مورد استفاده قرار نمی‌گیرند وبیشتر متعلق به زمانی است که می‌دانیم آن فیلد به شدت کم مورد استفاده قرار می‌گیرد.

پی نوشت دوم : در صورتی که فیلد شما مانند متن مقاله که عموما از نوع داده (varchar(max است استفاده می‌کنید و در صورتی که زبان مورد استفاده قرار نگیرد در خیلی از اوقات بی جهت فیلد‌های Blob ساخته اید که بهینه سازی آن را نیز باید در نظر بگیرید.


در مرحله‌ی بعد برای رفع مشکلات بالا یک جدول از زبان‌ها، مانند جدول زیر را ایجاد می‌کنیم:
 
 ID  کد
 Language  زبان
 ISO  کد دو رقمی آن زبان
 Flag  پرچم آن کشور
بعد از آن هر مقاله برای یک زبان ایجاد خواهد شد؛ چیزی مانند تصویر زیر:

مزایا:

  1. پیاده سازی آسان

معایب:

  1. ایجاد رکوردهای تکراری، هر مقاله برای بعضی از اطلاعاتش که چند زبانه نیستند داده‌های تکراری خواهد داشت.
  2. هر مقاله یک مقاله‌ی جدا شناخته می‌شود و ارتباطی میان آنان نخواهد بود. بدین ترتیب توانایی ایجاد گزارش‌هایی چون هر گروه از مقاله و دسته بندی آن‌ها از بین خواهد رفت. در ضمن مدیر عموما در یک سیستم مدیریتی می‌خواهد تنها یک لینک را به یک مقاله بدهد و سایت بنا به تشخیص در زبان مزبور، یکی از این مقالات را به کاربر نمایش دهد؛ نه اینکه مرتبا مدیر برای هر زبان، لینکی را مهیا کند و در این حالت چنین چیزی ممکن نخواهد بود.
  3. در یک سیستم فروشگاهی همانند تصویر بالا کار هم سخت‌تر می‌شود و هر رکورد، یک محصول جدا شناخته می‌شود و ویرایش‌ها هم برای هر کدام باید جداگانه صورت بگیرد که در عمل این طرح را رد می‌کند.


سومین راه حل این است که سه جدول ایجاد کنیم:

یک. جدول زبان‌ها (که بالاتر ایجاد شده بود)

دو . جدول نام مقاله به همراه اطلاعات پایه و فیلدها بی نیاز به چند زبانه بودن

سه : یک جدول که هر دو ستون آن کدهای کلید دو جدول بالا را دارند و فیلدهای چند زبانه در آن وجود دارند.

جدول پایه

 ID  کد
 Name  نام مقاله
 CreationDate  تاریخ ایجاد
 Writer  نویسنده
 Visibilty  وضعیت نمایش
 جدول مقالات
 LanguageCode کد زبان
 ArticleID  کد مقاله
 CreationDate  تاریخ ایجاد
 Visibility  وضعیت نمایش مقاله
Title
عنوان مقاله
ContentText
متن مقاله

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

مزایا:

  1. گزارش گیری آسان برای هر دسته مقاله با زبان‌های مختلف و ارتباط و یکپارچگی
  2. آسان در افزودن زبان.

معایب:

  1. ایجاد کوئری‌های پیچیده‌تر و جوین دار که به نسبت روش‌های قبلی کوئری‌ها پیچیده‌تر شده اند.
  2. کدنویسی زیادتر.

استفاده از ساختارهای XML یا JSON برای ذخیره سازی اطلاعات چند زبانه مانند ساختارهای زیر:

XML
<Articles>
<Article>
this is english text
</Article>
<Article>
این یک متن فارسی است
</Article>
</Articles>

یا 
<Articles>
<en-us>
this is english text
</en-us>
<fa-ir>
این یک متن فارسی است
</fa-ir>
</Articles>
JSON
"Articles":["en-us':{"title":"this is english text","content":" english content"},"fa-ir":{"title":"متن فارسی","content":"محتوای فارسی"}]
ازSQL Server 2005 به بعد از نوع داده xml پشتیبانی می‌شود و در نسخه‌ی 2016 آن نیز پشتیبانی از Json اضافه شده است که حتی شامل اندیکس‌های اختصاصی هم برای این دو نوع می‌باشد.
از مزایای این روش ذخیره‌ی همه داده‌ها در یک ستون و یک جدول است و نیازی به ستون‌های اضافه یا جداول اضافه نیست ولی معایب این روش استفاده از کوئری‌های پیچیده‌تر جهت ارتباط و خواندن است.

استفاده از بانک‌های اطلاعاتی NO SQL
در این بانک‌ها دیگر درگیر تعداد ستون‌ها و جنس آن‌ها نیستیم و میتوانیم برای هر مقاله یا محصول، هر تعداد زبان و یا فیلد را که می‌خواهیم، در نظر بگیریم و اضافه کنیم. برای آشنایی بیشتر با این نوع بانک‌ها و انواع آن، مقالات مربوط به nosql را در سایت دنبال کنید.

نکاتی که در یک سایت چند زبانه باید به آن‌ها توجه کرد.

یک . زبان آن صفحه را معرفی کنید: این کار هم به موتورهای جست و جو برای ثبت سایت شما کمک می‌کند و هم برای معلولین که از ابزارهای صفحه خوان استفاده می‌کنند، کمک بزرگی است. در این روش، صفحه خوان‌ها و دستگاه‌های خط بریل که زبان صفحه را تشخیص نمی‌دهند با خواندن کد زبان می‌توانند زبان صفحه را تشخیص دهند. با استفاده از خط زیر میتوانید زبان اصلی صفحه‌ی خود را تنظیم نمایید:
<html lang="en">

اگر از XHTML استفاده می‌کنید خاصیت زیر را فراموش نکنید. دریافت W3C Validation بدون آن امکان پذیر نخواهد بود.
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
با تغییر زبان هر صفحه، باید تنظیم زبان آن تغییر یابد:



دو. چند زبانه بودن صفحه: در بالا یاد گرفتیم که چگونه زبان اصلی صفحه را تنظیم کنیم، ولی گاهی اوقات صفحه به غیر از زبان اصلی، شامل زبان‌های دیگر هم می‌شود؛ مثل نقل قول‌ها یا موارد دیگر. برای این‌کار می‌توانید از خصوصیت lang که در اکثر تگ‌ها پشتیبانی می‌شود، استفاده کنید. مثال پایین یک نقل قول فرانسوی است که ما آن را به خصوصیت lang، جهت تایید زبانش مزین کرده‌ایم:
<blockquote lang=”fr”>

<p>Le plus grand faible des hommes, c'est l'amour qu'ils ont de la vie.</p>

</blockquote>

سه. لینک ها : اگر دارید در صفحه‌ای لینک به جایی می‌دهید که متفاوت از زبان شماست، حتما باید زبان صفحه یا سایت مقصد را مشخص کنید. مثلا لینک زیر برای صفحه‌ای است که از یک زبان غیر فرانسوی به یک صفحه‌ی با زبان فرانسوی هدایت می‌شود:
<a href="" hreflang="fr">French</a>

همچنین اگر متن لینک شما هم به زبان فرانسوی باشد خیلی خوب می‌شود که آن را هم بیان کنید و از خاصیت lang و هم hreflang همزمان استفاده کنید:
<a href="" hreflang="fr">Francais</a>

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

پنج. انکودینگ صفحه را مشخص کنید: برای اینکه نحوه‌ی رمزگذاری و رمزگشایی حروف و نمادها مشخص گردد، باید انکودینگ تنظیم شود و حتی برای بعضی از موتورهای جست و جو که ممکن است با وب سایت شما به مشکل بر بخورند. امروزه بیشتر از صفحات یونیکد استفاده می‌شود که سطح وسیعی از کاراکترها را پشتیبانی می‌کند.
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">

HTML5
<meta charset="UTF-8">

شش. اندازه‌ی فونت:
موقعی که یک سایت چند زبانه را طراحی می‌کنید این نکته خیلی مهم هست که بدانید اندازه فونت‌های زبان پیش فرض، برای باقی زبان‌ها مناسب نیستند. به عنوان مثال ممکن است اندازه فونتی برای زبان‌های انگلیسی، فرانسوی و آلمانی مناسب باشد ولی برای زبان‌های فارسی و عربی و چینی و ... مناسب نباشد و خواندن آن سخت شود. به همین جهت یکی از راه‌های حل این مشکل استفاده از قالب css است که وابسته به خصوصیت lang ای است که شما برای صفحه و هر المان یا تگی که از این خصوصیت استفاده می‌کند، تعیین کرده‌اید.
:lang(en) {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

:lang(zh) {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}

خط زیر تعیین میکند که از استایل اول استفاده شود:
<html lang="en">
و خط زیر تعیین می‌کند که از استایل دوم استفاده شود:
<html lang="zh">
البته این کد بالا در مرورگرهای فایرفاکس، اپرا و IE8 به بالا پاسخ می‌دهد. برای سایر مروگرها چون کروم و نسخه‌های پیشین IE باید از شیوه‌ی زیر بهره ببرید:
<body class="english"> or <body class="chinese">

و استایل:
.english {

font-size: 85%;

font-family: arial, verdana, sans-serif;

}

.chinese {

font-size: 125%;

font-family: helvetica, verdana, sans-serif;

}
در این شیوه برای تگ مربوطه یک کلاس با نام آن زبان ایجاد کرده که محتوای آن تنظیمات قلم آن زبان می‌باشد.

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

هشت : زمان را نیز تغییر دهید: یکی از مواردی که در کمتر سایت چند زبانه‌ای به چشم می‌خورد و به نظر بنده می‌تواند بسیار مهم باشد این است که time zone منطقه‌ی هر زبان را بدانید. به عنوان مثال برای مقاله‌ی خود، تاریخ ایجاد را به صورت UTC ذخیره کنید و سپس نمایش را بر اساس زبان یا حتی بهتر و دقیق‌تر از طریق IP کشور مربوطه به دست آورید. برای کاربران ثبت نام شده این تاریخ می‌تواند دقیق‌تر باشد همانند انجمن‌های وی بولتین.


شیوه‌های تشخیص زبان سایت

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

دومین راه، استفاده از IP کاربر مراجعه کننده است تا بر اساس آن زبان مورد نظر را انتخاب کنید.

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

پی نوشت: فراموش نگردد که امکان تغییر زبان همیشه برای کاربر مهیا باشد و طوری نباشد که کاربر در آینده نتواند زبان سایت را تغییر دهد؛ حتی اگر تشخیص خودکار سایت برای زبان فعال باشد.

پی نوشت: در روش‌های بالا بهتر است همان مرتبه‌ی اول اطلاعات را در یک کوکی ذخیره کنید تا مراحل پیگیری راحت‌تر و آسان‌تر شود.


پلاگین‌ها و ابزارهای مدیریت زبان

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

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

پلاگین بعدی International Telephone Input است که  پیاده سازی پلاگین بالا می‌باشد. برای مواردی مفید است که شما نیاز دارید کد تلفنی کشوری را انتخاب کنید.

در مقاله‌های زیر که در سایت جاری است در مورد Globalization و به خصوص استفاده از ریسورس‌ها مطالب خوبی بیان شده است:

قسمت بیست و دوم آموزش MVC که مبحث Globalization را دنبال می‌کند.

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

مطالب
آشنایی با Window Function ها در SQL Server بخش اول
Window Function‌ها برای اولین بار در نسخه SQL Server 2005 ارائه گردیدند، و در ورژن‌های جدیدتر SQL Server، به تعداد این فانکشنها افزوده شده است.

تعریف Window Function :
        معمولا از این نوع فانکشنها روی مجموعه ای از ROW‌های یک جدول، در جهت اعمال عملیاتهای محاسباتی ،ارزیابی داده ها، رتبه بندی و غیرو... استفاده می‌گردد، به بیان ساده‌تر بوسیله Window Function‌ها می‌توان، ROW‌های یک جدول را گروه بندی نمود. و روی گروه‌ها از توابع جمعی (Aggregate Functions ) استفاده کرد. این نوع فانکشنها از قابلیت و انعطاف پذیری زیادی برخوردار می‌باشند، و بوسیله آنها می‌توان نتایج (خروجی) بسیار مفیدی از Query ها، بدست آورد، معمولا از این نوع فانکشنها در            Data Mining (داده کاوی) و گزارشگیری‌ها استفاده می‌گردد. و آگاهی و روش استفاده از Window Function‌ها برای برنامه نویسان و DBA ها، می‌تواند بسیار مفید باشد.
مفهوم Window Function مطابق استاندارد ISO و ANSI می‌باشد، و دیتابیس هایی همچون Oracle،DB2،Sybase از آن پشتیبانی می‌نمایند.برای اطلاعات بیشتر می‌توانید به سایت‌های زیر مراجعه کنید:
کلمه "Window" در  Window Function، به مجموعه ROW هایی اشاره می‌کند، که محاسبات و ارزیابی و غیرو... روی آنها اعمال می‌گردد. 
Window Function‌ها برای ارائه قابلیت‌های خود، از Over Clause استفاده می‌کنند. اگر مقاله آشنایی با Row_Number،Rank،Dense_Rank،NTILE را مطالعه کرده باشید، می‌توان هریک از آنها را یک Window Function دانست.
برای شروع، به بررسی Over Clause می‌پردازیم، و Syntax آن به شرح ذیل می‌باشد:
OVER ( 
       [ <PARTITION BY clause> ]
       [ <ORDER BY clause> ] 
       [ <ROW or RANGE clause> ]
      )

<PARTITION BY clause> ::=
PARTITION BY value_expression , ... [ n ]

<ORDER BY clause> ::=
ORDER BY order_by_expression
    [ COLLATE collation_name ] 
    [ ASC | DESC ] 
    [ ,...n ]

<ROW or RANGE clause> ::=
{ ROWS | RANGE } <window frame extent>

<window frame extent> ::= 
{   <window frame preceding>
  | <window frame between>
}

<window frame between> ::= 
  BETWEEN <window frame bound> AND <window frame bound>

<window frame bound> ::= 
{   <window frame preceding>
  | <window frame following>
}

<window frame preceding> ::= 
{
    UNBOUNDED PRECEDING
  | <unsigned_value_specification> PRECEDING
  | CURRENT ROW
}

<window frame following> ::= 
{
    UNBOUNDED FOLLOWING
  | <unsigned_value_specification> FOLLOWING
  | CURRENT ROW
}

<unsigned value specification> ::= 
{  <unsigned integer literal> }
OVER دارای سه آرگومان اختیاری است که هر کدام را به تفصیل بررسی می‌کنیم:
1- PARTITION BY clause : بوسیله این پارامتر می‌توانیم Row‌های یک جدول را گروه بندی نماییم. این پارامتر یک  value_expression می پذیرد. یک Value_expression می‌تواند نام یک ستون ، یک Scalar Subquery ، Scalar Function و غیرو باشد.
2- ORDER BY clause : از نامش مشخص است و برای Sort استفاده می‌شود، و ویژگی‌های Order By در آن اعمال می‌گردد. به جز Offset.
3- ROW or RANGE clause :این پارامتر بیشتر برای محدود نمودن Row در یک Partition (گروه) مورد استفاده قرار می‌گیرد، به عنوان مثال نقطه شروع و پایان را می‌توان بوسیله پارامتر فوق تعیین نمود.
Row و Range نسبت به هم یک تفاوت عمده دارند،و آن این است که، اگر از ROW Clause استفاده نمایید، ارتباط ROW‌های قبلی یا بعدی، نسبت به Row جاری،بصورت فیزیکی (physical association ) سنجیده می‌شود، بطوریکه با استفاده از Range Clause ارتباط سطرهای قبلی و بعدی، نسبت به سطر جاری بصورت منطقی (logical association ) در نظر گرفته می‌شود. ممکن است درک این مطلب کمی سخت باشد، در ادامه با مثالهایی که بررسی می‌نماییم، براحتی تفاوت این دو را متوجه می‌شوید.
Row یا Range در قالب‌های متفاوتی مقدار می‌پذیرند، که هر کدام را بررسی می‌کنیم:
UNBOUNDED PRECEDING : بیانگر اولین سطر Partition می‌باشد. UNBOUNDED PRECEDING  فقط نقطه شروع را مشخص می‌نماید.
UNBOUNDED FOLLOWING : بیانگر آخرین سطر Partition می‌باشد. UNBOUNDED FOLLOWING فقط نقطه پایانی را مشخص می‌نماید.
CURRENT ROW : اولین سطر جاری یا آخرین سطر جاری را مشخص می‌نماید.
n PRECEDING یا unsigned value specification> PRECEDING> : تعداد سطر‌های قبل از سطر جاری را تعیین می‌کند، n یا <unsigned value specification>تعداد سطر‌های قبل از سطر جاری را تعیین می‌نماید. از n PRECEDING نمی توان برای Range استفاده نمود.  
n FOLLOWING یا unsigned value specification> FOLLOWING> : تعداد سطرهای بعد از سطر جاری را تعیین می‌کند، n یا<unsigned value specification> تعداد سطر  های بعد از سطر جاری را تعیین می‌نماید. از n FOLLOWING نمی توان برای Range استفاده نمود.
<BETWEEN <window frame bound > AND <window frame bound  : از چارچوب فوق برای Range و Row می‌توان استفاده نمود، و نقطه آغازین و نقطه پایانی توسط قالب فوق تعیین می‌گردد. نکته قابل توجه آن است که نقطه پایانی نمی‌تواند، کوچکتر از نقطه آغازین گردد.
در ادامه برای درک هرچه بیشتر تعاریف بیان شده، چندین مثال می‌زنیم و هر کدام را بررسی می‌نماییم:
در ابتدا Script زیر را اجرا نمایید، که شامل جدولی به نام Revenue (سود،درآمد) و درج چند درکورد در آن:
CREATE TABLE REVENUE
(
[DepartmentID] int,
[Revenue] int,
[Year] int
);
 
insert into REVENUE
values (1,10030,1998),(2,20000,1998),(3,40000,1998),
 (1,20000,1999),(2,60000,1999),(3,50000,1999),
 (1,40000,2000),(2,40000,2000),(3,60000,2000),
 (1,30000,2001),(2,30000,2001),(3,70000,2001)
 
مثال اول : می‌خواهیم براساس فیلد DepartmentID جدول Revenue را Partition بندی نماییم و از توابع جمعی AVG و SUM روی فیلد درآمد(Revenue) استفاده کنیم.
ابتدا Script زیر را اجرا می‌کنیم:
 select *,
 avg(Revenue) OVER (PARTITION by DepartmentID) as AverageRevenue,
 sum(Revenue) OVER (PARTITION by DepartmentID) as TotalRevenue
from REVENUE
order by departmentID, year;
خروجی بصورت زیر خواهد بود:

        مطابق شکل، جدول براساس  فیلد DepartmentID به سه Partition تقسیم شده است، و عملیات میانگین و جمع روی فیلد Revenue انجام شده است و عملیات Sort روی هرگروه بطور مستقل انجام گرفته است. چنین کاری را نمی‌توانستیم بوسیله Group By انجام دهیم.
مثال دوم : نحوه استفاده از ROWS PRECEDING،در این مثال قصد داریم عملیات جمع را روی فیلدRevenue انجام دهیم. بطوریکه جمع هر مقدار برابر است با سه مقدار قبلی + مقدار جاری:
 لطفا رکورد‌های زیر را به جدول فوق درج نمایید:
 insert into REVENUE
 values(1,90000,2002),(2,20000,2002),(3,80000,2002),
 (1,10300,2003),(2,1000,2003), (3,90000,2003),
 (1,10000,2004),(2,10000,2004),(3,10000,2004),
 (1,20000,2005),(2,20000,2005),(3,20000,2005),
 (1,40000,2006),(2,30000,2006),(3,30000,2006),
 (1,70000,2007),(2,40000,2007),(3,40000,2007),
 (1,50000,2008),(2,50000,2008),(3,50000,2008),
 (1,20000,2009),(2,60000,2009),(3,60000,2009),
 (1,30000,2010),(2,70000,2010),(3,70000,2010),
 (1,80000,2011),(2,80000,2011),(3,80000,2011),
 (1,10000,2012),(2,90000,2012),(3,90000,2012)
سپس Script زیر را اجرا می‌نماییم:
select Year, DepartmentID, Revenue,
sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
             ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
From REVENUE order by departmentID, year;
خروجی :

        در Script بالا، جدول را براساس فیلد DepartmentID گروه بندی می‌کنیم، که سه گروه ایجاد می‌شود، هر گروه را بطور مستقل، روی فیلد Year بصورت صعودی مرتب می‌نماییم. حال برای آنکه بتوانیم سیاست جمع، روی فیلد Revenue، را پیاده سازی نماییم ، قطعه کد زیر را در Script بالا اضافه کردیم.
ROWS BETWEEN 3 PRECEDING AND CURRENT ROW) as Prev3
     برای شرح چگونگی استفاده از PRECEDING،فقط به شرح گروه اول بسنده می‌کنیم. مقدار جمع فیلد Revenue سطر اول، که قبل از آن سطری وجود ندارد، برابر است با  مقدار خود، یعنی 10030، مقدار جمع فیلد Revenue سطر دوم برابر است با حاصل جمع مقدار فیلدRevenue سطر اول و دوم ، یعنی 30030 . این روند تا سطر چهار ادامه دارد، اما برای بدست آوردن مقدار جمع فیلدRevenue سطر پنجم، مقدار جمع فیلد Revenue سطر دوم،سوم،چهارم و پنجم در نظر گرفته می‌شود، و مقدار فیلدRevenue سطر اول در حاصل جمع در نظر گرفته نمی‌شود،بنابراین مقدار جمع فیلد Revenue سطر پنجم برابر است با 180000. در صورت مسئله گفته بودیم، مقدار جمع فیلد Revenue هر سطر جاری برابر است با حاصل جمع مقدارسطر جاری و مقادیر سه سطر ماقبل خود.

مثال سوم: نحوه استفاده از  ROWS FOLLOWING، این مثال عکس مثال دوم است، یعنی حاصل جمع مقدار فیلد Revenue هر سطر برابر است با حاصل جمع سطر جاری با سه سطر بعد از خود. بنابراین Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue,
 sum(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
              ROWS BETWEEN CURRENT ROW AND 3 FOLLOWING) as Next3
From REVENUE order by departmentID, year;
خروجی :

مطابق شکل مقدار جمع فیلد اول برابراست با حاصل جمع مقدار سطر جاری و سه سطر بعد از آن.
نکته ای که در مثالهای دوم و سوم،می بایست به آن توجه نمود، این است که در زمان استفاده از Row یا Range ، استفاده از Order by در Partition الزامی است، در غیر این صورت با خطا مواجه می‌شوید.


نحوه استفاده از UNBOUNDED PRECEDING ، این امکان در T-SQL Server 2012 افزوده شده است. 
مثال چهار: در این مثال می‌خواهیم کمترین سود بدست آمده در چند سال را بدست آوریم:
ابتدا Script زیر را اجرا نمایید:
select Year, DepartmentID, Revenue,
       min(Revenue) OVER (PARTITION by DepartmentID ORDER BY [YEAR]
                    ROWS UNBOUNDED PRECEDING) as MinRevenueToDate
From REVENUE order by departmentID, year;
خروجی:

طبق تعریف UNBOUNDED PRECEDING اولین سطر هر Partition را مشخص می‌نماید، و چون از PRECEDING استفاده کرده ایم، بنابراین مقایسه همیشه بین سطر جاری و  سطر‌های قبل از آن انجام می‌پذیرد. بنابراین خواهیم داشت، کمترین مقدار فیلد Revenue در سطر اول، برابر با مقدار خود می‌باشد، چون هیچ سطری ماقبل از آن وجود ندارد. در سطر دوم مقایسه کمترین مقدار، بین 20000 و 10030 انجام می‌گیرد، که برابر است با 10030، در سطر سوم، مقایسه بین مقادیر سطر اول،دوم و سطر سوم صورت می‌گیرد، یعنی کمترین مقدار بین 40000،20000 و 10030، بنابراین کمترین مقدار سطر سوم برابر است با 10030. 
به بیان ساده‌تر برای بدست آوردن کمترین مقدار هر سطر، مقدار سطر جاری با مقادیر همه سطرهای ماقبل خود مقایسه می‌گردد.
برای بدست آوردن کمترین مقدار در سطر ششم، مقایسه بین مقادیر سطر‌های اول،دوم،سوم،چهارم،پنجم و ششم صورت می‌گیرد که عدد 10000 بدست می‌آید و الی آخر...
نکنه: اگر در Over Clause شرط Order by را اعمال نماییم، اما از Row یا Range استفاده نکنیم، SQL Server بصورت پیش فرض از قالب زیر استفاده می‌نماید:
RANGE UNBOUNDED PRECEDING AND CURRENT ROW 
برای روشن‌تر شدن مطلب فوق مثالی می‌زنیم:
ابتدا Script زیر را اجرا نمایید، که شامل ایجاد یک جدول و درج چند رکورد در آن می‌باشد:
CREATE TABLE Employees (  
    EmployeeId INT IDENTITY PRIMARY KEY,  
    Name VARCHAR(50),  
    HireDate DATE NOT NULL,  
    Salary INT NOT NULL  
)  
GO  
INSERT INTO Employees (Name, HireDate, Salary)  
VALUES   
    ('Alice', '2011-01-01', 20000),  
    ('Brent', '2011-01-15', 19000),  
    ('Carlos', '2011-02-01', 22000),  
    ('Donna', '2011-03-01', 25000),  
    ('Evan', '2011-04-01', 18500)  
GO  
سپس Script زیر را اجرا نمایید:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate) AS avgSalary  
FROM Employees  
GO 
خروجی :

حال اگر Script زیر را نیز اجرا نمایید، خروجی آن مطابق شکل بالا خواهد بود:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate 
                 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS avgSalary  
FROM Employees  
GO
توضیح درباره Script بالا، در این روش برای بدست آوردن میانگین هر سطر، مقدار سطر جاری با مقادیر سطر‌های ماقبل خود جمع و تقسیم بر تعداد سطر می‌شود.
سطر دوم 20000 + 19000 تقسیم بر دو برابر است با 19500
میانگین سطر پنجم، حاصل جمع فیلد Salary همه مقادیر سطرها تقسیم بر 5 
*** اگر بخواهید بوسیله Over Clause ، میانگین همه سطر‌ها یکسان باشد می‌توانید از Script زیر استفاده نمایید:
SELECT  
    Name,   
    Salary,   
    AVG(Salary) OVER(ORDER BY HireDate   
                        RANGE   
                        BETWEEN UNBOUNDED PRECEDING   
                        AND UNBOUNDED FOLLOWING  
                    ) AS avgSalary  
FROM Employees  
GO  
خروجی :

منظور از  ROWS BETWEEN  UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING  یعنی در محاسبه میانگین برای هر سطر تمامی مقادیر سطر‌های دیگر در نظر گرفته شود.
پایان بخش اول
امیدوارم مفید واقع شده باشد.
مطالب
کوئری نویسی در EF Core - قسمت سوم - جوین نویسی
پس از آشنایی با نوشتن یک سری کوئری‌های ساده در EF Core، در این قسمت به نحوه‌ی گزارشگیری از اطلاعات چندین جدول مرتبط به هم توسط Joinها خواهیم پرداخت.

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

چگونه می‌توان زمان‌های شروع رزروهای کاربری به نام «David Farrell» را یافت؟


همانطور که در دیاگرام فوق مشاهده می‌کنید، به ازای هر ID کاربری در جدول کاربران، به دنبال ردیف‌هایی در جدول Bookings هستیم که این ID در آن‌ها درج شده‌است. اما ... در EF-Core برخلاف SQL نویسی معمولی، ما کاری به ذکر قسمت اتصالی ON [Bookings].[MemId] = [Members].[MemId] نداریم. همینقدر که در کوئری نوشته شده به یک سر دیگر رابطه و خاصیت راهبری (navigation property) دیگری اشاره شود، خود EF-Core جوینی را به صورت خودکار تشکیل خواهد داد و شرط یاد شده را نیز برقرار می‌کند.
در قسمت اول این سری، در حین طراحی موجودیت کاربر، برای تشکیل سر دیگر رابطه‌ی one-to-many آن، به جدول Bookings، خاصیت Member را نیز که بیانگر کلید خارجی به جدول کاربران است، اضافه کردیم:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

        public int MemId { set; get; }
        public virtual Member Member { set; get; }

       // ...
    }
}
خاصیت عددی MemId، کلید خارجی است که در بانک اطلاعاتی رابطه‌ای ثبت خواهد شد و خاصیت Member، خاصیت راهبری است که جوین نویسی به جدول کاربران را بدون ذکر صریح جوین میسر می‌کند:
var startTimes = context.Bookings
                        .Where(booking => booking.Member.FirstName == "David"
                                            && booking.Member.Surname == "Farrell")
                        .Select(booking => new { booking.StartTime })
                        .ToList();
در این کوئری همینقدر که در قسمت Where آن booking.Member ذکر شده، جوینی به جدول کاربران را به صورت خودکار تشکیل می‌دهد:




مثال 2: یافتن زمان‌های شروع به رزرو شدن یک امکان خاص در مجموعه.
لیست زمان‌های شروع به رزرو شدن زمین(های) تنیس را برای روز 2012-09-21 تولید کنید. خروجی آن باید به همراه ستون‌های StartTime, FacilityName باشد.

طراحی موجودیت Booking، به همراه یک کلید خارجی به Facility نیز هست:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

        public int FacId { set; get; }
        public virtual Facility Facility { set; get; }

       // ...
    }
}
خاصیت عددی FacId، کلید خارجی Facility است که در بانک اطلاعاتی رابطه‌ای ثبت خواهد شد و خاصیت Facility، خاصیت راهبری است که جوین نویسی به جدول Facilities را بدون ذکر صریح جوین میسر می‌کند:
int[] tennisCourts = { 0, 1 };
var date1 = new DateTime(2012, 09, 21);
var date2 = new DateTime(2012, 09, 22);
var startTimes = context.Bookings
                        .Where(booking => tennisCourts.Contains(booking.Facility.FacId)
                                && booking.StartTime >= date1
                                && booking.StartTime < date2)
                        .Select(booking => new { booking.StartTime, booking.Facility.Name })
                        .ToList();
- زمین‌های تنیس این مجموعه، دارای دو Id مساوی 0 و 1 هستند که در اینجا به صورت صریحی مشخص شده‌اند تا مانند مثال 6 قسمت قبل عمل شود. روش دیگر یافتن آن‌ها می‌تواند مانند مثال 5 قسمت قبل باشد که به صورت «Name.Contains("Tennis")» نوشته شد.
- در قسمت Where این کوئری چون booking.Facility ذکر شده، سبب ایجاد جوین خودکاری به جدول Facilities خواهد شد.
- علت استفاده‌ی از دو تاریخ در اینجا برای یافتن اطلاعات تنها یک روز، ثبت زمان، به همراه تاریخ رزرو است. ستون تاریخ شروع، به صورت «2012-09-21 18:00:00.0000000» مقدار دهی شده‌است و نه به صورت «2012-09-21». البته در EF-Core راه دیگری هم برای حل این مساله وجود دارد. هر خاصیت از نوع DateTime، به همراه خاصیت Date نیز هست. برای مثال اگر بجای booking.StartTime نوشته شود booking.StartTime.Date (به خاصیت Date اضافه شده دقت کنید)، کد SQL حاصل، به همراه «CONVERT(date, [b].[StartTime])» خواهد بود که سبب حذف خودکار قسمت زمان این ستون می‌شود.



مثال 3: تولید لیست کاربرانی که کاربر دیگری را توصیه کرده‌اند.

چگونه می‌توان لیست کاربرانی را یافت که کاربر دیگری را توصیه کرده‌اند؟ این لیست نباید به همراه ردیف‌های تکراری باشد و همچنین باید بر اساس surname, firstname مرتب شود.

در اینجا به مفهوم جوین کردن یک جدول با خودش رسیده‌ایم. جدول کاربران، یک جدول خود ارجاع دهنده‌است:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Member> Children { get; set; }
        public virtual Member Recommender { set; get; }
        public int? RecommendedBy { set; get; }

       // ...
    }
}
که در اینجا RecommendedBy، یک کلید خارجی نال پذیر است که به Id همین جدول اشاره می‌کند. دو خاصیت دیگر تعریف شده، مکمل این خاصیت عددی، جهت سهولت کوئری نویسی‌های EF-Core هستند. برای مثال اگر در کوئری Recommender != null ذکر شود، سبب تشکیل جوینی به همین جدول شده و لیست کاربرانی را ارائه می‌دهد که کاربر دیگری را توصیه کرده‌اند:
var members = context.Members
                        .Where(member => member.Recommender != null)
                        .Select(member => new { member.Recommender.FirstName, member.Recommender.Surname })
                        .Distinct()
                        .OrderBy(member => member.Surname).ThenBy(member => member.FirstName)
                        .ToList();
وجود Distinct سبب بازگشت ردیف‌هایی غیرتکراری می‌شود (چون دو خاصیت نام و نام خانوادگی انتخاب شده‌اند، ردیف غیرتکراری، ردیفی خواهد بود که هر دوی این ستون‌ها در آن وجود نداشته باشد) و روش مرتب سازی بر اساس دو خاصیت را نیز مشاهده می‌کنید. در اینجا نباید دوبار OrderBy را پشت سر هم ذکر کرد. بار اول OrderBy است و بار دوم ThenBy تعریف می‌شود:



مثال 4: تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها.

چگونه می‌توان لیست کاربران را به همراه توصیه کننده‌ی آن‌ها تولید کرد؟ این لیست باید بر اساس surname, firstname مرتب شود.
var members = context.Members
                        .Select(member => new
                        {
                            memFName = member.FirstName,
                            memSName = member.Surname,
                            recFName = member.Recommender.FirstName ?? "",
                            recSName = member.Recommender.Surname ?? ""
                        })
                        .OrderBy(member => member.memSName).ThenBy(member => member.memFName)
                        .ToList();
در اینجا نیز می‌توان با ذکر member.Recommender سبب تولید یک جوین خودکار شد. همچنین همانطور که در مثال 7 قسمت قبل نیز بررسی کردیم، می‌توان بر روی خواص ذکر شده‌ی در Select، محاسباتی را نیز انجام داد. برای مثال در اینجا بجای درج مقدار null برای کاربرانی که کاربر دیگری را توصیه نکرده‌اند، ترجیح داده‌ایم که یک رشته‌ی خالی بازگشت داده شود که به صورت «COALESCE ([m0].[FirstName], N'')» ترجمه می‌شود:


همانطور که ملاحظه می‌کنید، نوع جوین خودکار تشکیل شده، Left join است و دیگر مانند جوین‌های مثال‌های ابتدای بحث، inner join نیست. در inner join، جدول سمت راست و چپ بر اساس شرط ON آن‌ها با هم مقایسه شده و ردیف‌های کاملا تطابق یافته‌ای بازگشت داده می‌شوند. کار Left join نیز مشابه است، با این تفاوت که در اینجا ممکن است برای جدول سمت چپ، هیچ ردیف تطابق یافته‌ای در جدول سمت راست وجود نداشته باشد (نوع آن بر اساس نال پذیری خاصیت RecommendedBy تشخیص داده شده‌است)؛ برای مثال یک کاربر ممکن است توسط کاربر دیگری توصیه نشده باشد (و RecommendedBy او نال باشد)، اما علاقمندیم که نام او در لیست نهایی حضور داشته باشد و حذف نشود.


یک نکته: در SQL Server تفاوتی بین left join و left outer join وجود ندارد و ذکر واژه‌ی کلیدی outer کاملا اختیاری است. جدول موارد مشابهی در SQL Server که به یک معنا هستند، صورت زیر است:
A LEFT JOIN B            A LEFT OUTER JOIN B
A RIGHT JOIN B           A RIGHT OUTER JOIN B
A FULL JOIN B            A FULL OUTER JOIN B
A INNER JOIN B           A JOIN B


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

چگونه می‌توان لیست کاربرانی را تولید کرد که از زمین(های) تنیس استفاده کرده‌اند؟ خروجی این گزارش باید به همراه یک ستون جمع نام و نام خانوادگی و ستون نام زمین باشد. این گزارش نباید دارای ردیف‌های تکراری باشد و همچنین باید بر اساس حاصل جمع نام و نام خانوادگی، مرتب شده باشد.

جدول Bookings به همراه دو کلید خارجی به جداول Facilities و Members است:
namespace EFCorePgExercises.Entities
{
    public class Booking
    {
       // ...

        public int FacId { set; get; }
        public virtual Facility Facility { set; get; }

        public int MemId { set; get; }
        public virtual Member Member { set; get; }

       // ...
    }
}
بنابراین برای تولید گزارشی که اطلاعات هر دوی این‌ها را به همراه دارد (اطلاعات کاربر و اطلاعات امکاناتی که استفاده کرده)، نیاز است دو جوین به دو جدول یاد شده نوشته شود. برای اینکار نیاز است در کوئری خود به booking.Member و booking.Facility برسیم. به همین جهت از جدول کاربران که دارای خاصیت از نوع ICollection  اشاره کننده‌ی به Bookings کاربران است شروع می‌کنیم:
namespace EFCorePgExercises.Entities
{
    public class Member
    {
       // ...

        public virtual ICollection<Booking> Bookings { set; get; }
    }
}
سپس بر روی این خاصیت مجموعه‌ای، اینبار یک SelectMany را فراخوانی می‌کنیم تا خروجی آن، تک تک رکوردهای booking متناظر باشد. اکنون که به هر رکورد booking کاربران دسترسی یافته‌ایم، می‌توانیم از طریق خواص راهبری booking.Member و booking.Facility هر ردیف، اطلاعات نهایی گزارش را تولید کنیم:
int[] tennisCourts = { 0, 1 };
var members = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => tennisCourts.Contains(booking.Facility.FacId))
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name
                        })
                        .Distinct()
                        .OrderBy(x => x.Member)
                        .ToList();
ID زمین‌های تنیس مشخص هستند که توسط tennisCourts.Contains به FacId‌های موجود اعمال شده‌اند. همچنین در قسمت Select نیز خاصیت Member آن به جمع دو خاصیت از booking.Member اشاره می‌کند و چون نتیجه‌ی حاصل یک ستون از پیش تعریف شده نیست، نیاز است تا برای آن نام صریحی انتخاب شود.
پس از آن برای حذف ردیف‌های تکراری حاصل از گزارش، از متد Distinct استفاده شده و OrderBy نیز بر اساس خاصیت جدید Member، قابل تعریف است:



مثال 6: تولید لیست رزروهای گران قیمت

لیست رزروهای روز 2012-09-14 را تولید کنید که هزینه‌ی آن‌ها بیشتر از 30 دلار باشد. باید بخاطر داشت که هزینه‌های کاربران با مهمان‌ها متفاوت است و هزینه‌ها بر اساس Slotهای نیم ساعته محاسبه می‌شوند و ID کاربر مهمان همیشه صفر است. خروجی  این گزارش باید به همراه نام کامل کاربر، نام امکانات مورد استفاده و هزینه‌ی نهایی باشد. همچنین باید بر اساس هزینه‌های نهایی به صورت نزولی مرتب شود.
var date1 = new DateTime(2012, 09, 14);
var date2 = new DateTime(2012, 09, 15);

var items = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2
                        && (
                            (((booking.Slots * booking.Facility.GuestCost) > 30) && (booking.MemId == 0)) ||
                            (((booking.Slots * booking.Facility.MemberCost) > 30) && (booking.MemId != 0))
                        ))
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name,
                            Cost = booking.MemId == 0 ?
                                        booking.Slots * booking.Facility.GuestCost
                                        : booking.Slots * booking.Facility.MemberCost
                        })
                        .Distinct()
                        .OrderByDescending(x => x.Cost)
                        .ToList();
در اینجا نیز چون نیاز است خروجی نهایی به همراه نام کاربر و نام امکانات مورد استفاده باشد، همانند مثال قبلی، به حداقل دو جوین نیاز است. به همین جهت از جدول Members به همراه SelectMany بر روی تک تک Bookings آن شروع می‌کنیم.
سپس بر اساس صفر بودن یا نبودن booking.MemId  (کاربر مهمان بودن یا خیر)، شرط هزینه‌ی بیشتر از 30 دلار اعمال شده‌است.
در آخر Select گزارش مورد نیاز، به همراه جمع نام و نام خانوادگی، نام امکانات استفاده شده و خاصیت محاسباتی Cost است که بر اساس مهمان بودن یا نبودن کاربر، متفاوت است.
متد Distinct ردیف‌های تکراری حاصل از این گزارش را حذف می‌کند (محل درج آن مهم است) و متد OrderByDescending، مرتب سازی نزولی بر اساس خاصیت محاسباتی Cost را انجام می‌دهد.



مثال 7: تولید لیست کاربران به همراه توصیه کننده‌ی آن‌ها، بدون استفاده از جوین.

در اینجا می‌خواهیم همان مثال 4 را بدون استفاده از جوین بررسی کنیم. بدون استفاده از جوین در اینجا به معنای استفاده از sub-query است (نوشتن یک کوئری داخل کوئری اصلی).
var members = context.Members
                        .Select(member =>
                        new
                        {
                            Member = member.FirstName + " " + member.Surname,
                            Recommender = context.Members
                                .Where(recommender => recommender.MemId == member.RecommendedBy)
                                .Select(recommender => recommender.FirstName + " " + recommender.Surname)
                                .FirstOrDefault() ?? ""
                        })
                        .Distinct()
                        .OrderBy(member => member.Member)
                        .ToList();
این کوئری به صورت متداولی بر روی جدول Members اعمال شده‌است، با این تفاوت که در حین Select نهایی آن، یکبار دیگر کوئری جدید شروع شده‌ی با context.Members را مشاهده می‌کنید که سبب تولید یک sub-query، زمانیکه ToList نهایی فراخوانی می‌شود، خواهد شد. این sub-query در حقیقت یک outer join را با ذکر recommender.MemId == member.RecommendedBy (بیان صریح روش اتصال ID‌های دو سر رابطه) شبیه سازی می‌کند.



مثال 8: تولید لیست رزروهای گران قیمت با استفاده از یک sub-query.

هدف از این مثال، ارائه‌ی روش حل دیگری برای مثال 6، به نحو تمیزتری است. در مثال 6، هزینه‌ی رزرو را دوبار، یکبار در متد Where و یکبار در متد Select محاسبه کردیم. اینبار می‌خواهیم با استفاده از sub-query‌ها این محاسبه را یکبار انجام دهیم.
var date1 = new DateTime(2012, 09, 14);
var date2 = new DateTime(2012, 09, 15);

var items = context.Members
                        .SelectMany(x => x.Bookings)
                        .Where(booking => booking.StartTime >= date1 && booking.StartTime < date2)
                        .Select(booking => new
                        {
                            Member = booking.Member.FirstName + " " + booking.Member.Surname,
                            Facility = booking.Facility.Name,
                            Cost = booking.MemId == 0 ?
                                        booking.Slots * booking.Facility.GuestCost
                                        : booking.Slots * booking.Facility.MemberCost
                        })
                        .Where(x => x.Cost > 30)
                        .Distinct()
                        .OrderByDescending(x => x.Cost)
                        .ToList();
اینبار یک Select نوشته شده که در آن Cost، در ابتدا محاسبه شده و سپس Where دومی ذکر شده که از این Cost استفاده می‌کند.
هرچند کوئری SQL نهایی تولید شده‌ی توسط EF-Core آن، تفاوتی چندانی با نگارش قبلی ندارد:



کدهای کامل این قسمت را در اینجا می‌توانید مشاهده کنید.
مطالب
مبانی TypeScript؛ تهیه فایل‌های تعاریف نوع‌ها
فایل‌های تعاریف نوع‌ها (Type Definitions) امکان استفاده‌ی ساده‌تر از انواع و اقسام کتابخانه‌های جاوا اسکریپتی موجود را فراهم می‌کنند. این فایل‌ها حاوی تعاریف نوع‌های استفاده شده‌ی در کتابخانه‌های جاوا اسکریپتی هستند که بر اساس TypeScript تهیه نشده‌اند. حاوی هیچ نوع پیاده سازی نیستند و تنها از اینترفیس‌هایی تشکیل می‌شوند که راهنمای کامپایلر TypeScript جهت بررسی نوع‌ها هستند و همچنین به عنوان راهنمای ادیتورهای TypeScript جهت ارائه‌ی Intellisense کاملتر و دقیق‌تری نیز می‌توانند بکار روند. به آن‌ها TypeScript wrapper for JavaScript libraries هم می‌گویند. این فایل‌ها دارای پسوند d.ts. هستند.


منابع یافتن فایل‌های تعاریف نوع‌ها

- بزرگترین مخزن کد فایل‌های تعاریف نوع‌های TypeScript، در سایت Github و در مخزن کد DefinitelyTyped قابل مشاهده است:
https://github.com/DefinitelyTyped/DefinitelyTyped
- همچنین ابزار دیگری به نام «Typings type definition manager» نیز می‌تواند برای این منظور بکار رود.
- علاوه بر این‌ها، بسته‌های npm نیز می‌توانند به همراه تعاریف فایل‌های .d.ts باشند.


مفهوم Ambient Modules

پروژه‌های TypeScript عموما به همراه تعداد زیادی ماژول هستند. به این ترتیب هر ماژول نیاز به d.ts. فایل مخصوص خودش خواهد داشت که نگهداری آن‌ها مشکل خواهد بود. به همین جهت یک Solution متشکل از تعدادی ماژول، می‌تواند تمام تعاریف نوع‌ها را در یک تک فایل d.ts. نگهداری کند که به آن Ambient Module نیز می‌گویند. برای نمونه فایل d.ts. ذیل را درنظر بگیرید:
 // cardCatalog.d.ts
declare module "CardCatalog"{
   export function printCard(callNumber: string): void;
}
در اینجا نحوه‌ی تعریف یک module از نوع ambient را مشاهده می‌کنید که تنها حاوی تعاریف export شده‌است؛ بدون به همراه داشتن پیاده سازی آن‌ها.
سپس برای استفاده‌ی از این فایل d.ts. خواهیم داشت:
 // app.ts
/// <reference path="cardCatalog.d.ts" />
import * as catalog from "CardCatalog";
چون فایل‌های d.ts. دارای پیاده سازی‌های مرتبط نیستند، کار import آن‌ها همانند سایر ماژول‌ها نخواهد بود. ابتدا نیاز است با استفاده از Triple-Slash Directives به ابتدای ماژول فعلی الحاق شوند (مانند مثال فوق). سپس سطر import آن مانند قبل است؛ با این تفاوت که مسیر فایل ماژول را به همراه ندارد و بجای آن نام ماژولی که در فایل d.ts. ذکر شده‌است، تعریف می‌شود.


بررسی مخرن DefinitelyTyped

DefinitelyTyped مخزن کد عظیمی از فایل‌های تعاریف نوع‌های TypeScript است. هرچند دریافت این فایل‌ها از مخزن کد Github آن مانند سایر فایل‌های متداول آن سایت، اما چندین روش دیگر نیز برای کار با این مخزن کد وجود دارد:
- استفاده از NuGet. تقریبا تمام فایل‌های d.ts. آن به صورت یک بسته‌ی نیوگت مجزا نیز وجود دارند.
- استفاده از برنامه‌ی tsd. این برنامه یا type definition manager، به صورت اختصاصی برای کار با این نوع فایل‌ها طراحی شده‌است.
- استفاده از برنامه‌ی typings. این برنامه نیز یک type definition manager دیگر است. مزیت آن کار با چندین منبع مجزای ارائه‌ی فایل‌های d.ts. است که DefinitelyTyped تنها یکی از آن‌ها است.


یک مثال: دریافت مستقیم و افزودن فایل d.ts. مربوط به کتابخانه‌ی جاوا اسکریپتی lodash از مخزن کد DefinitelyTyped

در ادامه قصد داریم فایل تعاریف نوع‌های کتابخانه‌ی معروف lodash را به پروژه‌ی جدیدی در VSCode اضافه کنیم. قدم اول، نصب خود کتابخانه است؛ از این جهت که فایل‌های d.ts.، فاقد هرگونه پیاده سازی هستند.
در مطلب «چرا TypeScript» نحوه‌ی کار با npm را جهت به روز رسانی کامپایلر TypeScript پیش فرض VSCode ملاحظه کردید. در اینجا نیز از npm برای نصب lodash استفاده می‌کنیم:
ابتدا خط فرمان را گشوده و سپس به پوشه‌ی پروژه‌ی خود وارد شوید. سپس دو دستور ذیل را صادر کنید:
npm init -f
npm install lodash --save


در ادامه به مخزن کد DefinitelyTyped وارد شده و پوشه‌ی مربوط به lodash را با جستجو پیدا کنید:
https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/lodash
در این پوشه تنها به فایل lodash.d.ts آن نیاز است. روی لینک این فایل کلیک کرده و سپس در صفحه‌ی باز شده، بر روی دکمه‌ی raw کلیک نمائید. این فایل نهایی را در ریشه‌ی پروژه‌ی جاری ذخیره کنید.
https://github.com/DefinitelyTyped/DefinitelyTyped/raw/master/lodash/lodash.d.ts

اگر به انتهای فایل lodash.d.ts دقت کنید، تعریف ambient module آن چنین شکلی را دارد و export آن lo dash است:
declare module "lodash" {
   export = _;
}
در ادامه برای استفاده‌ی از آن در فایل test.ts، به ابتدای فایل، با استفاده از Triple-Slash Directive، تعریف فایل d.ts. را اضافه کنید:
 /// <reference path="lodash.d.ts" />
سپس جهت دریافت یکجای تمام امکانات این کتابخانه خواهیم داشت:
 import * as _ from "lodash";
و اکنون بلافاصله intellisense به همراه مشخص بودن نوع پارامترهای یک متد فراهم است:
 let snakeCaseTitle = _.snakeCase("test this");
console.log(snakeCaseTitle);


برای گرفتن خروجی از این مثال همانند قبل، ابتدا Ctrl+Shift+P را فشرده و سپس انتخاب tasks:Run build task< و در ادامه فشردن F5 برای اجرا برنامه، نیاز است صورت گیرند:



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

tsd یک برنامه‌ی خط فرمان است که کار یافتن و دریافت فایل‌های d.ts. را ساده می‌کند. این برنامه منحصرا با مخزن کد DefinitelyTyped کار می‌کند و پس از دریافت هر فایل d.ts.، ارجاعی به آن‌را در فایل tsd.json در ریشه‌ی پروژه ذخیره می‌کند. همچنین یک تک فایل tsd.d.ts حاوی تعاریف Triple-Slash Directive‌ها را نیز تولید می‌کند که در ادامه می‌توان تنها این فایل را به فایل‌های مدنظر الحاق کرد.
البته باید دقت داشت که این برنامه در ابتدای سال 2016 منسوخ شده اعلام گردید و با برنامه‌ی typings جایگزین شده‌است؛ هرچند هنوز هم مفید است و قابل استفاده.
روش دریافت tsd را در سایت definitelytyped.org می‌توانید مشاهده کنید:
http://definitelytyped.org/tsd
نصب آن نیز به صورت یک بسته‌ی npm است:
 npm install tsd -g
توضیحات بیشتر در مورد نحوه‌ی استفاده‌ی از tsd را در مخزن کد آن می‌توانید مشاهده کنید:
https://github.com/Definitelytyped/tsd#readme

برای مثال برای نصب فایل تعاریف نوع‌های lodash، ابتدا به پوشه‌ی پروژه از طریق خط فرمان وارد شده و سپس دستور ذیل را صادر کنید:
 D:\Prog\1395\VSCodeTypeScript>tsd install lodash --save
البته اگر موفق به اجرای این دستور نشدید؛ با خطای ذیل
 [ERR!] Error: connect ECONNREFUSED 10.10.34.36:443
به این معنا است که آدرس فایل‌های raw در github در ایران فیلتر شده‌است و قابل دسترسی نیست (آدرس IP فوق رنج خصوصی است).
اگر موفق به اجرای این دستور شدید، پوشه‌ی جدید typings در ریشه‌ی پروژه ایجاد خواهد شد. داخل آن فایل tsd.d.ts را نیز می‌توان مشاهده کرد که حاوی تعاریف فایل‌های نوع‌های دریافت شده‌است. از این پس در ابتدای فایل‌های ts، بجای تعریف جداگانه‌ی این فایل‌ها، تنها می‌توان نوشت:
 /// <reference path="./typings/tsd.d.ts" />
این تک فایل، reference pathهای تک تک فایل‌های نصب شده‌ی توسط tsd را به همراه دارد.


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

برنامه‌ی typings نیز بسیار شبیه به برنامه‌ی tsd است؛ با این تفاوت که منابع آن منحصر به مخزن کد definitelytyped نیست.
مخزن کد این برنامه در گیت‌هاب قرار دارد: https://github.com/typings/typings
و نصب آن با استفاده از دستور ذیل است:
 npm install typings --global
و اینبار دستور tsd قسمت قبل به نحو ذیل تغییر می‌کند:
 typings install lodash --ambient --save
این مورد نیز قابل استفاده نیست؛ چون به نظر تنها مرجع lodash در حال حاضر github است و آدرس https://raw.githubusercontent.com در ایران فیلتر شده‌است:
 typings ERR! caused by Unable to connect to "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/299b5caa22876ef27dc8e9a5b7fd7bf93457b6f4/lodash/lodash-3.10.d.ts"
typings ERR! caused by connect ECONNREFUSED 10.10.34.36:443
اگر موفق به نصب این بسته شدید، اکنون پوشه‌ی جدیدی به نام typings در ریشه‌ی سایت ایجاد شده‌است. داخل این پوشه علاوه بر فایل‌های دریافت شده، دو فایل browser.d.ts و main.d.ts را نیز می‌توان مشاهده کرد. فایل browser آن مخصوص برنامه‌های سمت کلاینت است و فایل main آن جهت برنامه‌های NodeJS طراحی شده‌است (که البته در مثال ما هر دو فایل حاوی یک محتوا هستند). این فایل‌ها حاوی تعاریف reference pathهای به فایل‌های نوع‌های نصب شده هستند. بنابراین ابتدای هر فایل ts می‌توان نوشت:
 /// <reference path="./typings/main.d.ts" />
مطالب
آموزش TypeScript #2
در این پست قصد دارم به بررسی چند نکته که از پیش نیاز‌های کار با TypeScript  است بپردازم. همان طور که در پست قبلی مشاهده شد بعد از دانلود و نصب افزونه  TypeScript در VS.Net یک Template به نام Html Application With TypeScript به Installed Template اضافه خواهد شد. بعد از انتخاب این قسمت شما به راحتی می‌توانید در هر فایل با پسوند ts کد‌های مورد نظر به زبان TypeScript را نوشته و بعد از build پروژه این کد‌ها تبدیل به کد‌های JavaScript خواهند شد. بعد کافیست فایل مورد نظر را با استفاده از تک Script در فایل خود رفرنس دهید. دقت کنید که پسوند فایل حتما باید js باشد(به دلیل اینکه بعد از build پروژه فایل‌های ts تبدیل به js می‌شوند).
برای مثال:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="utf-8" />
    <title>TypeScript HTML App</title>
    <link rel="stylesheet" href="app.css" type="text/css" />
    <script src="app.js"></script>
</head>
<body>
    <h1>Number Type in TypeScript</h1>
    <div id="content"/>
</body>
</html>

اما اگر یک پروژه وب نظیر Asp.Net MVC داشته باشیم و می‌خواهیم یک یا چند فایل که حاوی کد‌های typeScript هستند را به این پروژه اضافه کرده و از آن‌ها در صفخات وب خود استفاده کنیم باید به این صورت عمل نمود:
بعد از اضافه کردن فایل‌های مورد نیاز، پروژه مورد نظر را Unload کنید. بعد به صورت زیر فایل پروژه(csproj) رو با یک ویرایشگر متنی باز کنید:

در این مرحله باید دو قسمت اضافه شود. یک بخش  ItemGroup است که هر فایلی که در پروژه شما دارای پسوند ts است باید در این جا تعریف شود. در واقع این قسمت فایل هایی را که باید کامپایل شده تا در نهایت تبدیل به فایل‌های JavaScript شوند را مشخص می‌کند.
بخش دوم target است که مراحل Build پروژه را برای این فایل‌های مشخص شده تعیین می‌کند. برای مثال:

همان طور که می‌بینید در قسمت ItemGroup تمام فایل‌های با پسوند ts در پروژه include شده اند. در قسمت target دستور کامپایل این فایل‌ها تعیین شد. اما نکته مهم این است که TypeScript به صورت پیش فرض از ECMAScript 3 در هنگام کامپایل کد‌ها استفاده می‌کند.(ECMAScript 3 در سال 1999 منتشر شد و تقریبا با تمام مرورگرها سازگاری دارد اما از امکانات جدید در Javascript پشتیبانی نمی‌کند). اگر قصد دارید که از ECMAScript 5 در هنگام کامپایل کد‌ها استفاده نمایید کافیست دستور زیر را اضافه نمایید:
--target ES5
مثال:

اما به این نکته دقت داشته باشید که ECMAScript 5 در سال 2009 منتشر شده است در نتیجه فقط با مرورگرهای جدید سازگار خواهد بود و ممکن است کد‌های شما در مرورگرهای قدیمی با مشکل مواجه شود.
مرورگرهایی که از ECMAScript 5 پشتیبانی می‌کنند عبارتند از:
  • IE 9 و نسخه‌های بعد از آن؛
  • FireFox 4 و نسخه‌های بعد از آن؛ 
  • Opera 12 و نسخه‌های بعد از آن؛ 
  • Safari 5.1 و نسخه‌های بعد از آن؛ 
  • Chrome 7 و نسخه‌های بعد از آن.
ادامه دارد...
مطالب
کار با Docker بر روی ویندوز - قسمت دوم - نصب Docker
پس از آشنایی با مفهوم Containers، در این قسمت قصد داریم برنامه‌ی تقریبا 500 مگابایتی Docker for Windows Installer.exe را نصب کنیم.
 
پیش‌نیازهای نصب Docker بر روی ویندوز

مطابق مستندات آن، برای نصب داکر بر روی ویندوز به حداقل‌های زیر نیاز است:
- استفاده از ویندوز 10 نگارش enterprise، که شماره نگارش آن حداقل 1607 باشد (حداقل Anniversary Update باشد).
- مجازی سازی در BIOS فعال شده باشد.
البته مجازی سازی عموما به صورت پیش‌فرض فعال است. برای بررسی آن، taskmanager ویندوز را اجرا کرده و در برگه‌ی Performance آن، جائیکه مشخصات CPU را نمایش می‌دهد، یک سطر به Virtualization اختصاص دارد که مقدار آن باید enabled باشد (تصویر زیر) و اگر نیست، برای فعال کردن آن باید به تنظیمات BIOS سیستم خود مراجعه کنید:


روش دیگر دریافت این اطلاعات، اجرای دستور systeminfo در خط فرمان، با دسترسی مدیریتی است. در خروجی آن، عبارت «Virtualization Enabled In Firmware» را جستجو کنید که باید مقدار آن yes باشد.

- داشتن CPU با قابلیت SLAT یا Second Level Address Translation.
برای یافتن این موضوع، برنامه‌ی coreinfo را دریافت کرده و آن‌را به صورت coreinfo -v اجرا کنید. خروجی آن سه سطر مرتبط با مجازی سازی را به همراه دارد. اگر قابلیتی موجود نباشد، جلوی آن یک خط تیره و اگر قابلیتی موجود باشد، روبروی آن یک ستاره را مشاهده خواهید کرد.

روش دیگر بررسی آن، اجرای دستور msinfo32 در قسمت run ویندوز و سپس enter است. در قسمت system summary، اطلاعات Second Level Address Translation قابل مشاهده هستند (اگر No باشد، امکان اجرای containerهای لینوکسی را بر روی ویندوز نخواهید داشت):


- داشتن حداقل 4 گیگابایت RAM.
- فعال بودن Hyper-V نیز برای اجرای Linux Containers بر روی ویندوز، ضروری است (نصاب Docker، این‌کار را به صورت خودکار انجام می‌دهد).


دریافت نصاب Docker for Windows

برای دریافت نصاب داکر مخصوص ویندوز، به آدرس زیر مراجعه کنید:
https://store.docker.com/editions/community/docker-ce-desktop-windows
که بلافاصله با تصویر کریه زیر مواجه خواهید شد:


برای رفع این مشکل، می‌توان از روش مطرح شده‌ی در مطلب «یک روش ساده برای دور زدن تحریم‌ها!» استفاده کرد؛ یعنی تنظیم DNS به 178.22.122.100 به صورت زیر:


پس از این تغییر، چون IP قابل مشاهده‌ی سیستم شما توسط سایت داکر تغییر می‌کند، اینبار صفحه‌ی دریافت Docker Community Edition for Windows به صورت زیر ظاهر می‌شود:


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


نصب Docker for Windows

پس از اجرای نصاب آن و پایان عملیات نصب (که تنها کافی است در صفحه‌ی ابتدایی آن تیک مربوط به Windows Containers را نیز قرار دهید)، نیاز دارد تا شما را یکبار از سیستم Logout و login کند. پس از ورود به سیستم، تنظیمات ابتدایی آن به صورت خودکار صورت گرفته و در صورت فعال نبودن Hyper-V، پیام زیر را مشاهده خواهید کرد:


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

یک نکته: کاری که در قسمت فعالسازی Hyper-V به صورت خودکار انجام می‌شود، شامل اجرای سه دستور زیر، در کنسول پاور شل، با دسترسی مدیریتی و سپس ری استارت سیستم است:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V -All -Verbose
Enable-WindowsOptionalFeature -Online -FeatureName Containers -All -Verbose
bcdedit /set hypervisorlaunchtype Auto
پس از آن، خط فرمان را باز کرده و با ستفاده از docker CLI نصب شده، دستور docker info را صادر کنید، تا بتوانید مشخصات نگارش نصب شده را مشاهده نمائید.
C:\Users\Vahid>docker info
Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 0
Server Version: 18.06.1-ce
OSType: windows
OSType را در صورتیکه سیستم شما توانمندی‌های سخت افزاری لازم را داشته باشد، می‌توان به Linux نیز تغییر داد.


بررسی تنظیمات سوئیچ کردن بین Linux Containers و Windows Containers

پس از اتمام ری‌استارت‌ها، برای آزمایش فعال بودن Hyper-V، در قسمت Run ویندوز، عبارت Virtmgmt.msc را نوشته و enter کنید. اگر تصویر زیر را مشاهده نمی‌کنید:


یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Linux containers را انتخاب کنید تا پس از مدتی، آیکن MobyLinuxVM در قسمت virtual machines (تصویر فوق) ظاهر شود.


اگر پس از انتخاب این گزینه، پیام زیر را دریافت کردید:


و یا اگر بر روی این ماشین مجازی کلیک راست کردید و گزینه‌ی Start آن‌را انتخاب کردید و پیام زیر ظاهر شد:


قسمت «پیش‌نیازهای نصب Docker بر روی ویندوز» را با دقت بررسی کنید (خصوصا قسمت BIOS و SLAT). نبود یکی از موارد ذکر شده، سبب بروز این مشکل می‌شود.
برای مثال اجرای دستور coreinfo -v بر روی سیستم من چنین خروجی را به همراه دارد:
E:\>coreinfo -v

AuthenticAMD
Microcode signature: 00000000
HYPERVISOR      -       Hypervisor is present
SVM             *       Supports AMD hardware-assisted virtualization
NP              -       Supports AMD nested page tables (SLAT)
روبروی HYPERVISOR و همچنین SLAT یک - را قرار داده‌است. یعنی این موارد یا پشتیبانی نمی‌شوند و یا در BIOS فعال نشده‌اند.
همانطور که مشاهده می‌کنید، قابلیت SLAT در CPU این سیستم وجود ندارد. به همین جهت نمی‌توان به Linux containers سوئیچ کرد. هرچند windows containers آن کار می‌کند.

روش دیگر مشاهده‌ی این خطا، مراجعه‌ی به event viewer ویندوز است. در قسمت خطاهای سیستم، ممکن است خطای زیر را مشاهده کنید:
Hypervisor launch failed; Second Level Address Translation is required to launch the hypervisor.


آزمایش Docker نصب شده

پس از نصب docker، خط فرمان ویندوز را گشوده و دستور زیر را صادر کنید:
docker run hello-world
اگر از قسمت قبل به‌خاطر داشته باشید، hello-world در اینجا نام یک image است. البته چون این image بر روی سیستم ما موجود نیست، این دستور، ابتدا آن‌را از docker hub دریافت کرده و سپس اجرا می‌کند. بنابراین می‌شد ابتدا دستور pull را صادر کرد و سپس run. اما دستور run قادر است هر دو عمل را با هم انجام دهد.

یک نکته: این image، یک image لینوکسی است. به همین جهت پیش از اجرای این دستور، همانطور که پیشتر نیز عنوان شد، یکبار بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Linux containers را انتخاب کنید. سپس دستور docker run hello-world را اجرا نمائید.

و یا در همین حال دستور docker run -p 80:80 nginx را صادر کنید تا وب سرور لینوکسی nginx را بتوانید تحت ویندوز اجرا کنید. پس از خاتمه‌ی عملیات دریافت و اجرای وب سرور، با توجه به تنظیم  p 80:80-، پورت 80 میزبان (اولین عدد)، به پورت 80 کانتینر نگاشت شده‌است. به همین جهت تنها با اجرای دستور http://localhost، خروجی این وب سرور را می‌توانید در مرورگر سیستم خود مشاهده کنید.
همانطور که مشاهده می‌کنید، با استفاده از داکر، پیش از آنکه بدانیم چگونه باید یک نرم افزار را نصب کرد، می‌توان از آن استفاده کرد!


روش متوقف کردن Containers در حال اجرا

اگر دستور docker ps را در خط فرمان ویندوز اجرا کنید، لیست پروسه‌های اجرا شده‌ی توسط آن قابل مشاهده هستند. در این لیست container id در حال اجرا نیز مشخص است. برای خاتمه‌ی کار آن، تنها کافی است دستور docker stop id را اجرا کنید.
یک نکته: ضرورتی به ذکر کامل id نیست. برای مثال ذکر سه حرف اول آن نیز کفایت می‌کند.


روش اجرای مجدد یک Container

فرض کنید می‌خواهیم سرور nginx را مجددا اجرا کنیم. یک روش آن، اجرای مجدد دستور docker run -p 80:80 nginx است که پیشتر آن‌را انجام دادیم. در این حالت این image تبدیل به container شده و همانند روش‌های متداول نصب نرم افزار، اکنون به عنوان یک نرم افزار نصب شده در دسترس است. برای مشاهده‌ی لیست آن‌ها، دستور docker ps -a را اجرا کنید. این لیست تا این لحظه باید شامل containerهای nginx و hello-world باشد. متوقف کردن یک container، سبب تخریب یا حذف آن نمی‌شود. در این حالت در لیستی که توسط دستور docker ps -a نمایش داده شده‌است، باز هم container idها قابل مشاهده هستند. فقط کافی است برای اجرای یکی از آن‌ها، دستور docker start id را اجرا کرد. به این صورت دیگر نیازی به ذکر دستور کامل docker run با تمام پارامترهای آن نیست. این id نیز همانطور که ذکر شد، می‌تواند سه حرف ابتدایی این id باشد تا حدی که نسبت به سایر idهای موجود، منحصربفرد شناخته شود. یا بجای container id می‌توان container name نمایش داده شده‌ی در این لیست را استفاده کرد.
پس از اجرای nginx توسط دستور docker start id، دو روش برای بررسی در حال اجرا بودن آن وجود دارد:
الف) مرورگر را باز کنیم و آدرس http://localhost را بررسی کنیم.
ب) دستور docker ps را در خط فرمان اجرا کنیم، تا مشخص شود که آیا پروسه‌ی nginx در حال اجرا است یا خیر؟

بنابراین دستور docker ps -a لیست تمام containers در حال اجرا و همچنین متوقف شده را نمایش می‌دهد. اما دستور docker ps تنها لیست containers در حال اجرا را نمایش خواهد داد.


روش حذف Containers از Docker

همانطور که در قسمت قبل نیز بحث شد، معادل نصب نرم افزار در اینجا، ایجاد یک container از یک image دریافتی از docker hub است. روش عکس آن، یعنی تخریب یک container، دقیقا معادل عزل نرم افزار از سیستم، در حالت‌های متداول است. برای اینکار مجددا دستور docker ps -a را اجرا می‌کنیم تا لیست تمام containerهای در حال اجرا و همچنین متوقف شده نمایش داده شوند. لیستی که در اینجا نمایش داده می‌شود، شبیه به لیستی است که در قسمت add/remove programs ویندوز مشاهده می‌کنید. این لیست معادل لیست نرم افزارهای نصب شده‌ی بر روی سیستم است و یا برای مشاهده‌ی لیست imageهای دریافتی از docker hub می‌توان دستور docker images را صادر کرد.
قبل از حذف یک container نیاز است آن‌را متوقف کنیم. برای این منظور از دستور docker stop id استفاده می‌شود. سپس اجرای دستور docker rm id، سبب حذف کامل این container خواهد شد. برای آزمایش آن، مجددا دستور docker ps -a را اجرا کنید.
دستور docker rm چندین id را نیز می‌پذیرد. می‌توان این idها و یا حتی سه حرف ابتدایی آن‌ها را با فاصله در اینجا ذکر کرد. علاوه بر id، ذکر نام containers نیز مجاز است.


روش حذف Imageهای دریافتی از Docker Hub

دستور docker rm، فقط containers را از سیستم حذف می‌کند (نرم افزارهای نصب شده). اما خود imageهای اصلی دریافت شده‌ی از docker hub را حذف نمی‌کند (معادل همان فایل‌های zip دریافت نرم افزار یا برنامه‌های نصاب، در حالت متداول و سنتی نصب نرم افزار). برای آزمایش آن دستور docker images را اجرا کنید. هنوز هم در لیست آن، تمام موارد دریافتی موجود هستند.
برای حذف یک image می‌توان از دستور docker rmi id استفاده کرد (rmi بجای rm). این id نیز در لیست docker images ظاهر می‌شود و ذکر قسمتی از آن، تا حدی که نسبت به سایر idهای لیست شده منحصربفرد باشد، کافی است. در اینجا بجای id، از نام image نیز می‌توان استفاده کرد. همچنین ذکر چندین id و یا نام نیز پس از دستور docker rmi، میسر است.


روش جستجوی imageها در Docker Hub توسط Docker CLI

فرض کنید می‌خواهیم image مربوط به راهنمای Docker را از Docker Hub دریافت کنیم. یک روش آن مراجعه‌ی مستقیم به سایت آن است و استفاده از امکانات جستجوی فراهم شده‌ی در آن سایت. روش دیگر، استفاده از Docker CLI است. اگر دستور docker search docs را در خط فرمان اجرا کنیم، لیست تمام مخازن کدی که در آن‌ها واژه‌ی docs قرار دارد، نمایش داده می‌شود. البته پیش از نصب image آن بهتر است به برگه‌ی tags مخزن کد آن نیز مراجعه کنید تا بتوانید حجم آن‌را نیز مشاهده نمائید که حدود یک گیگابایت است. مخازن docker hub، حاوی imageهای نصاب containerهای متناظر هستند. برای دریافت و اجرای آن می‌توان دستور docker run -p 4000:4000 docs/docker.github.io را اجرا کرد.
پس از دریافت یک گیگابایت مستندات، container آن بر روی پورت 4000 در سیستم ما (http://localhost:4000)، به صورت یک وب سایت استاتیک، قابل دسترسی خواهد بود. به این صورت می‌توان به مستندات کامل داکر به صورت آفلاین دسترسی داشت.


مفهوم Interactive Terminal در Docker

زمانیکه دستور اجرای مستندات آفلاین را صادر می‌کنید، در انتهای آن عنوان می‌کند که وب سایت محلی آن بر روی پورت 4000 قابل دسترسی است. سپس در ذیل آن ذکر شده‌است که اگر ctrl+c را فشار دهید، اجرای آن به پایان می‌رسد. اما عملا اینطور نیست و اگر دستور docker ps را صادر کنید، هنوز container در حال اجرای آن را می‌توان مشاهده کرد.
اما اگر اینبار دستور اجرای docker run را به همراه یک interactive terminal با سوئیچ it و نام docs صادر کنیم:
 docker run -p 4000:4000 -it --name docs docs/docker.github.io
اکنون اگر ctrl+c را فشار دهیم و پس از آن دستور docker ps را صادر کنیم، دیگر در لیست آن، container در حال اجرای docs مشاهده نمی‌شود.
سوئیچ it یا interactive terminal سبب می‌شود تا یک container در foreground، بجای background اجرا شود. به این ترتیب دستور ctrl+c، سبب خاتمه‌ی واقعی پروسه‌ی درحال اجرای در container می‌شود.
روش دیگر خاتمه‌ی این container، استفاده از نام ذکر شده‌است؛ یعنی اجرای دستور docker stop docs.

یک نکته: اگر می‌خواهید از terminal باز شده قطع شوید (مجددا به command prompt باز گردید) اما سبب خاتمه‌ی container آن نشوید، از ترکیب ctrl+p+q استفاده کنید.


اجرای containerهای ویندوزی

در مورد نحوه‌ی سوئیچ بین نوع‌های مختلف containerهای ویندوزی و لینوکسی پیشتر توضیح دادیم. برای این منظور می‌توان بر روی آیکن Docker در قسمت Tray Icons ویندوز کلیک راست کرده و گزینه‌ی switch to Windows/Linux containers را انتخاب کرد. باید دقت داشت که پشتیبانی از containerهای ویندوزی، از ویندوز 10، نگارش  1607، یا همان Anniversary Update آن به بعد، به ویژگی‌های ویندوز اضافه شده‌اند که به صورت خودکار توسط docker فعالسازی می‌شوند:



اجرای IIS به عنوان یک Windows Container

تا اینجا imageهای دریافتی، لینوکسی بودند. اگر گزینه‌ی Windows Containers را به روشی که گفته شد، فعال کنید، اینبار با اجرای دستورات docker ps و یا docker images، هیچ خروجی را دریافت نخواهید کرد. از این جهت که کانتینرهای ویندوزی و لینوکسی، به صورت کاملا ایزوله‌ای از هم اجرا و مدیریت می‌شوند. علت آن‌را هم در MobyLinuxVM که پیشتر با اجرای دستور Virtmgmt.msc بررسی کردیم، می‌توان یافت. Containerهای لینوکسی، در داخل MobyLinuxVM اجرا می‌شوند.
در اینجا به عنوان مثال می‌توان image رسمی مربوط به IIS را از docker hub دریافت و به صورت یک کانتینر ویندوزی اجرا کرد. البته پیش از اجرای دستورات آن بهتر است به برگه‌ی tags آن مراجعه کرده و حجم‌های نگارش‌های مختلف آن‌را بررسی کرد. اجرای دستور docker pull microsoft/iis به معنای دریافت tag ای به نام latest است (به حجم 6 گیگابایت!)؛ یعنی با دستور docker pull microsoft/iis:latest یکی است. بنابراین در اینجا بر اساس tagهای مختلف، می‌توان دستور pull متفاوتی را صادر کرد. برای مثال اگر دستور docker pull microsoft/iis:nanoserver را صادر کردید، نگارش مخصوص nano server آن‌را که فقط 449 مگابایت است، دریافت می‌کند. بنابراین از این پس به tagهای هر مخزن docker hub خوب دقت کنید و نگارش مختص به سیستم عامل خود را دریافت نمائید. عدم ذکر tag ای، همواره tag ویژه‌ای را به نام latest، دریافت می‌کند.
با اجرای دستور زیر
 docker run -p 81:80 -d --name iis microsoft/iis:nanoserver
داکر، ابتدا image مخصوص nanoserver آن‌را دریافت و سپس اجرا می‌کند. چون وب سرور است، نیاز به تنظیمات پورت آن‌را داریم. p 81:80- به این معنا است که پورت 80 کانتینر را (پورتی که IIS درون آن بر روی آن اجرا می‌شود)، به پورت 81 بر روی سیستم میزبان (یا همین ویندوز فعلی که داکر را اجرا می‌کند)، نگاشت کن. پارامتر d در اینجا به معنای detach است. یعنی به محض اجرای این دستور، کار اجرای این کانتینر در background انجام شده و سپس به خط فرمان، بازگشت داده می‌شویم. همچنین نامی نیز به این container انتساب داده شده‌است تا ساده‌تر بتوان با آن کار کرد.

یک نکته: مشکلی با اجرای IIS مخصوص نانوسرور بر روی ویندوز 10 به این صورت و توسط داکر نیست. بنابراین پس از اجرای دستور فوق، کار دریافت image و ساخت container و سپس اجرای آن به صورت خودکار انجام شده و بلافاصله به command prompt بازگشت داده می‌شویم (به علت استفاده‌ی از پارامتر d). اکنون اگر دستور docker ps را صادر کنیم، مشاهده می‌کنیم که کانتینر IIS مخصوص نانوسرور، هم اکنون بر روی ویندوز 10 در حال اجرا است و در آدرس http://localhost:81 قابل دسترسی است.

جهت تکمیل این بحث، بهتر است image مخصوص nanoserver را نیز از docker hub دریافت و اجرا کنیم:
 docker run microsoft/windowsservercore
حجم image آن 6GB است.


تنظیمات کارت شبکه‌ی Containers

هنگامیکه پروسه‌ای درون یک container اجرا می‌شود، ایزوله سازی‌های بسیاری نیز در مورد آن اعمال خواهد شد؛ به همین جهت گاهی از اوقات عده‌ای containerها را با ماشین‌های مجازی نیز مقایسه می‌کنند. برای مثال کانتینرها به همراه network adapter خاص خود نیز هستند؛ درست مانند اینکه یک کامپیوتر مجزای از سیستم جاری می‌باشند و اگر این network adapter را ping کنیم، می‌توان به این صورت نیز به آن کانتینر، دسترسی داشته باشیم.
برای یافتن آن، دستور docker inspect iis را صادر می‌کنیم. خروجی آن به همراه یک قسمت network نیز هست که داخل آن یک IP Address قابل مشاهده است. این IP است که مختص و منحصربفرد این container است. در ابتدا برای آزمایش آن، می‌توان آن‌را ping کرد؛ مانند ping 172.27.49.47. همچنین به تمام برنامه‌های داخل این container توسط این IP نیز می‌توان دسترسی یافت. برای مثال فراخوانی http://172.27.49.47:81 در مرورگر، سبب نمایش صفحه‌ی اول IIS می‌شود. البته اگر اینکار را انجام دهیم، کار نمی‌کند. علت اینجا است، نگاشت پورتی را که تعریف کرده‌ایم (پورت 81)، به پورتی در کامپیوتر میزبان است و نه این IP ویژه. برنامه‌ی اصلی IIS در داخل container، به پورت 80 بر روی این آدرس IP گوش فرا می‌دهد. اکنون اگر آدرس http://172.27.49.47:80 را در کامپیوتر میزبان فراخوانی کنیم، کار می‌کند.
بنابراین هرچند containerها به معنای نرم افزارهای از پیش نصب شده‌ی در حال اجرا هستند، اما ... به همراه ایزوله سازی‌های قابل توجهی بر روی کامپیوتر میزبان اجرا می‌شوند؛ درست مانند یک کامپیوتر مجزای از آن.
مطالب دوره‌ها
نگاهی به SignalR Hubs
Hubs کلاس‌هایی هستند جهت پیاده سازی push services در SignalR و همانطور که در قسمت قبل عنوان شد، در سطحی بالاتر از اتصال ماندگار (persistent connection) قرار می‌گیرند. کلاس‌های Hubs بر مبنای یک سری قرار داد پیش فرض کار می‌کنند (ایده Convention-over-configuration) تا استفاده نهایی از آن‌ها را ساده‌تر کنند.
Hubs به نوعی یک فریم ورک سطح بالای RPC نیز محسوب می‌شوند (Remote Procedure Calls) و آن‌را برای انتقال انواع و اقسام داده‌ها بین سرور و کلاینت و یا فراخوانی متدی در سمت کلاینت یا سرور، بسیار مناسب می‌سازد. برای مثال اگر قرار باشد با persistent connection به صورت مستقیم کار کنیم، نیاز است تا بسیاری از مسایل serialization و deserialization اطلاعات را خودمان پیاده سازی و اعمال نمائیم.


قرار دادهای پیش فرض Hubs

- متدهای public کلاس‌های Hubs از طریق دنیای خارج قابل فراخوانی هستند.
- ارسال اطلاعات به کلاینت‌ها از طریق فراخوانی متدهای سمت کلاینت انجام خواهد شد. (نحوه تعریف این متدها در سمت سرور بر اساس قابلیت‌های dynamic اضافه شده به دات نت 4 است که در ادامه در مورد آن بیشتر بحث خواهد شد)


مراحل اولیه نوشتن یک Hub
الف) یک کلاس Hub را تهیه کنید. این کلاس، از کلاس پایه Hub تعریف شده در فضای نام Microsoft.AspNet.SignalR باید مشتق شود. همچنین این کلاس می‌تواند توسط ویژگی خاصی به نام HubName نیز مزین گردد تا در حین برپایی اولیه سرویس، از طریق زیرساخت‌های SignalR به نامی دیگر (یک alias یا نام مستعار خاص) قابل شناسایی باشد. متدهای یک هاب می‌توانند نوع‌های ساده یا پیچیده‌ای را بازگشت دهند و همه چیز در اینجا نهایتا به فرمت JSON رد و بدل خواهد شد (فرمت پیش فرض که در پشت صحنه از کتابخانه معروف JSON.NET استفاده می‌کند؛ این کتابخانه سورس باز به دلیل کیفیت بالای آن، از زمان ارائه MVC4 به عنوان جزئی از مجموعه کارهای مایکروسافت قرار گرفته است).
ب) مسیریابی و Routing را تعریف و اصلاح نمائید.
و ... از نتیجه استفاده کنید.


تهیه اولین برنامه با SignalR

ابتدا یک پروژه خالی ASP.NET را آغاز کنید (مهم نیست MVC باشد یا WebForms). برای سادگی بیشتر، در اینجا یک ASP.NET Empty Web application درنظر گرفته شده است. در ادامه قصد داریم یک برنامه Chat را تهیه کنیم؛ از این جهت که توسط یک برنامه Chat بسیاری از مفاهیم مرتبط با SignalR را می‌توان در عمل توضیح داد.
اگر از VS 2012 استفاده می‌کنید، گزینه SignalR Hub class جزئی از آیتم‌های جدید قابل افزودن به پروژه است (منوی پروژه، گزینه new item آن) و پس از انتخاب این قالب خاص، تمامی ارجاعات لازم نیز به صورت خودکار به پروژه جاری اضافه خواهند شد.


و اگر از VS 2010 استفاده می‌کنید، نیاز است از طریق NuGet ارجاعات لازم را به پروژه خود اضافه نمائید:
 PM> Install-Package Microsoft.AspNet.SignalR
اکنون یک کلاس خالی جدید را به نام ChatHub، به آن اضافه کنید. سپس کدهای آن را به نحو ذیل تغییر دهید:
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR02
{
    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            Clients.All.hello(message);
        }
    }
}
همانطور که ملاحظه می‌کنید این کلاس از کلاس پایه Hub مشتق شده و توسط ویژگی HubName، نام مستعار chat را یافته است.
کلاس پایه Hub یک سری متد و خاصیت را در اختیار کلاس‌های مشتق شده از آن قرار می‌دهد. ساده‌ترین راه برای آشنایی با این متدها و خواص مهیا، کلیک راست بر روی نام کلاس پایه Hub و انتخاب گزینه Go to definition است.
برای نمونه در کلاس ChatHub فوق، از خاصیت Clients برای دسترسی به تمامی آن‌ها و سپس فراخوانی متد dynamic ایی به نام hello که هنوز وجود خارجی ندارد، استفاده شده است.
اهمیتی ندارد که این کلاس در اسمبلی اصلی برنامه وب قرار گیرد یا مثلا در یک class library به نام Services. همینقدر که از کلاس Hub مشتق شود به صورت خودکار در ابتدای برنامه اسکن گردیده و یافت خواهد شد.

مرحله بعد، افزودن فایل global.asax به برنامه است. زیرا برای کار با SignalR نیاز است تنظیمات Routing و مسیریابی خاص آن‌را اضافه نمائیم. پس از افرودن فایل global.asax، به فایل Global.asax.cs مراجعه کرده و در متد Application_Start آن تغییرات ذیل را اعمال نمائید:
using System;
using System.Web;
using System.Web.Routing;

namespace SignalR02
{
    public class Global : HttpApplication
    {
        protected void Application_Start(object sender, EventArgs e)
        {
            // Register the default hubs route: ~/signalr
            RouteTable.Routes.MapHubs();
        }
    }
}

یک نکته مهم
 اگر از ASP.NET MVC استفاده می‌کنید، این تنظیم مسیریابی باید پیش از تعاریف پیش فرض موجود قرار گیرد. در غیراینصورت مسیریابی‌های SignalR کار نخواهند کرد.

اکنون برای آزمایش برنامه، برنامه را اجرا کرده و مسیر ذیل را فراخوانی کنید:
 http://localhost/signalr/hubs
در این حال اگر برنامه را برای مثال با مرورگر chrome باز کنید، در این آدرس، فایل جاوا اسکریپتی SignalR، قابل مشاهده خواهد بود. مرورگر IE پیغام می‌دهد که فایل را نمی‌تواند باز کند. اگر به انتهای خروجی آدرس مراجعه کنید، چنین سطری قابل مشاهده است:
  proxies.chat = this.createHubProxy('chat');
و کلمه chat دقیقا از مقدار معرفی شده توسط ویژگی HubName دریافت گردیده است.

تا اینجا ما موفق شدیم اولین Hub خود را تشکیل دهیم.


بررسی پروتکل Hub

اکنون که اولین Hub خود را ایجاد کرده‌ایم، بد نیست اندکی با زیر ساخت آن نیز آشنا شویم.
مطابق مسیریابی تعریف شده در Application_Start، مسیر ابتدایی دسترسی به SignalR با افزودن اسلش SignalR به انتهای مسیر ریشه سایت بدست می‌آید و اگر به این آدرس یک اسلش hubs را نیز اضافه کنیم، فایل js metadata مرتبط را نیز می‌توان دریافت و مشاهده کرد.

زمانیکه یک کلاینت قصد اتصال به یک Hub را دارد، دو مرحله رخ خواهد داد:
الف) negotiate: در این حالت امکانات قابل پشتیبانی از طرف سرور مورد پرسش قرار می‌گیرند و سپس بهترین حالت انتقال، انتخاب می‌گردد. این انتخاب‌ها به ترتیب از چپ به راست خواهند بود:
 Web socket -> SSE -> Forever frame -> long polling


به این معنا که اگر برای مثال امکانات Web sockets مهیا بود، در همینجا کار انتخاب نحوه انتقال اطلاعات خاتمه یافته و Web sockets انتخاب می‌شود.
تمام این مراحل نیز خودکار است و نیازی نیست تا برای تنظیمات آن کار خاصی صورت گیرد. البته در سمت کلاینت، امکان انتخاب یکی از موارد یاد شده به صورت صریح نیز وجود دارد.
ب) connect: اتصالی ماندگار برقرار می‌گردد.

در پروتکل Hub تمام اطلاعات JSON encoded هستند و یک سری مخفف‌هایی را در این بین نیز ممکن است مشاهده نمائید که معنای آن‌ها به شرح زیر است:
 C: cursor
M: Messages
H: Hub name
M: Method name
A: Method args
T: Time out
D: Disconnect
این مراحل را در قسمت بعد، پس از ایجاد یک کلاینت، بهتر می‌توان توضیح داد.


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

به چندین روش می‌توان اطلاعاتی را به کلاینت‌ها ارسال کرد:
1) استفاده از خاصیت Clients موجود در کلاس Hub
2) استفاده از خواص و متد‌های dynamic
در این حالت اطلاعات متد dynamic و پارامترهای آن به صورت JSON encoded به کلاینت ارسال می‌شوند (به همین جهت اهمیتی ندارند که در سرور وجود خارجی دارند یا خیر و به صورت dynamic تعریف شده‌اند).
using Microsoft.AspNet.SignalR;
using Microsoft.AspNet.SignalR.Hubs;

namespace SignalR02
{
    [HubName("chat")]
    public class ChatHub : Hub
    {
        public void SendMessage(string message)
        {
            var msg = string.Format("{0}:{1}", Context.ConnectionId, message);
            Clients.All.hello(msg);
        }
    }
}
برای نمونه در اینجا متد hello به صورت dynamic تعریف شده است (جزئی از متدهای خاصیت All نیست و اصلا در سمت سرور وجود خارجی ندارد) و خواص Context و Clients، هر دو در کلاس پایه Hub قرار دارند.
حالت Clients.All به معنای ارسال پیامی به تمام کلاینت‌های متصل به هاب ما هستند.

3) روش‌های دیگر، استفاده از خاصیت dynamic دیگری به نام Caller است که می‌توان بر روی آن متد دلخواهی را تعریف و فراخوانی کرد.
 //این دو عبارت هر دو یکی هستند
Clients.Caller.hello(msg);
Clients.Client(Context.ConnectionId).hello(msg);
انجام اینکار با روش ارائه شده در سطر دومی که ملاحظه می‌کنید، در عمل یکی است؛ از این جهت که Context.ConnectionId همان ConnectionId فراخوان می‌باشد.
در اینجا پیامی صرفا به فراخوان جاری سرویس ارسال می‌گردد.

4) استفاده از خاصیت dynamic ایی به نام Clients.Others
 Clients.Others.hello(msg);
در این حالت، پیام، به تمام کلاینت‌های متصل، منهای کلاینت فراخوان ارسال می‌گردد.

5) استفاده از متد Clients.AllExcept
این متد می‌تواند آرایه‌ای از ConnectionId‌هایی را بپذیرد که قرار نیست پیام ارسالی ما را دریافت کنند.

6) ارسال اطلاعات به گروه‌ها
تعداد مشخصی از ConnectionIdها یک گروه را تشکیل می‌دهند؛ مثلا اعضای یک chat room.
        public void JoinRoom(string room)
        {
            this.Groups.Add(Context.ConnectionId, room);
        }

        public void SendMessageToRoom(string room, string msg)
        {
            this.Clients.Group(room).hello(msg);
        }
در اینجا نحوه الحاق یک کلاینت به یک room یا گروه را مشاهده می‌کنید. همچنین با مشخص بودن نام گروه، می‌توان صرفا اطلاعاتی را به اعضای آن گروه خاص ارسال کرد.
خاصیت Group در کلاس پایه Hub تعریف شده است.
نکته مهمی را که در اینجا باید درنظر داشت این است که اطلاعات گروه‌ها به صورت دائمی در سرور ذخیره نمی‌شوند. برای مثال اگر سرور ری استارت شود، این اطلاعات از دست خواهند رفت.


آشنایی با مراحل طول عمر یک Hub

اگر به تعاریف کلاس پایه Hub دقت کنیم:
    public abstract class Hub : IHub, IDisposable
    {
        protected Hub();
        public HubConnectionContext Clients { get; set; }
        public HubCallerContext Context { get; set; }
        public IGroupManager Groups { get; set; }

        public void Dispose();
        protected virtual void Dispose(bool disposing);
        public virtual Task OnConnected();
        public virtual Task OnDisconnected();
        public virtual Task OnReconnected();
    }
در اینجا، تعدادی از متدها virtual تعریف شده‌اند که تمامی آن‌ها را در کلاس مشتق شده نهایی می‌توان override و مورد استفاده قرار داد. به این ترتیب می‌توان به اجزا و مراحل مختلف طول عمر یک Hub مانند برقراری اتصال یا قطع شدن آن، دسترسی یافت. تمام این متدها نیز با Task معرفی شده‌اند؛ که معنای غیرهمزمان بودن پردازش آن‌ها را بیان می‌کند.
تعدادی از این متدها را می‌توان جهت مقاصد logging برنامه مورد استفاده قرار داد و یا در متد OnDisconnected اگر اطلاعاتی را در بانک اطلاعاتی ذخیره کرده‌ایم، بر این اساس می‌توان وضعیت نهایی را تغییر داد.


ارسال اطلاعات از یک Hub به Hub دیگر در برنامه

فرض کنید یک Hub دوم را به نام MinitorHub به برنامه اضافه کرده‌اید. اکنون قصد داریم از داخل ChatHub فوق، اطلاعاتی را به آن ارسال کنیم. روش کار به نحو زیر است:
        public override System.Threading.Tasks.Task OnDisconnected()
        {
            sendMonitorData("OnDisconnected", Context.ConnectionId);
            return base.OnDisconnected();
        }

        private void sendMonitorData(string type, string connection)
        {
            var ctx = GlobalHost.ConnectionManager.GetHubContext<MonitorHub>();
            ctx.Clients.All.newEvenet(type, connection);
        }
در اینجا با override کردن OnDisconnected به رویداد خاتمه اتصال یک کلاینت دسترسی یافته‌ایم. سپس قصد داریم این اطلاعات را توسط متد sendMonitorData به Hub دومی به نام MonitorHub ارسال کنیم که نحوه پیاده سازی آن‌را در کدهای فوق ملاحظه می‌کنید. GlobalHost.ConnectionManager یک dependency resolver توکار تعریف شده در SignalR است.
مورد استفاده دیگر این روش، ارسال اطلاعات به کلاینت‌ها از طریق کدهای یک برنامه تحت وب است (که در همان پروژه هاب واقع شده است). برای مثال در یک اکشن متد یا یک روال رویدادگردان کلیک نیز می‌توان از GlobalHost.ConnectionManager استفاده کرد.
مطالب
نوشتن آزمون‌های واحد به کمک کتابخانه‌ی Moq - قسمت سوم - تنظیم مقادیر خواص اشیاء
در قسمت قبل، چون متد Validate سرویس تصدیق هویت استفاده شده، همواره مقدار false را بر می‌گرداند:
_identityVerifier.Initialize();
var isValidIdentity = _identityVerifier.Validate(
     application.Applicant.Name, application.Applicant.Age, application.Applicant.Address);
شیء Mock آن‌را طوری تنظیم کردیم که بر اساس یک applicant مشخص، خروجی true را بازگشت دهد. اما در این بین، کدهای بررسی سرویس creditScorer را کامنت کردیم:
_creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
if (_creditScorer.Score < MinimumCreditScore)
{
    return application.IsAccepted;
}
تا آزمایش واحد ما با موفقیت به پایان برسد. در این قسمت، کار تنظیم مقادیر خواص آن‌را در آزمون واحد، به کمک Mocked objects انجام می‌دهیم تا این قسمت از کد نیز پوشش داده شود. برای این منظور به کلاس LoanApplicationProcessor مراجعه کرده و در متد Process آن، ابتدا مجددا از همان overload ساده‌ی فوق متد Validate بجای نمونه‌ی ref دار استفاده کرده و سپس کدهای creditScorer را نیز از حالت کامنت خارج می‌کنیم.


تنظیم مقدار خاصیت Score شیء Mock شده

اینترفیس ICreditScorer به صورت زیر تعریف شده‌است و دارای خاصیت Score می‌باشد که مقدار عددی آن با مقدار حداقل اعتبار تنظیم شده‌ی در کلاس LoanApplicationProcessor مقایسه خواهد شد (MinimumCreditScore = 100_000):
namespace Loans.Services.Contracts
{
    public interface ICreditScorer
    {
        int Score { get; }

        void CalculateScore(string applicantName, string applicantAddress);
    }
}
برای تنظیم مقدار خاصیت Score، در متد Accept آزمون‌های واحد تهیه شده، می‌توان به صورت زیر عمل کرد:
var mockCreditScorer = new Mock<ICreditScorer>();
mockCreditScorer.Setup(x => x.Score).Returns(110_000);
که بسیار شبیه به نحوه‌ی تنظیم مقادیر بازگشتی متدها است. در متد Setup می‌توان به صورت strongly typed به تمام خواص اینترفیس ICreditScorer دسترسی یافت و سپس توسط متد Returns، مقدار بازگشتی آن‌ها را تنظیم نمود.
اکنون اگر متد آزمایش واحد Accept را بررسی کنیم، چون شخص درخواست دهنده، دارای اعتبار بیشتری از حداقل اعتبار مورد نیاز است، این آزمایش با موفقیت به پایان خواهد رسید. اگر این تنظیم صورت نمی‌گرفت، شیء mockCreditScorer، مقدار پیش‌فرض int یا همان صفر را به عنوان مقدار Score بازگشت می‌داد.


تنظیم مقادیر خواص تو در تو و سلسله مراتبی اشیاء Mock شده

برای کار با خواص تو در تو، ابتدا دو مدل زیر را ایجاد می‌کنیم:
namespace Loans.Models
{
    public class ScoreResult
    {
        public ScoreValue ScoreValue { get; }
    }

    public class ScoreValue
    {
        public int Score { get; }
    }
}
اکنون بجای مقدار ساده‌ی int Score { get; }، از نمونه‌ی ScoreResult فوق، در اینترفیس ICreditScorer استفاده خواهیم کرد:
using Loans.Models;

namespace Loans.Services.Contracts
{
    public interface ICreditScorer
    {
        int Score { get; }

        void CalculateScore(string applicantName, string applicantAddress);
        
        ScoreResult ScoreResult { get; }
    }
}
در ادامه برای استفاده‌ی از ScoreResult، به کلاس LoanApplicationProcessor مراجعه کرده و در انتهای متد Process آن، این تغییر را ایجاد می‌کنیم:
//if (_creditScorer.Score < MinimumCreditScore)
if (_creditScorer.ScoreResult.ScoreValue.Score < MinimumCreditScore)
اینبار اگر متد آزمون واحد Accept را اجرا کنیم، با یک null reference exception به پایان می‌رسد؛ چون اولین سطح این شیء تو در تو، یعنی ScoreResult، مساوی نال است.
برای رفع این مشکل در متد آزمون واحد Accept، باید به صورت زیر عمل کرد:
var mockCreditScorer = new Mock<ICreditScorer>();
mockCreditScorer.Setup(x => x.Score).Returns(110_000);

var mockScoreValue = new Mock<ScoreValue>();
mockScoreValue.Setup(x => x.Score).Returns(110_000);

var mockScoreResult = new Mock<ScoreResult>();
mockScoreResult.Setup(x => x.ScoreValue).Returns(mockScoreValue.Object);

mockCreditScorer.Setup(x => x.ScoreResult).Returns(mockScoreResult.Object);
ابتدا از پایین‌ترین سطح یعنی ScoreValue شروع و مقدار خاصیت Score آن‌را تنظیم می‌کنیم.
سپس یک سطح بالاتر را یعنی ScoreResult را تنظیم خواهیم کرد. در اینجا نیاز است خاصیت ScoreValue آن به mock object قبلی تنظیم شود. به همین جهت Returns آن به خاصیت Object شیء mockScoreValue، تنظیم شده‌است.
در آخر برای تنظیم خاصیت ScoreResult شیء mockCreditScorer اصلی، از شیء mockScoreResult استفاده خواهیم کرد.

در این حالت اگر متد آزمون واحد Accept را اجرا کنیم، اینبار به خطای زیر برخواهیم خورد:
Test method Loans.Tests.LoanApplicationProcessorShould.Accept threw exception:
System.NotSupportedException: Unsupported expression: x => x.Score
Non-overridable members (here: ScoreValue.get_Score) may not be used in setup / verification expressions.
عنوان می‌کند که خاصیت Score شیء ScoreValue، قابل بازنویسی نیست (Non-overridable). منظورش این است که برای mocking آن خاصیت، باید آن‌را virtual تعریف کنیم تا کتابخانه‌ی Moq بتواند آن‌را بازنویسی کند. به همین جهت، هر دو خاصیتی را که در اینجا قصد بازنویسی آن‌ها را داریم، به صورت virtual تعریف می‌کنیم:
namespace Loans.Models
{
    public class ScoreResult
    {
        public virtual ScoreValue ScoreValue { get; }
    }

    public class ScoreValue
    {
        public virtual int Score { get; }
    }
}
اکنون اگر متد آزمایش واحد Accept را بررسی کنیم با موفقیت به پایان خواهد رسید.


ساده سازی روش تنظیم مقادیر خواص تو در تو و سلسله مراتبی اشیاء Mock شده

روش دیگری نیز برای تنظیم مقادیر خواص تو در تو در کتابخانه‌ی Moq وجود دارد:
mockCreditScorer.Setup(x => x.ScoreResult.ScoreValue.Score).Returns(110_000);
کتابخانه‌ی Moq قادر است به نحوی که مشاهده می‌کنید، سلسله مراتب اشیاء را به صورت strongly typed ایجاد کرده و در نهایت خاصیت Score آن‌را به 110_000 تنظیم کند.
بدیهی است در این حالت نیز باید شرط virtual بودن این خواص، برقرار باشد؛ در غیراینصورت همان استثنای NotSupportedException را دریافت خواهیم کرد.

یک نکته: اگر در زمان تشکیل یک Mock object، مقدار خاصیت DefaultValue آن‌را به صورت زیر تنظیم کنیم:
var mockCreditScorer = new Mock<ICreditScorer> { DefaultValue = DefaultValue.Mock };
تمام خواص تو در توی موجود در ICreditScorer، به صورت خودکار با نمونه‌های پیش‌فرض آن‌ها مقدار دهی و آماده‌ی استفاده خواهند شد. اگر بجای مقدار DefaultValue.Mock از DefaultValue.Empty استفاده شود، این مقادیر پیش‌فرض، نال خواهد بود (که همان حالت پیش‌فرض new Mock است).


بررسی تغییرات مقادیر خواص اشیاء Mock شده

کتابخانه‌ی Moq، امکان ردیابی تغییرات مقادیر خواص اشیاء Mock شده را نیز داراست. برای نمایش آن، فرض کنید خاصیت جدید Count را به اینترفیس ICreditScorer اضافه کرده‌ایم:
using Loans.Models;

namespace Loans.Services.Contracts
{
    public interface ICreditScorer
    {
        int Score { get; }

        void CalculateScore(string applicantName, string applicantAddress);
        
        ScoreResult ScoreResult { get; }
        
        int Count { get; set; }
    }
}
سپس در کلاس LoanApplicationProcessor و متد Process آن، هربار که CalculateScore فراخوانی می‌شود، یکبار مقدار Count را افزایش می‌دهیم:
_creditScorer.CalculateScore(application.Applicant.Name, application.Applicant.Address);
_creditScorer.Count++;
اکنون در متد آزمون واحد Accept، بررسی می‌کنیم که آیا پس از یکبار فراخوانی متد CalculateScore، مقدار Count برای مثال 1 شده‌است یا خیر؟
Assert.AreEqual(1, mockCreditScorer.Object.Count);
تا اینجا اگر آزمون واحد را اجرا کنیم، با شکست مواجه خواهد شد. چون کتابخانه‌ی Moq تغییرات مقادیر خواص شیء mockCreditScorer.Object را ردیابی نمی‌کند و مقدار mockCreditScorer.Object.Count، همان مقدار پیش‌فرض نوع int، یعنی صفر می‌باشد.
برای فعال سازی ردیابی تغییرات مقادیر خاصیت Count، تنها کافی است آن‌را توسط متد SetupProperty، معرفی کنیم:
mockCreditScorer.SetupProperty(x => x.Count);
پس از این تغییر، بررسی متد آزمون واحد Accept با موفقیت به پایان می‌رسد.

در اینجا می‌توان یک مقدار اولیه را هم درنظر گرفت:
mockCreditScorer.SetupProperty(x => x.Count, 10);
بدیهی است در این صورت Assert.AreEqual ما با شکست مواجه می‌شود؛ چون اینبار مقدار Count نهایی، بر اساس این مقدار اولیه، 11 خواهد بود.


فعالسازی بررسی تغییرات تمام مقادیر خواص اشیاء Mock شده

اگر تعداد خواصی که قرار است مورد ردیابی قرارگیرند زیاد است، بجای فراخوانی متد SetupProperty بر روی تک تک آن‌ها، می‌توان تمام آن‌ها را به صورت زیر تحت کنترل قرار داد:
mockCreditScorer.SetupAllProperties();

نکته‌ی مهم: محل قرارگیری SetupAllProperties مهم است. برای مثال اگر این سطر را پس از سطر تنظیم مقدار پیش‌فرض x.ScoreResult.ScoreValue.Score قرار دهید، آزمایش با شکست مواجه می‌شود؛ چون تنظیمات بازگشت مقادیر پیش‌فرض خواص را به طور کامل بازنویسی می‌کند. بنابراین این سطر باید پیش از سطر تنظیم مقادیر پیش‌فرض خواص Mock شده، فراخوانی شود تا بر روی این مقادیر تنظیمی، تاثیری نداشته باشد.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MoqSeries-03.zip
مطالب
Query Options در پروتکل OData
در قسمت قبل  با OData به صورت مختصر آشنا شدیم. در این قسمت به امکانات توکار OData و جزئیات query options پرداخته و همچنین قابلیت‌های امنیتی این پروتکل را بررسی مینماییم.
در قسمت قبلی، config مربوط به OData و همچنین Controller و Crud مربوط به آن entity پیاده سازی شد. در این قسمت ابتدا سه موجودیت را به نام‌های Product ، Category و همچنین Supplier، به صورت زیر تعریف مینماییم:

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

public class Supplier
{
    [Key]
    public string Key {get; set; }
    public string Name { get; set; }
}
public class Category
{
    public int ID { get; set; }
    public string Name { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int ID { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }

    [ForeignKey("Supplier")]
    public string SupplierId { get; set; }
    public virtual Supplier Supplier { get; set; }
}

پکیج Microsoft.AspNet.OData به تازگی ورژن 6 آن به صورت رسمی منتشر شده و شامل تغییراتی نسبت به نسخه‌ی قبلی آن است. اولین نکته‌ی حائز اهمیت، Config آن است که به صورت زیر تغییر کرده و باید Option‌های مورد نیاز، کانفیگ شوند. در این نسخه DI نیز به Odata اضافه شده است:

public static void Register(HttpConfiguration config)
        {
            ODataModelBuilder odataModelBuilder = new ODataConventionModelBuilder();

            var product = odataModelBuilder.EntitySet<Product>("Products");
            var category = odataModelBuilder.EntitySet<Category>("Categories");
            var supplier = odataModelBuilder.EntitySet<Supplier>("Suppliers");

            var edmModel = odataModelBuilder.GetEdmModel();

            supplier.EntityType.Ignore(c => c.Name);

            config.Select(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.MaxTop(25);
            config.OrderBy(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Expand(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Filter(System.Web.OData.Query.QueryOptionSetting.Allowed);
            config.Count(System.Web.OData.Query.QueryOptionSetting.Allowed);

            //config.MapODataServiceRoute("ODataRoute", "odata", edmModel); // کانفیگ به صورت معمولی
           config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                });
}
باید همه‌ی Query option‌هایی را که به آنها نیاز داریم، معرفی نماییم و فرض کنید که ProductsController و متد Get آن بدین صورت پیاده سازی شده باشد:
[EnableQuery]
        public IQueryable<Product> Get()
        {
            return new List<Product>
            {
                new Product { Id = 1, Name = "name 1", Price = 11, Category = new Category {Id =1, Name = "Cat1" } },
                new Product { Id = 2, Name = "name 2", Price = 12, Category = new Category {Id =2, Name = "Cat2" } },
                new Product { Id = 3, Name = "name 3", Price = 13, Category = new Category {Id =3, Name = "Cat3" } },
                new Product { Id = 4, Name = "name 4", Price = 14, Category = new Category {Id =4, Name = "Cat4" } },
            }.AsQueryable(); ;
        }
در این پروتکل به صورت توکار، optionهای زیر قابل استفاده است:

 Description Option 
 بسط دادن موجودیت مرتبط  $expand
 فیلتر کردن نتیجه، بر اساس شرط‌های Boolean ی  $filter
 فرمان به سرور که تعداد رکورد‌های بازگشتی را نیز نمایش دهد(مناسب برای پیاده سازی server-side pagging )  $count
 مرتب کردن نتیجه‌ی بازگشتی  $orderby
 select زدن روی پراپرتی‌های درخواستی  $select
 پرش کردن از اولین رکورد به اندازه‌ی n عدد  $skip
 فقط بازگرداندن n رکورد اول  $top
کوئری‌های زیر را در نظر بگیرید:

 در کوئری اول، فقط فیلد‌های Id,Name از Products برگشت داده خواهند شد و در کوئری دوم، از 2 رکورد اول، صرفنظر می‌شود و از بقیه‌ی آنها، فقط 3 رکورد بازگشت داده میشود:
/odata/Products?$select=Id,Name
/odata/Products?$top=3&$skip=2
/odata/Products?$count=true
در پاسخ کوئری فوق، تعداد رکورد‌های بازگشتی نیز نمایش داده میشوند:
{@odata.context: "http://localhost:4516/odata/$metadata#Products", @odata.count: 4,…}
@odata.context:"http://localhost:4516/odata/$metadata#Products"
@odata.count:4
value:[{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0},…]
0:{Id: 1, Name: "name 1", Price: 11, SupplierId: 0, CategoryId: 0}
1:{Id: 2, Name: "name 2", Price: 12, SupplierId: 0, CategoryId: 0}
2:{Id: 3, Name: "name 3", Price: 13, SupplierId: 0, CategoryId: 0}
3:{Id: 4, Name: "name 4", Price: 14, SupplierId: 0, CategoryId: 0}
در response، این مقادیر به همراه تعداد رکورد بازگشتی، نمایش داده میشوند که برای پیاده سازی paging مناسب است.
/odata/Products?$filter=Id eq 1
در کوئری فوق eq مخفف equal و به معنای برابر است و بجای آن میتوان از gt به معنای بزرگتر و lt به معنای کوچکتر نیز استفاده کرد:
/odata/Products?$filter=Id gt 1 and Id lt 3
/odata/Products?$orderby=Id desc
در کوئری فوق نیز به صورت واضح، بر روی فیلد Id، مرتب سازی به صورت نزولی خواهد بود و در صورت وجود نداشتن کلمه کلیدی desc، به صورت صعودی خواهد بود.
بسط دادن موجودیت‌های دیگر نیز بدین شکل زیر میباشد:
/odata/Products?$expand=Category
برای اینکه چندین موجودیت دیگر نیز بسط داده شوند، اینگونه رفتار مینماییم:
/odata/Products?$expand=Category,Supplier
برای اینکه به صورت عمیق به موجودیت‌های دیگر بسط داده شود، بصورت زیر:
/odata/Categories(1)?$expand=Products/Supplier
و برای اینکه حداکثر تعداد رکورد بازگشتی را مشخص نماییم:
[EnableQuery(PageSize = 10)]


محدود کردن Query Options
به صورت زیر میتوانیم فقط option‌های دلخواه را فراخوانی نماییم. مثلا در اینجا فقط اجازه‌ی Skip و Top داده شده است و بطور مثال Select قابل فراخوانی نیست:
[EnableQuery (AllowedQueryOptions= AllowedQueryOptions.Skip | AllowedQueryOptions.Top)]
برای اینکه فقط اجازه‌ی logical function زیر را بدهیم (فقط eq):
[EnableQuery(AllowedLogicalOperators=AllowedLogicalOperators.Equal)]
برای اینکه Property خاصی در edm قابلیت نمایش نباشد، در config بطور مثال Price را ignore مینماییم:
var product = odataModelBuilder.EntitySet<Product>("Products");
product.EntityType.Ignore(e => e.Price);
برای اینکه فقط بر روی فیلد‌های خاصی بتوان از orderby استفاده نمود:
[EnableQuery(AllowedOrderByProperties = "Id,Name")]
یک option به نام value$ برای بازگرداندن تنها آن رکورد مورد نظر، به صورت مجزا میباشد. برای اینکار بطور مثال متد زیر را به کنترلر خود اضافه کنید:
public System.Web.Http.IHttpActionResult GetName(int key)
        {
            Product product = Get().Single(c => c.Id == key);            
            return Ok(product.Name);
        }
/odata/Products(1)/Name/$value
و حاصل کوئری فوق، مقداری بطور مثال برابر زیر خواهد بود و نه به صورت convention پاسخ‌های OData، فرمت بازگشتی "text/plain" خواهد بود و نه json:
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
DataServiceVersion: 3.0
Content-Length: 3

Ali

Attribute Convention
هایی هم برای اعتبارسنجی پراپرتی‌ها موجود است که نام آن‌ها واضح تعریف شده‌اند:
 Description  Attribute
 اجازه‌ی فیلتر زدن بر روی آن پراپرتی داده نخواهد شد  NotFilterable
 اجازه‌ی مرتب کردن بر روی آن پراپرتی داده نخواهد شد  NotSortable
 اجازه‌ی select زدن بر روی آن پراپرتی داده نمیشود  NotNavigable
 اجازه‌ی شمارش دهی بر روی آن Collection داده نمیشود  NotCountable
 اجازه‌ی بسط دادن آن Collection داده نمیشود  NotExpandable

و همچنین [AutoExpand] به صورت اتوماتیک آن موجودیت مورد نظر را بسط میدهد.

بطور مثال کد‌های زیر را در مدل خود میتوانید مشاهده نمائید:

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

        [NotFilterable, NotSortable]
        public decimal Price { get; set; }

        [ForeignKey(nameof(SupplierId))]
        [NotNavigable]
        public virtual Supplier Supplier { get; set; }
        public int SupplierId { get; set; }


        [ForeignKey(nameof(CategoryId))]
        public virtual Category Category { get; set; }
        public int CategoryId { get; set; }

        [NotExpandable]
        public virtual ICollection<TestEntity> TestEnities { get; set; }
    }

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

public DateTimeOffset CreatedOn { get; set; }

حال کوئری زیر را برای فیلتر زدن، بر روی آن در اختیار داریم:

/odata/Products?$filter=year(CreatedOn) eq 2016

در اینجا فقط Product هایی بازگردانده میشوند که در سال 2016 ثبت شده‌اند:

/odata/Products?$filter=CreatedOn lt cast(2017-04-01T04:11:31%2B08:00,Edm.DateTimeOffset)

کوئری فوق تاریخ مورد نظر را Cast کرده و همه‌ی Product هایی را که قبل از این تاریخ ثبت شده‌اند، باز می‌گرداند.

Nested Filter In Expand

/odata/Categories?$expand=Products($filter=Id gt 1 and Id lt 5)

همه‌ی Category‌ها به علاوه بسط دادن Product هایشان، در صورتیکه Id آنها بیشتر از 1 باشد

و یا حتی بر روی موجودیت بسط داده شده، select زده شود:

/odata/Categories?$expand=Products($select=Id,Name)


Custom Attribute

ضمنا به سادگی میتوان اتریبیوت سفارشی نوشت:

public class MyEnableQueryAttribute : EnableQueryAttribute
{
    public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
    {
       // Don't apply Skip and Top.
       var ignoreQueryOptions = AllowedQueryOptions.Skip | AllowedQueryOptions.Top;
       return queryOptions.ApplyTo(queryable, ignoreQueryOptions);
    }
}

روی هر متدی از کنترلر خود که اتریبیوت [MyEnableQuery] را قرار دهید، دیگر قابلیت Skip, Top را نخواهد داشت.

Dependency Injection در آخرین نسخه‌ی OData اضافه شده است. بطور پیشفرض OData بصورت case-sensitive رفتار میکند. برای تغییر دادن آن در نسخه‌های قدیمی، Extension Methodی به نام EnableCaseSensitive وجود داشت. اما در نسخه‌ی جدید شما میتوانید پیاده سازی خاص خود را از هر کدام از بخش‌های OData داشته باشید و با استفاده از تزریق وابستگی، آن را به config برنامه‌ی خود اضافه کنید؛ برای مثال:

 public class CaseInsensitiveResolver : ODataUriResolver
    {
        private bool _enableCaseInsensitive;

        public override bool EnableCaseInsensitive
        {
            get { return true; }
            set { _enableCaseInsensitive = value; }
        }
    }

اینجا پیاده سازی از ODataUriResolver انجام شده و متد EnableCaseInsensitive به صورت جدیدی override و در حالت default مقدار true را برمیگرداند.

حال به صورت زیر آن را می‌توان به وابستگی‌های config برنامه، اضافه نمود:

config.MapODataServiceRoute("ODataRoute", "odata",
                builder =>
                {
                    builder.AddService(ServiceLifetime.Singleton, sp => edmModel);
                    builder.AddService<IEnumerable<IODataRoutingConvention>>(ServiceLifetime.Singleton, sp => ODataRoutingConventions.CreateDefault());
                    builder.AddService<ODataUriResolver>(ServiceLifetime.Singleton, sp => new CaseInsensitiveResolver()); // how enable case sensitive
                });

در قسمت بعدی به Action‌ها و Function‌ها در OData میپردازیم.