نظرات مطالب
استفاده از Flashback Table در Oracle
با سلام
قابلیتی که اشاره کردید در SQL Server وجود ندارد (در SQL Server می‌توان آن را با Point In Time Restore هم انجام داد) به نظر می‌رسد که با استفاده از این حالت جدول شما حذف منطقی شده و از دسترسی عادی کاربران  خارج می‌گردد (این خوب و هم بد)
خوبی آن در این است که هر موقع خواستید می‌توانید اطلاعات آن را برگردانید
بدی آن هم در این است که فضای مربوط به این جدول هنگام حذف از دیسک آزاد نخواهد شد
نظرات اشتراک‌ها
دوراهی انتخاب NHibernate و Entityframework
1 - با EF Code first بدون نیاز به دیتابیس می‌تونید یک برنامه رو کامل کنید. (منهای بحث آزمایش)
- کد نهایی تمیزتر. چون کلاس‌ها را خودتان طراحی می‌کنید و توسط ابزارها به صورت خودکار تولید نمی‌شوند، کنترل بیشتر و نهایتا کیفیت بالاتری دارند.
- ساده است. درگیر نگهداری edmx modelها نخواهید بود. به روز رسانی بانک اطلاعاتی آن هم می‌تواند کاملا خودکار شود.
 
2 - دیتاست که کلا کارآیی بالایی نداره. اما ... نهایتا مطمئن هستم خروجی EF (به همراه تمام best practices لحاظ شده در آن) سرعت بالاتری از کلاس‌های دست ساز sql helper موجود در وب دارد. برای مثال سطح اول کش آن خیلی از کوئری‌ها را مجددا به بانک اطلاعاتی ارسال نمی‌کند. قابلیت اجرای به تعویق افتاده کوئری‌های لینک امکان تهیه کوئری‌های بسیار پیچیده را در یک رفت و برگشت مهیا می‌کند. کاری که با sql helperهای معمولی نیازی به چندبار رفت و برگشت دارد. قابلیت‌های lazy loading آن می‌تواند مصرف حافظه و بار سرور را درصورت استفاده صحیح کاهش دهد. کوئری‌های آن strongly typed و پارامتری هستند (تحت نظر کامپایلر + امنیت + سرعت (کوئری‌های پارامتری مانند رویه‌های ذخیره شده کش می‌شوند)). به صورت پیش فرض از تراکنش‌ها استفاده می‌کند و ... خیلی از الگوهای مفید دیگر که چندین سال باید وقت صرف کنید تا نمونه آن‌ها را پیاده سازی کنید. یعنی کار اصولی با بانک اطلاعاتی صرفا یک select ساده نیست که بر اساس آن کارآیی و یا بهتر بودن روشی را مشخص کنید.
مطالب
مفهوم READ_COMMITTED_SNAPSHOT در EF 6
مدتی است که حالت READ_COMMITTED_SNAPSHOT بسیار مورد توجه واقع شده:
- در سایت Stack overflow از آن استفاده می‌شود (^).
- در SQL Server Azure حالت پیش فرض ایجاد دیتابیس‌ها و تراکنش‌های جدید است  (^).
- در Entity framework 6 حالت پیش فرض تراکنش‌های ایجاد شده، قرار گرفته است  (^ ).

و ... در Oracle، تنها حالت مدیریت مسایل همزمانی است! (البته به نام MVCC، اما با همین عملکرد)


اما READ_COMMITTED_SNAPSHOT در SQL Server چیست و کاربرد آن کجا است؟

اگر استفاده گسترده و سنگینی از SQL Server داشته باشید، حتما به پیغام‌های خطای deadlock آن برخورده‌اید:
 Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. 
Rerun the transaction.
روش پیش فرض مدیریت مسایل همزمانی در SQL Server، حالت READ COMMITTED است. به این معنا که اگر در طی یک تراکنش مشغول به تغییر اطلاعاتی باشیم، سایر کاربران از خواندن نتیجه آن (اصطلاحا به آن Dirty read گفته می‌شود) منع خواهند شد؛ تا زمانیکه این تراکنش با موفقیت به پایان برسد. هرچند در این حالت سایر تراکنش‌ها امکان ویرایش یا حذف اطلاعات را خواهند داشت. به علاوه اگر در طی این تراکنش، اطلاعاتی خوانده شوند، سایر تراکنش‌ها تا پایان تراکنش جاری، قادر به تغییر این اطلاعات خوانده شده نخواهند بود (منشاء بروز خطاهای deadlock یاد شده در سیستم‌های پرترافیک).
در SQL Server 2005 برای بهبود مقیاس پذیری SQL Server و کاهش خطاهای deadlock، مکانیزم READ_COMMITTED_SNAPSHOT معرفی گشت.
به صورت خلاصه زمانیکه که تراکنش مورد نظر تحت حالت READ COMMITTED SNAPSHOT انجام می‌شود، optimistic reads and pessimistic writes خواهیم داشت (خواند‌ن‌های خوشبینانه و نوشتن‌های بدبینانه). در این حالت تضمین می‌شود که خواندن اطلاعات داخل یک تراکنش، شامل اطلاعات تغییر یافته توسط سایر تراکنش‌های همزمان نخواهد بود. همچنین زمانیکه در این بین، اطلاعاتی خوانده می‌شود، بر روی این اطلاعات برخلاف حالت READ COMMITTED قفل قرار داده نمی‌شود. بنابراین تراکنش‌هایی که درحال خواندن اطلاعات هستند، تراکنش‌های همزمانی را که در حال نوشتن اطلاعات می‌باشند، قفل نخواهد کرد و برعکس.


نحوه فعال سازی READ_COMMITTED_SNAPSHOT

فعال سازی  READ_COMMITTED_SNAPSHOT باید ابتدا در سطح یک بانک اطلاعاتی SQL Server انجام شود:
 ALTER DATABASE testDatabase SET ALLOW_SNAPSHOT_ISOLATION ON;
ALTER DATABASE testDatabase SET READ_COMMITTED_SNAPSHOT ON;
کاری که در اینجا انجام خواهد شد، ایجاد یک snapshot یا یک کپی فقط خواندنی، از بانک اطلاعاتی کاری شما می‌باشد. بنابراین در این حالت، زمانیکه یک عبارت Select را فراخوانی می‌کنید، این خواندن، از بانک اطلاعاتی فقط خواندنی تشکیل شده، صورت خواهد گرفت. اما تغییرات بر روی دیتابیس اصلی کاری درج شده و سپس این snapshot به روز می‌شود.
حالت READ_COMMITTED_SNAPSHOT خصوصا برای برنامه‌های وبی که تعداد بالایی Read در مقابل تعداد کمی Write دارند، به شدت بر روی کارآیی و بالا رفتن سرعت و مقیاس پذیری آن‌ها تاثیر خواهد داشت؛ به همراه حداقل تعداد deadlockهای حاصل شده.


در Entity framework وضعیت به چه صورتی است؟

EF از حالت پیش فرض مدیریت مسایل همزمانی در SQL Server یا همان حالت READ COMMITTED در زمان فراخوانی متد SaveChanges استفاده می‌کند.
در EF 6 این حالت پیش فرض به READ_COMMITTED_SNAPSHOT تغییر کرده است. البته همانطور که عنوان شد، پیشتر باید بانک اطلاعاتی را نیز جهت پذیرش این نوع تراکنش‌ها آماده ساخت.
اگر از نگارش‌های پایین‌تر از EF 6 استفاده می‌کنید، برای استفاده از حالت READ_COMMITTED_SNAPSHOT باید صراحتا IsolationLevel را مشخص ساخت:
using (var transactionScope =
  new TransactionScope(TransactionScopeOption.Required,
  new TransactionOptions { IsolationLevel= IsolationLevel.Snapshot }))
{
   // update some tables using entity framework  
   context.SaveChanges();  
   transactionScope.Complete();
}
نظرات مطالب
بررسی روش ارتقاء به NET Core 1.1.
چند نکته در مورد VS 2015 و به روز رسانی‌های جدید NET Core.

آخرین نگارشی را که می‌توانید بدون مشکل با VS 2015 اجرا کنید، 1.0.0-preview2-1-003177 است (واقع در پوشه‌ی C:\Program Files\dotnet\sdk). پس از آن، این سیستم از نگارش JSON ایی فعلی به XML ایی تغییر کرده‌است و ابزارهای آن فقط برای VS 2017 ارائه شده‌اند و VS 2015 از این لحاظ دیگر هیچ پشتیبانی نخواهد داشت (حتی NuGet 4.0 هم برای آن به روز رسانی نشده‌است).

اگر برای مثال SDK مربوط به .NET Core 1.1.1. را نصب کنید و سپس فایل global.json را به 1.0.1 تغییر دهید:
 C:\Users\Vahid>dotnet --version
1.0.1
پروژه با خطای ذیل روبرو شده و در VS 2015 باز نخواهد شد.
 The following error occurred attempting to run the project model server process (1.0.1).
Unable to start the process. No executable found matching command "dotnet-projectmodel-server"
بنابراین فایل global.json را که با VS 2017 منسوخ شده و حذف شده در نظر گرفته شده‌است، دیگر به روز رسانی نکنید.

اگر برای مثال SDK مربوط به .NET Core 1.1.1. را نصب کنید، درون VS 2015 قادر به Restore بسته‌های نیوگت نخواهید شد و با پیام خطای ذیل مواجه می‌شوید:
 \.vs\restore.dg(1,1): error MSB4025: The project file could not be loaded. Data at the root level is invalid. Line 1, position 1.
در این حالت از این پس سه راه را پیش رو خواهید داشت:
1- ارتقاء به VS 2017 و فراموش کردن VS 2015
2- استفاده از VS 2015 و بازیابی بسته‌ها از طریق خط فرمان (چون دیگر ابزارهای VS 2015 با نگارش جدید SDK سازگار نیستند)
 برای این منظور دقت کنید در پنجره‌ی output ویژوال استودیوی 2015، چه فرمانی صادر شده‌است که سبب بروز خطای فوق گردیده‌است:
 "C:\Program Files\dotnet\dotnet.exe" restore "D:\project1\.vs\restore.dg"
از طریق خط فرمان به پوشه‌‌های پروژه‌ها وارد شده (دکمه‌ی shift را نگه داشته و کلیک راست کنید. سپس گزینه‌ی open command window here را انتخاب نمائید) و دستور فوق را اجرا کنید. این دستور از این پس تنها در خط فرمان بدون مشکل اجرا می‌شود و نه در داخل VS 2015.
پس از آن پروژه بدون مشکل Build می‌شود (در داخل VS 2015).
3- و یا ... این SDK جدید 1.0.1 را حذف کنید از سیستم (اگر می‌خواهید با VS 2015 بدون دردسر کار کنید).


و یا کلا به VSCode مهاجرت کنید و VS کامل را فراموش کنید. VSCode با ابزارهای خط فرمان NET Core. کار می‌کند و در این حالت به سادگی می‌توان همواره آخرین نگارش NET Core. را مورد استفاده قرار داد؛ بدون نگرانی از سازگاری ابزارهای ویژوال استودیو با آن. چون اساسا هیچ نوع وابستگی به این ابزارها ندارد. همچنین حجم بسیار کمتری هم داشته و اگر با دریافت VS 2017 مشکل دارید، مهاجرت به VSCode انتخاب بسیار مناسبی است.


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


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


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

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

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

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

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

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

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


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

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

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

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

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

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

            prefix = prefix.ApplyCorrectYeKe();

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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


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

قسمت عمده مطالب فوق با وب فرم‌ها نیز یکی است. خصوصا توضیحات مرتبط با متد EnableSearchAutocomplete ذکر شده.
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LuceneSearch.WebForms.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>جستجو</title>
    <link href="Content/Site.css" rel="stylesheet" type="text/css" />
    <script src="Scripts/jquery-1.7.1.min.js" type="text/javascript"></script>
    <script src="Scripts/jquery.autocomplete.js" type="text/javascript"></script>
    <script src="Scripts/custom.js" type="text/javascript"></script>
</head>
<body dir="rtl">
    <h2>
        جستجو</h2>
    <form id="form1" runat="server">
    <div align="center">
        <asp:TextBox runat="server" dir="ltr" ID="term"></asp:TextBox>
        <br />
        جهت آزمایش lu را وارد نمائید
    </div>
    </form>
    <script type="text/javascript">
        EnableSearchAutocomplete('Search.ashx', 'Content/Images/bullet_shape.png');
    </script>
</body>
</html>

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

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

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

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

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

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

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


کدهای کامل مثال فوق را از اینجا می‌توانید دریافت کنید:
همچنین باید دقت داشت که پروژه MVC آن از نوع MVC4 است (VS2010) و فرض براین می‌باشد که IIS Express 7.5 را نیز پیشتر نصب کرده‌اید.
کلمه عبور فایل: dotnettips91
 
مطالب
Http Batch Processing در Asp.Net Web Api
بعد از معرفی نسخه‌ی 2 از Asp.Net Web Api و  پشتیبانی رسمی آن از OData بسیاری از توسعه دهندگان سیستم نفس راحتی کشیدند؛ زیرا از آن پس می‌توانستند علاوه بر امکانات جالب و مهمی که تحت پروتکل OData میسر بود، از سایر امکانات تعبیه شده در نسخه‌ی دوم web Api نیز استفاده نمایند. یکی از این قابلیت‌ها، مبحث مهم Batching Processing است که در طی این پست با آن آشنا خواهیم شد.
منظور از Batch Request این است که درخواست دهنده بتواند چندین درخواست (Multiple Http Request) را به صورت یک Pack جامع، در قالب فقط یک درخواست (Single Http Request) ارسال نماید و به همین روال تمام پاسخ‌های معادل درخواست ارسال شده را به صورت یک Pack دیگر دریافت کرده و آن را پردازش نماید. نوع درخواست نیز مهم نیست یعنی می‌توان در قالب یک Pack چندین درخواست از نوع Post و Get یا حتی Put و ... نیز داشته باشید.  بدیهی است که پیاده سازی این قابلیت در جای مناسب و در پروژه‌هایی با تعداد کاربران زیاد می‌تواند باعث بهبود چشمگیر کارآیی پروژه شود.

برای شروع همانند سایر مطالب می‌توانید از این پست جهت راه اندازی هاست سرویس‌های Web Api استفاده نمایید. برای فعال سازی قابلیت batching Request نیاز به یک MessageHandler داریم تا بتوانند درخواست‌هایی از این نوع را پردازش نمایند. خوشبختانه به صورت پیش فرض این Handler پیاده سازی شده‌است و ما فقط باید آن را با استفاده از متد MapHttpBatchRoute به بخش مسیر یابی (Route Handler) پروژه معرفی نماییم.
public class Startup
    {
        public void Configuration(IAppBuilder appBuilder)
        {
            var config = new HttpConfiguration();           
          
            config.Routes.MapHttpBatchRoute(
                routeName: "Batch",
                routeTemplate: "api/$batch",
                batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer));

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "Default",
                routeTemplate: "{controller}/{action}/{name}",
                defaults: new { name = RouteParameter.Optional }
            );

            config.Formatters.Clear();
            config.Formatters.Add(new JsonMediaTypeFormatter());
            config.Formatters.JsonFormatter.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented;
            config.Formatters.JsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            config.EnsureInitialized();          
            appBuilder.UseWebApi(config);
        }
    }

مهم‌ترین نکته‌ی آن استفاده از DefaultHttpBatchHandler و معرفی آن به بخش batchHandler مسیریابی است. کلاس DefaultHttpBatchHandler  برای وهله سازی نیاز به آبجکت سروری که سرویس‌های WebApi در آن هاست شده‌اند دارد که با دستور GlobalConfiguration.DefaultServer به آن دسترسی خواهید داشت. در صورتی که HttpServer خاص خود را دارید به صورت زیر عمل نمایید:
 var config = new HttpConfiguration();
 HttpServer server = new HttpServer(config);
تنظیمات بخش سرور به اتمام رسید. حال نیاز داریم بخش کلاینت را طوری طراحی نماییم که بتواند درخواست را به صورت دسته‌ای ارسال نماید. در زیر یک مثال قرار داده شده است:
using System.Net.Http;
using System.Net.Http.Formatting;

public class Program
    {
        private static void Main(string[] args)
        {
            string baseAddress = "http://localhost:8080";
            var client = new HttpClient();
            var batchRequest = new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/$batch")
            {
                Content = new MultipartContent("mixed")
                {                   
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Post, baseAddress + "/api/Book/Add")
                    {
                        Content = new ObjectContent<string>("myBook", new JsonMediaTypeFormatter())
                    }),                   
                    new HttpMessageContent(new HttpRequestMessage(HttpMethod.Get, baseAddress + "/api/Book/GetAll"))
                }
            };

            var batchResponse = client.SendAsync(batchRequest).Result;

            MultipartStreamProvider streamProvider = batchResponse.Content.ReadAsMultipartAsync().Result;
            foreach (var content in streamProvider.Contents)
            {
                var response = content.ReadAsHttpResponseMessageAsync().Result;                
            }
        }
    }
همان طور که می‌دانیم برای ارسال درخواست به سرویس Web Api باید یک نمونه از کلاس HttpRequestMessage وهله سازی شود سازنده‌ی آن به نوع HttpMethod اکشن نظیر (POST یا GET) و آدرس سرویس مورد نظر نیاز دارد. نکته‌ی مهم آن این است که خاصیت Content این درخواست باید از نوع MultipartContent و subType آن نیز باید mixed باشد. در بدنه‌ی آن نیز می‌توان تمام درخواست‌ها را به ترتیب و با استفاده از وهله سازی از کلاس HttpMessageContent تعریف کرد.
برای دریافت پاسخ این گونه درخواست‌ها نیز از متد الحاقی ReadAsMultipartAsync استفاده می‌شود که امکان پیمایش بر بدنه‌ی پیام دریافتی را می‌دهد.


مدیریت ترتیب درخواست ها

شاید این سوال به ذهن شما نیز خطور کرده باشد که ترتیب پردازش این گونه پیام‌ها چگونه خواهد بود؟ به صورت پیش فرض ترتیب اجرای درخواست‌ها حائز اهمیت است. بعنی تا زمانیکه پردازش درخواست اول به اتمام نرسد، کنترل اجرای برنامه، به درخواست بعدی نخواهد رسید که این مورد بیشتر زمانی رخ می‌دهد که قصد دریافت اطلاعاتی را داشته باشید که قبل از آن باید عمل Persist در پایگاه داده اتفاق بیافتد. اما در حالاتی غیر از این می‌توانید این گزینه را غیر فعال کرده تا تمام درخواست‌ها به صورت موازی پردازش شوند که به طور قطع کارایی آن نسبت به حالت قبلی بهینه‌تر است.
برای غیر فعال کردن گزینه‌ی ترتیب اجرای درخواست‌ها، به صورت زیر عمل نمایید:
 config.Routes.MapHttpBatchRoute(
                routeName: "WebApiBatch",
                routeTemplate: "api/$batch",
                batchHandler: new DefaultHttpBatchHandler(GlobalConfiguration.DefaultServer)
                {
                    ExecutionOrder = BatchExecutionOrder.NonSequential
                });
تفاوت آن فقط در مقدار دهی خاصیت ExecutionOrder به صورت NonSequential است.
مطالب
معرفی ASP.NET Identity

سیستم ASP.NET Membership بهمراه ASP.NET 2.0 در سال 2005 معرفی شد، و از آن زمان تا بحال تغییرات زیادی در چگونگی مدیریت احزار هویت و اختیارات کاربران توسط اپلیکیشن‌های وب بوجود آمده است. ASP.NET Identity نگاهی تازه است به آنچه که سیستم Membership هنگام تولید اپلیکیشن‌های مدرن برای وب، موبایل و تبلت باید باشد.

پیش زمینه: سیستم عضویت در ASP.NET


ASP.NET Membership

ASP.NET Membership طراحی شده بود تا نیازهای سیستم عضویت وب سایت‌ها را تامین کند، نیازهایی که در سال 2005 رایج بود و شامل مواردی مانند مدل احراز هویت فرم، و یک پایگاه داده SQL Server برای ذخیره اطلاعات کاربران و پروفایل هایشان می‌شد. امروزه گزینه‌های بسیار بیشتری برای ذخیره داده‌های وب اپلیکیشن‌ها وجود دارد، و اکثر توسعه دهندگان می‌خواهند از اطلاعات شبکه‌های اجتماعی نیز برای احراز هویت و تعیین سطوح دسترسی کاربرانشان استفاده کنند. محدودیت‌های طراحی سیستم ASP.NET Membership گذر از این تحول را دشوار می‌کند:
  • الگوی پایگاه داده آن برای SQL Server طراحی شده است، و قادر به تغییرش هم نیستید. می‌توانید اطلاعات پروفایل را اضافه کنید، اما تمام داده‌ها در یک جدول دیگر ذخیره می‌شوند، که دسترسی به آنها نیز مشکل‌تر است، تنها راه دسترسی Profile Provider API خواهد بود.
  • سیستم تامین کننده (Provider System) امکان تغییر منبع داده‌ها را به شما می‌دهد، مثلا می‌توانید از بانک‌های اطلاعاتی MySQL یا Oracle استفاده کنید. اما تمام سیستم بر اساس پیش فرض هایی طراحی شده است که تنها برای بانک‌های اطلاعاتی relational درست هستند. می‌توانید تامین کننده (Provider) ای بنویسید که داده‌های سیستم عضویت را در منبعی به غیر از دیتابیس‌های relational ذخیره می‌کند؛ مثلا Windows Azure Storage Tables. اما در این صورت باید مقادیر زیادی کد بنویسید. مقادیر زیادی هم  System.NotImplementedException باید بنویسید، برای متد هایی که به دیتابیس‌های NoSQL مربوط نیستند.
  • از آنجایی که سیستم ورود/خروج سایت بر اساس مدل Forms Authentication کار می‌کند، سیستم عضویت نمی‌تواند از OWIN استفاده کند. OWIN شامل کامپوننت هایی برای احراز هویت است که شامل سرویس‌های خارجی هم می‌شود (مانند Microsoft Accounts, Facebook, Google, Twitter). همچنین امکان ورود به سیستم توسط حساب‌های کاربری سازمانی (Organizational Accounts) نیز وجود دارد مانند Active Directory و Windows Azure Active Directory. این کتابخانه از OAuth 2.0، JWT و CORS نیز پشتیبانی می‌کند.

ASP.NET Simple Membership

ASP.NET simple membership به عنوان یک سیستم عضویت، برای فریم ورک Web Pages توسعه داده شد. این سیستم با WebMatrix و Visual Studio 2010 SP1 انتشار یافت. هدف از توسعه این سیستم، آسان کردن پروسه افزودن سیستم عضویت به یک اپلیکیشن Web Pages بود.
این سیستم پروسه کلی کار را آسان‌تر کرد، اما هنوز مشکلات ASP.NET Membership را نیز داشت. محدودیت هایی نیز وجود دارند:
  • ذخیره داده‌های سیستم عضویت در بانک‌های اطلاعاتی non-relational مشکل است.
  • نمی توانید از آن در کنار OWIN استفاده کنید.
  • با فراهم کننده‌های موجود ASP.NET Membership بخوبی کار نمی‌کند. توسعه پذیر هم نیست.

ASP.NET Universal Providers   

ASP.NET Universal Providers برای ذخیره سازی اطلاعات سیستم عضویت در Windows Azure SQL Database توسعه پیدا کردند. با SQL Server Compact هم بخوبی کار می‌کنند. این تامین کننده‌ها بر اساس Entity Framework Code First ساخته شده بودند و بدین معنا بود که داده‌های سیستم عضویت را می‌توان در هر منبع داده ای که توسط EF پشتیبانی می‌شود ذخیره کرد. با انتشار این تامین کننده‌ها الگوی دیتابیس سیستم عضویت نیز بسیار سبک‌تر و بهتر شد. اما این سیستم بر پایه زیر ساخت ASP.NET Membership نوشته شده است، بنابراین محدودیت‌های پیشین مانند محدودیت‌های SqlMembershipProvider هنوز وجود دارند. به بیان دیگر، این سیستم‌ها همچنان برای بانک‌های اطلاعاتی relational طراحی شده اند، پس سفارشی سازی اطلاعات کاربران و پروفایل‌ها هنوز مشکل است. در آخر آنکه این تامین کننده‌ها هنوز از مدل احراز هویت فرم استفاده می‌کنند.


ASP.NET Identity

همانطور که داستان سیستم عضویت ASP.NET طی سالیان تغییر و رشد کرده است، تیم ASP.NET نیز آموخته‌های زیادی از بازخورد‌های مشتریان شان بدست آورده اند.
این پیش فرض که کاربران شما توسط یک نام کاربری و کلمه عبور که در اپلیکیشن خودتان هم ثبت شده است به سایت وارد خواهند شد، دیگر معتبر نیست. دنیای وب اجتماعی شده است. کاربران از طریق وب سایت‌ها و شبکه‌های اجتماعی متعددی با یکدیگر در تماس هستند، خیلی از اوقت بصورت زنده! شبکه هایی مانند Facebook و Twitter.
همانطور که توسعه نرم افزار‌های تحت وب رشد کرده است، الگو‌ها و مدل‌های پیاده سازی نیز تغییر و رشد کرده اند. امکان Unit Testing روی کد اپلیکیشن‌ها، یکی از مهم‌ترین دلواپسی‌های توسعه دهندگان شده است. در سال 2008 تیم ASP.NET فریم ورک جدیدی را بر اساس الگوی (Model-View-Controller (MVC اضافه کردند. هدف آن کمک به توسعه دهندگان، برای تولید برنامه‌های ASP.NET با قابلیت Unit Testing بهتر بود. توسعه دهندگانی که می‌خواستند کد اپلیکیشن‌های خود را Unit Test کنند، همین امکان را برای سیستم عضویت نیز می‌خواستند.

با در نظر گرفتن تغییراتی که در توسعه اپلیکیشن‌های وب بوجود آمده ASP.NET Identity با اهداف زیر متولد شد:
  • یک سیستم هویت واحد (One ASP.NET Identity system)
    • سیستم ASP.NET Identity می‌تواند در تمام فریم ورک‌های مشتق از ASP.NET استفاده شود. مانند ASP.NET MVC, Web Forms, Web Pages, Web API و SignalR 
    • از این سیستم می‌توانید در تولید اپلیکیشن‌های وب، موبایل، استور (Store) و یا اپلیکیشن‌های ترکیبی استفاده کنید. 
  • سادگی تزریق داده‌های پروفایل درباره کاربران
    • روی الگوی دیتابیس برای اطلاعات کاربران و پروفایل‌ها کنترل کامل دارید. مثلا می‌توانید به سادگی یک فیلد، برای تاریخ تولد در نظر بگیرید که کاربران هنگام ثبت نام در سایت باید آن را وارد کنند.
  • کنترل ذخیره سازی/واکشی اطلاعات 
    • بصورت پیش فرض ASP.NET Identity تمام اطلاعات کاربران را در یک دیتابیس ذخیره می‌کند. تمام مکانیزم‌های دسترسی به داده‌ها توسط EF Code First کار می‌کنند.
    • از آنجا که روی الگوی دیتابیس، کنترل کامل دارید، تغییر نام جداول و یا نوع داده فیلد‌های کلیدی و غیره ساده است.
    • استفاده از مکانیزم‌های دیگر برای مدیریت داده‌های آن ساده است، مانند SharePoint, Windows Azure Storage Table و دیتابیس‌های NoSQL.
  • تست پذیری
    • ASP.NET Identity تست پذیری اپلیکیشن وب شما را بیشتر می‌کند. می‌توانید برای تمام قسمت هایی که از ASP.NET Identity استفاده می‌کنند تست بنویسید.
  • تامین کننده نقش (Role Provider)
    • تامین کننده ای وجود دارد که به شما امکان محدود کردن سطوح دسترسی بر اساس نقوش را می‌دهد. بسادگی می‌توانید نقش‌های جدید مانند "Admin" بسازید و بخش‌های مختلف اپلیکیشن خود را محدود کنید.
  • Claims Based
    • ASP.NET Identity از امکان احراز هویت بر اساس Claims نیز پشتیبانی می‌کند. در این مدل، هویت کاربر بر اساس دسته ای از اختیارات او شناسایی می‌شود. با استفاده از این روش توسعه دهندگان برای تعریف هویت کاربران، آزادی عمل بیشتری نسبت به مدل Roles دارند. مدل نقش‌ها تنها یک مقدار منطقی (bool) است؛ یا عضو یک نقش هستید یا خیر، در حالیکه با استفاده از روش Claims می‌توانید اطلاعات بسیار ریز و دقیقی از هویت کاربر در دست داشته باشید.
  • تامین کنندگان اجتماعی
    • به راحتی می‌توانید از تامین کنندگان دیگری مانند Microsoft, Facebook, Twitter, Google و غیره استفاده کنید و اطلاعات مربوط به کاربران را در اپلیکیشن خود ذخیره کنید.
  • Windows Azure Active Directory
    • برای اطلاعات بیشتر به این لینک مراجعه کنید.
  • یکپارچگی با OWIN
    • ASP.NET Identity بر اساس OWIN توسعه پیدا کرده است، بنابراین از هر میزبانی که از OWIN پشتیبانی می‌کند می‌توانید استفاده کنید. همچنین هیچ وابستگی ای به System.Web وجود ندارد. ASP.NET Identity یک فریم ورک کامل و مستقل برای OWIN است و می‌تواند در هر اپلیکیشنی که روی OWIN میزبانی شده استفاده شود.
    • ASP.NET Identity از OWIN برای ورود/خروج کاربران در سایت استفاده می‌کند. این بدین معنا است که بجای استفاده از Forms Authentication برای تولید یک کوکی، از OWIN CookieAuthentication استفاده می‌شود.
  • پکیج NuGet
    • ASP.NET Identity در قالب یک بسته NuGet توزیع می‌شود. این بسته در قالب پروژه‌های ASP.NET MVC, Web Forms و Web API که با Visual Studio 2013 منتشر شدند گنجانده شده است.
    • توزیع این فریم ورک در قالب یک بسته NuGet این امکان را به تیم ASP.NET می‌دهد تا امکانات جدیدی توسعه دهند، باگ‌ها را برطرف کنند و نتیجه را بصورت چابک به توسعه دهندگان عرضه کنند.

شروع کار با ASP.NET Identity

ASP.NET Identity در قالب پروژه‌های ASP.NET MVC, Web Forms, Web API و SPA که بهمراه Visual Studio 2013 منتشر شده اند استفاده می‌شود. در ادامه به اختصار خواهیم دید که چگونه ASP.NET Identity کار می‌کند.
  1. یک پروژه جدید ASP.NET MVC با تنظیمات Individual User Accounts بسازید.

  2. پروژه ایجاد شده شامل سه بسته می‌شود که مربوط به ASP.NET Identity هستند: 

  • Microsoft.AspNet.Identity.EntityFramework این بسته شامل پیاده سازی ASP.NET Identity با Entity Framework می‌شود، که تمام داده‌های مربوطه را در یک دیتابیس SQL Server ذخیره می‌کند.
  • Microsoft.AspNet.Identity.Core این بسته محتوی تمام interface‌‌های ASP.NET Identity است. با استفاده از این بسته می‌توانید پیاده سازی دیگری از ASP.NET Identity بسازید که منبع داده متفاوتی را هدف قرار می‌دهد. مثلا Windows Azure Storage Table و دیتابیس‌های NoSQL.
  • Microsoft.AspNet.Identity.OWIN این بسته امکان استفاده از احراز هویت OWIN را در اپلیکیشن‌های ASP.NET فراهم می‌کند. هنگام تولید کوکی‌ها از OWIN Cookie Authentication استفاده خواهد شد.
اپلیکیشن را اجرا کرده و روی لینک Register کلیک کنید تا یک حساب کاربری جدید ایجاد کنید.

هنگامیکه بر روی دکمه‌ی Register کلیک شود، کنترلر Account، اکشن متد Register را فراخوانی می‌کند تا حساب کاربری جدیدی با استفاده از ASP.NET Identity API ساخته شود.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            AddErrors(result);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}

اگر حساب کاربری با موفقیت ایجاد شود، کاربر توسط فراخوانی متد SignInAsync به سایت وارد می‌شود.

[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Register(RegisterViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = new ApplicationUser() { UserName = model.UserName };
        var result = await UserManager.CreateAsync(user, model.Password);
        if (result.Succeeded)
        {
            await SignInAsync(user, isPersistent: false);
            return RedirectToAction("Index", "Home");
        }
        else
        {
            AddErrors(result);
        }
    }

    // If we got this far, something failed, redisplay form
    return View(model);
}
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
    AuthenticationManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);

    var identity = await UserManager.CreateIdentityAsync(
       user, DefaultAuthenticationTypes.ApplicationCookie);

    AuthenticationManager.SignIn(
       new AuthenticationProperties() { 
          IsPersistent = isPersistent 
       }, identity);
}

از آنجا که ASP.NET Identity و OWIN Cookie Authentication هر دو Claims-based هستند، فریم ورک، انتظار آبجکتی از نوع ClaimsIdentity را خواهد داشت. این آبجکت تمامی اطلاعات لازم برای تشخیص هویت کاربر را در بر دارد. مثلا اینکه کاربر مورد نظر به چه نقش هایی تعلق دارد؟ و اطلاعاتی از این قبیل. در این مرحله می‌توانید Claim‌‌های بیشتری را به کاربر بیافزایید.

کلیک کردن روی لینک Log off در سایت، اکشن متد LogOff در کنترلر Account را اجرا می‌کند.

// POST: /Account/LogOff
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult LogOff()
{
    AuthenticationManager.SignOut();
    return RedirectToAction("Index", "Home");
}

همانطور که مشاهده می‌کنید برای ورود/خروج کاربران از AuthenticationManager استفاده می‌شود که متعلق به OWIN است. متد SignOut همتای متد FormsAuthentication.SignOut است.


کامپوننت‌های ASP.NET Identity

تصویر زیر اجزای تشکیل دهنده ASP.NET Identity را نمایش می‌دهد. بسته هایی که با رنگ سبز نشان داده شده اند سیستم کلی ASP.NET Identity را می‌سازند. مابقی بسته‌ها وابستگی هایی هستند که برای استفاده از ASP.NET Identity در اپلیکیشن‌های ASP.NET لازم اند.

دو پکیج دیگر نیز وجود دارند که به آنها اشاره نشد:

  • Microsoft.Security.Owin.Cookies این بسته امکان استفاده از مدل احراز هویت مبتنی بر کوکی (Cookie-based Authentication) را فراهم می‌کند. مدلی مانند سیستم ASP.NET Forms Authentication.
  • EntityFramework که نیازی به معرفی ندارد.


مهاجرت از Membership به ASP.NET Identity

تیم ASP.NET و مایکروسافت هنوز راهنمایی رسمی، برای این مقوله ارائه نکرده اند. گرچه پست‌های وبلاگ‌ها و منابع مختلفی وجود دارند که از جنبه‌های مختلفی به این مقوله پرداخته اند. امیدواریم تا در آینده نزدیک مایکروسافت راهنمایی‌های لازم را منتشر کند، ممکن است ابزار و افزونه هایی نیز توسعه پیدا کنند. اما در یک نگاه کلی می‌توان گفت مهاجرت بین این دو فریم ورک زیاد ساده نیست. تفاوت‌های فنی و ساختاری زیادی وجود دارند، مثلا الگوی دیتابیس‌ها برای ذخیره اطلاعات کاربران، مبتنی بودن بر فریم ورک OWIN و غیره. اگر قصد اجرای پروژه جدیدی را دارید پیشنهاد می‌کنم از فریم ورک جدید مایکروسافت ASP.NET Identity استفاده کنید.


قدم‌های بعدی

در این مقاله خواهید دید چگونه اطلاعات پروفایل را اضافه کنید و چطور از ASP.NET Identity برای احراز هویت کاربران توسط Facebook و Google استفاده کنید.
پروژه نمونه ASP.NET Identity می‌تواند مفید باشد. در این پروژه نحوه کارکردن با کاربران و نقش‌ها و همچنین نیازهای مدیریتی رایج نمایش داده شده. 
مطالب
پیاده سازی Full-Text Search با SQLite و EF Core - قسمت اول - ایجاد و به روز رسانی جدول مجازی FTS
SQLite به صورت توکار از full-text search پشتیبانی می‌کند؛ اما اهمیت آن چیست؟ هدف از full-text search، انجام جستجوهای بسیار سریع، در ستون‌های متنی یک جدول بانک اطلاعاتی است. بدون وجود یک چنین قابلیتی، عموما برای انجام اینکار از دستور LIKE استفاده می‌شود:
SELECT Title FROM Book WHERE Desc LIKE '%cat%';
کار این کوئری، یافتن ردیف‌هایی است که در آن واژه‌ی cat وجود دارند. مشکل این روش، عدم استفاده‌ی از ایندکس‌ها و اصطلاحا انجام یک full table scan است. با استفاده از دستور LIKE، باید تک تک ردیف‌های بانک اطلاعاتی برای یافتن واژه‌ی مدنظر، اسکن و بررسی شوند و انجام اینکار با بالا رفتن تعداد رکوردهای بانک اطلاعاتی، کندتر و کندتر خواهد شد. برای رفع این مشکل، راه حلی به نام full-text search ارائه شده‌است که کار آن ایندکس کردن تمام ستون‌های متنی مدنظر و سپس جستجوی بر روی این ایندکس از پیش آماده شده‌است.
معادل دستور LIKE در کوئری فوق، متد Contains در EF Core است:
var cats = context.Chapters.Where(item => item.Text.Contains("cat")).ToList();
بنابراین هدف از این سری، جایگزین کردن متدهای الحاقی Contains ، StartsWith و EndsWith، با روشی بسیار سریعتر است.

یک نکته: کوئری فوق توسط EF Core و به همراه پروایدر SQLite آن، به صورت زیر ترجمه می‌شود (که آن نیز یک full table scan است):
SELECT  "c"."Text" FROM "Chapters" AS "c" WHERE ('cat' = '') OR (instr("c"."Text", 'cat') > 0)
اما دقیقا دستور Like را به همراه متدهای الحاقی StartsWith و یا EndsWith می‌توان مشاهده کرد:
var cats = context.Chapters.Where(item => item.Text.StartsWith("cat")).ToList();
// SELECT "c"."Text", FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE 'cat%')
var cats = context.Chapters.Where(item => item.Text.EndsWith("cat")).ToList();
// SELECT "c"."Title" FROM "Chapters" AS "c" WHERE "c"."Text" IS NOT NULL AND ("c"."Text" LIKE '%cat')


معرفی موجودیت‌های مثال این سری

هدف اصلی ما، ایندکس کردن full-text ستون‌های متنی عنوان و متن جدول بانک اطلاعاتی متناظر با Chapter است:
using System.Collections.Generic;

namespace EFCoreSQLiteFTS.Entities
{
    public class User
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<Chapter> Chapters { get; set; }
    }

    public class Chapter
    {
        public int Id { get; set; }

        public string Title { get; set; }

        public string Text { get; set; }

        public User User { get; set; }
        public int UserId { get; set; }
    }
}


ایجاد جدول مجازی Full-text search

زمانیکه عملیات Migration را در EF Core فعال و اجرا می‌کنیم، دو جدول متناظر با Chapter و User ایجاد می‌شوند. اما برای کار با full-text search، نیاز به ایجاد جداول دیگری است، تا کار نگهداری ایندکس‌های تشکیل شده‌ی از ستون‌های متنی مدنظر ما را انجام دهند. به این نوع جداول در SQLite، جدول مجازی و یا virtual table گفته می‌شود. یک virtual table در اصل تفاوتی با یک جدول معمولی ندارد. تفاوت در اینجا است که منطق دسترسی به این جدول مجازی از موتور FTS5 مربوط به SQLite باید عبور کند. تاکنون نگارش‌های مختلفی از موتور full-text search آن منتشر شده‌اند؛ مانند FTS3 ، FTS4 و غیره که آخرین نگارش آن، FTS5 می‌باشد و به همراه توزیعی که مایکروسافت ارائه می‌دهد، وجود دارد و نیازی به تنظیمات خاصی ندارد.
در اینجا روش ایجاد یک جدول مجازی جدید Chapters_FTS را مشاهده می‌کنید:
CREATE VIRTUAL TABLE "Chapters_FTS"
USING fts5("Text", "Title", content="Chapters", content_rowid="Id")
جدول مجازی، با اجرای دستور CREATE VIRTUAL TABLE  ایجاد می‌شود و USING fts5 آن به معنای استفاده‌ی از موتور full-text search نگارش پنجم آن است. سپس لیست ستون‌هایی را که می‌خواهیم ایندکس کنیم، ذکر می‌شوند؛ مانند Text و Title در اینجا. همانطور که مشاهده می‌کنید، فقط نام این ستون‌ها قابل تعریف هستند و هیچ نوع اطلاعات اضافه‌تری را نمی‌توان ذکر کرد.
ذکر پارامتر "content="Chapters اختیاری بوده و به این معنا است که نیازی نیست تا اصل داده‌های مرتبط با ستون‌های ذکر شده نیز ذخیره شوند و آن‌ها را می‌توان از جدول Chapters، بازیابی کرد. در این حالت برای برقراری ارتباط بین این جدول مجازی و جدول chapters، پارامتر "content_rowid="Id مقدار دهی شده‌است. content_rowid به primary key جدول content اشاره می‌کند. ذکر هر دوی این پارامترها اختیاری بوده و در صورت تنظیم، حجم نهایی بانک اطلاعاتی را کاهش می‌دهند. چون در این حالت دیگری نیازی به ذخیره سازی جداگانه‌ی اصل اطلاعات متناظر با ایندکس‌های FTS نیست.

اکنون که با دستور ایجاد جدول مجازی FTS آشنا شدیم، روش ایجاد آن در برنامه‌های مبتنی بر EF Core نیز دقیقا به همین صورت است:
private static void createFtsTables(ApplicationDbContext context)
{
    // For SQLite FTS
    // Note: This can be added to the `protected override void Up(MigrationBuilder migrationBuilder)` method too.
    context.Database.ExecuteSqlRaw(@"CREATE VIRTUAL TABLE IF NOT EXISTS ""Chapters_FTS""
    USING fts5(""Text"", ""Title"", content=""Chapters"", content_rowid=""Id"");");
}
فقط کافی است در ابتدای اجرای برنامه با استفاده از متد ExecuteSqlRaw، عبارت SQL متناظر با ایجاد جدول مجازی را اجرا کنیم. این یک روش ایجاد این نوع جداول است؛ روش دیگر آن، قرار دادن همین قطعه کد در متد "protected override void Up(MigrationBuilder migrationBuilder)" مربوط به کلاس‌های ایجاد شده‌ی توسط عملیات Migration است.


به روز رسانی اطلاعات جدول مجازی FTS، توسط تریگرها

پس از اجرای دستورCREATE VIRTUAL TABLE  فوق، SQLite پنج جدول را به صورت خودکار ایجاد می‌کند که در تصویر زیر قابل مشاهده هستند:


البته ما مستقیما با این جداول کار نخواهیم کرد و این جداول برای نگهداری اطلاعات ایندکس‌های full-text موتور FTS5، توسط خود SQLite نگهداری و مدیریت می‌شوند.

اما ... نکته‌ی مهم اینجا است که جدول مجازی Chapters_FTS، هرچند به جدول اصلی Chapters توسط پارامتر content آن متصل شده‌است، اما تغییرات آن‌را ردیابی نمی‌کند. یعنی هر نوع insert/update/delete ای که در جدول اصلی Chapters رخ می‌دهد، سبب ایندکس شدن اطلاعات جدید آن در جدول مجازی Chapters_FTS نمی‌شود و برای اینکار باید اطلاعات را مستقیما در جدول Chapters_FTS درج کرد.
روش پیشنهاد شده‌ی در مستندات رسمی آن، استفاده از تریگرهای پس از درج اطلاعات، پس از حذف اطلاعات و پس از به روز رسانی اطلاعات به صورت زیر است:
-- Create a table. And an external content fts5 table to index it.
CREATE TABLE tbl(a INTEGER PRIMARY KEY, b, c);
CREATE VIRTUAL TABLE fts_idx USING fts5(b, c, content='tbl', content_rowid='a');

-- Triggers to keep the FTS index up to date.
CREATE TRIGGER tbl_ai AFTER INSERT ON tbl BEGIN
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
CREATE TRIGGER tbl_ad AFTER DELETE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
END;
CREATE TRIGGER tbl_au AFTER UPDATE ON tbl BEGIN
  INSERT INTO fts_idx(fts_idx, rowid, b, c) VALUES('delete', old.a, old.b, old.c);
  INSERT INTO fts_idx(rowid, b, c) VALUES (new.a, new.b, new.c);
END;
در اینجا ابتدا روش ایجاد یک جدول جدید و سپس ایجاد یک جدول مجازی FTS را از روی آن مشاهده می‌کنید.
در ادامه سه تریگر بر روی جدول اصلی که ما به صورت متداولی با آن در برنامه‌های خود کار می‌کنیم، تعریف شده‌اند. این تریگرها کار insert اطلاعات را در جدول مجازی ایجاد شده، به صورت خودکار انجام می‌دهند.
همانطور که مشاهده می‌کنید، یک rowid نیز در اینجا قابل تعریف است؛ rowid، ستون مخفی یک جدول مجازی FTS است و هرچند در حین ایجاد، آن‌را ذکر نمی‌کنیم، اما جزئی از ساختار آن بوده و قابل کوئری گرفتن است.

نکته‌ی مهم: به فرمت دستورات به روز رسانی جدول مجازی FTS دقت کنید. حتی در حالت تریگرهای update و یا delete نیز در اینجا دستور insert، مشاهده می‌شوند. این فرمت دقیقا باید به همین نحو رعایت شود؛ در غیراینصورت اگر از دستورات delete و یا update معمولی بر روی این جدول مجازی استفاده کنید، دفعه‌ی بعدی که برنامه را اجرا می‌کنید، خطای «این بانک اطلاعاتی تخریب شده‌است» را مشاهده کرده (database disk image is malformed) و دیگر نمی‌توانید با فایل بانک اطلاعاتی خود کار کنید.


به روز رسانی اطلاعات جدول مجازی FTS توسط EF Core

روش تعریف تریگرهای یاد شده، مستقل از EF Core بوده و راسا توسط خود بانک اطلاعاتی مدیریت می‌شود. بنابراین فقط کافی است دستور CREATE TRIGGER را به همان نحوی که عنوان شد، توسط متد ExecuteSqlRaw اجرا کنیم تا جزئی از ساختار بانک اطلاعاتی شوند؛ اما ... این روش برای برنامه‌هایی با متن‌های پیچیده کارآیی ندارد. برای مثال فرض کنید اطلاعات اصلی شما با فرمت HTML است. ایندکس ایجاد شده، تگ‌های HTML را حذف نمی‌کند و آن‌ها را نیز ایندکس می‌کند که نه تنها سبب بالا رفتن حجم بانک اطلاعاتی می‌شود، بلکه زمانیکه ما قصد جستجویی را بر روی اطلاعات HTML ای داریم، اساسا کاری به تگ‌های آن نداشته و هدف اصلی ما، متن‌های درج شده‌ی در آن است. نمونه‌ی دیگر آن داشتن اطلاعاتی با «اعراب» است و یا شاید نیاز به یک‌دست سازی ی و ک فارسی وجود داشته باشد. به این نوع عملیات، «نرمال سازی متن» گفته می‌شود و با روش تریگرهای فوق قابل تعریف و مدیریت نیست. به همین جهت می‌توان از روش پیشنهادی زیر استفاده کرد:

الف) یافتن لیست اطلاعات تغییر یافته‌ی حاصل از اعمال insert/update/delete
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class EFChangeTrackerExtensions
    {
        public static List<(EntityState State, TEntity NewEntity, TEntity OldEntity)>
                    GetChangedEntities<TEntity>(this DbContext dbContext) where TEntity : class, new()
        {
            if (!dbContext.ChangeTracker.AutoDetectChangesEnabled)
            {
                // ChangeTracker.Entries() only calls `Try`DetectChanges() behind the scene.
                dbContext.ChangeTracker.DetectChanges();
            }

            return dbContext.ChangeTracker.Entries<TEntity>()
                    .Where(IsEntityChanged)
                    .Select(entityEntry => (entityEntry.State,
                                            entityEntry.Entity,
                                            createWithValues<TEntity>(entityEntry.OriginalValues)))
                    .ToList();
        }

        private static bool IsEntityChanged(EntityEntry entry)
        {
            return entry.State == EntityState.Added
                    || entry.State == EntityState.Modified
                    || entry.State == EntityState.Deleted
                    || entry.References.Any(r => r.TargetEntry?.Metadata.IsOwned() == true && IsEntityChanged(r.TargetEntry));
        }

        private static T createWithValues<T>(PropertyValues values) where T : new()
        {
            var entity = new T();
            foreach (var prop in values.Properties)
            {
                var value = values[prop.Name];
                if (value is PropertyValues)
                {
                    throw new NotSupportedException("nested complex object");
                }
                else
                {
                    prop.PropertyInfo.SetValue(entity, value);
                }
            }
            return entity;
        }
    }
}
هدف از متد GetChangedEntities فوق این است که با استفاده از سیستم tracking، نوع عملیات انجام شده و همچنین اصل موجودیت‌ها را پیش و پس از تغییر، بتوان لیست کرد و سپس بر اساس آن‌ها، جدول مجازی FTS را به روز رسانی نمود.
علت نیاز به نمونه‌ی اصل و سپس تغییر کرده‌ی موجودیت‌ها، به نحوه‌ی تعریف تریگرهای مخصوص به به روز رسانی FTS بر می‌گردد. اگر دقت کرده باشید در این تریگرها، new.a و همچنین old.a را داریم که برای شبیه سازی آن‌ها دقیقا باید به اطلاعات یک رکورد، در پیش و پس از به روز رسانی آن، دسترسی یافت.

ب) تعریف تریگرهای SQL توسط سیستم tracking؛ به همراه عملیات نرمال سازی اطلاعات
using System.Collections.Generic;
using System.Data;
using System.Text.RegularExpressions;
using EFCoreSQLiteFTS.Entities;
using Microsoft.EntityFrameworkCore;

namespace EFCoreSQLiteFTS.DataLayer
{
    public static class FtsNormalizer
    {
        private static readonly Regex _htmlRegex = new Regex("<[^>]*>", RegexOptions.Compiled);

        public static string NormalizeText(this string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return string.Empty;
            }

            // Remove html tags
            text = _htmlRegex.Replace(text, string.Empty);

            // TODO: add other normalizers here, such as `remove diacritics`, `fix Persian Ye-Ke` and so on ...

            return text;
        }
    }

    public static class UpdateFtsTriggers
    {
        public static void UpdateChapterFTS(
            this DbContext context,
            List<(EntityState State, Chapter NewEntity, Chapter OldEntity)> changedChapters)
        {
            var database = context.Database;

            try
            {
                database.BeginTransaction(IsolationLevel.ReadCommitted);

                foreach (var (State, NewEntity, OldEntity) in changedChapters)
                {
                    var chapterNew = NewEntity;
                    var chapterOld = OldEntity;

                    var normalizedNewText = chapterNew.Text.NormalizeText();
                    var normalizedOldText = chapterOld.Text.NormalizeText();
                    var normalizedNewTitle = chapterNew.Title.NormalizeText();
                    var normalizedOldTitle = chapterOld.Title.NormalizeText();
                    switch (State)
                    {
                        case EntityState.Added:
                            if (shouldSkipAddedChapter(chapterNew))
                            {
                                continue;
                            }
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Modified:
                            if (shouldSkipModifiedChapter(chapterNew, chapterOld))
                            {
                                continue;
                            }
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                                        chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            database.ExecuteSqlRaw("INSERT INTO Chapters_FTS(rowid, Text, Title) values({0}, {1}, {2});",
                                    chapterNew.Id, normalizedNewText, normalizedNewTitle);
                            break;
                        case EntityState.Deleted:
                            // This format is important! Otherwise we will get `SQLite Error 11: 'database disk image is malformed'.` error!
                            database.ExecuteSqlRaw(@"INSERT INTO Chapters_FTS(Chapters_FTS, rowid, Text, Title)
                                                        VALUES('delete', {0}, {1}, {2}); ",
                                    chapterOld.Id, normalizedOldText, normalizedOldTitle);
                            break;
                    }
                }
            }
            finally
            {
                database.CommitTransaction();
            }
        }

        private static bool shouldSkipAddedChapter(Chapter chapterNew)
        {
            // TODO: add your logic to avoid indexing this item
            return false;
        }

        private static bool shouldSkipModifiedChapter(Chapter chapterNew, Chapter chapterOld)
        {
            // TODO: add your logic to avoid indexing this item
            return chapterNew.Text == chapterOld.Text && chapterNew.Title == chapterOld.Title;
        }
    }
}
در اینجا نحوه‌ی تعریف متد UpdateChapterFTS را مشاهده می‌کند که اطلاعات خودش را از متد GetChangedEntities دریافت کرده و سپس یکی یکی آن‌ها را در جدول مجازی FTS، با فرمت مخصوصی که عنوان شد (دقیقا متناظر با فرمت تریگرهای مستندات رسمی FTS)، درج می‌کند.
همچنین در اینجا متد NormalizeText را نیز مشاهده می‌کند که بر روی ستون‌های متنی اعمال شده‌است. کار آن پاکسازی تگ‌های یک متن HTML ای است و نگهداری اطلاعات صرفا متنی آن. در اینجا اگر نیاز بود می‌توان منطق‌های پاکسازی اطلاعات دیگری را نیز اعمال کرد.
اکنون که این اطلاعات به صورت پاکسازی شده در جدول مجازی درج می‌شوند، زمانیکه بر روی آن‌ها جستجویی صورت می‌گیرد، دیگر شامل جستجوی بر روی تگ‌های HTML ای نیست و دقت بسیار بیشتری دارد.

ج) اتصال به سیستم
پس از تعریف متدهای الحاقی GetChangedEntities و UpdateChapterFTS، اکنون روش اتصال آن‌ها به DbContext برنامه، با بازنویسی متد SaveChanges آن است:
namespace EFCoreSQLiteFTS.DataLayer
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions options)
            : base(options)
        {
        }

        public DbSet<Chapter> Chapters { get; set; }
        public DbSet<User> Users { get; set; }

        public override int SaveChanges()
        {
            var changedChapters = this.GetChangedEntities<Chapter>();

            this.ChangeTracker.AutoDetectChangesEnabled = false; // for performance reasons, to avoid calling DetectChanges() again.
            var result = base.SaveChanges();
            this.ChangeTracker.AutoDetectChangesEnabled = true;

            this.UpdateChapterFTS(changedChapters);
            return result;
        }
    }
}
از این پس تمام عملیات insert/update/delete برنامه تحت کنترل قرار گرفته و به صورت خودکار سبب به روز رسانی جدول مجازی FTS نیز می‌شوند.


در قسمت بعدی، روش کوئری گرفتن از این جدول مجازی FTS را بررسی می‌کنیم.
نظرات اشتراک‌ها
بررسی سلامت SQL Server از طریق PowerShell
چون من وهله‌ی سفارشی تعریف نکرده‌ام و از همان وهله‌ی پیش فرض استفاده می‌شود، یک سطر ذیل جواب داد:
.; dbName
+ راهنمای آن‌را مطالعه کنید. سرویس SQL Browser service هم باید فعال باشد. در مورد اتصال ریموت هم در انتهای راهنما یک لینک نحوه‌ی تنظیمات را ارائه داده‌است.
مطالب
کار با Docker بر روی ویندوز - قسمت سوم - نصب Docker بر روی ویندوز سرور
در قسمت قبل، Docker for Windows را بر روی ویندوز 10 نصب کردیم تا بتوانیم از هر دوی Linux Containers و Windows Containers استفاده کنیم. در این قسمت، نحوه‌ی نصب Docker را بر روی ویندوز سرور، صرفا جهت اجرای Windows Containers، بررسی می‌کنیم؛ از این جهت که در دنیای واقعی، عموما Linux Containers را بر روی سرورهای لینوکسی و Windows Containers را بر روی سرورهای ویندوزی اجرا می‌کنند.


Docker for Windows چگونه از هر دوی کانتینرهای ویندوزی و لینوکسی پشتیبانی می‌کند؟

زمانیکه docker for windows را اجرا می‌کنیم، سرویسی را ایجاد می‌کند که سبب اجرای پروسه‌ی ویژه‌ای به نام com.docker.proxy.exe می‌شود:


هنگامیکه برای مثال فرمان docker run nginx را توسط Docker CLI اجرا می‌کنیم، Docker CLI از طریق واسط یاد شده، دستورات را به MobyLinuxVM منتقل می‌کند. به این صورت است که امکان اجرای Linux Containers، بر روی ویندوز میسر می‌شوند:


اکنون اگر به Windows Containers سوئیچ کنیم (از طریق کلیک راست بر روی آیکن Docker در قسمت Tray Icons ویندوز)، پروسه‌ی dockerd.exe یا docker daemon شروع به کار خواهد کرد:


اینبار اگر مجددا از Docker CLI برای اجرای مثلا IIS Container استفاده کنیم، دستور ما از طریق واسط‌های com.docker.proxy و dockerd‌، به کانتینر ویندوزی منتقل و اجرا می‌شود:



نگاهی به معماری Docker بر روی ویندوز سرور

داکر بر روی ویندوز سرور، تنها به همراه موتور مدیریت کننده‌ی Windows Containers است:


در اینجا با صدور فرمان‌های Docker CLI، پیام‌ها مستقیما به dockerd یا موتور داکر بر روی ویندوز سرور ارسال شده و سپس کار اجرا و مدیریت یک Windows Container انجام می‌شود.


نصب Docker بر روی ویندوز سرور

جزئیات مفصل و به روز Windows Containers را همواره می‌توانید در این آدرس در سایت مستندات مجازی سازی مایکروسافت مطالعه کنید (قسمت Container Host Deployment - Windows Server آن). پیشنیاز کار با آن نیز نصب حداقل ویندوز سرور 2016 می‌باشد و بهتر است تمام به روز رسانی‌های آن‌را نیز نصب کرده باشید؛ چون تعدادی از بهبودهای کار با کانتینرهای آن، به همراه به روز رسانی‌ها آن ارائه شده‌اند.
برای شروع به نصب، نیاز است کنسول PowerShell ویندوز را با دسترسی Admin اجرا کنید.
سپس اولین دستوراتی را که نیاز است اجرا کنید، کار نصب موتور Docker و CLI آن‌را به صورت خودکار بر روی ویندوز سرور انجام می‌دهند:
Install-Module -Name DockerMsftProvider -Repository PSGallery -Force
Install-Package -Name docker -ProviderName DockerMsftProvider
Restart-Computer -Force
- که پس از نصب و ری‌استارت سیستم، نتیجه‌ی آن‌را در پوشه‌ی c:\Program Files\Docker می‌توانید ملاحظه کنید.
- به علاوه اگر دستور *get-service *docker را در کنسول PowerShell صادر کنید، مشاهده خواهید کرد که سرویس جدیدی را به نام Docker نیز نصب و راه اندازی کرده‌است که به dockerd.exe اشاره می‌کند.
- و یا اگر در کنسول PowerShell دستور docker را صادر کنید، ملاحظه خواهید کرد که CLI آن، فعال و قابل استفاده‌است. برای مثال، دستور docker version را صادر کنید تا بتوانید نگارش docker نصب شده را ملاحظه نمائید.


اجرای Image مخصوص NET Core. بر روی ویندوز سرور

تگ‌های مختلف Image مخصوص NET Core. را در اینجا ملاحظه می‌کنید. در ادامه قصد داریم tag مرتبط با nanoserver آن‌را نصب کنیم (با حجم 802MB):
docker run microsoft/dotnet:nanoserver
زمانیکه این دستور را اجرا می‌کنیم، پس از اجرای آن، ابتدا یک \:C را نمایش می‌دهد و بعد خاتمه یافته و به command prompt بازگشت داده می‌شویم. برای مشاهده‌ی علت آن، اگر دستور docker ps -a را اجرا کنیم، در ستون command آن، قسمتی از دستوری را که اجرا کرده‌است، می‌توان مشاهده کرد. برای مشاهده‌ی کامل این دستور، نیاز است دستور docker ps -a --no-trunc را اجرا کنیم. در اینجا سوئیچ no-trunc به معنای no truncate است یا عدم حذف قسمت انتهایی یک دستور طولانی. در این حالت مشاهده خواهیم کرد که این دستور، کار اجرای cmd.exe واقع در پوشه‌ی ویندوز را انجام می‌دهد (یا همان command prompt معمولی ویندوز). چون دستور docker run فوق به آن متصل نشده‌است، این پروسه ابتدا \:c را نمایش می‌دهد و سپس خاتمه پیدا می‌کند. برای رفع این مشکل، از interactive command که در قسمت قبل توضیح دادیم، استفاده خواهیم کرد:
docker run -it microsoft/dotnet:nanoserver
اینبار اگر این دستور را اجرا کنیم، به command prompt آغاز شده‌ی توسط آن، متصل خواهیم شد. اکنون اگر در همینجا (داخل container در حال اجرا) دستور dotnet --info را صادر کنید، می‌توان مشخصات NET Core SDK. نصب شده را مشاهده کرد. برای خروج از آن نیز دستور exit را صادر کنید.


چرا حجم Image مخصوص NET Core. نگارش nanoserver آن حدود 800 مگابایت است؟

در مثال قبلی، دسترسی به command prompt مجزایی نسبت به command prompt اصلی سیستم، در داخل یک container، شاید اندکی غیر منتظره بود و اکنون این سؤال مطرح می‌شود که یک image، شامل چه چیزهایی است؟
یک image شاید در ابتدای کار صرفا شامل فایل‌های اجرایی یک برنامه‌ی خاص به نظر برسد؛ اما زمانیکه قرار است تبدیل به یک container قابل اجرا شود، شامل بسیاری از فایل‌های دیگر نیز خواهد شد. برای درک این موضوع نیاز است لایه‌های نرم افزاری که یک سیستم را تشکیل می‌دهند، بررسی کنیم:


در این تصویر از پایین‌ترین لایه‌ای را که با سخت افزار ارتباط برقرار می‌کند تا بالاترین لایه‌ی موجود نرم افزاری را مشاهده می‌کنید. دراینجا هر چیزی را که در ناحیه‌ی کرنل قرار نمی‌گیرد، User Space می‌نامند. برنامه‌های قرار گرفته‌ی در User Space برای کار با سخت افزار نیاز است با کرنل ارتباط برقرار کنند و برای این منظور از System Calls استفاده می‌کنند که عموما کتابخانه‌هایی هستند که جزئی از سیستم عامل می‌باشند؛ مانند API ویندوز. برای مثال MongoDB توسط Win32 API و System Calls، فرامینی را به کرنل منتقل می‌کند.
در روش متداول توزیع و نصب نرم افزار، ما عموما همان بالاترین سطح را توزیع و نصب می‌کنیم؛ برای مثال خود MongoDB را. در اینجا نصاب MongoDB فرض می‌کند که در سیستم جاری، تمام لایه‌های دیگر، موجود و آماده‌ی استفاده هستند و اگر اینگونه نباشد، به مشکل برخواهد خورد و اجرا نمی‌شود. برای اجتناب از یک چنین مشکلاتی مانند عدم حضور وابستگی‌هایی که یک برنامه برای اجرا نیاز دارد، imageهای docker، نحوه‌ی توزیع نرم افزارها را تغییر داده‌اند. اینبار یک image بجای توزیع فقط MongoDB، شامل تمام قسمت‌های مورد نیاز User Space نیز هست:


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


به همین جهت بود که برای مثال در قسمت قبل موفق شدیم IIS مخصوص ویندوز سرور با تگ nanoserver را بر روی ویندوز 10 که بسیاری از وابستگی‌های مرتبط را به همراه ندارد، با موفقیت اجرا کنیم.
به علاوه چون یک container صرفا به معنای یک running process از یک image است، هر فایل اجرایی داخل آن image را نیز می‌توان به صورت یک container اجرا کرد؛ مانند cmd.exe داخل image مرتبط با NET Core. که آن‌را بررسی کردیم.


کارآیی Docker Containers نسبت به ماشین‌های مجازی بسیار بیشتر است

مزیت دیگر یک چنین توزیعی این است که اگر چندین container در حال اجرا را داشته باشیم:


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

شاید مطابق تصویر فوق اینطور به نظر برسد که هرچند تمام این containers از یک کرنل استفاده می‌کنند، اما اگر قرار باشد هر کدام OS Apps & Libs خاص خودشان را در حافظه بارگذاری کنند، با کمبود شدید منابع روبرو شویم. دقیقا مانند حالتیکه چند ماشین مجازی را اجرا کرده‌ایم و دیگر سیستم اصلی قادر به پاسخگویی به درخواست‌های رسیده به علت کمبود منابع نیست. اما در واقعیت، یک image داکر، از لایه‌های مختلفی تشکیل می‌شود که فقط خواندنی هستند و غیرقابل تغییر و زمانیکه docker یک لایه‌ی فقط خواندنی را در حافظه بارگذاری کرد، اگر container دیگری، از همان لایه‌ی تعریف شده‌، در image خود نیز استفاده می‌کند، لایه‌ی بارگذاری شده‌ی فقط خواندنی در حال اجرای موجود را با آن به اشتراک می‌گذارد (مانند تصویر زیر). به این ترتیب میزان مصرف منابع docker containers نسبت به ماشین‌های مجازی بسیار کمتر است:



روش کنترل پروسه‌ای که درون یک کانتینر اجرا می‌شود

با اجرای دستور docker run -it microsoft/dotnet:nanoserver ابتدا به command prompt داخلی و مخصوص این container منتقل می‌شویم و سپس می‌توان برای مثال با NET Core CLI. کار کرد. اما امکان اجرای این CLI به صورت زیر نیز وجود دارد:
docker run -it microsoft/dotnet:nanoserver dotnet --info
این دستور، مشخصات SDK نصب شده را نمایش می‌دهد و سپس مجددا به command prompt سیستم اصلی (که به آن میزبان، host و یا container host نیز گفته می‌شود) بازگشت داده خواهیم شد؛ چون کار NET Core CLI. خاتمه یافته‌است، پروسه‌ی متعلق به آن نیز خاتمه می‌یابد.
بدیهی است در این حالت تمام فایل‌های اجرایی داخل این container را نیز می‌توان اجرا کرد. برای مثال می‌توان کنسول پاورشل داخل این container را اجرا کرد:
docker run -it microsoft/dotnet:nanoserver powershell
زمانیکه به این کنسول دسترسی پیدا کردید، برای مثال دستور get-process را اجرا کنید. به این ترتیب می‌توانید لیست تمام پروسه‌هایی ر که هم اکنون داخل این container در حال اجرا هستند، مشاهده کنید.


هر کانتینر دارای یک File System ایزوله‌ی خاص خود است

تا اینجا دریافتیم که هر image، به همراه فایل‌های user space مورد نیاز خود نیز می‌باشد. به عبارتی هر image یک file system را نیز ارائه می‌دهد که تنها درون همان container قابل دسترسی می‌باشد و از مابقی سیستم جاری ایزوله شده‌است.
برای آزمایش آن، کنسول پاورشل را در سیستم میزبان (سیستم عامل اصلی که docker را اجرا می‌کند)، باز کرده و دستور \:ls c را صادر کنید. به این ترتیب می‌توانید لیست پوشه‌ها و فایل‌های موجود در درایو C میزبان را مشاهده نمائید. سپس دستور docker run -it microsoft/dotnet:nanoserver powershell را اجرا کنید تا به powershell داخل کانتینر NET Core. دسترسی پیدا کنیم. اکنون دستور \:ls c را مجددا اجرا کنید. خروجی آن کاملا متفاوت است نسبت به گزارشی که پیشتر بر روی سیستم میزبان تهیه کردیم؛ دقیقا مانند اینکه هارد درایو یک container متفاوت است با هارد درایو سیستم میزبان.


این تصویر زمانی تهیه شده‌است که دستور docker run یاد شده را صادر کرده‌ایم و درون powershell آن قرار داریم. همانطور که مشاهده می‌کنید یک Disk جدید، به ازای این Container در حال اجرا، به سیستم میزبان اضافه شده‌است. این Disk زمانیکه در powershell داخل container، دستور exit را صادر کنیم، بلافاصله محو می‌شود. چون پروسه‌ی container، به این ترتیب خاتمه یافته‌است.
اگر دستور docker run یاد شده را دو بار اجرا کنیم، دو Disk جدید ظاهر خواهند شد:


یک نکته: اگر بر روی این درایوهای مجازی کلیک راست کرده، گزینه‌ی change drive letter or path را انتخاب نموده و یک drive letter را به آن‌ها نسبت دهید، می‌توانید محتویات داخل آن‌ها را توسط Windows Explorer ویندوز میزبان نیز به صورت یک درایو جدید، مشاهده کنید.


خلاصه‌ای از ایزوله سازی‌های کانتینرها تا به اینجا

تا اینجا یک چنین ایزوله سازی‌هایی را بررسی کردیم:
- ایزوله سازی File System و وجود یک disk مجازی مجزا به ازای هر کانتینر در حال اجرا.

- پروسه‌های کانتینرها از پروسه‌های میزبان ایزوله هستند. برای مثال اگر دستور get-process را داخل یک container اجرا کنید، خروجی آن با خروجی اجرای این دستور بر روی سیستم میزبان یکی نیست. یعنی نمی‌توان از داخل کانتینرها، به پروسه‌های میزبان دسترسی داشت و دخل و تصرفی را در آن‌ها انجام داد که از لحاظ امنیتی بسیار مفید است. هر چند اگر به task manager ویندوز میزبان مراجعه کنید، می‌توان پروسه‌های داخل یک container را توسط Job Object ID یکسان آن‌ها تشخیص دهید (مثال آخر قسمت قبل)، اما یک container، قابلیت شمارش پروسه‌های خارج از مرز خود را ندارد.

- ایزوله سازی شبکه مانند کارت شبکه‌ی مجازی کانتینر IIS که در قسمت قبل بررسی کردیم. برای آزمایش آن دستور ipconfig را در داخل container و سپس در سیستم میزبان اجرا کنید. نتیجه‌ای را که مشاهده خواهید کرد، کاملا متفاوت است. یعنی network stack این دو کاملا از هم مجزا است. شبیه به اینکه به یک سیستم، چندین کارت شبکه را متصل کرده باشید. اینکار در اینجا با تعریف virtual network adaptors انجام می‌شود و لیست آن‌ها را در قسمت «All Control Panel Items\Network Connections» سیستم میزبان می‌توانید مشاهده کنید. یکی از مهم‌ترین مزایای آن این است که اگر در یک container، وب سروری را بر روی پورت 80 آن اجرا کنید، مهم نیست که در سیستم میزبان، یک IIS در حال سرویس دهی بر روی پورت 80 هم اکنون موجود است. این دو پورت با هم تداخل نمی‌کنند.

- در حالت کار با Windows Containers، رجیستری کانتینر نیز از میزبان آن مجزا است و یا متغیرهای محیطی این‌ها یکی نیست. برای مثال دستور \:ls env را در کانتینر و سیستم میزبان اجرا کنید تا environment variables را گزارش گیری کنید. خروجی این دو کاملا متفاوت است. برای مثال حداقل computer name، user name‌های قابل مشاهده‌ی در این گزارش‌ها، متفاوت است و یا دستور \:ls hkcu را در هر دو اجرا کنید تا خروجی رجیستری متعلق به کاربر جاری هر کدام را مشاهده کنید که در هر دو متفاوت است.

- در حالت کار با Linux Containers هر چیزی که ذیل عنوان namespace مطرح می‌شود مانند شبکه، PID، User، UTS، Mount و غیره شامل ایزوله سازی می‌شوند.


دو نوع Windows Containers وجود دارند

در ویندوز، Windows Server Containers و Hyper-V Containers وجود دارند. در این قسمت تمام کارهایی را که بر روی ویندوز سرور انجام دادیم، در حقیقت بر روی Windows Server Containers انجام شدند و تمام Containerهای ویندوزی را که در قسمت قبل بر روی ویندوز 10 ایجاد کردیم، از نوع Hyper-V Containers بودند.
تفاوت مهم این‌ها در مورد نحوه‌ی پیاده سازی ایزوله سازی آن‌ها است. در حالت Windows Server Containers، کار ایزوله سازی پروسه‌ها توسط کرنل اشتراکی بین کانتینرها صورت می‌گیرد اما در Hyper-V Containers، این ایزوله سازی توسط hypervisor آن انجام می‌شود؛ هرچند نسبت به ماشین‌های مجازی متداول بسیار سریع‌تر است، اما بحث به اشتراک گذاری کرنل هاست را که پیشتر در این قسمت بررسی کردیم، در این حالت شاهد نخواهیم بود. ویندوز سرور 2016 می‌تواند هر دوی این ایزوله سازی‌ها را پشتیبانی کند، اما ویندوز 10، فقط نوع Hyper-V را پشتیبانی می‌کند.


روش اجرای Hyper-V Containers بر روی ویندوز سرور

در صورت نیاز برای کار با Hyper-V Containers، نیاز است مانند قسمت قبل، ابتدا Hyper-V را بر روی ویندوز سرور، فعالسازی کرد:
Install-WindowsFeature hyper-v
Restart-Computer -Force
اکنون برای اجرای دستور docker run ای که توسط Hyper-V مدیریت می‌شود، می‌توان به صورت زیر، از سوئیچ isolation استفاده کرد:
docker run -it --isolation=hyperv microsoft/dotnet:nanoserver powershell
در این حالت اگر به disk management سیستم میزبان مراجعه کنید، دیگر حالت اضافه شدن disk مجازی را مشاهده نمی‌کنید. همچنین اگر به task manager ویندوز میزبان مراجعه کنید، دیگر لیست پروسه‌های داخل container را نیز در اینجا نمی‌بینید. علت آن روش ایزوله سازی متفاوت آن با Windows Server Containers است و بیشتر شبیه به ماشین‌های مجازی عمل می‌کند. در کل اگر نیاز به حداکثر و شدیدترین حالت ایزوله سازی را دارید، از این روش استفاده کنید.