مطالب
استفاده از Full text search توسط Entity Framework
پیشنیاز مطلب:
پشتیبانی از Full Text Search در SQL Server

Full Text Search یا به اختصار FTS یکی از قابلیت‌های SQL Server جهت جستجوی پیشرفته در متون میباشد. این قابلیت تا کنون در EF 6.1.1 ایجاد نشده است.
در ادامه پیاده سازی از FTS در EF را مشاهده مینمایید.
جهت ایجاد قابلیت FTS از متد Contains در Linq استفاده شده است.
ابتدا متد‌های الحاقی جهت اعمال دستورات FREETEXT و CONTAINS اضافه میشود.سپس دستورات تولیدی EF را قبل از اجرا بر روی بانک اطلاعاتی توسط امکان Command Interception به دستورات FTS تغییر میدهیم.
همانطور که میدانید دستور Contains در Linq توسط EF به دستور LIKE تبدیل میشود. به جهت اینکه ممکن است بخواهیم از دستور LIKE نیز استفاده کنیم یک پیشوند به مقادیری که میخواهیم به دستورات FTS تبدیل شوند اضافه مینماییم.
جهت استفاده از Fts در EF کلاس‌های زیر را ایجاد نمایید.
 
کلاس FullTextPrefixes :
/// <summary>
    /// 
    /// </summary>
    public static class FullTextPrefixes
    {
        /// <summary>
        /// 
        /// </summary>
        public const string ContainsPrefix = "-CONTAINS-";

        /// <summary>
        /// 
        /// </summary>
        public const string FreetextPrefix = "-FREETEXT-";

        /// <summary>
        /// 
        /// </summary>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static string Contains(string searchTerm)
        {
            return string.Format("({0}{1})", ContainsPrefix, searchTerm);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static string Freetext(string searchTerm)
        {
            return string.Format("({0}{1})", FreetextPrefix, searchTerm);
        }

    }

کلاس جاری جهت علامت گذاری دستورات FTS میباشد و توسط متد‌های الحاقی و کلاس FtsInterceptor استفاده میگردد.

کلاس FullTextSearchExtensions :
public static class FullTextSearchExtensions
    {

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static IQueryable<TEntity> FreeTextSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class
        {
            return FreeTextSearchImp(source, expression, FullTextPrefixes.Freetext(searchTerm));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        public static IQueryable<TEntity> ContainsSearch<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm) where TEntity : class
        {
            return FreeTextSearchImp(source, expression, FullTextPrefixes.Contains(searchTerm));
        }

        /// <summary>
        /// 
        /// </summary>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="source"></param>
        /// <param name="expression"></param>
        /// <param name="searchTerm"></param>
        /// <returns></returns>
        private static IQueryable<TEntity> FreeTextSearchImp<TEntity>(this IQueryable<TEntity> source, Expression<Func<TEntity, object>> expression, string searchTerm)
        {
            if (String.IsNullOrEmpty(searchTerm))
            {
                return source;
            }

            // The below represents the following lamda:
            // source.Where(x => x.[property].Contains(searchTerm))

            //Create expression to represent x.[property].Contains(searchTerm)
            //var searchTermExpression = Expression.Constant(searchTerm);
            var searchTermExpression = Expression.Property(Expression.Constant(new { Value = searchTerm }), "Value");
            var checkContainsExpression = Expression.Call(expression.Body, typeof(string).GetMethod("Contains"), searchTermExpression);

            //Join not null and contains expressions

            var methodCallExpression = Expression.Call(typeof(Queryable),
                                                       "Where",
                                                       new[] { source.ElementType },
                                                       source.Expression,
                                                       Expression.Lambda<Func<TEntity, bool>>(checkContainsExpression, expression.Parameters));

            return source.Provider.CreateQuery<TEntity>(methodCallExpression);
        }

در این کلاس متدهای الحاقی جهت اعمال قابلیت Fts ایجاد شده است.
متد FreeTextSearch جهت استفاده از دستور FREETEXT  استفاده میشود.در این متد پیشوند -FREETEXT- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو.
متد ContainsSearch جهت استفاده از دستور CONTAINS استفاده میشود.در این متد پیشوند -CONTAINS- جهت علامت گذاری این دستور به ابتدای مقدار جستجو اضافه میشود. این متد دارای دو پارامتر میباشد ، اولی ستونی که میخواهیم بر روی آن جستجوی FTS انجام دهیم و دومی عبارت جستجو. 
متد FreeTextSearchImp جهت ایجاد دستور Contains در Linq میباشد.
 
کلاس FtsInterceptor :
 public class FtsInterceptor : IDbCommandInterceptor
    {

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            RewriteFullTextQuery(command);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            RewriteFullTextQuery(command);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="command"></param>
        /// <param name="interceptionContext"></param>
        public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="cmd"></param>
        public static void RewriteFullTextQuery(DbCommand cmd)
        {
            var text = cmd.CommandText;
            for (var i = 0; i < cmd.Parameters.Count; i++)
            {
                var parameter = cmd.Parameters[i];
                if (
                    !parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength,
                        DbType.AnsiStringFixedLength)) continue;
                if (parameter.Value == DBNull.Value)
                    continue;
                var value = (string)parameter.Value;
                if (value.IndexOf(FullTextPrefixes.ContainsPrefix, StringComparison.Ordinal) >= 0)
                {
                    parameter.Size = 4096;
                    parameter.DbType = DbType.AnsiStringFixedLength;
                    value = value.Replace(FullTextPrefixes.ContainsPrefix, ""); // remove prefix we added n linq query
                    value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE
                    parameter.Value = value;
                    cmd.CommandText = Regex.Replace(text,
                        string.Format(
                            @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName),
                        string.Format(@"CONTAINS([$1].[$2], @{0})", parameter.ParameterName));
                    if (text == cmd.CommandText)
                        throw new Exception("FTS was not replaced on: " + text);
                    text = cmd.CommandText;
                }
                else if (value.IndexOf(FullTextPrefixes.FreetextPrefix, StringComparison.Ordinal) >= 0)
                {
                    parameter.Size = 4096;
                    parameter.DbType = DbType.AnsiStringFixedLength;
                    value = value.Replace(FullTextPrefixes.FreetextPrefix, ""); // remove prefix we added n linq query
                    value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE
                    parameter.Value = value;
                    cmd.CommandText = Regex.Replace(text,
                        string.Format(
                            @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName),
                        string.Format(@"FREETEXT([$1].[$2], @{0})", parameter.ParameterName));
                    if (text == cmd.CommandText)
                        throw new Exception("FTS was not replaced on: " + text);
                    text = cmd.CommandText;
                }
            }
        }

    }

در این کلاس دستوراتی را که توسط متد‌های الحاقی جهت امکان Fts علامت گذاری شده بودند را یافته و دستور LIKE را با دستورات  CONTAINSو FREETEXT جایگزین میکنیم.

در ادامه برنامه ای جهت استفاده از این امکان به همراه دستورات تولیدی آنرا مشاهده مینمایید.
public class Note
    {
        /// <summary>
        /// 
        /// </summary>
        public int Id { get; set; }

        /// <summary>
        /// 
        /// </summary>
        public string NoteText { get; set; }
    }

  public class FtsSampleContext : DbContext
    {
        /// <summary>
        /// 
        /// </summary>
        public DbSet<Note> Notes { get; set; }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="modelBuilder"></param>
        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Configurations.Add(new NoteMap());
        }
    }

/// <summary>
    /// 
    /// </summary>
    class Program
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            DbInterception.Add(new FtsInterceptor());
            const string searchTerm = "john";
            using (var db = new FtsSampleContext())
            {
                var result1 = db.Notes.FreeTextSearch(a => a.NoteText, searchTerm).ToList();
                //SQL Server Profiler result ===>>>
                //exec sp_executesql N'SELECT 
                //    [Extent1].[Id] AS [Id], 
                //    [Extent1].[NoteText] AS [NoteText]
                //    FROM [dbo].[Notes] AS [Extent1]
                //    WHERE FREETEXT([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 
                //char(4096)',@p__linq__0='(john)'
                var result2 = db.Notes.ContainsSearch(a => a.NoteText, searchTerm).ToList();
                //SQL Server Profiler result ===>>>
                //exec sp_executesql N'SELECT 
                //    [Extent1].[Id] AS [Id], 
                //    [Extent1].[NoteText] AS [NoteText]
                //    FROM [dbo].[Notes] AS [Extent1]
                //    WHERE CONTAINS([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 
                //char(4096)',@p__linq__0='(john)'
            }
            Console.ReadKey();
        }
    }

ابتدا کلاس FtsInterceptor را به EF معرفی مینماییم. سپس از دو متد الحاقی مذکور استفاده مینماییم. خروجی هر دو متد توسط SQL Server Profiler در زیر هر متد مشاهده مینمایید.
تمامی کدها و مثال مربوطه در آدرس https://effts.codeplex.com قرار گرفته است.
منبع:
Full Text Search in Entity Framework 6
مطالب
نحوه اضافه کردن 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
 
مطالب
بررسی معادل‌های LINQ to Objects در TypeScript
اگر برنامه نویس NET. باشید، پس از مدتی کار با LINQ، در سایر زبان‌های دیگر نیز به دنبال این قابلیت فوق العاده‌ی functional یا تابعی خواهید گشت. در این مطلب، خلاصه‌ای از متدهای توکار جاوا اسکریپت را که می‌توانند معادل‌هایی برای متدهای LINQ to Objects دات نت باشند، بررسی خواهیم کرد.


تدارک ساختار ابتدایی این مطلب

در اینجا اینترفیسی را که بیانگر ساختار شیء شخص است، به صورت ذیل ایجاد می‌کنیم:
export interface Person {
  name: string;
  age: number;
}
سپس آرایه‌ای را بر اساس این شیء تدارک خواهیم دید:
export class LinqTestsComponent {

  people: Person[] = [
    { name: "User 4", age: 27 },
    { name: "User 5", age: 42 },
    { name: "User 6", age: 8 },
    { name: "User 1", age: 20 },
    { name: "User 2", age: 35 },
    { name: "User 3", age: 78 }
  ];

}
در ادامه تمام اعمال مدنظر را بر روی این آرایه انجام می‌دهیم.
همچنین سه متد ذیل را نیز برای لاگ کردن عنوان آزمایش، نمایش محتوای آرایه‌ی اصلی و نمایش نتیجه‌ی آزمایش، به این کلاس اضافه می‌کنیم:
  logTitle(title: string) {
    console.log(`%c${title}`, "background: #222; color: #bada55");
  }

  logOriginalArray() {
    console.log(`original this.people:${JSON.stringify(this.people)}`);
  }

  logResult(message: string, result: any) {
    console.log(`${message}:${JSON.stringify(result)}`);
  }


معادل متد Where در TypeScript

متد filter که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Where، جهت فیلتر کردن عناصر بر اساس یک خاصیت، یا چندین خاصیت باشد:
  equivalentToWhere() {
    const youngerThan40 = this.people.filter(person => person.age < 40); // ECMAScript 5
    this.logResult("People younger than 40", youngerThan40);

    // Filtering on Multiple Criteria
    const youngsters = this.people.filter(
      person => person.age < 40 && person.name.toLocaleLowerCase().indexOf("user") !== -1);
    this.logResult("Users younger than 40", youngsters);
  }
با این خروجی
People younger than 40:[
{"name":"User 4","age":27},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]

Users younger than 40:[
{"name":"User 4","age":27},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]
در اینجا نحوه‌ی استفاده‌ی از arrow functions ES6 را نیز جهت ساده سازی تعریف callback متد filter مشاهده می‌کنید که نمایش آن بسیار شبیه به عبارات LINQ در دات نت است.


معادل متد Any در TypeScript

متد some که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Any باشد. اگر یکی از عناصر آرایه، بر اساس شرط تعیین شده یافت شود، این متد مقدار true را باز می‌گرداند:
  equivalentToAny() {

    const anyUnder40 = this.people.some(person => person.age < 40); // ECMAScript 5
    this.logResult("Are any people under 40?", anyUnder40); // true

    // Filtering on Criteria Matching any Object Properties
    const filterBy = "user";
    const anyUsers = this.people.filter(person =>
      Object.keys(person).some(property => {
        let value = (<any>person)[property];
        if (typeof value === "string") {
          value = value.toLocaleLowerCase();
        }
        return value.toString().indexOf(filterBy) !== -1;
      })
    );
    this.logResult("anyUsers", anyUsers);
  }
با این خروجی:
Are any people under 40?:true
anyUsers:[
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78}
]
در مثال اول، بررسی شده‌است که آیا شخصی با سن کمتر از 40 در این لیست وجود دارد؟
در مثال دوم، جستجویی بر روی تمام خواص شیء شخص انجام شده‌است. در اینجا توسط متد Object.keys، لیست خواص شیء یافت شده‌است. سپس بر روی این لیست توسط متد some، بررسی شده‌است که آیا خاصیت رشته‌ای وجود دارد که مساوی عبارت filterBy باشد؟ حاصل این بررسی به عنوان شرط متد filter جهت بازگشت آرایه‌ی متناظری از اشخاص یافت شده، استفاده شده‌است.


معادل متد ‍Contains در TypeScript

متد includes که جزو متدهای توکار ES7 است، می‌تواند معادلی برای متد Contains باشد و کار آن بررسی وجود عنصری در یک لیست است:
  equivalentToContains() {

    const searchElement: Person = { name: "User 4", age: 27 };
    const containsUser4 = this.people.includes(searchElement); // ECMAScript 2016 = ECMAScript 7
    this.logResult("Contains searchElement", containsUser4); // false -> only compares by reference and not by value.

    const indexOfUser4 = this.people.indexOf(searchElement); // ECMAScript 5
    this.logResult("indexOfUser4", indexOfUser4); // -1 -> only compares by reference and not by value.

    const stringifiedObj = JSON.stringify(searchElement);
    const includesUser4 = this.people.some(person => JSON.stringify(person) === stringifiedObj);
    this.logResult("includesUser4", includesUser4); // true -> compares by by value.
  }
در اینجا باید دقت داشت که اگر آرایه‌ی مدنظر رشته‌ای و یا عددی باشد، متد includes نتیجه‌ی مطلوبی را بازگشت می‌دهد. اما چون در اینجا وجود یک شیء را در لیست اشخاص بررسی می‌کنیم، این مقایسه بر اساس ارجاع عناصر خواهد بود و نتیجه‌ی نهایی یافت شدن آن، منفی است (شیء searchElement هیچ ارجاعی را در آرایه‌ی اشخاص ندارد، هرچند ظاهر آن شبیه به یکی از عناصر آن است). حتی متد indexOf نیز به همین صورت عمل می‌کند.
یکی از روش‌های مقایسه‌ی بر اساس تمام مقادیر خواص یک شیء، استفاده از متد JSON.stringify است که اگر آن‌را با متد some ترکیب کنیم، می‌توان به نتیجه‌ی مطلوبی رسید:
Contains searchElement:false
indexOfUser4:-1
includesUser4:true


معادل متد ‍All در TypeScript

متد every که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد All باشد و کار آن بررسی صحت شرط اعمالی، بر روی تک تک عناصر لیست است. اگر این بررسی با موفقیت صورت گرفت، مقدار true را بازگشت می‌دهد:
  equivalentToAll() {
    const allUnder30 = this.people.every(person => person.age < 30); // ECMAScript 5
    this.logResult("Are all people under 30?", allUnder30); // false
  }
با این خروجی:
 Are all people under 30?:false


معادل متدهای First و FirstOrDefault در TypeScript

می‌توان از متدهای filter و یا find بومی ES5 و ES 6 برای شبیه سازی متدهای First  (یافتن اولین عنصر درخواستی در یک لیست) و  FirstOrDefault استفاده کرد:
  equivalentToFirstOrDefault() {
    const vahidOrDefault = this.people.filter(item => item.name === "Vahid")[0] || null; // ECMAScript 5
    this.logResult("vahidOrDefault", vahidOrDefault);

    const user1OrDefault = this.people.find(item => item.name === "User 1") || null; // ECMAScript 2015 = ECMAScript 6
    this.logResult("user1OrDefault", user1OrDefault);
  }
متد filter، در صورت برآورده نشدن شرط آن، یک آرایه‌ی خالی را بازگشت می‌دهد که مقدار [0] آن، undefined است. بنابراین ترکیب آن با null ||، سبب بازگشت نال، در صورت خالی بودن آرایه می‌شود؛ یا همان حالت OrDefault (یا بازگشت مقدار پیش فرض، یا نال در اینجا). متد find نیز در صورت نیافتن عنصر درخواستی، مقدار undefined را بازگشت می‌دهد.


معادل متد FindIndex در TypeScript

متد findIndex که جزو متدهای توکار ES6 است، می‌تواند معادلی برای متد FindIndex در جهت یافتن ایندکس عنصری در یک لیست، بر اساس شرط درخواستی، باشد.
  equivalentToFindIndex() {
    const index = this.people.findIndex(person => person.age === 8); // ECMAScript 2015 = ECMAScript 6
    this.logResult("index of the user with age 8", index)
  }
با این خروجی:
 index of the user with age 8:2


معادل متد Select در TypeScript

متد map که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد Select، برای تغییر شکل نهایی خروجی یک لیست باشد:
  equivalentToSelect() {
    const names = this.people.map(person => person.name); // ECMAScript 5
    this.logResult("Selected the names of people", names);
  }
برای مثال در اینجا در لیست اشخاص، تنها خاصیت name آن‌ها، انتخاب و بازگشت داده شده‌است:
 Selected the names of people:["User 4","User 5","User 6","User 1","User 2","User 3"]


معادل متد Aggregate در TypeScript

متد reduce که جزو متدهای توکار ES5 است، می‌تواند شبیه به متد Aggregate عمل کند و لیستی از عناصر را به یک مقدار کاهش دهد:
  equivalentToAggregate() {
    // ECMAScript 5
    const aggregate = this.people.reduce((person1, person2) => {
      return { name: "", age: person1.age + person2.age };
    });
    this.logResult("Aggregate age", aggregate.age); // { age: 210 }
  }
با این خروجی:
 Aggregate age:210


معادل متد ForEach در TypeScript

متد forEach که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد ForEach باشد که روشی functional برای پیمودن عناصر یک لیست است:
  equivalentToForEach() {
    // ECMAScript 5
    this.people.forEach(person => {
      this.logResult("person", person);
    });
  }
با این خروجی:
 person:{"name":"User 4","age":27}
 person:{"name":"User 5","age":42}
 person:{"name":"User 6","age":8}
 person:{"name":"User 1","age":20}
 person:{"name":"User 2","age":35}
 person:{"name":"User 3","age":78}


معادل متد OrderBy در TypeScript

متد sort که جزو متدهای توکار ES5 است، می‌تواند معادلی برای متد OrderBy باشد که جهت مرتب سازی عناصر یک لیست از آن استفاده می‌شود:
    // ECMAScript 5
    let orderedByAgeAscending = this.people.sort((person1, person2) => {
      const a = person1.age;
      const b = person2.age;
      return a > b ? 1 : -1;
    });
    this.logResult("Ordered by age ascending", orderedByAgeAscending);
متد sort یک callback را می‌پذیرد و هر بار دو آیتم در حال مرتب سازی را به آن ارسال می‌کند. در این حالت اگر خروجی این callback:
 - مساوی صفر باشد، تغییری را به وجود نمی‌آورد.
 - کمتر از صفر باشد، اولین عنصر را قبل از دومین عنصر قرار می‌دهد.
 - بیشتر از صفر باشد، دومین عنصر را پس از اولین عنصر قرار می‌دهد.

منطق مقایسه‌ی فوق را به صورت ذیل نیز می‌توان خلاصه کرد:
    orderedByAgeAscending = this.people.sort((person1, person2) => person1.age - person2.age);
    this.logResult("Ordered by age ascending", orderedByAgeAscending);
با این خروجی:
Ordered by age ascending:[
{"name":"User 6","age":8},
{"name":"User 1","age":20},
{"name":"User 4","age":27},
{"name":"User 2","age":35},
{"name":"User 5","age":42},
{"name":"User 3","age":78}
]
و یا اگر بخواهیم این لیست را بر اساس نام اشخاص مرتب سازی کنیم، به منطق ذیل خواهیم رسید:
    const orderedByName = this.people.sort((person1, person2) => {
      // name1.localeCompare(name2) // is case insensitive
      // or use toUpperCase() to ignore character casing
      const name1 = person1.name.toUpperCase();
      const name2 = person2.name.toUpperCase();
      return name1 > name2 ? 1 : -1;
    })
    this.logResult("Ordered by name", orderedByName);
    this.logOriginalArray();
با این خروجی:
Ordered by name:[
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]

original this.people:[
{"name":"User 1","age":20},
{"name":"User 2","age":35},
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]
نکته‌ی مهم: همانطور که ملاحظه می‌کنید، متد sort نه فقط یک خروجی مرتب شده را بازگشت داده‌است، بلکه اصل آرایه را نیز درجا مرتب سازی کرده‌است و ترتیب عناصر این آرایه، دیگر با آرایه‌ی قبلی و اصلی یکی نیست.


امکان ترکیب زنجیروار متدهای کار بر روی لیست‌ها در TypeScript

همانند LINQ، در اینجا نیز می‌توان متدهای فوق را به صورت زنجیروار بر روی یک لیست فراخوانی و اجرا کرد:
  chainFunctionCalls() {
    const namesOfPeopleOver30OrderedDesc =
      this.people
        .filter(person => person.age > 30)
        .map(person => person.name)
        .sort((name1, name2) => {
          // name1.localeCompare(name2) // is case insensitive
          // or use toUpperCase() to ignore character casing
          name1 = name1.toUpperCase();
          name2 = name2.toUpperCase();
          return name2 > name1 ? 1 : -1;
        });
    this.logResult("the names of all people over 30 ordered by name descending", namesOfPeopleOver30OrderedDesc);
  }
با این خروجی:
 the names of all people over 30 ordered by name descending:["User 5","User 3","User 2"]
در اینجا ابتدا اشخاص بالای 30 سال فیلتر شده‌اند. سپس فقط خاصیت رشته‌ای نام آن‌ها انتخاب شده‌است و در آخر این نام‌ها به صورت نزولی مرتب شده‌اند.


معادل متد Skip در TypeScript

متد splice که جزو متدهای توکار ES5 است، می‌تواند شبیه به متد Skip عمل کند. این متد آرایه‌ای را بازگشت می‌دهد که حاوی عناصری است که پس از تعداد ذکر شده، در آرایه‌ی اصلی وجود دارند:
  equivalentToSkip() {
    const skip2 = this.people.splice(2); // ECMAScript 5
    this.logResult("skip2 -> the deleted elements", skip2);
    this.logOriginalArray();
  }
با این خروجی:
skip2 -> the deleted elements:[
{"name":"User 3","age":78},
{"name":"User 4","age":27},
{"name":"User 5","age":42},
{"name":"User 6","age":8}
]

original this.people:[
{"name":"User 1","age":20},
{"name":"User 2","age":35}
]
کار واقعی متد splice، حذف عناصر باقیمانده‌ی در آرایه‌است و خروجی آن دقیقا لیست موارد حذف شده‌است. به همین جهت است که در نتیجه‌ی فوق، اکنون آرایه‌ی اصلی تنها دارای دو عضو باقیمانده است (و دیگر با آرایه‌ی اصلی و ابتدایی یکی نیست).
نظرات مطالب
Defensive Programming - بازگشت نتایج قابل پیش بینی توسط متدها
public class Result
{
    public bool Success { get; private set; }
    public string Error { get; private set; }
    public bool Failure { /* … */ }
    protected Result(bool success, string error) { /* … */ }
    public static Result Fail(string message) { /* … */ }
    public static Result<T> Ok<T>(T value) {  /* … */ }
}

public class Result<T> : Result
{
    public T Value { get; set; }
    protected internal Result(T value, bool success, string error)
        : base(success, error)
    {
        /* … */
    }
}

مطالب
Web.config File Transformation #2
در مطلب قبلی Web.config File Transformation #1 با مفهوم انتقال وب کانفیگ و برخی از روش‌های آن آشنا شدید در ادامه به موارد دیگری خواهم پرداخت.

قواعد انتقال وب کانفیگ
در کل دو ویژگی اصلی در انتقال وب کانفیگ وجود دارد که یک xdt:Transform و دیگری xdt:Locator می باشد. این دو در واقع چگونگی تغییر فایل انتقالی در زمان deploy آن را تعیین می‌کنند. این ویژگی‌ها از نوع xml می‌باشد که در فضای نام XML-Document-Transform تعریف شده و با پسوند xdt شروع می‌شود.

قاعده ویژگی Locator
این ویژگی برای جستجو در فایل وب کانفیگ استفاده می‌شود
Condition:
عبارت condition برای تعیین شرط قاعده Locator استفاده می‌شود
Locator="Condition(XPath expression)"
مثال:
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add name="AWLT" connectionString="newstring"
       providerName="newprovider"
       xdt:Transform="Replace" 
       xdt:Locator="Condition(@name='oldname'
         or @providerName='oldprovider')" />
  </connectionStrings>
</configuration>
مثال بالا نشان می‌دهد که چگونه دنبال connectionstring ی بگردیم که نام آن oldname یا نام ارائه دهنده آن oldprovider باشد.
Match:
برای انتخاب عنصر یا عناصری که مقدار آن برابر ویژگی یا ویژگی‌های تعیین شده باشد. در صورتی که چندین ویژگی یافت شود، فقط عناصری که همه ویژگی‌های تعیین شده آن‌ها برابر باشد انتخاب می‌شود. نام ویژگی‌ها را می‌بایست با کما از هم جدا نمود.
Locator="Match(comma-delimited list of one or more attribute names)"
مثال:
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add name="AWLT" connectionString="newstring"
       providerName="newprovider"
       xdt:Transform="Replace" 
       xdt:Locator="Match(name)" />
  </connectionStrings>
</configuration>
مثال فوق دنبال عناصری که ویژگی نام آنها برابر AWLT باشد می‌گردد.
XPath:
عبارتی از مسیر‌های عناصر xml را جستجو می‌کند.
Locator="XPath(XPath expression)"
مثال:
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add name="AWLT" connectionString="newstring"
       providerName="newprovider"
       xdt:Transform="Replace" 
       xdt:Locator="XPath(configuration/connectionStrings[@name='AWLT'
         or @providerName='System.Data.SqlClient'])" />
  </connectionStrings>
</configuration>
در این مثال همانند مثال Condition همان عناصر را با استفاده از XPath جستجو می‌نماید.
قاعده ویژگی Transform 
  این ویژگی نحوه انتقال عنصر در فایل وب کانفیگ را مشخص می‌سازد.
Replace:
عناصر تعیین شده با عناصر انتقالی جایگزین می‌شود
Transform="Replace"
Insert:
عنصر انتقالی به عنوان فرزند عنصر تعیین شده اضافه می‌گردد.
Transform="Insert"
مثال:
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add name="AWLT" connectionString="newstring"
       providerName="newprovider"
       xdt:Transform="Insert" />
  </connectionStrings>
</configuration>
connectionstring انتقالی را به لیست رشته‌های اتصال فایل انتقالی اضافه می‌نماید.
InsertBefore:
عنصر انتقالی را قبل از مسیر تعیین شده با XPath قرار می‌دهد.
Transform="InsertBefore(XPath expression)"
مثال:
<configuration xmlns:xdt="...">
  <authorization>
    <allow roles="Admins"
      xdt:Transform="InsertBefore(/configuration/system.web/authorization/deny[@users='*'])" />
  </authorization>
</configuration>
عنصر انتقالی که همان allow می‌باشد که قبل از عنصر مسیر configuration/system.web/authorization/deny برای همه کاربر است قرار می‌دهد.
InsertAfter:
عنصر تعیین شده را بعد از مسیر تعیین شده با XPath قرار می‌دهد.
Transform="InsertAfter(XPath expression)"
مثال:
<configuration xmlns:xdt="...">
  <authorization>
    <deny users="UserName"
      xdt:Transform="InsertAfter
        (/configuration/system.web/authorization/allow[@roles='Admins'])" />
  </authorization>
</configuration>
Remove:
عنصر تعیین شده را از فایل انتفالی حذف می‌نماید. اگر چندین عنصر یافت شود اولین عنصر حذف خواهد شد.
Transform="Remove"
مثال
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add xdt:Transform="Remove" />
  </connectionStrings>
</configuration>

همه عناصر add عنصر connectionstring را انتخاب و اولین add را حذف می‌نماید.
RemoveAll:
عنصر یا عناصر تعیین شده را از فایل انتقالی حذف می‌نماید.
Transform="RemoveAll"

 مثال:
<configuration xmlns:xdt="...">
  <connectionStrings>
    <add xdt:Transform="RemoveAll" />
  </connectionStrings>
</configuration>
همه عناصر add را از فایل انتقالی حذف می‌نماید.
RemoveAttribute:
ویژگی تعیین شده را از عناصر انتخاب شده حذف می‌نماید.
Transform="RemoveAttributes(comma-delimited list of one or more attribute names)"
مثال:
<configuration xmlns:xdt="...">
  <compilation 
    xdt:Transform="RemoveAttributes(debug,batch)">
  </compilation>
</configuration>

ویژگی debug و batch را از عنصر compilation وب کانفیگ انتقالی حذف می‌نماید.
SetAttribute:
ویژگی تعیین شده را با مقادیر انتقالی مقدار دهی می‌کند.
Transform="SetAttributes(comma-delimited list of one or more attribute names)"

مثال:
<configuration xmlns:xdt="...">
  <compilation 
    batch="false"
    xdt:Transform="SetAttributes(batch)">
  </compilation>
</configuration>

ویژگی batch وب کانفیگ انتقالی را با مقدار false مقدار دهی می‌کند.

ادامه دارد...

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

Value 

اگر مقدار null مدیریت نشود، میتواند باعث ایجاد نتایج نامطلوب، یا باعث از کار افتادن برنامه شود. شئ null به خودی خود مخرب نیست؛ اما اگر بخواهیم به یکی از متدها یا خاصیت‌های آن دسترسی داشته باشیم، با استثنای معروف NullReferenceException روبرو می‌شویم. برای در امان ماندن، باید همیشه اطمینان داشته باشیم که پیش از استفاده از امکانات شئ، ارجاع آن null نباشد. در قطعه کد زیر برخی از رفتارهای null value آورده شده:
// Behavior 1 
object obj = null;
bool objValueEqual = obj.Equals(null);

// Behavior 2 
object obj = null;
Type objType = obj.GetType();

// Behavior 3
string str = (string)null;
bool strType = str is string;

// Behavior 4
int num = 5;
Nullable<int> nullableNum = 5;
bool typeEqual = num.GetType() == nullableNum.GetType();

// Behavior 5
Type inType = typeof(int);
Type nullableIntType = typeof(Nullable<int>);
bool typeEqual = inType == nullableIntType;
  • در رفتار اول هرچند که متد Equals از شی null در دسترس است و با مقدار null مقایسه شده اما در زمان اجرا پیغام خطای NullReferenceException را خواهیم داشت. 
  • در رفتار دوم هم پیغام خطا را خواهیم داشت. شئ با مقدار null، در زمان اجرا هیچ نوعی را برنمیگرداند. 
  • در رفتار سوم هر چند که مقدار null صریحا به رشته تبدیل شده و برای چاپ متغیر str پیام خطایی را نخواهیم داشت، اما متغیر strType در خروجی، false خواهد بود. همانطور که در رفتار دوم گفته شد، شیء با مقدار null هیچ نوعی را برنمیگرداند. 
  • خروجی رفتار چهارم true خواهد بود. به این صورت که هر دو از نوع System.int32 خواهند بود.
  • در رفتار پنجم اگر از نوع‌ها، خروجی جداگانه بگیریم، خواهیم دیدکه نوع int از System.int32 و <Nullable<int از نوع System.Nullable`1[System.Int32] میباشند، در نتیجه خروجی false است. اشیای nullable بعد از اینکه مقداری مشخص را دریافت کردند، به صورت یک شیء غیر nullable رفتار خواهند کرد.

مدیریت مقادیر null در سربارگذاری متدها   

        static void Main(string[] args)
        {
            Console.WriteLine(Method(null));
            Console.ReadLine();
        }
        private static string Method(object obj)
        {
            return "Object parameter";
        }
        private static string Method(string str)
        {
            return "String parameter";
        }
در قطعه کد بالا، فراخوانی متد سربارگذاری شده با مقدار ورودی null، باعث اجرای متدی میشود که پارامتر ورودی آن از نوع رشته است. تا زمانیکه یکی از پارامترها بتواند به دیگری تبدیل شود، برنامه بدون خطا کامپایل خواهد شد. اما اگر هیچ تبدیل نوعی بین پارامترها وجود نداشته باشد، کد کامپایل نخواهد شد. بین متدهای سربارگذاری شده، متدی که نوع پارامتر آن مشخص‌تر است، فراخوانی میشود. برای اینکه متد خاصی را مجبور به اجرا کنیم، باید مقدار null را پیش از ارسال، به نوع پارامتر آن متد تبدیل کنید.(object)null

رفتارهای ()Math.Round

var rounded = Math.Round(1.5); // 2
var rounded = Math.Round(2.5); // 2

var rounded = Math.Round(2.5, MidpointRounding.ToEven); // 2
var rounded = Math.Round(2.5, MidpointRounding.AwayFromZero); // 3

var value = 1.4f;
var rounded = Math.Round(value + 0.1f); // 1
متد Round از کلاس Math، ورودی را که عددی اعشاری است، گرد میکند. اگر مقدار اعشار کمتر از ۰.۵ باشد، به سمت پایین و اگر بیشتر از ۰.۵ باشد، به سمت بالا گرد میشود. اما اگر ورودی دقیقا مقدار اعشاری ۰.۵ را داشته باشد چطور؟ متد Round به صورت پیش‌فرض ورودی  را به نزدیکترین عدد زوج گرد میکند، به این دلیل خط‌های ۱ و ۲ از قطعه کد بالا، خروجی یکسان ۲ را خواهند داشت. این متد آرگومان دومی هم دارد که دو حالت MidpointRounding.ToEven و MidpointRounding.AwayFromZero را می‌توان برای آن مشخص کرد. ToEven همان رفتار پیش‌فرض متد است که ورودی را به نزدیکترین عدد زوج گرد میکند و از حالت AwayFromZero میشود برای گرد کردن ورودی به عدد بزرگتر استفاده کرد (خط ۵). 
در خط ۸ یک حالت خاص دیگر نیز داریم. انتظار میرود که خروجی، به نزدیکترین عدد زوج گرد شود و نتیجه ۲ باشد؛ مثل خط ۱، اما خروجی ۱ خواهد بود. وقتی ورودی‌ها را از نوع float در نظر بگیریم، مقدار 0.1f کمی کمتر از ۰.۱ خواهد بود و نتیجه محاسبه کمی کمتر از ۱.۵. برای پرهیز از این مسئله بهتر است ورودی متد Round را از نوع decimal در نظر بگیریم.
 

مقدار دهی اولیه کلاسها 

پیشنهاد میشود برای جلوگیری از وقوع استثناءها از مقدار دهی اولیه کلاسها در سازنده کلاس، بخصوص اگر سازنده استاتیک داشته باشیم، پرهیز کنیم. ترتیب مقدار دهی اولیه زمانیکه از یک کلاس یه وهله ساخته میشود، به قرار زیر است:
  • فیلدهای استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
  • سازنده استاتیک (زمانیکه کلاس برای اولین بار در دسترس قرار میگیرد)
  • فیلدهایی از کلاس که در نمونه ساخته شده در دسترس قرار میگیرند.
  • سازنده کلاس که در زمان ایجاد یک نمونه از کلاس در دسترس قرار میگیرد.
در قطعه کد زیر اگر نمونه‌ای از کلاس FailingClass ساخته شود، انتظار میرود که خطای InvalidOperationException صادر شود؛ اما برنامه با خطای TypeInitializationException متوقف میشود. در واقع در زمان اجرا به صورت خودکار خطای TypeInitializationException، خطای InvalidOperationException را پوشش میدهد. اگر بجای  InvalidOperationException یک دستور ساده WriteLine داشته باشیم، سازنده کلاس FailingClass مجال کامل شدن را خواهد داشت. اما با خطایی که داخل سازنده صادر کرده‌ایم، سازنده کلاس بدون اینکه به طور کامل به پایان برسد، متوقف خواهد شد. 
    public static class Config
    {
        public static bool ThrowException { get; set; } = true;
    }

    public class FailingClass
    {
        static FailingClass()
        {
            if (Config.ThrowException)
            {
                throw new InvalidOperationException();
            }
        }
    }
حال که میدانیم خطای اصلی که در این مواقع صادر میشود چیست، شاید بخواهیم به روش زیر آن را مدیریت کنیم.
try
{
   var failedInstance = new FailingClass();
}
catch (TypeInitializationException) { }

Config.ThrowException = false;
var instance = new FailingClass();
اگر قطعه کد بالا را بدون بخش try  اجرا کنیم، برنامه ابتدا صدور خطا را false میکند و بدون مشکل از کلاس نمونه‌ای ساخته میشود. اما اگر بخش try را داشته باشیم، هر چند که خطا در بخش try گرفته میشود و تنظیم صدور خطا false است، باز هم در خط آخر و در زمان ایجاد یک نمونه از کلاس، پیام خطای TypeInitializationException خواهیم داشت. علت آن است که سازنده استاتیک کلاس فقط یک بار فراخوانی میشود و اگر در این فراخوانی خطایی رخ دهد، این خطا در اثر ایجاد سایر نمونه‌ها و یا استفاده مستقیم از کلاس، مجددا صادر خواهد شد. در نتیجه این کلاس تا زمانیکه پردازش آن در جریان است، غیرقابل استفاده خواهد بود. یک مثال دیگر از ترتیب فراخوانی‌ها را بررسی میکنیم.
public class BaseClass
{
    {
        public BaseClass()
        {
            VirtualMethod(1);
        }
        public virtual int VirtualMethod(int dividend)
        {
            return dividend / 1;
        }
    }

    public class DerivedClass : BaseClass
    {
        int divisor;
        public DerivedClass()
        {
            divisor = 1;
        }
        public override int VirtualMethod(int dividend)
        {
            return base.VirtualMethod(dividend / divisor);
        }
    }
در قطعه کد بالا هر چند که همه چیز درست به نظر میرسد، اما اگر از کلاس DerivedClass نمونه‌ای ساخته شود، با پیام خطای DivideByZeroException مواجه میشویم. علت این مشکل ترتیب مقدار دهی اولیه در کلاسهای فرزند است. ابتدا فیلدهای کلاس فرزند مقدار دهی میشوند و بعد فیلدهای کلاس پایه، بعد سازنده کلاس پایه فراخوانی میشود و پس از آن سازنده کلاس فرزند. ترتیب فراخوانی‌ها به همین جا محدود نمیشود. 
در مثال بالا متد VirtualMethod که در سازنده کلاس پایه فراخوانی شده، پیش از این که کد داخل خود را اجرا کند، متد VirtualMethod را در کلاس فرزند، فراخوانی میکند و کلاس فرزند مجالی را برای مقدار دهی متغیر divisor، در سازنده خود نخواهد داشت. در نتیجه مقدار این متغیر در متد VirtualMethod صفر خواهد ماند و باعث صدور استثناء میشود. برای پرهیز از چنین مشکلاتی بهتر است فیلدهای یک کلاس به صورت مستقیم مقدار دهی اولیه بشوند. مقدار دهی اولیه و یا فراخوانی متدهای virtual در سازنده کلاس‌ها میتواند باعث بروز رفتارهای پیش بینی نشده‌ای شوند.

چند ریختی 

 چند ریختی قابلیتی است برای کلاسهای متفاوت تا بتوانند یک اینترفیس مشابه را به صورت‌های مختلفی پیاده‌سازی کنند. اما قطعه کد زیر قاعده چند ریختی را نقض میکند. 
 class Program
    {
        static void Main(string[] args)
        {
            var instance = new DerivedClass();
            var result = instance.Method();
            result = ((BaseClass)instance).Method();
            Console.WriteLine(instance + " -> " + result); // Derived Class ...  -> Method in BaseClass
            Console.ReadLine();

        }
    }

    public class BaseClass
    {
        public virtual string Method()
        {
            return "Method in BaseClass";
        }
    }

    public class DerivedClass : BaseClass
    {
        public override string ToString()
        {
            return "Derived Class ... ";
        }

        public new string Method()
        {
            return "Method in DerivedClass";
        }
    }
در خروجی کنسول هرچند که Instance همچنان وهله‌ای از DerivedClass است اما به دلیل تبدیل در خط ۷، Method کلاس DerivedClass به وسیله کلاس پایه پنهان شده و Method کلاس پایه فراخوانی میشود. در قطعه کد زیر حالت مشابه‌ای را که در بالا داشتیم، برای interface‌ها دیده میشود.
class Program
    {
        static void Main(string[] args)
        {
            var instance = new DerivedClass();
            var result = instance.Method(); // -> Method in DerivedClass
            result = ((IInterface)instance).Method(); // -> Method belonging to IInterface
            Console.WriteLine(result);
            Console.ReadLine();
        }
    }

    public interface IInterface
    {
        string Method();
    }

    public class DerivedClass : IInterface
    {
        public string Method()
        {
            return "Method in DerivedClass";
        }
        string IInterface.Method()
        {
            return "Method belonging to IInterface";
        }
}
هرچند که به نظر میرسد دلیلی برای استفاده از روشهای گفته شده وجود ندارد، اما اگر بخواهیم بیش از یک پیاده‌سازی را برای یک متد در یک کلاس داشته باشیم، میتواند مورد توجه قرار گیرد. بخصوص اگر نیاز باشد که پیاده‌سازی دوم خودش به طور مستقلی در کلاسی دیگر استفاده شود.

Iterators 

Iterator‌ها (تکرار شونده‌ها) ساختارهایی هستند که برای حرکت در عناصر یک collection استفاده میشوند. عموما از دستور foreach استفاده و نوع جنریک <IEnumerable<T را نمایندگی میکنند. هر چند که استفاده از آنها ساده است، اما اگر کارکرد داخلی iteratorها را درک نکنیم ممکن است به دام استفاده نادرست از آنها گرفتار شویم. در قطعه کد زیر کلاس Test صدا زده میشود و مقادیر یک تا پنج به صورت یک IEnumerable از داخل بلوک using بازگشت داده میشود. 
private IEnumerable<int> GetEnumerable(StringBuilder log)
{
     using (var test = new Test(log))
      {
          return Enumerable.Range(1, 5);
      }
}

فرض کنیم کلاس Test اینترفیس IDisposable را پیاده‌سازی کرده و در سازنده و متد Dispose خود پیامهایی را به log اضافه کند. در مثالهای واقعی، کلاس Testمیتواند اتصالی به پایگاه داده باشد و رکوردهای خوانده شده، بازگشت داده شوند. توسط حلقه زیر مقدار خروجی تابع را چاپ میکنیم.
var log = new StringBuilder();
            
foreach (var number in GetEnumerable(log))
{
     log.AppendLine($"{number}");
}
انتظار میرود که خروجی به این صورت باشد که ابتدا رشته Created (از سازنده کلاس Test) چاپ شود بعد اعداد یک تا پنج و در نهایت رشته Disposed (از متد Dispose کلاس Test). به عبارتی در ابتدای کار، بلوک using، سازنده کلاس را فراخوانی کند و بعد از اینکه بلوک به پایان کارش رسید متد Dispose کلاس فراخوانی شود. اما در واقع خروجی به صورت زیر خواهد بود. 
Created
Disposed
1
2
3
4
5
این تفاوت در دنیای واقعی مهم است؛ به اینصورت که مثلا اتصال به پایگاه داده قبل از اینکه داده‌ها خوانده شوند، بسته میشود و قطعه کد به درستی عمل نخواهد کرد. تنها راه حل، پیمایش در collection داخل using و بازگشت هر مقدار به صورت مجزا است، که در زیر آمده است.
 using (var test = new Test(log))
 {
     foreach (var i in Enumerable.Range(1,5))
     {
         yield return i;
     }
 }
فقط در این صورت است که کلاس Test بعد از اتمام کار حلقه و در زمان درست به پایان میرسد. توسط کلمه کلیدی yield و برای متدی که خروجی قابل پیمایش داشته باشد میتوان چندین مقدار را بازگشت داد. ترتیب اجرای دستورات در قطعه کد بالا به این صورت است که ابتدا نمونه‌ای از کلاس Test ایجاد میشود و سازنده کلاس فراخوانی میشود، سپس حلقه foreach به تعداد مشخص شده در Range مقادیر بازگشتی را در خروجی تابع قرار میدهد. وقتی که کار حلقه تمام شد، بلوک using دستورات را ادامه خواهد داد که برابر با خاتمه دادن به تمام نمونه‌ها و منابع استفاده شده در بلوک است؛ یعنی فراخوانی متد Dispose. با استفاده از این روش خروجی به شکل زیر خواهد بود. 
Created
1
2
3
4
5
Disposed

مطالب
مروری بر کاربردهای Action و Func - قسمت سوم
در ادامه مثال سوم قسمت قبل، در مورد حذف کدهای تکراری توسط Action و Func، در این قسمت به یک مثال نسبتا پرکاربرد دیگر آن جهت ساده سازی try/catch/finally اشاره خواهد شد.
احتمالا هزاران بار در کدهای خود چنین قطعه کدی را تکرار کرده‌اید:
try {
       // code
} catch(Exception ex) {
       // do something
}
این مورد را نیز می‌توان توسط Actionها کپسوله کرد و پیاده سازی قسمت بدنه try آن‌را به فراخوان واگذار نمود:
void Execute(Action action) {
    try {
       action();
    } catch(Exception ex) {
       // log errors
    }
}
و برای نمونه جهت استفاده از آن خواهیم داشت:
Execute(() => {open a file});

یا اگر عمل انجام شده باید خروجی خاصی را بازگرداند (برخلاف یک Action که خروجی از آن انتظار نمی‌رود)، می‌توان طراحی متد Execute را با Func انجام داد:
public static class SafeExecutor
{
    public static T Execute<T>(Func<T> operation)
    {
        try
        {
            return operation();
        }
        catch (Exception ex)
        {
            // Log Exception
        }
        return default(T);
    }
}
در این حالت فراخوانی متد Execute به نحو زیر خواهد بود:
var data = SafeExecutor.Execute<string>(() =>
{
    // do something
    return "result";
});
و اگر در این بین استثنایی رخ دهد، علاوه بر ثبت جزئیات خطای رخ داده شده، نال را بازگشت خواهد داد.

از همین دست می‌توان به کپسوله سازی منطق «سعی مجدد» در انجام کاری اشاره کرد:
public static class RetryHelper
{
   public static void RetryOperation(Action action, int numRetries, int retryTimeout)
   {
       if( action == null )
           throw new ArgumentNullException("action");

       do
       {
          try {  action(); return;  }
          catch
          { 
              if( numRetries <= 0 ) throw;
              else 
                 Thread.Sleep( retryTimeout );
           }
       } while( numRetries-- > 0 );
   }
}
برای مثال فرض کنید برنامه قرار است اطلاعاتی را از وب دریافت کند. ممکن است در سعی اول آن، خطای اتصال یا در دسترس نبودن لحظه‌ای سایت رخ دهد. در اینجا نیاز خواهد بود تا این عملیات چندین بار تکرار شود؛ که نمونه‌ای از آن‌را در ذیل ملاحظه می‌کنید:
RetryHelper.RetryOperation(() => SomeFunction(), 3, 1000);

مطالب
اجرای SSIS Package از طریق برنامه کاربردی

مقدمه

در اکثر موارد در یک Landscape عملیاتی، چنانچه به تجمیع و انتقال داده‌ها از بانک‌های اطلاعاتی مختلف نیاز باشد، از SSIS Package اختصار (SQL Server Integration Service) استفاده می‌شود و معمولاً با تعریف یک Job در سطح SQL Server به اجرای Package در زمانهای مشخص می‌پردازند. چنانچه در موقعیتی لازم باشد که از طریق برنامه کاربردی توسعه یافته، به اجرای Package مبادرت ورزیده شود و البته نخواهیم Job تعریف شده را از طریق کد برنامه، اجرا کنیم و در واقع این امکان را داشته باشیم که همانند یک رویه ذخیره شده تعریف شده در سطح بانک اطلاعاتی به اجرای عمل فوق بپردازیم، یک راه حل می‌تواند تعریف یک CLR Stored Procedures باشد. در این مقاله به بررسی این موضوع پرداخته می‌شود، در ابتدا لازم است به بیان تئوری موضوع پرداخته شود (قسمت‌های 1 الی 5) در ادامه به ذکر پیاده سازی روش پیشنهادی پرداخته می‌شود.

1- اجرای Integration Service Package 

جهت اجرای یک Package از ابزارهای زیر می‌توان استفاده کرد:
• command-line ابزار خط فرمان dtexec.exe
• ابزار اجرائی پکیج dtexecui.exe
• استفاده از SQL Server Agent job
توجه: همچنین یک Package را در زمان طراحی در  Business Intelligence Development Studio) BIDS)  می‌توان اجرا نمود.

2- استفاده از dtexec جهت اجرای Package

با استفاده از ابزار dtexec می‌توان Package‌های ذخیره شده در فایل سیستم، یک SQL Instance و یا Package‌های ذخیره شده در Integration Service را اجرا نمود.

توجه:
در سیستم عامل‌های 64 بیتی، ابزار dtexec موجود در Integration Service با نسخه 64 بیتی نصب می‌شود. چنانچه بایست Package‌های معینی را در حالت 32 بیتی اجرا کنید، لازم است ابزار dtexec نسخه 32 بیتی نصب شود. ابزار dtexec دستیابی به تمامی ویژگی‌های پیکربندی و اجرای Package از قبیل اتصالات، مشخصات(Properties)، متغیرها، logging و شاخص‌های پردازشی را فراهم می‌کند.
توجه: زمانی که از نسخه‌ی ابزار dtexec که با SQL Server 2008 ارائه شده استفاده می‌کنید برای اجرای یک SSIS Package نسخه 2005، Integration Service به صورت موقت Package را به نسخه 2008 ارتقا می‌دهد، اما نمی‌توان از ابزار dtexec برای ذخیره این تغییرات استفاده کرد.

2-1- ملاحظات نصب dtexec روی سیستم‌های 64 بیتی

به صورت پیش فرض، یک سیستم عامل 64 بیتی که هر دو نسخه 64 بیتی و 32 بیتی ابزار خط فرمان Integration Service را دارد، نسخه 32 بیتی نصب شده را در خط فرمان اجرا خواهد کرد. نسخه 32 بیتی بدین دلیل اجرا می‌شود که در متغیر محیطی (Path (Path environment variable مسیر directory نسخه 32 بیتی قرار گرفته است.به طور معمول:
(<drive>:\Program Files(x86)\Microsoft SQL Server\100\DTS\Binn)
توجه: اگر از SQL Server Agent برای اجرای Package استفاده می‌کنید، SQL Server Agent به طور خودکار از ابزار نسخه 64 بیتی استفاده می‌کند. SQL Server Agent از Registry و نه از متغیر محیطی Path استفاده می‌کند. برای اطمینان از اینکه نسخه 64 بیتی این ابزار را در خط فرمان اجرا می‌کنید، directory را به directory ای تغییر دهید که شامل نسخه 64 بیتی این ابزار است(<drive>:\Program Files\Microsoft SQL Server\100\DTS\Binn) و ابزار را از این مسیر اجرا کنید و یا برای همیشه مسیر قرار گرفته در متغیر محیطی path را با مسیری که نسخه 64 بیتی قرار دارد، جایگزین کنید.

2-2- تفسیر کدهای خروجی

هنگامی که یک Package اجرا می‌شود، dtexec یک کد خروجی (Return Code) بر می‌گرداند:

 مقدار توصیف
 0  Package با موفقیت اجرا شده است.
 1  Package با خطا مواجه شده است.
 3 Package در حال اجرا توسط کاربر لغو شده است.
 4  Package پیدا نشده است.
 5  Package بارگذاری نشده است.
 6  ابزار با یک خطای نحوی یا خطای معنایی در خط فرمان برخورد کرده است.

2-3- قوانین نحوی dtexec

تمامی گزینه‌ها (Options) باید با یک علامت Slash (/)  و یا Minus (-)  شروع شوند.
یک آرگومان باید در یک quotation mark محصور شود چنانچه شامل یک فاصله خالی باشد.
گزینه‌ها و آرگومان‌ها بجز رمزعبور حساس به حروف کوچک و بزرگ نیستند.

2-3-1- Syntax

 dtexec /option [value] [/option [value]]…


2-3-2- Parameters

نکته: در Integration Service، ابزار خط فرمان dtsrun که برایData Transformation Service) DTS)‌های نسخه SQL Server 2000 استفاده می‌شد، با ابزار خط فرمان dtexec جایگزین شده است.
• تعدادی از گزینه‌های خط فرمان dtsrun به طور مستقیم در dtexec معادل دارند برای مثال نام Server و نام Package.
• تعدادی از گزینه‌های dtsrun به طور مستقیم در dtexec معادل ندارند.
• تعدادی گزینه‌های خط فرمان جدید dtsexec وجود دارد که در ویژگی‌های جدید Integration Service پشتیبانی می‌شود.

2-3-3- مثال

1) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده است، با استفاده از Windows Authentication :
 dtexec /sq <Package Name> /ser <Server Name>

2) به منظور اجرای یک SSIS Package که در پوشه File System در SSIS Package Store ذخیره شده است :
 dtexec /dts “\File System\<Package File Name>”

3) به منظور اجرای یک SSIS Package که در سیستم فایل ذخیره شده است و مشخص کردن گزینه logging:
 dtexec /f “c:\<Package File Name>” /l “DTS.LogProviderTextFile; <Log File Name>”

4) به منظور اجرای یک SSIS Package که در SQL Server ذخیره شده با استفاده از SQL Server Authentication برای نمونه(user:ssis;pwd:ssis@ssis)و رمز (Package(123:
 dtexec  /server “<Server Name>”  /sql “<Package Name>”  / user “ssis” /Password “ssis@ssis” /De “123”


3- تنظیمات سطح حفاظتی یک Package

به منظور حفاظت از داده‌ها در Package‌های Integration Service می‌توانید یک سطح حفاظتی (protection level) را تنظیم کنید که به حفاظت از داده‌های صرفاً حساس یا تمامی داده‌های یک Package کمک نماید. به  علاوه می‌توانید این داده‌ها را با یک Password یا یک User Key رمزگذاری نمائید یا به رمزگذاری داده‌ها در بانک اطلاعاتی اعتماد کنید. همچنین سطح حفاظتی که برای یک Package استفاده می‌کنید، الزاماً ایستا (static) نیست و در طول چرخه حیات یک Package می‌تواند تغییر کند. اغلب سطح حفاظتی در طول توسعه یا به محض (deploy) استقرار Package تنظیم می‌شود.
توجه: علاوه بر سطوح حفاظتی که توصیف شد، Package‌ها در بانک اطلاعاتی msdb ذخیره می‌شوند که همچنین می‌توانند توسط نقش‌های ثابت در سطح بانک اطلاعاتی (fixed database-level roles) حفاظت شوند. Integration Service شامل 3 نقش ثابت بانک اطلاعاتی برای نسبت دادن مجوزها به Package است که عبارتند از db_ssisadmin  ،db_ssisltduser و db_ssisoperator

3-1- درک سطوح حفاظتی

در یک Package اطلاعات زیر به عنوان حساس تعریف می‌شوند:
• بخش password در یک connection string. گرچه، اگر گزینه ای را که همه چیز را رمزگذاری کند، انتخاب کنید تمامی connection string حساس در نظر گرفته می‌شود.
• گره‌های task-generated XML که برچسب (tagged) هایی حساس هستند.
• هر متغییری که به عنوان حساس نشان گذاری شود.

3-1-1- Do not save sensitive

هنگامی که Package ذخیره می‌شود از ذخیره مقادیر ویژگی‌های حساس در Package جلوگیری می‌کند. این سطح حفاظتی رمزگذاری نمی‌کند اما در عوض از ذخیره شدن ویژگی هایی که حساس نشان گذاری شده اند به همراه Package جلوگیری می‌کند.

3-1-2- Encrypt all with password

به منظور رمزگذاری تمامی Package از یک Password استفاده می‌شود. Package توسط Password ای رمزگذاری می‌شود  که کاربر هنگامی که Package را ایجاد یا Export می‌کند، ارائه می‌دهد. به منظور باز کردن Package در SSIS Designer یا اجرای Package توسط ابزار خط فرمان dtexec کاربر بایست رمز Package را ارائه نماید. بدون رمز کاربر قادر به دستیابی و اجرای Package نیست.

3-1-3- Encrypt all with user key

به منظور رمزگذاری تمامی Package از یک کلید که مبتنی بر Profile کاربر جاری می‌باشد، استفاده می‌شود. تنها کاربری که Package را ایجاد یا Export می‌کند، می‌تواند Package را در SSIS Designer باز کند و یا Package را توسط ابزار خط فرمان dtexec اجرا کند.

3-1-4- Encrypt sensitive with password

به منظور رمزگذاری تنها مقادیر ویژگی‌های حساس در Package از یک Password استفاده می‌شود. برای رمزگذاری از DPAPI استفاده می‌شود. داده‌های حساس به عنوان بخشی از Package ذخیره می‌شوند اما آن داده‌ها با استفاده از Password رمزگذاری می‌شوند. به منظور باز نمودن Package در SSIS Designer کاربر باید رمز Package را ارائه دهد. اگر رمز ارائه نشود، Package بدون داده‌های حساس باز می‌شود و کاربر باید مقادیر جدیدی برای داده‌های حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه می‌شود.

3-1-5- Encrypt sensitive with user key

به منظور رمزگذاری تنها مقادیر ویژگی‌های حساس در Package از یک کلید که مبتنی بر Profile کاربر جاری می‌باشد، استفاده می‌شود. تنها کاربری که از همان Profile استفاده می‌کند، Package را می‌تواند بارگذاری (load) کند. اگر کاربر متفاوتی Package را باز نماید، اطلاعات حساس با مقادیر پوچی جایگزین می‌شود و کاربر باید مقادیر جدیدی برای داده‌های حساس فراهم کند. اگر کاربر سعی نماید Package را بدون ارائه رمز اجرا کند، اجرای Package با خطا مواجه می‌شود. برای رمزگذاری از DPAPI استفاده می‌شود.

3-1-6- (Rely on server storage for encryption (ServerStorage

با استفاده از نقش‌های بانک اطلاعاتی، SQL Server تمامی Package را حفاظت می‌کند. این گزینه تنها زمانی پشتیبانی می‌شود که Package در بانک اطلاعاتی msdb ذخیره شده است.

4- استفاده از نقش‌های Integration Service

برای کنترل کردن دستیابی به Package، SSIS شامل 3 نقش ثابت در سطح بانک اطلاعاتی است. نقش‌ها می‌توانند تنها روی Package هایی که در بانک اطلاعاتی msdb ذخیره شده اند، بکار روند. با استفاده از SSMS می‌توانید نقش‌ها را به Package‌ها نسبت دهید، این انتساب نقش‌ها در بانک اطلاعاتی msdb ذخیره می‌شود.

 Write action  Read action Role
 Import packages
Delete own packages
Delete all packages
Change own package roles
Change all package roles

* به نکته رجوع شود

 Enumerate own packages
Enumerate all packages
View own packages
View all packages
Execute own packages
Execute all packages
Export own packages
Export all packages
Execute all packages in SQL Server Agent
 db_ssisadmin
or
sysadmin

Import packages
Delete own packages
Change own package roles

Enumerate own packages
Enumerate all packages
View own packages
Execute own packages
Export own packages
db_ssisltduser
None
Enumerate all packages
View all packages
Execute all packages
Export all packages
Execute all packages in SQL Server Agent
db_ssisoperator
Stop all currently running packages
View execution details of all running packages
Windows administrators
* نکته: اعضای نقش‌های db_ssisadmin و dc_admin ممکن است قادر باشند مجوزهای خودشان را تا سطح sysadmin ارتقا دهند براساس این ترفیع مجوز امکان اصلاح و اجرای Package‌ها از طریق SQL Server Agent میسر می‌شود. برای محافظت در برابر این ارتقا، با استفاده از یک (account) حساب Proxy  با دسترسی محدود، Job هایی که این Package‌ها را اجرا می‌کنند، پیکربندی شوند یا  تنها اعضای نقش sysadmin به نقش‌های db_ssisadmin و dc_admin افزوده شوند.

همچنین جدول sysssispackages در بانک اطلاعاتی msdb شامل Package هایی است که در SQL Server ذخیره می‌شوند. این جدول شامل ستون هایی که اطلاعاتی درباره نقش هایی که به Package‌ها نسبت داده شده است، می‌باشد.
به صورت پیش فرض، مجوزهای نقش‌های ثابت بانک اطلاعاتی db_ssisadmin و db_ssisoperator و شناسه منحصر به فرد کاربری (unique security identifier) که Package را ایجاد کرده برای خواندن Package بکار می‌رود، و مجوزهای نقش db_ssisadmin و شناسه منحصر به فرد کاربری که Package را ایجاد کرده برای نوشتن Package به کار می‌رود. یک User باید عضو نقش db_ssisadmin و db_ssisltduser یا db_ssisoperator برای داشتن دسترسی خواندن Package باشد. یک User باید عضو نقش db_ssisadmin برای داشتن دسترسی نوشتن Package باشد.

5- اتصال به صورت Remote به Integration Service

زمانی که یک کاربر بدون داشتن دسترسی کافی تلاش کند به یک Integration Service به صورت Remote متصل شود، با پیغام خطای "Access is denied" مواجه می‌شود. برای اجتناب از این پیغام خطا می‌توان تضمین کرد که کاربر مجوز مورد نیاز DCOM را دارد.
به منظور پیکربندی کردن دسترسی کاربر به صورت Remote به سرویس Integration  مراحل زیر را دنبال کنید:
- Component Service را باز نمایید ( در Run عبارت dcomcnfg را تایپ کنید).
- گره Component Service را باز کنید، گره Computer و سپس My Computer را باز نمایید و روی DCOM Config کلیک نمایید.
- گره DCOM Config را باز کنید و از لیست برنامه هایی که می‌توانند پیکربندی شوند MsDtsServer را انتخاب کنید.
- روی Properties برنامه MsDtsServer رفته و قسمت Security را انتخاب کنید.
- در قسمت Lunch and Activation Permissions، مورد Customize را انتخاب و سپس روی Edit کلیک نمایید تا پنجره Lunch Permission باز شود.
- در پنجره Lunch Permission، کاربران را اضافه و یا حذف کنید و مجوزهای مناسب را به کاربران یا گروه‌های مناسب نسبت دهید. مجوزهای موجود عبارتند از Local Lunch، Remote Lunch، Local Activation و Remote Activation .
- در قسمت Access Permission مراحل فوق را به منظور نسبت دادن مجوزهای مناسب به کاربران یا گروه‌های مناسب انجام دهید.
- سرویس Integration  را Restart کنید.
مجوز دسترسی  Lunch به منظور شروع و خاتمه سرویس، اعطا  یا رد  می‌شود و مجوز دسترسیActivation به منظور متصل شدن به سرویس، اعطا (grant) یا رد (deny) می‌شود.

6- پیاده سازی

در ابتدا به ایجاد یک CLR Stored Procedures پرداخته می‌شود نام اسمبلی ساخته شده به این نام RunningPackage.dll می‌باشد و حاوی کد زیر است:
Partial Public Class StoredProcedures
    '------------------------------------------------
    'exec dbo.Spc_NtDtexec 'Package','ssis','ssis@ssis','1234512345'
    '------------------------------------------------
    <Microsoft.SqlServer.Server.SqlProcedure()> _
    Public Shared Sub Spc_NtDtexec(ByVal PackageName As String, _
                                   ByVal UserName As String, _
                                   ByVal Password As String, _
                                   ByVal Decrypt As String)
        Dim p As New System.Diagnostics.Process()
        p.StartInfo.FileName = "C:\Program Files\Microsoft SQL Server\100\DTS\Binn\DTExec.exe"
        p.StartInfo.RedirectStandardOutput = True
        p.StartInfo.Arguments = "/sql " & PackageName & " /User " & UserName & " /Password " & Password & " /De " & Decrypt
        p.StartInfo.UseShellExecute = False
        p.Start()
        p.WaitForExit()
        Dim output As String
        output = p.StandardOutput.ReadToEnd()
        Microsoft.SqlServer.Server.SqlContext.Pipe.Send(output)
    End Sub
End Class
در حقیقت توسط این رویه به اجرای برنامه dtexec.exe و ارسال پارامترهای مورد نیاز جهت اجرا پرداخته می‌شود. با توجه به توضیحات تئوری بیان شده، سطح حفاظتی Package ایجاد شده Encrypt all with password توصیه می‌شود که رمز مذکور در قالب یکی از پارامتر ارسالی به رویه ساخته شده موسوم به Spc_NtDtexec ارسال می‌گردد.

در قدم بعدی نیاز به Register کردن dll ساخته شده در سطح بانک اطلاعاتی SQL Server است، این گام‌ها پس از اتصال به SQL Server Management Studio به شرح زیر است:
1- فعال کردن CLR در سرویس SQL Server
SP_CONFIGURE 'clr enabled',1
GO
RECONFIGURE

2- فعال کردن ویژگی TRUSTWORTHY در بانک اطلاعاتی مورد نظر
 ALTER DATABASE <Database Name> SET TRUSTWORTHY ON
GO
RECONFIGURE

3- ایجاد Assembly و Stored Procedure در بانک اطلاعاتی مورد نظر
Assembly ساخته شده با نام RunningPacakge.dll در ریشه :C کپی شود. بعد از ثبت نمودن این Assembly لزومی به وجود آن نمی‌باشد.
USE <Database Name>
GO
CREATE ASSEMBLY [RunningPackage]
AUTHORIZATION [dbo]
FROM 'C:\RunningPackage.dll'
WITH PERMISSION_SET = UNSAFE
Go
CREATE PROCEDURE [dbo].[Spc_NtDtexec]
@PackageName [nvarchar](50),
@UserName [nvarchar](50),
@Password [nvarchar](50),
@Decrypt [nvarchar](50)
WITH EXECUTE AS CALLER
AS
EXTERNAL NAME [RunningPackage].[RunningPackage.StoredProcedures].[Spc_NtDtexec]
GO
توجه: Application User برنامه بایست دسترسی اجرای رویه ذخیره شده Spc_NtDtexec را در بانک اطلاعاتی مورد نظر داشته باشد همچنین بایست عضو نقش db_ssisoperator در بانک اطلاعاتی msdb باشد.( منظور از Application User، لاگین است که در Connection string برنامه قرار داده اید.)

در برنامه کاربردی تان کافی است متدی به شکل زیر ایجاد و با توجه به نیازتان در برنامه به فراخوانی آن و اجرای Package بپردازید.
    Private Sub ExecutePackage()
        Dim oSqlConnection As SqlClient.SqlConnection
        Dim oSqlCommand As SqlClient.SqlCommand
        Dim strCnt As String = String.Empty
        strCnt = "Data Source=" & txtServer.Text & ";User ID=" & txtUsername.Text & ";Password=" & txtPassword.Text & ";Initial Catalog=" & cmbDatabaseName.SelectedValue.ToString() & ";"
        Try
            oSqlConnection = New SqlClient.SqlConnection(strCnt)
            oSqlCommand = New SqlClient.SqlCommand
            With oSqlCommand
                .Connection = oSqlConnection
                .CommandType = System.Data.CommandType.StoredProcedure
                .CommandText = "dbo.Spc_NtDtexec"
                .Parameters.Clear()
                .Parameters.Add("@PackageName", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@UserName", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@Password", System.Data.SqlDbType.VarChar, 50)
                .Parameters.Add("@Decrypt", System.Data.SqlDbType.VarChar, 50)
                .Parameters("@PackageName").Value = txtPackageName.Text.Trim()
                .Parameters("@UserName").Value = txtUsername.Text.Trim()
                .Parameters("@Password").Value = txtPassword.Text.Trim()
                .Parameters("@Decrypt").Value = txtDecrypt.Text.Trim()
            End With
            If (oSqlCommand.Connection.State <> System.Data.ConnectionState.Open) Then
                oSqlCommand.Connection.Open()
                oSqlCommand.ExecuteNonQuery()
                System.Windows.Forms.MessageBox.Show("Success")
            End If
            If (oSqlCommand.Connection.State = System.Data.ConnectionState.Open) Then
                oSqlCommand.Connection.Close()
            End If
        Catch ex As Exception
            MessageBox.Show(ex.Message, "Error")
        End Try
    End Sub 'ExecutePackage
مطالب دوره‌ها
متدهای توکار استفاده از نوع داده‌ای XML - قسمت اول
در دو قسمت قبل، XQuery را به عنوان یک زبان برنامه نویسی استاندارد مورد بررسی قرار دادیم. در ادامه قصد داریم ترکیب آن‌را با توابع ویژه توکار SQL Server جهت کار با نوع داده‌ای XML، مانند exists، modify و امثال آن، تکمیل نمائیم. اگر بخاطر داشته باشید، 5 متد توکار جهت کار با نوع داده‌ای XML در SQL Server پیش بینی شده‌اند:
- query : xml را به عنوان ورودی گرفته و نهایتا یک خروجی XML دیگر را بر می‌گرداند.
- exist : خروجی bit دارد؛ true یا false. ورودی آن یک XQuery است.
- value : یک خروجی SQL Type را ارائه می‌دهد.
- nodes : خروجی جدولی دارد.
- modify : برای تغییر اطلاعات بکار می‌رود.


استفاده از متد exist به عنوان جایگزین سبک وزن XML Schema

یکی از کاربردهای متد exist، تعریف قید بر روی یک ستون XML ایی جدول است. این روش، راه حل دوم و ساده‌ای است بجای استفاده از XML Schema برای ارزیابی و اعتبارسنجی کل سند. پیشنیاز اینکار، تعریف قید مدنظر توسط یک تابع جدید است:
CREATE FUNCTION dbo.checkPerson(@data XML)
RETURNS BIT WITH SCHEMABINDING AS
BEGIN
   RETURN @data.exist('/people/person')
END
GO

CREATE TABLE tblXML
(
id INT PRIMARY KEY,
doc XML CHECK(dbo.checkPerson(doc)=1)  
)
GO
متد checkPerson به دنبال وجود نود people/person، در ریشه‌ی سند XML در حال ذخیره شدن می‌گردد. پس از تعریف این متد، نحوه‌ی استفاده از آن‌را توسط عبارت check در حین تعریف ستون doc ملاحظه می‌کنید.

اکنون برای آزمایش آن خواهیم داشت:
 INSERT INTO tblXML (id,  doc) VALUES
(
 1, '<people><person name="Vahid"/></people>'
)

INSERT INTO tblXML (id,  doc) VALUES
(
 2, '<people><emp name="Vahid"/></people>'
)
Insert اول با موفقیت انجام خواهد شد. اما Insert دوم با خطای ذیل متوقف می‌شود:
 The INSERT statement conflicted with the CHECK constraint "CK__tblXML__doc__060DEAE8".
The conflict occurred in database "testdb", table "dbo.tblXML", column 'doc'.
The statement has been terminated.
همچنین باید در نظر داشت که امکان ترکیب یک XML Schema و تابع اعمال قید نیز با هم وجود دارند. برای مثال از XML Schema برای تعیین اعتبار ساختار کلی سند در حال ذخیره سازی استفاده می‌شود و همچنین نیاز است تا منطق تجاری خاصی را توسط یک تابع، پیاده سازی کرده و در این بین اعمال نمود.


استفاده از متد value برای دریافت اطلاعات

با کاربرد مقدماتی متد value در بازگشت یک مقدار scalar در قسمت‌های قبل آشنا شدیم. در ادامه مثال‌های کاربردی‌تر را بررسی خواهیم کرد.
ابتدا جدول زیر را با یک ستون XML در آن درنظر بگیرید:
 CREATE TABLE xml_tab
(
 id INT IDENTITY PRIMARY KEY,
 xml_col  XML
)
سپس چند ردیف را به آن اضافه می‌کنیم:
 INSERT INTO xml_tab
VALUES ('<people><person name="Vahid"/></people>')
INSERT INTO xml_tab
VALUES ('<people><person name="Farid"/></people>')
در ادامه می‌خواهیم id و نام اشخاص ذخیره شده در جدول را بازیابی کنیم:
SELECT
   id,
   xml_col.value('(/people/person/@name)[1]', 'varchar(50)') AS name
FROM
xml_tab
متد vlaue یک XPath را دریافت کرده، به همراه نوع آن و صفر یا یک نود را بازگشت خواهد داد. به همین جهت، با توجه به عدم تعریف اسکیما برای سند XML در حال ذخیره شدن، نیاز است اولین نود را صریحا مشخص کنیم.


یک نکته
اگر نیاز به خروجی از نوع XML است، بهتر است از متد query که در دو قسمت قبل بررسی شد، استفاده گردد. خروجی متد query همیشه یک untyped XML است یا نال. البته می‌توان خروجی آن‌را به یک typed XML دارای Schema نیز نسبت داد. در اینجا اعتبارسنجی در حین انتساب صورت خواهد گرفت.


استفاده از متد value برای تعریف قیود

از متد value همچنین می‌توان برای تعریف قیود پیشرفته نیز استفاده کرد. برای مثال فرض کنیم می‌خواهیم ویژگی Id سند XML در حال ذخیره شدن، حتما مساوی ستون Id جدول باشد. برای این منظور ابتدا نیاز است همانند قبل یک تابع جدید را ایجاد نمائیم:
 CREATE FUNCTION getIdValue(@doc XML)
RETURNS int WITH SCHEMABINDING AS
BEGIN
  RETURN @doc.value('/*[1]/@Id', 'int')
END
این تابع یک int را باز می‌گرداند که حاصل مقدار ویژگی Id اولین نود ذیل ریشه است. اگر این نود، ویژگی Id نداشته باشد، null بر می‌گرداند.
سپس از این تابع در عبارت check برای مقایسه ویژگی Id سند XML در حال ذخیره شدن و id ردیف جاری استفاده می‌شود:
 CREATE TABLE docs_tab
(
id INT PRIMARY KEY,
doc XML,
CONSTRAINT id_chk CHECK(dbo.getIdValue(doc)=id)  
)
نحوه‌ی تعریف آن اینبار توسط عبارت CONSTRAINT است؛ زیرا در سطح جدول باید عمل کند (ارجاعی را به یک فیلد آن دارد) و نه در سطح یک فیلد؛ مانند مثال ابتدای بحث جاری.
در ادامه برای آزمایش آن خواهیم داشت:
 INSERT INTO docs_tab (id,  doc) VALUES
(
 1, '<Invoice Id="1"/>'
)

INSERT INTO docs_tab (id,  doc) VALUES
(
 2, '<Invoice Id="1"/>'
)
Insert اول با توجه به یکی بودن مقدار ویژگی Id آن با id ردیف، با موفقیت ثبت می‌شود. ولی رکورد دوم خیر:
 The INSERT statement conflicted with the CHECK constraint "id_chk".
The conflict occurred in database "testdb", table "dbo.docs_tab".
The statement has been terminated.


استفاده از متد value برای تعریف primary key

پیشتر عنوان شد که از فیلدهای XML نمی‌توان به عنوان کلید یک جدول استفاده کرد؛ چون امکان مقایسه‌ی محتوای کل آن‌ها وجود ندارد. اما با استفاده از متد value می‌توان مقدار دریافتی را به عنوان یک کلید اصلی محاسبه شده، ثبت کرد:
 CREATE TABLE Invoices
(
 doc XML,
 id AS dbo.getIdValue(doc) PERSISTED PRIMARY KEY
)
Id در اینجا یک computed column است. همچنین باید به صورت PERSISTED علامتگذاری شود تا سپس به عنوان PRIMARY KEY  قابل استفاده باشد.
برای آزمایش آن سعی می‌کنیم دو رکورد را که حاوی ویژگی id برابری هستند، ثبت کنیم:
 INSERT INTO Invoices VALUES
(
 '<Invoice Id="1"/>'
)
INSERT INTO Invoices VALUES
(
 '<Invoice Id="1"/>'
)
مورد اول با موفقیت ثبت می‌شود. مورد دوم خیر:
 Violation of PRIMARY KEY constraint 'PK__Invoices__3213E83F145C0A3F'.
Cannot insert duplicate key in object 'dbo.Invoices'. The duplicate key value is (1).
The statement has been terminated.


توابع دسترسی به مقدار داده‌ها در XQuery

تابع data ، string و text برای دسترسی به مقدار داده‌ها در XQuery پیش بینی شده‌اند.
اگر سعی کنیم مثال زیر را اجرا نمائیم:
 DECLARE @doc XML
SET @doc = '<foo bar="baz" />'
SELECT @doc.query('/foo/@bar')
با خطای ذیل متوقف خواهیم شد:
 XQuery [query()]: Attribute may not appear outside of an element
علت اینجا است که خروجی query از نوع XML است و ما در XPath نوشته شده درخواست بازگشت مقدار یک ویژگی را کرده‌ایم که نمی‌تواند به عنوان ریشه یک سند XML بازگشت داده شود. برای بازگشت مقدار ویژگی bar که baz است باید از متد data استفاده کرد:
 DECLARE @doc XML
SET @doc = '<foo bar="baz" />'
SELECT @doc.query('data(/foo/@bar)')
متد data می‌تواند بیش از یک مقدار را در یک توالی بازگشت دهد:
 DECLARE @x XML
SET @x = '<x>hello<y>world</y></x><x>again</x>'
SELECT @x.query('data(/*)')
در اینجا توسط متد data درخواست بازگشت کلیه root elementsهای سند XML را کرده‌ایم. خروجی آن helloworld again خواهد بود.
اما اگر همین مثال را با متد string اجرا کنیم:
 DECLARE @x XML
SET @x = '<x>hello<y>world</y></x><x>again</x>'
SELECT @x.query('string(/*)')
به خطای آشنای ذیل برخواهیم خورد:
 XQuery [query()]: 'string()' requires a singleton (or empty sequence), found operand of type 'element(*,xdt:untyped) *'
در اینجا چون تابع string باید بیش از یک نود را پردازش کند، خطایی را صادر کرده‌است. برای رفع آن باید دقیقا مشخص کنیم که برای مثال تنها اولین عضو توالی را بازگشت بده:
 SELECT @x.query('string(/*[1])')
خروجی آن helloworld است.
برای دریافت تمام کلمات توسط متد string می‌توان از اسلش کمک گرفت:
 SELECT @x.query('string(/)')
با خروجی helloworldagain که تنها یک string value محسوب می‌شود؛ برخلاف حالت استفاده از متد data که دو مقدار یک توالی را بازگشت داده است.
نمونه‌ی دیگر آن مثال زیر است:
 DECLARE @x XML = '<age>12</age>'
SELECT @x.query('string(/age[1])')
در اینجا نیز باید حتما اولین المان، صراحتا مشخص شود. هرچند به نظر این سند untyped XML تنها یک المان دارد، اما XQuery ذکر شده پیش از اجرای آن، تعیین اعتبار می‌شود. برای عدم ذکر اولین آیتم (در صورت نیاز)، باید XML Schema سند مرتبط، تعریف و در حین تعریف و انتساب مقدار آن، مشخص گردد. همچنین در اینجا به مباحث content و document که در قسمت‌های پیشین نیز ذکر شد باید دقت داشت. حالت پیش فرض content است و می‌تواند بیش از یک root element داشته باشد.

متد text اندکی متفاوت عمل می‌کند. برای بررسی آن، ابتدا یک schema collection جدید را تعریف می‌کنیم که داری تک المانی رشته‌ای است به نام Root.
 CREATE XML SCHEMA COLLECTION root_el AS
'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
                  targetNamespace="urn:geo">
      <xs:element name="Root" type="xs:string" />    
</xs:schema>
'
GO
در ادامه اگر متد text را بر روی یک untyped XML که SChema آن مشخص نشده‌است، فراخوانی کنیم:
 DECLARE @xmlDoc XML
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
/g:Root/text()
')
مقدار datadata... این المان Root را بازگشت خواهد داد. اینبار اگر untyped XML را با تعریف schema آن تبدیل به typed XML کنیم:
 DECLARE @xmlDoc XML(root_el)
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
/g:Root[1]/text()
')
به خطای ذیل برخواهیم خورد:
 XQuery [query()]: 'text()' is not supported on simple typed or 'http://www.w3.org/2001/XMLSchema#anyType'
elements, found 'element(g{urn:geo}:Root,xs:string) *'.
زمانیکه از Schema استفاده می‌شود، دیگر نیازی به استفاده از متد text نیست. فقط کافی است متد text را حذف کرده و بجای آن از متد data استفاده کنیم:
 DECLARE @xmlDoc XML(root_el)
SET @xmlDoc = '<g:Root xmlns:g="urn:geo">datadata...</g:Root>'
SELECT @xmlDoc.query('
declare namespace g="urn:geo";
data(/g:Root[1])
')
به علاوه، در خطا ذکر شده‌است که متد text را بر روی  simple types نمی‌توان بکار برد. این محدودیت در مورد complex types که نمونه‌ای از آن‌را در قسمت معرفی Schema با تعریف Point مشاهده کردید، وجود ندارد. اما متد data قابل استفاده بر روی complex types نیست. ولی می‌توان متد data و text را با هم ترکیب کرد؛ برای مثال
data(/age/text())
 اگر complex node را untyped تعریف کنیم (schema را قید نکنیم)، استفاده از متد data در اینجا نیز وجود خواهد داشت.
مطالب
مسیریابی در Angular - قسمت نهم - محافظ‌های مسیرها
جهت مقاصد امنیتی، اعتبارسنجی کاربران و یا تحت نظر قرار دادن مسیرها، نیاز است بتوان بررسی کرد که آیا پیمایش یک مسیر، مجاز است یا خیر؟ برای پیاده سازی یک چنین ویژگی‌هایی در Angular، مفهوم Route Guards یا محافظ‌های مسیرها پیش بینی شده‌است که شامل چندین نوع محافظ می‌شوند:
 - canActivate : جهت محافظت دسترسی به یک مسیر
 -  canActivateChild: برای محافظت دسترسی به یک Child Route
 - canDeactivate : برای جلوگیری کردن از ترک مسیر جاری و هدایت به مسیری دیگر (برای مثال جهت نمایش پیام «هنوز اطلاع تغییر یافته را ذخیره نکرده‌اید»)
 - canLoad : برای جلوگیری از مسیریابی غیرهمزمان (async routing) که در قسمت بعدی بررسی خواهد شد
 - resolve: برای پیش واکشی اطلاعات، پیش از نمایش مسیر (که آن‌را در قسمت چهارم این سری بررسی کردیم)


لزوم استفاده‌ی از محافظ‌های مسیرها


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


ترتیب اجرای محافظ‌های مسیرها

مسیریاب سیستم، ابتدا محافظ canDeactivate را اجرا می‌کند تا مشخص شود که آیا کاربر می‌تواند مسیر جاری را ترک کند یا خیر؟ سپس اگر مسیریابی تعریف شده غیرهمزمان باشد، محافظ canLoad اجرا می‌شود. پس از آن محافظ canActivateChild بررسی می‌شود. در ادامه محافظ canActivate اجرا می‌گردد. در پایان کار بررسی محافظ‌های موجود، کار بررسی محافظ resolve‌، جهت پیش واکشی اطلاعات مسیر درخواستی، انجام خواهد شد.
در اینجا اگر یکی از محافظ‌ها مقدار false را برگرداند، پردازش مابقی آن‌ها لغو خواهد شد و کار هدایت کاربر به مسیر درخواستی، خاتمه می‌یابد.


مراحل ساخت و اعمال یک محافظ مسیر

ساخت و اعمال یک محافظ مسیر شامل سه مرحله است:
الف) یک محافظ مسیر عموما به صورت یک سرویس جدید پیاده سازی می‌شود:
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';

@Injectable()
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {

    }
}
در اینجا برای اینکه این سرویس به صورت یک محافظ مسیر عمل کند، نیاز است نوع محافظ مدنظر را نیز پیاده سازی نماید؛ مانند CanActivate در اینجا. پس از آن باید متد مرتبط با این اینترفیس که در اینجا canActivate است، پیاده سازی شود. اگر این متد false را برگرداند، سبب لغو هدایت کاربر به آن مسیر خواهد شد و این متد می‌تواند خروجی پیچیده‌تری مانند یک Observable را نیز داشته باشد. اگر یک چنین نوع خروجی درنظر گرفته شود، فراخوان آن، تا پایان کار این Observable صبر خواهد کرد.

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

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

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


یک مثال: ساخت محافظ canActivate‌

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

در ادامه‌ی مثال این سری می‌خواهیم کاربران را پیش از دسترسی به قسمت‌های مختلف مرتبط با محصولات، وادار به لاگین کنیم. برای این منظور دستور ذیل را اجرا کنید:
 >ng g guard user/auth -m user/user.module
به این ترتیب تغییرات ذیل در ماژول کاربران رخ خواهند داد:
 installing guard
  create src\app\user\auth.guard.spec.ts
  create src\app\user\auth.guard.ts
  update src\app\user\user.module.ts
در اینجا قالب ابتدایی کلاس سرویس AuthGuard ایجاد می‌شود (در فایل auth.guard.ts) و همچنین اگر به سطر آخر آن دقت کنید، این سرویس را به قسمت providers ماژول کاربران (در فایل user.module.ts) نیز افزوده‌است.

در ادامه کدهای این محافظ را به صورت ذیل تکمیل کنید:
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router } from '@angular/router';

import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService,
    private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
    return this.checkLoggedIn(state.url);
  }

  checkLoggedIn(url: string): boolean {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    this.authService.redirectUrl = url;
    this.router.navigate(['/login']);
    return false;
  }
}
خاصیت redirectUrl نیز به کلاس سرویسAuthService ، جهت به اشتراک گذاری اطلاعات، اضافه شده‌است:
export class AuthService {
   currentUser: IUser;
   redirectUrl: string;

توضیحات:

این سرویس چون از نوع CanActivate است، این اینترفیس را پیاده سازی کرده‌است و همچنین متد canActivate آن‌را نیز به همراه دارد:
export class AuthGuard implements CanActivate {
    canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean {
در اینجا از ActivatedRouteSnapshot می‌توان اطلاعات مسیرجاری، مانند پارامترهای آن‌را بدست آورد. پارامتر RouterStateSnapshot نیز وضعیت مسیریابی را بازگشت می‌دهد. برای مثال state.url، حاوی آدرس کامل مسیر درخواستی به صورت یک رشته است که از آن در اینجا جهت حفظ و به اشتراک گذاری مسیر اولیه‌ی درخواستی استفاده شده‌است. خاصیت route.url حاوی آرایه‌ای از URL segments است.

یک نکته: هرچند در اینجا می‌توان به پارامتر id مسیر، مانند route.params['id'] در صورت نیاز دسترسی یافت، اما امکان دسترسی به اطلاعات از پیش واکشی شده مانند route.data['product'] وجود ندارد. علت آن‌را نیز در قسمت «ترتیب اجرای محافظ‌های مسیرها» ابتدای بحث جاری، بررسی کردیم: محافظ resolve در انتهای کار پردازش تمام محافظ‌های موجود فراخوانی می‌شود.

در متد canActivate می‌خواهیم بررسی کنیم که آیا کاربر، لاگین کرده‌است یا خیر؟ اگر بله، تنها کافی است true را بازگشت دهیم تا کار این محافظ پایان یابد. در غیراینصورت false را بازگشت داده و همچنین سبب هدایت کاربر به صفحه‌ی لاگین می‌شویم.
به همین منظور سرویس AuthService را به سازنده‌ی این کلاس تزریق کرده‌ایم تا بتوانیم به متد isLoggedIn آن دسترسی پیدا کنیم (این سرویس را در قسمت دوم این سری تکمیل کردیم).
این متد نیز به صورت ذیل تعریف شده‌است:
isLoggedIn(): boolean {
   return !this.currentUser;
}
در اینجا استفاده‌ی از ! سبب بازگشت true، در صورت نال نبودن شیء کاربر جاری وارد شده‌ی به سیستم می‌شود.

در ادامه برای استفاده‌ی از این محافظ مسیر، به فایل src\app\product\product-routing.module.ts مراجعه کرده و آن‌را به نحو ذیل اعمال خواهیم کرد:
import { AuthGuard } from './../user/auth.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],
    children: [    ]
  }
];
در قسمت ششم، کار گروه بندی مسیرها را انجام دادیم. اکنون در اینجا نمونه‌ای از استفاده‌ی از آن‌را مشاهده می‌کنید. بجای اینکه AuthGuard  را به تک تک مسیرهای فرزند تعریف شده‌ی محصولات، اعمال کنیم، آن‌را به والد این مسیر اعمال کرده‌ایم تا به صورت خودکار به تمام فرزندان آن نیز اعمال شود.

اکنون برنامه را با دستور ng s -o ساخته و اجرا کنید. سپس بر روی لینک لیست محصولات و یا افزودن یک محصول جدید کلیک کنید. بلافاصله صفحه‌ی لاگین را مشاهده خواهید کرد.


به خاطر سپاری و بازیابی مسیر درخواستی کاربر پس از لاگین

در اینجا اگر کاربر بر روی لینک افزودن یک محصول جدید کلیک کند، صفحه‌ی لاگین را مشاهده خواهد کرد. اما پس از لاگین، همواره به مسیر لیست محصولات هدایت می‌شود و در این حالت مسیر درخواستی اولیه فراموش خواهد شد. برای رفع این مشکل نیاز است آدرس درخواستی کاربر را نیز ذخیره و بازیابی کرد. به همین جهت خاصیت this.authService.redirectUrl = url را در متد checkLoggedIn محافظ تعریف شده مقدار دهی کردیم. در اینجا از سرویس Auth، برای به اشتراک گذاری اطلاعات با محافظ‌های مسیر استفاده کرده‌ایم. طول عمر یک سرویس، singleton است. بنابراین تنها یک وهله از آن در طول عمر برنامه وجود خواهد داشت. به این ترتیب با ذخیره‌ی اطلاعاتی در آن، این اطلاعات در تمام برنامه قابل دسترسی خواهد شد.
با توجه به این نکته، اکنون به فایل src\app\user\login\login.component.ts مراجعه کرده و قسمت this.router.navigate آن‌را به صورت ذیل بهبود خواهیم بخشید:
      if (this.authService.login(userName, password)) {
        if (this.authService.redirectUrl) {
          this.router.navigateByUrl(this.authService.redirectUrl);
        } else {
          this.router.navigate(['/products']);
        }
      }
در اینجا بررسی می‌شود که آیا پیشتر خاصیت redirectUrl پس از لاگین مقدار دهی شده‌است یا خیر؟ اگر بله، از متد navigateByUrl جهت هدایت به آن مسیر استفاده خواهد شد.

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


بررسی محافظ canActivateChild

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

یک مثال: اگر کاربر در حال مشاهده‌ی صفحه‌ی لیست محصولات باشد و بر روی لینک مشاهده‌ی یک محصول کلیک کند، تنها قسمت child مسیر تغییر می‌کند. در این حالت canActivate مسیر اصلی دیگر اجرا نخواهد شد؛ اما تمام محافظ‌های canActivateChild مرتبط مجددا اجرا خواهند شد.


بررسی محافظ canDeactivate

محافظ canDeactivate پیش از ترک یک مسیر، فعالسازی و بررسی می‌شود. عموما از آن جهت بررسی وضعیت اطلاعات ذخیره نشده و اطلاع رسانی به کاربر، پیش از ترک مسیر جاری استفاده استفاده می‌گردد. این محافظ با هر تغییری در آدرس جاری مسیر، بررسی می‌شود. بدیهی است این تغییر صرفا درون یک برنامه‌ی Angular معنا پیدا می‌کند و نه هدایت به سایتی دیگر.
در حال حاضر در مثال جاری این سری، اگر کاربر، تغییری را در صفحه‌ی ویرایش اطلاعات ایجاد کند و بدون کلیک بر روی دکمه‌ی Save به صفحه‌ی دیگری مراجعه کند، این اطلاعات تغییر یافته، از دست خواهند رفت. برای رفع این مشکل می‌توان محافظ canDeactivate ایی را برای آن طراحی کرد. به همین جهت دستور ذیل را اجرا کنید:
 >ng g guard product/ProductEdit -m product/product.module
تا سبب انجام تغییرات ذیل در ماژول محصولات شود:
 installing guard
  create src\app\product\product-edit.guard.spec.ts
  create src\app\product\product-edit.guard.ts
  update src\app\product\product.module.ts
در اینجا علاوه بر ایجاد قالب ابتدایی محافظ ProductEdit، سبب به روز رسانی قسمت providers ماژول محصولات نیز شده‌است.

امضای ابتدایی یک محافظ CanDeactivate به صورت ذیل است:
export  class ProductEditGuard implements CanDeactivate<ProductEditComponent> {
    canDeactivate(component: ProductEditComponent): boolean {
اینترفیس CanDeactivate جنریک بوده و پارامتر جنریک آن نوع کامپوننتی را که قرار است از این محافظ استفاده کند، مشخص می‌کند. سپس نوع پارامتر متد canDeactivate آن بر اساس نوع پارامتر جنریک، تعیین می‌گردد.
اکنون این محافظ نیاز دارد تا بداند که آیا کامپوننت ویرایش محصولات، دارای اطلاعات ذخیره نشده‌ای هست یا خیر؟ چون کامپوننت ویرایش محصولات، به عنوان پارامتر به متد canDeactivate آن ارسال شده‌است، بنابراین می‌تواند به خواص و متد‌های عمومی آن کلاس نیز دسترسی پیدا کند. به همین جهت تغییرات ذیل را به کامپوننت ویرایش محصولات در فایل src\app\product\product-edit\product-edit.component.ts اعمال می‌کنیم:
  get product(): IProduct {
    return this.currentProduct;
  }
  set product(value: IProduct) {
    this.currentProduct = value;
    // Clone the object to retain a copy
    this.originalProduct = Object.assign({}, value);
  }

  get isDirty(): boolean {
    return JSON.stringify(this.originalProduct) !== JSON.stringify(this.currentProduct);
  }
در اینجا یک کپی از اصل محصول در حال ویرایش، برای مقایسه‌ی آن با محصول جاری در حال ویرایش، نگهداری می‌شود. به این ترتیب خاصیت isDirty می‌تواند مشخص کند که آیا تغییری بر روی خواص این شیء صورت گرفته‌است یا خیر؟ استفاده از متد JSON.stringify، یکی از ساده‌ترین روش‌هایی است که از آن می‌توان جهت مقایسه‌ی تمام خواص دو شیء استفاده کرد. البته چون در اینجا ترتیب خواص این دو شیء یکی است، این روش کار می‌کند.
برای اینکه این امر میسر شود، خاصیت product به حالت get/set دار تغییر یافته‌است تا بتوان کپی اولیه‌ی محصول را جهت مقایسه، نگهداری کرد. استفاده از متد Object.assign سبب ایجاد یک کپی از شیء اولیه شده و به این صورت دو وهله‌ی غیرمشترک را خواهیم داشت. اگر value مستقیما به originalProduct  انتساب داده می‌شد، در این حالت هر دوی currentProduct و originalProduct به یک شیء اشاره می‌کردند.

اکنون می‌توان از این خاصیت جدید کامپوننت ویرایش محصولات، در محافظ ترک صفحه‌ی آن استفاده کرد:
import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';

import { ProductEditComponent } from './product-edit/product-edit.component';

@Injectable()
export class ProductEditGuard implements CanDeactivate<ProductEditComponent> {

  canDeactivate(component: ProductEditComponent): boolean {
    if (component.isDirty) {
      let productName = component.product.productName || 'New Product';
      return confirm(`Navigate away and lose all changes to ${productName}?`);
    }
    return true;
  }
}
در اینجا اگر فرم، تغییر یافته و هنوز ذخیره نشده باشد، خاصیت isDirty برقرار شده و سبب نمایش یک دیالوگ confirm می‌شود. اگر کاربر آن‌را تائید کند، آنگاه مسیر درخواستی جدید فعال می‌شود. در غیراینصورت، هدایت به مسیر جدید لغو خواهد شد.

در آخر برای استفاده‌ی از این محافظ جدید، باید آن‌را به تنظیمات مسیریابی برنامه اضافه کنیم. به همین جهت به فایل src\app\product\product-routing.module.ts مراجعه کرده و این محافظ را به والد مسیریابی ویرایش یک محصول اضافه می‌کنیم:
import { ProductEditGuard } from './product-edit.guard';

const routes: Routes = [
  {
    path: 'products',
    canActivate: [ AuthGuard ],    
    children: [
      {
        path: '',
        component: ProductListComponent
      },
      {
        path: ':id',
        component: ProductDetailComponent,
        resolve: { product: ProductResolverService }
      },
      {
        path: ':id/edit',
        component: ProductEditComponent,
        resolve: { product: ProductResolverService },
        canDeactivate: [ ProductEditGuard ],
        children: [
          { path: '', redirectTo: 'info', pathMatch: 'full' },
          { path: 'info', component: ProductEditInfoComponent },
          { path: 'tags', component: ProductEditTagsComponent }
        ]
      }
    ]
  }
];
با افزودن canDeactivate به والد ویرایش محصولات، از هر دو child route تعریف شده محافظت می‌کند.


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

مشکل! در همین حالت بر روی دکمه‌ی Ok کلیک کنید تا اطلاعات ذخیره نشده را از دست داده و به مسیر دیگری هدایت شویم. مجددا همین پروسه را تکرار کنید. اینبار اگر بر روی دکمه‌ی Save کلیک کنید، باز هم دیالوگ confirm ظاهر می‌شود. علت اینجا است که شیء محصول اصلی و جاری، پس از ذخیره سازی به حالت اولیه بازگشت داده نشده‌اند. برای این منظور متد reset را به کامپوننت ویرایش اطلاعات اضافه کرده:
reset(): void {
    this.dataIsValid = null;
    this.currentProduct = null;
    this.originalProduct = null;
  }
و سپس آن‌را به متد onSaveComplete، اضافه می‌کنیم:
  onSaveComplete(message?: string): void {
    if (message) {
      this.messageService.addMessage(message);
    }
    this.reset();

    // Navigate back to the product list
    this.router.navigate(['/products']);
  }


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: angular-routing-lab-08.zip
برای اجرای آن فرض بر این است که پیشتر Angular CLI را نصب کرده‌اید. سپس از طریق خط فرمان به ریشه‌ی پروژه وارد شده و دستور npm install را صادر کنید تا وابستگی‌های آن دریافت و نصب شوند. در آخر با اجرای دستور ng s -o برنامه ساخته شده و در مرورگر پیش فرض سیستم نمایش داده خواهد شد.