مطالب
کارهایی جهت بالابردن کارآیی Entity Framework #3

در قسمت‌های قبلی (^ و ^) راهکارهایی جهت بالا بردن کارآیی، ارائه شد. در ادامه، به آخرین قسمت این سری اشاره خواهم کرد.

فراخوانی متد شناسایی تغییرات

یادآوری: قبل از هر چیز با توجه به این مقاله دانستن این نکته الزامی است که فراخوانی برخی متدها مانند DbSet.Add سبب فراخوانی DataContext.ChangeTracker.DetectChanges خواهند شد.

فرض کنید قصد افزودن 2000 موجودیت دانش آموز را دارید:

for (int i = 0; i < 2000; i++)
{
    Pupil pupil = GetNewPupil();
    db.Pupils.Add(pupil);
}
db.SaveChanges();
در کد بالا بدلیل فراخوانی متد DbSet.Add و به دنبال آن فراخوانی متد DetectChanges در هر بار اجرای حلقه (2000بار) مدت زمان اجرای کد بالا بسیار زیاد است و اگر به پروفایلر نگاهی بیاندازید، اشغال CPU توسط کوئری بالا، بیش از حد متعارف است.

اگر به تصویر بالا دقت کنید بیش از 34 ثانیه (خط 193 قسمت سوم شکل) جهت افزودن 2000 موجودیت به کانتکست سپری شده است. در حالی که درج این 2000 موجودیت کمی بیش از 1 ثانیه (خط 195 قسمت سوم شکل) که 379 میلی ثانیه (قسمت دوم شکل) آن مربوط به اجرای کوئری اختصاص یافته  طول کشیده است.
بیشترین زمان صرف شده‌ی برای درج 2000 موجودیت، در کد برنامه سپری شده است که با بررسی بیشتر در پروفایلر، متوجه زمان بر بودن فراخوانی متد ()DetectChanges که در فضای نام Data.Entity.Core وجود دارد خواهید شد. این متد 2000 بار به تعداد موجودیت هایی که قصد داریم به بانک اطلاعاتی اضافه نماییم، فراخوانی شده است.

همانطور که در شکل بالا مشخص است همان 34 ثانیه جهت ردیابی تغییرات صرف شده است. EF ردیابی تغییرات را بصورت پیش فرض هر زمانی که قصد افزودن یا ویرایش موجودیتی را داشته باشید، انجام خواهد داد و هر چه موجودیت‌های بیشتری را بخواهید ویرایش یا اضافه نمایید، این زمان نیز بیشتر خواهد شد. در حقیقت زمان لازم برای الگوریتم ردیابی تغییرات بصورت نمایی با رشد موجودیت‌ها افزوده می‌شود. به عبارت دیگر اگر این تعداد موجودیت‌ها را به 4000 عدد برسانید، مدت زمان لازم بیش از 2 برابر افزوده خواهد شد.

راه حل اول:
استفاده از متد ()AddRange ارائه شده در EF6 که جهت درج دسته‌ای (Bulk Insert) ارائه شده است:
var list = new List<Pupil>();

for (int i = 0; i < 2000; i++)
{
    Pupil pupil = GetNewPupil();
    list.Add(pupil);
}

db.Pupils.AddRange(list);
db.SaveChanges();

راه حل دوم:

در سناریوهای پیچیده، مانند درج دسته‌ای چندین موجودیت، شاید مجبور به خاموش نمودن این قابلیت شوید:
db.Configuration.AutoDetectChangesEnabled = false;
توجه داشته باشید اگر قصد دارید از امکان ردیابی تغییرات مجددا بهره‌مند شوید، باید این قابلیت را نیز فعال نمایید. با خاموش نمودن ردیابی تغییرات بار دیگر کوئری ابتدایی را اجرا نمایید. مدت زمان لازم جهت افزودن 2000 موجودیت به کانتکست از بیش از 34 ثانیه به 85 میلی ثانیه کاهش یافته است؛ ولی اعمال تغییرات به بانک اطلاعاتی همانند مرتبه اول بیش از 1 ثانیه طول خواهد کشید.


ردیابی تغییرات

هنگامی که موجودیتی را از بانک اطلاعاتی دریافت نمایید، می‌توانید آن را ویرایش نمایید و مجددا به بانک اطلاعاتی اعمال نمایید. چون EF اطلاعی از قصد شما برای موجودیت نمی‌داند، مجبور است تغییرات شما را زیر نظر بگیرد که این زیر نظر گرفتن، هزینه و سربار دارد و این سربار و هزینه برای داده‌های زیاد، بیشتر خواهد شد. بنابراین اگر قصد دارید اطلاعاتی فقط خواندنی را از بانک اطلاعاتی دریافت نمایید، بهتر است صراحتا به EF بگویید این موجودیت را تحت ردیابی قرار ندهد.
string city = "New York";

List<School> schools = db.Schools
    .AsNoTracking()
    .Where(s => s.City == city)
    .Take(100)
    .ToList();

استفاده از متد AsNoTracking  در کد بالا سبب خواهد شد 100 مدرسه که در شهر نیویورک وجود دارد توسط EF، بدون تحت نظر گرفتن آن‌ها از بانک اطلاعاتی دریافت شوند و سرباری نیز تحمیل نشود.

ویوهای از قبل کامپایل شده

معمولا، هنگامی که از EF برای اولین بار استفاده می‌نمایید، ویوهایی ایجاد می‌گردد که برای ایجاد کوئری‌ها مورد استفاده قرار می‌گیرند. این ویوها در طول حیات برنامه فقط یکبار ایجاد می‌شوند. ولی همین یکبار هم زمانبر هستند. خوشبختانه راه‌هایی وجود دارد که ایجاد این ویوها را در زمان runtime انجام نداد و آن راه، استفاده از ویوهای از پیش کامپایل شده است. یکی از راههای ایجاد این ویوها استفاده از Entity Framework Power Tools است. بعد از نصب اکستنشن، بر روی فایل کانتکست راست کلیک کرده و سپس گزینه‌ی Generate Views را از منوی Entity Framework انتخاب کنید.

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


حذف کوئری‌های ابتدایی غیر ضروری

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

با توجه به توضیحات، در صورتیکه اطلاعی از نسخه‌ی دیتابیس دارید، می‌توانید این کوئری ابتدایی را تحریف نمایید. برای اینکار می‌توان توسط متد ()ResolveManifestToken کلاسی که اینترفیس IManifestTokenResolver را پیاده سازی کرده است، نسخه‌ی دیتابیس را برگردانیم و از یک رفت و برگشت به دیتابیس جلوگیری نماییم.

 public class CustomManifestTokenResolver : IManifestTokenResolver
{
    public string ResolveManifestToken(DbConnection connection)
    {
        return "2014";
    }
}
و توسط کلاسی که از کلاس DbConfiguration ارث بری کرده است آن را استفاده نماییم.
 public class CustomDbConfiguration : DbConfiguration
{
    public CustomDbConfiguration()
    {
        SetManifestTokenResolver(new CustomManifestTokenResolver());
    }
}


تخریب کانتکست

تخریب و از بین بردن کانتکست هنگامی که به آن نیاز نداریم بسیار ضروری است. یکی از روشهای اصولی برای Disposing کانتکست، محصور کردن آن بوسیله دستور Using است (البته فرض بر این است که قرار نیست از الگوهای اشاره شده استفاده نماییم). در صورت عدم تخریب صحیح کانتکست باید منتظر آسیب جدی به کارایی Garbage Collector جهت آزاد سازی منابع مورد استفاده کانتکست و هم چنین باز نمودن اتصالات جدید به دیتابیس باشید.


پاسخگویی به چندین درخواست بر روی یک کانکشن

EF از قابلیتی بنام Multiple Result Sets می‌تواند بهره ببرد که این قابلیت باعث می‌شود بر روی یک کانکشن ایجاد شده، یک یا چند درخواست از دیتابیس ارسال و یا دریافت شود که سبب کاهش تعداد رفت و برگشت به دیتابیس می‌شود. کاربرد دیگر این قابلیت، زمانی است که تاخیر زیادی (latency) بین اپلیکیشن و دیتابیس وجود دارد.

برای فعالسازی کافی است مقدار زیر را در کانکشن استرینگ اضافه نمایید:

MultipleActiveResultSets=True;


استفاده از متدهای ناهمگام

در C#5 و EF6 پشتیبانی خوبی از متدهای ناهمگام فراهم شده است و اکثر متدهایی مانند ToListAsync, CountAsync, FirstAsync, SaveChangesAsync و غیره که باعث رفت و برگشت به دیتابیس می‌شوند امکان پشتیبانی ناهمگام را نیز دارند. البته این قابلیت برای برنامه‌های یک درخواست در یک زمان شاید مفید نباشد؛ ولی برای برنامه‌های وبی برعکس. در برنامه وب جهت پشتیبانی از بارگذاری همزمان (concurrent) قابلیت ناهمگام (Async) سبب خواهد شد منابع تا زمان اجرای کوئری به ThreadPool بازگردانده شود و برای سرویس دهی مهیا باشند که باعث افزایش scalability خواهد شد.

بررسی و آزمایش با داده‌های واقعی

در اکثر مواقع کارآیی با حجیم شدن داده‌ها کاهش پیدا می‌کند (البته در صورت عدم رعایت اصول استاندارد). بنابراین بررسی کارآیی در محیط هایی با حجم داده‌های بالا ضروری است. هیچ چیز بدتر از آن نیست که همه چیز در محیط توسعه خوب و بی نقص باشد ولی در محیط عملیاتی به شکست بیانجامد. به همین جهت سعی کنید از ابزارهای تولید داده (^ و ^ و ^) برای ایجاد داده‌های آزمایشی استفاده نمایید. سپس کارآیی کوئری خود را مورد بررسی و آزمایش قرار دهید.

مطالب
مروری بر کاربردهای Action و Func - قسمت اول
delegate‌ها، نوع‌هایی هستند که ارجاعی را به یک متد دارند؛ بسیار شبیه به function pointers در C و CPP هستند، اما برخلاف آن‌ها، delegates شی‌ء‌گرا بوده، به امضای متد اهمیت داده و همچنین کد مدیریت شده و امن به شمار می‌روند.
سیر تکاملی delegates را در مثال ساده زیر می‌توان ملاحظه کرد:
using System;

namespace ActionFuncSamples
{
    public delegate int AddMethodDelegate(int a);
    public class DelegateSample
    {
        public void UseDelegate(AddMethodDelegate addMethod)
        {
            Console.WriteLine(addMethod(5));
        }
    }

    public class Helper
    {
        public int CustomAdd(int a)
        {
            return ++a;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Helper helper = new Helper();

            // .NET 1            
            AddMethodDelegate addMethod = new AddMethodDelegate(helper.CustomAdd);
            new DelegateSample().UseDelegate(addMethod);

            // .NET 2, anonymous delegates
            new DelegateSample().UseDelegate(delegate(int a) { return helper.CustomAdd(a); });

            // .NET 3.5
            new DelegateSample().UseDelegate(a => helper.CustomAdd(a));
        }
    }
}
معنای کلمه delegate، واگذاری مسئولیت است. به این معنا که ما در متد UseDelegate، نمی‌دانیم addMethod به چه نحوی تعریف خواهد شد. فقط می‌دانیم که امضای آن چیست.
در دات نت یک، یک وهله از شیء AddMethodDelegate ساخته شده و سپس متدی که امضایی متناسب و متناظر با آن را داشت، به عنوان متد انجام دهنده مسئولیت معرفی می‌شد. در دات نت دو، اندکی نحوه تعریف delegates با ارائه delegates بی‌نام، ساده‌تر شد و در دات نت سه و نیم با ارائه lambda expressions ، تعریف و استفاده از delegates باز هم ساده‌تر و زیباتر گردید.
به علاوه در دات نت 3 و نیم، دو Generic delegate به نام‌های Action و Func نیز ارائه گردیده‌اند که به طور کامل جایگزین تعریف طولانی delegates در کدهای پس از دات نت سه و نیم شده‌اند. تفاوت‌های این دو نیز بسیار ساده است:
اگر قرار است واگذاری قسمتی از کد را به متدی محول کنید که مقداری را بازگشت می‌دهد، از Func و اگر این متد خروجی ندارد از Action استفاده نمائید:
Action<int> example1 = x => Console.WriteLine("Write {0}", x);
example1(5);

Func<int, string> example2 = x => string.Format("{0:n0}", x);
Console.WriteLine(example2(5000));
در دو مثال فوق، نحوه تعریف inline یک Action و یا Func را ملاحظه می‌کنید. Action به متدی اشاره می‌کند که خروجی ندارد و در اینجا تنها یک ورودی int را می‌پذیرد. Func در اینجا به تابعی اشاره می‌کند که یک ورودی int را دریافت کرده و یک خروجی string را باز می‌گرداند.

پس از این مقدمه، در ادامه قصد داریم مثال‌های دنیای واقعی Action و Func را که در سال‌های اخیر بسیار متداول شده‌اند، بررسی کنیم.


مثال یک) ساده سازی تعاریف API ارائه شده به استفاده کنندگان از کتابخانه‌های ما
عنوان شد که کار delegates، واگذاری مسئولیت انجام کاری به کلاس‌های دیگر است. این مورد شما را به یاد کاربردهای interfaceها نمی‌اندازد؟
در interfaceها نیز یک قرارداد کلی تعریف شده و سپس کدهای یک کتابخانه، تنها با امضای متدها و خواص تعریف شده در آن کار می‌کنند و کتابخانه ما نمی‌داند که این متدها قرار است چه پیاده سازی خاصی را داشته باشند.
برای نمونه طراحی API زیر را درنظر بگیرید که در آن یک interface جدید تعریف شده که تنها حاوی یک متد است. سپس کلاس Runner از این interface استفاده می‌کند:
using System;

namespace ActionFuncSamples
{
    public interface ISchedule
    {
        void Run();
    }

    public class Runner
    {
        public void Exceute(ISchedule schedule)
        {
            schedule.Run();
        }
    }

    public class HelloSchedule : ISchedule
    {
        public void Run()
        {
            Console.WriteLine("Just Run!");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new Runner().Exceute(new HelloSchedule());
        }
    }
}
در اینجا ابتدا باید این interface را در طی یک کلاس جدید (مثلا HelloSchedule) پیاده سازی کرد و سپس حاصل را در کلاس Runner استفاده نمود.
نظر شما در مورد این طراحی ساده شده چیست؟
using System;

namespace ActionFuncSamples
{
    public class Schedule
    {
        public void Exceute(Action run)
        {
            run();
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            new Schedule().Exceute(() => Console.WriteLine("Just Run!"));
        }
    }
}
با توجه به اینکه هدف از معرفی interface در طراحی اول، واگذاری مسئولیت نحوه تعریف متد Run به کلاسی دیگر است، به همین طراحی با استفاده از یک Action delegate نیز می‌توان رسید. مهم‌ترین مزیت آن، حجم بسیار کمتر کدنویسی استفاده کننده نهایی از API تعریف شده ما است. به علاوه امکان inline coding نیز فراهم گردیده است و در همان محل تعریف Action، بدنه آن‌را نیز می‌توان تعریف کرد.
بدیهی است delegates نمی‌توانند به طور کامل جای interfaceها را پر کنند. اگر نیاز است قرارداد تهیه شده بین ما و استفاده کنندگان از کتابخانه، حاوی بیش از یک متد باشد، استفاده از interfaceها بهتر هستند.
از دیدگاه بسیاری از طراحان API، اشیاء delegate معادل interface ایی با یک متد هستند و یک وهله از delegate معادل وهله‌ای از کلاسی است که یک interface را پیاده سازی کرده‌است.
علت استفاده بیش از حد interfaceها در سایر زبان‌ها برای ابتدایی‌ترین کارها، کمبود امکانات پایه‌ای آن زبان‌ها مانند نداشتن lambda expressions، anonymous methods و anonymous delegates هستند. به همین دلیل مجبورند همیشه و در همه‌جا از interfaceها استفاده کنند.

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

 
مطالب
تبدیل زیرنویس‌های یوتیوب به فایل srt

تعدادی از ویدیوهای یوتیوب، خصوصا مواردی که از Google videos به یوتیوب منتقل شده‌اند، دارای زیر نویس هستند. این زیر نویس هم فرمت جالبی دارد:
<?xml version="1.0" encoding="UTF-8"?>
<transcript>
<text start="23.49" dur="5.97">
&amp;gt;&amp;gt;Commentator: We have here today, Steve with
us whom I met not too long ago at something
</text>
...
</transcript>
و از هر لحاظ بهتر است از فرمت srt متداول (خصوصا از لحاظ قابلیت بهتر parse آن) ولی ... این فرمت xml ایی، متداول نیست و بهتر است جهت استفاده از آن در برنامه‌های پخش کننده فایل‌های ویدیویی، به srt تبدیل شود.
ابتدا شاید بپرسید که این‌ها را چگونه می‌توان دریافت کرد. روش کار به صورت زیر است:
http://video.google.com/timedtext?hl=en&v=Id&lang=en

در قسمت‌های hl و lang ، زبان مورد نظر قرار می‌گیرد و قسمت v همان id معروف ویدیوی یوتیوب مورد نظر است. برای مثال اگر لینک ویدیوی ما http://www.youtube.com/watch?v=-PA-buwI3q4 باشد، لینک زیرنویس انگلیسی هماهنگ با آن http://video.google.com/timedtext?hl=en&v=-PA-buwI3q4&lang=en خواهد بود.

البته زمان‌های این فایل xml یک نکته‌ی خاص گوگلی دارد!
اگر عدد 3.4 را مشاهده کردید یعنی سه ثانیه و 400 میلی ثانیه
اگر عدد 3.04 را مشاهده کردید یعنی سه ثانیه و 40 میلی ثانیه
به عبارتی با قسمت اعشاری آن باید به صورت رشته برخورد کنید. اگر طول آن یک بود، دو صفر و اگر طول آن 2 بود، 1 صفر در جلوی آن باید قرار گیرد تا قابل استفاده شود.

کلاس تهیه شده زیر کار تبدیل Xml گوگل را به Srt انجام می‌دهد:
using System;
using System.Linq;
using System.Text;
using System.Web;
using System.Xml.Linq;

namespace YtSubs
{
public class GoogleXmlToSrt
{
public string ConvertXmlToSrt(string url)
{
var transcript = loadTranscript(url);

var srtBuf = new StringBuilder();
var lineNumber = 0;

foreach (var item in transcript.Root.Elements("text"))
{
lineNumber++;
srtBuf.Append(lineNumber.ToString());

var timeLine = getTimeLine(item);
srtBuf.AppendFormat("{0}{1} --> {2}{3}", Environment.NewLine, timeLine.Item1, timeLine.Item2, Environment.NewLine);

var msg = decodeText(item);
srtBuf.AppendLine(msg + Environment.NewLine);
}
return srtBuf.ToString();
}

private static XDocument loadTranscript(string url)
{
var transcript = XDocument.Load(url);

if (transcript.Root == null)
throw new InvalidOperationException("couldn't load received data.");
return transcript;
}

private static string decodeText(XElement text)
{
var textValue = text.Value.Split('\n');
var msg = textValue.Aggregate(
string.Empty, (current, line) =>
current + (HttpUtility.HtmlDecode(line.Trim()) + Environment.NewLine)
);
return msg.Trim();
}

private static int fractionalPart(string data)
{
var idx = data.IndexOf(".");
if (idx == -1) return 0;
var fractionalPart = data.Substring(idx + 1);
if (fractionalPart.Length == 1) //3.4 --> 3.400
return int.Parse(fractionalPart + "00");

if (fractionalPart.Length == 2) //3.04 --> 3.040
return int.Parse(fractionalPart + "0");

return int.Parse(fractionalPart.Substring(0, 3));
}

private Tuple<string, string> getTimeLine(XElement text)
{
var startTs = getStartTs(text);
TimeSpan endTs = getEndTs(text, startTs);
return new Tuple<string, string>(timeSpanToString(startTs), timeSpanToString(endTs));
}

private static TimeSpan getStartTs(XElement text)
{
var startData = text.Attribute("start");
if (startData == null)
throw new InvalidOperationException("This is not a valid subtitle file.");

var start = startData.Value;
var startTs = new TimeSpan(0, 0, 0, (int)Math.Truncate(double.Parse(start)), fractionalPart(start));
return startTs;
}

private static TimeSpan getEndTs(XElement text, TimeSpan startTs)
{
TimeSpan endTs;
var durData = text.Attribute("dur");
if (durData == null)
{
endTs = startTs + new TimeSpan(0, 0, 0, 3, 0);
}
else
{
var dur = durData.Value;
var durTs = new TimeSpan(0, 0, 0, (int)Math.Truncate(double.Parse(dur)), fractionalPart(dur));
endTs = startTs + durTs;
}
return endTs;
}

private static string timeSpanToString(TimeSpan lineTs)
{
return string.Format("{0}:{1}:{2},{3}",
lineTs.Hours.ToString("D2"),
lineTs.Minutes.ToString("D2"),
lineTs.Seconds.ToString("D2"),
lineTs.Milliseconds.ToString("D3"));
}
}
}
و یک نمونه از استفاده آن می‌تواند به شکل زیر باشد:
using System.IO;

namespace YtSubs
{
class Program
{
static void Main(string[] args)
{
var url = "http://video.google.com/timedtext?hl=en&v=-PA-buwI3q4&lang=en";
//var url = "subtitle.xml";
var srtBuf = new GoogleXmlToSrt().ConvertXmlToSrt(url);
File.WriteAllText("-PA-buwI3q4.srt", srtBuf);
}
}
}


مطالب تکمیلی:
همه چیز درباره زیرنویس ویدئوهای گوگل

مطالب
اجرای 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
مطالب
آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core
تا اینجا دو روش را برای آزمایش کلی یک سیستم Web API به همراه تمام زیر ساخت‌های آن، بررسی کردیم:
- استفاده از Postman برای آزمایش یک برنامه‌ی Web API
- استفاده از strest برای آزمایش یک برنامه‌ی Web API

روش سومی هم برای انجام اینکار وجود دارد که به صورت توکار از زمان ارائه‌ی ASP.NET Core 2.1 به همراه TestServer آزمایشی آن میسر شد. این روش در نگارش 3.1، با تغییر روش تعریف فایل program.cs، جهت سازگاری آن با آزمون‌های یکپارچگی/آزمایش کل سیستم، بهبود یافته‌است که خلاصه‌ای از آن را در این مطلب بررسی می‌کنیم.


آزمون‌های یکپارچگی در ASP.NET Core

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

برای ایجاد آزمون‌های یکپارچگی در برنامه‌های ASP.NET Core، حداقل سه مرحله باید طی شوند:
الف) ایجاد یک class library که ارجاعی را به پروژه‌ی اصلی دارد. این پروژه حاوی آزمایش‌های ما خواهد بود.
ب) راه اندازی یک هاست وب آزمایشی برای ارسال درخواست‌ها به آن و دریافت پاسخ‌های نهایی.
ج) استفاده از یک test runner (انواع و اقسام فریم ورک‌های unit testing) برای اجرای آزمایش‌ها


ایجاد یک پروژه‌ی کتابخانه برای هاست و اجرای آزمایش‌های یکپارچگی

فرض کنید می‌خواهیم برای همان پروژه‌ی ایجاد JWTها، آزمایش یکپارچگی بنویسیم. پس از ایجاد یک پروژه‌ی کتابخانه‌ی جدید که قرار است هاست آزمایش‌های ما شود، نیاز است محتوای فایل csproj آن‌را به صورت زیر تغییر داد:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <NoWarn>RCS1090</NoWarn>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ASPNETCore2JwtAuthentication.WebApp\ASPNETCore2JwtAuthentication.WebApp.csproj" />
  </ItemGroup>
  <ItemGroup>
    <None Include="..\ASPNETCore2JwtAuthentication.WebApp\appsettings.json" CopyToOutputDirectory="PreserveNewest" />
  </ItemGroup>
  <ItemGroup>
    <Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c329}" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="fluentassertions" Version="5.10.3" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.1.8" />
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
    <PackageReference Include="MSTest.TestAdapter" Version="2.1.2" />
    <PackageReference Include="MSTest.TestFramework" Version="2.1.2" />
  </ItemGroup>
</Project>
در اینجا، این نکات قابل مشاهده هستند:
1) TargetFramework آن باید به netcoreapp تنظیم شود.
2) باید ارجاع مستقیمی به کل پروژه‌ی نهایی WebApp در آن وجود داشته باشد. چون در ادامه می‌خواهیم فایل Program.cs آن‌را برای راه اندازی یک هاست وب آزمایشی، فراخوانی کنیم.
3) بسته‌ی نیوگتی که کار راه اندازی هاست وب آزمایشی را انجام می‌دهد، Microsoft.AspNetCore.Mvc.Testing نام دارد. این بسته، کار کپی فایل‌های پروژه‌ی اصلی و همچنین تنظیم مسیر پروژه را به این مسیر جدید نیز انجام می‌دهد.
4) روش افزودن بسته‌های MSTest را مشاهده می‌کنید.
5) همچنین جهت ساده‌تر شدن بررسی نتایج آزمون‌های انجام شده می‌توان از fluentassertions نیز استفاده کرد.


راه اندازی هاست وب آزمایشی جهت انجام آزمون‌های واحد

پس از انجام تنظیمات ابتدایی پروژه‌ی آزمون یکپارچگی، نیاز است یک WebApplicationFactory سفارشی را ایجاد کرد:
using ASPNETCore2JwtAuthentication.WebApp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public class CustomWebApplicationFactory : WebApplicationFactory<Program>
    {
        protected override IWebHostBuilder CreateWebHostBuilder()
        {
            var builder = base.CreateWebHostBuilder();
            builder.ConfigureLogging(logging =>
            {
                //TODO: ...
            });
            return builder;
        }

        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureTestServices(services =>
            {
                // Don't run `IHostedService`s when running as a test
                services.RemoveAll(typeof(IHostedService));
            });
        }
    }
}
در این تعریف، Program در <WebApplicationFactory<Program، دقیقا به همان کلاس Program برنامه‌ی اصلی وب اشاره می‌کند. به همین جهت امضای این کلاس در نگارش 3.1 تغییر کرده‌است تا با WebApplicationFactory بسته‌ی Microsoft.AspNetCore.Mvc.Testing هماهنگ شود.
در ادامه روش سفارشی سازی WebApplicationFactory  را مشاهده می‌کنید. برای مثال اگر خواستید سرویس‌ها و تنظیمات پیش‌فرض برنامه‌ی اصلی را تغییر دهید می‌توانید متد CreateWebHostBuilder را بازنویسی کنید و یا اگر خواستید سرویس جدیدی را اضافه و یا حذف کنید، می‌توان متد ConfigureWebHost را بازنویسی کرد.


استفاده از WebApplicationFactory سفارشی، جهت ایجاد یک HttpClient

هدف اصلی از ایجاد CustomWebApplicationFactory نه فقط راه اندازی یک هاست وب سفارشی است، بلکه توسط متد CreateClient آن می‌توان به یک HttpClient دسترسی یافت که قابلیت ارسال اطلاعات را به برنامه‌ی وبی که در پشت صحنه راه اندازی می‌شود، دارا است. کار CustomWebApplicationFactory شبیه به راه اندازی dotnet run در پشت صحنه‌است. در اینجا دیگر نیازی نیست تا اینکار را به صورت دستی انجام داد. به همین جهت چون برنامه‌ی وب اصلی به نحو متداولی در پشت صحنه اجرا می‌شود، عموما راه اندازی آن که شامل تنظیمات اولیه و یا حتی ایجاد بانک اطلاعاتی است، کمی کند است و اگر قرار باشد هربار اینکار صورت گیرد، به آزمون‌های بسیار کندی خواهیم رسید. به همین جهت می‌توان یک کلاس singleton را برای مدیریت تک وهله‌ی نهایی HttpClient آن به صورت زیر ایجاد کرد:
using System;
using System.Threading;
using System.Net.Http;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    public static class TestsHttpClient
    {
        private static readonly Lazy<HttpClient> _serviceProviderBuilder =
                new Lazy<HttpClient>(getHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);

        /// <summary>
        /// A lazy loaded thread-safe singleton
        /// </summary>
        public static HttpClient Instance { get; } = _serviceProviderBuilder.Value;

        private static HttpClient getHttpClient()
        {
            var services = new CustomWebApplicationFactory();
            return services.CreateClient(); //NOTE: This action is very time consuming, so it should be defined as a singleton.
        }
    }
}
مزیت کار با این کلاس، عدم راه اندازی مجدد برنامه به ازای هر آزمون انجام شده‌است. چون به ازای هر آزمونی که خواهیم نوشت، نیاز است به HttpClient دسترسی داشته باشیم.


نوشتن اولین آزمون یکپارچگی

پس از تنظیم هاست وب آزمایشی و ایجاد یک HttpClient از پیش تنظیم شده که به آن اشاره می‌کند، اکنون می‌توان اولین آزمون یکپارچگی را به صورت زیر نوشت:
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ASPNETCore2JwtAuthentication.IntegrationTests
{
    [TestClass]
    public class JwtTests
    {
        [TestMethod]
        public async Task TestLoginWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();
        }

        [TestMethod]
        public async Task TestCallProtectedApiWorks()
        {
            // Arrange
            var client = TestsHttpClient.Instance;

            // Act
            var token = await doLoginAsync(client);

            // Assert
            token.Should().NotBeNull();
            token.AccessToken.Should().NotBeNullOrEmpty();
            token.RefreshToken.Should().NotBeNullOrEmpty();

            // Act
            const string protectedApiUrl = "/api/MyProtectedApi";
            client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
            var response = await client.GetAsync(protectedApiUrl);
            response.EnsureSuccessStatusCode();

            // Assert
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            var options = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
            var apiResponse = JsonSerializer.Deserialize<MyProtectedApiResponse>(responseString, options);
            apiResponse.Title.Should().NotBeNullOrEmpty();
            apiResponse.Title.Should().Be("Hello from My Protected Controller! [Authorize]");
        }

        private static async Task<Token> doLoginAsync(HttpClient client)
        {
            const string loginUrl = "/api/account/login";
            var user = new { Username = "Vahid", Password = "1234" };
            var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Post, loginUrl)
            {
                Content = new StringContent(JsonSerializer.Serialize(user), Encoding.UTF8, "application/json")
            });
            response.EnsureSuccessStatusCode();
            var responseString = await response.Content.ReadAsStringAsync();
            responseString.Should().NotBeNullOrEmpty();
            return JsonSerializer.Deserialize<Token>(responseString);
        }
    }
}
توضیحات:
- در هر آزمونی نیاز است در ابتدا به TestsHttpClient.Instance، که همان HttpClient ساخته شده‌ی توسط CustomWebApplicationFactory است، دسترسی یافت و همانطور که عنوان شد، دسترسی به وهله‌ای از HttpClient که به هاست وب آزمایشی برنامه‌ی اصلی اشاره می‌کند، عموما بسیار زمانبراست و برای مثال در دو آزمایش نوشته شده‌ی در اینجا اگر قرا باشد هربار اینکار از صفر انجام شود، زمان به اتمام رسیدن این آزمایش‌ها بسیار طولانی خواهد شد. به همین جهت طول عمر TestsHttpClient را singleton تعریف کردیم تا فقط یکبار کار برپایی وب سرور آزمایشی در پشت صحنه انجام شود.
- سپس مابقی کار، همان روش استاندارد کار با HttpClient است. در ابتدا درخواستی را به سمت سرور آزمایشی که در پشت صحنه در حال اجرا است، ارسال می‌کنیم. چون HttpClient دریافتی توسط  CustomWebApplicationFactory تنظیم شده‌است، دیگر نیازی به ذکر آدرس پایه‌ی وب سایت مانند https://localhost:5001 نیست و آدرس‌های ذکر شده‌ی در اینجا، نسبی هستند. سپس محتوای Response دریافتی از سرور را جهت تکمیل آزمایشات، بررسی خواهیم کرد.


یک نکته: اگر OpenAPI را در برنامه‌های Web API فعال کنید، می‌توان با استفاده از ابزارهای تولید کد، کدهای مرتبط با HttpClient را نیز به صورت خودکار تولید و سپس از آن‌ها در اینجا استفاده کرد.


اجرای آزمون‌های یکپارچگی نوشته شده

چون ظاهر این آزمون‌ها با آزمون‌های واحد MSTest یا هر فریم ورک مشابه دیگری یکسان است، می‌توان از امکانات IDEها برای اجرای آن‌ها استفاده کرد و یا حتی می‌توان دستور dotnet test را نیز در ریشه‌ی این پروژه‌ی جدید برای اجرای تمام آزمون‌های نوشته شده، اجرا کرد:



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


این زیرنویس‌ها فرمت ویژه‌ای دارند:
                <li class="transcript-module">
                    Introduction to ASP.NET MVC 4
                    <ul>
                            <li class="transcript-clip" data-p="author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building"><a href="javascript:void(0)" onclick="LaunchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=0&amp;course=mvc4-building');">Introduction</a><br />
                                <div>
                                        <a href="javascript:void(0)" onclick="p(this);" data-s="1.636">Hi, this is Scott Allen and this is the first module in the course design</a>
                                </div>
                            </li>
                            <li class="transcript-clip" data-p="author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=1&amp;course=mvc4-building"><a href="javascript:void(0)" onclick="LaunchPlayerWindow('http://pluralsight.com/training', 'author=scott-allen&amp;name=mvc4-building-m1-intro&amp;mode=live&amp;clip=1&amp;course=mvc4-building');">Web Platform Installer</a><br />
                                <div>
                                ...
در آن، هر li که دارای کلاسی به نام transcript-clip است، حاوی یک div می‌باشد و این div دارای تعدادی لینک است. این لینک‌ها توسط ویژگی datas آن‌ها که بیانگر زمان شروع گفتگو است، مشخص می‌شوند و همینطور الی آخر. بنابراین اگر بخواهیم برای آن‌ها ساختاری را تهیه کنیم، به کلاس‌های ذیل خواهیم رسید:
    public class TranscriptClip
    {
        public string Title { set; get; }
        public IList<TranscriptItem> TranscriptItems { set; get; }
    }

    public class TranscriptItem
    {
        public double StartTime { set; get; }
        public string Text { set; get; }
    }
هر li دارای کلاس transcript-clip، یک شیء TranscriptClip را تشکیل می‌دهد. هر شیء TranscriptClip می‌تواند داری چندین TranscriptItem باشد.
برای استخراج این اطلاعات، یکی از بهترین ابزارها، کتابخانه HTML Agility pack است که توسط آن می‌توان به liهای یاد شده دسترسی یافت:
 var nodes = doc.DocumentNode.SelectNodes("//li[@class='transcript-clip']/div");
و سپس اطلاعات آن‌ها را استخراج نمود.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;
using HtmlAgilityPack;

namespace PluralsightTranscripts
{
    public class TranscriptClip
    {
        public string Title { set; get; }
        public IList<TranscriptItem> TranscriptItems { set; get; }
    }

    public class TranscriptItem
    {
        public double StartTime { set; get; }
        public string Text { set; get; }
    }

    public class ExtractSubtitle
    {
        public static void ConvertToSrt(string fileName)
        {
            var transcriptClips = extractItems(fileName);
            var itemNumber = 1;
            foreach (var item in transcriptClips)
            {
                transcriptClipToSrt(item, itemNumber);
                itemNumber++;
            }
        }

        private static void transcriptClipToSrt(TranscriptClip item, int itemNumber)
        {                        
            var count = item.TranscriptItems.Count;
            var srtFileContent = transcriptItemsToSrt(item.TranscriptItems, count);
            var fileName = removeIllegalCharacters(string.Format("{0}-{1}.srt", itemNumber.ToString("00"), item.Title));            
            File.WriteAllText(fileName, srtFileContent);
        }

        private static string transcriptItemsToSrt(IList<TranscriptItem> items, int count)
        {
            var lineNumber = 1;
            var sb = new StringBuilder();
            for (int row = 0; row < count; row++)
            {
                sb.AppendLine(lineNumber.ToString(CultureInfo.InvariantCulture));
                sb.AppendLine(getTimeLine(items, count, row));
                sb.AppendLine(items[row].Text);
                sb.AppendLine(string.Empty);
                lineNumber++;
            }
            return sb.ToString();            
        }

        private static string getTimeLine(IList<TranscriptItem> items, int count, int row)
        {
            var startTs = TimeSpan.FromSeconds(items[row].StartTime);
            var endTs = row + 1 < count ? TimeSpan.FromSeconds(items[row + 1].StartTime) : TimeSpan.FromSeconds(items[row].StartTime + 5);
            return string.Format("{0} --> {1}", timeSpanToString(startTs), timeSpanToString(endTs));
        }

        private static string timeSpanToString(TimeSpan lineTs)
        {
            return string.Format("{0}:{1}:{2},{3}", lineTs.Hours.ToString("D2"), lineTs.Minutes.ToString("D2"), lineTs.Seconds.ToString("D2"), lineTs.Milliseconds.ToString("D3"));
        }

        private static string removeIllegalCharacters(string fileName)
        {
            string regexSearch = string.Format("{0}{1}",
                                               new string(Path.GetInvalidFileNameChars()),
                                               new string(Path.GetInvalidPathChars()));
            var r = new Regex(string.Format("[{0}]", Regex.Escape(regexSearch)));
            return r.Replace(fileName, ".");
        }

        private static IList<TranscriptClip> extractItems(string fileName)
        {
            var htmlContent = File.ReadAllText(fileName);
            var results = new List<TranscriptClip>();

            var doc = new HtmlDocument
            {
                OptionCheckSyntax = true,
                OptionFixNestedTags = true,
                OptionAutoCloseOnEnd = true,
                OptionDefaultStreamEncoding = Encoding.UTF8
            };
            doc.LoadHtml(htmlContent);

            var nodes = doc.DocumentNode.SelectNodes("//li[@class='transcript-clip']/div");

            foreach (var node in nodes)
            {
                var itemsList = new List<TranscriptItem>();
                var title = node.ParentNode.ChildNodes.First(x => x.Name == "a").InnerText;

                foreach (var childNode in node.ChildNodes)
                {
                    if (childNode.Name != "a") continue;

                    var dataS = childNode.Attributes.First(x => x.Name == "data-s");
                    itemsList.Add(new TranscriptItem
                    {
                        StartTime = double.Parse(dataS.Value),
                        Text = HttpUtility.HtmlDecode(childNode.InnerText.Trim())
                    });
                }

                results.Add(new TranscriptClip { TranscriptItems = itemsList, Title = title });
            }

            return results;
        }
    }
}
اگر این اطلاعات را کنار هم قرار دهیم، به کلاس کمکی فوق خواهیم رسید. کار با گره‌های li شروع می‌شود. سپس در این گره‌ها، کلیه گره‌های a یا لینک‌ها، یافت شده و سپس dataS و متن آن‌ها استخراج می‌شوند. اگر این‌ها را نهایتا کنار هم قرار دهیم، می‌توان به فرمت SRT متداول که اکثر پخش کننده‌های فایل‌های تصویری قادر به پردازش آن‌ها هستند، رسید.
فرمت SRT ساختار ساده‌ای دارد. هر گفتگوی آن حداقل از سه سطر تشکیل می‌شود. سطر اول یک شماره خود افزاینده است. سطر دوم زمان شروع و پایان گفتگو را مشخص می‌کند و سطر سوم بیانگر متن گفتگو است. برای مثال:
 1
00:00:01,636 --> 00:00:05,616
Hi, this is Scott Allen and this is the first module in the course design

دریافت پروژه کامل این مطلب
PluralsightTranscripts.zip
نظرات مطالب
فعال سازی قسمت آپلود تصویر و فایل Kendo UI Editor
اگر thumb‌ها به درستی  نمایش داده نمی‌شود و فقط قسمتی از عکس رو مشاهده می‌کنید ، با استفاده از قطعه کد زیر این مشکل رفع خواهد شد :
1- تعریف کلاس به صورت زیر
public class ImageSize
    {
        public int Height
        {
            get;
            set;
        }

        public int Width
        {
            get;
            set;
        }
    }
2- تعریف کلاس ImageResizer همانند زیر :
public class ImageResizer
    {
        public ImageSize Resize(ImageSize originalSize, ImageSize targetSize)
        {
            var aspectRatio = (float)originalSize.Width / (float)originalSize.Height;
            var width = targetSize.Width;
            var height = targetSize.Height;

            if (originalSize.Width > targetSize.Width || originalSize.Height > targetSize.Height)
            {
                if (aspectRatio > 1)
                {
                    height = (int)(targetSize.Height / aspectRatio);
                }
                else
                {
                    width = (int)(targetSize.Width * aspectRatio);
                }
            }
            else
            {
                width = originalSize.Width;
                height = originalSize.Height;
            }

            return new ImageSize
            {
                Width = Math.Max(width, 1),
                Height = Math.Max(height, 1)
            };
        }
    }

3- تعریف کلاس ThumbnailCreator  همانند نمونه زیر :
 public class ThumbnailCreator
    {
        private static readonly IDictionary<string, ImageFormat> ImageFormats = new Dictionary<string, ImageFormat>{
            {"image/png", ImageFormat.Png},
            {"image/gif", ImageFormat.Gif},
            {"image/jpeg", ImageFormat.Jpeg}
        };

        private readonly ImageResizer resizer;

        public ThumbnailCreator()
        {
            this.resizer = new ImageResizer();
        }

        public byte[] Create(Stream source, ImageSize desiredSize, string contentType)
        {
            using (var image = Image.FromStream(source))
            {
                var originalSize = new ImageSize
                {
                    Height = image.Height,
                    Width = image.Width
                };

                var size = resizer.Resize(originalSize, desiredSize);

                using (var thumbnail = new Bitmap(size.Width, size.Height))
                {
                    ScaleImage(image, thumbnail);

                    using (var memoryStream = new MemoryStream())
                    {
                        thumbnail.Save(memoryStream, ImageFormats[contentType]);

                        return memoryStream.ToArray();
                    }
                }
            }
        }

        private void ScaleImage(Image source, Image destination)
        {
            using (var graphics = Graphics.FromImage(destination))
            {
                graphics.CompositingMode = CompositingMode.SourceCopy;
                graphics.CompositingQuality = CompositingQuality.HighQuality;
                graphics.SmoothingMode = SmoothingMode.AntiAlias;
                graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
                graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

                graphics.DrawImage(source, 0, 0, destination.Width, destination.Height);
            }
        }
    }

4- تعریف اکشن Thumbnail همانند زیر :
private FileContentResult CreateThumbnail(string physicalPath)
        {
            using (var fileStream = System.IO.File.OpenRead(physicalPath))
            {
                var desiredSize = new ImageSize
                {
                    Width = ThumbnailWidth,
                    Height = ThumbnailHeight
                };


                string contentType = MimeMapping.GetMimeMapping(physicalPath);
                return File(thumbnailCreator.Create(fileStream, desiredSize, contentType), contentType);
            }
        }

و در پایان اکشن GetThumbnail  را همانند زیر تغییر خواهیم داد :
 public virtual ActionResult GetThumbnail(string path)
        {
            path = GetSafeFileAndDirPath(path);
          //  return File(path, contentType); 
            return CreateThumbnail(path);
        }



مطالب
امکان تعریف ویژگی‌ها بر روی توابع محلی در C# 9.0

یکی از ویژگی‌های جدید اضافه شده به سی شارپ 9، Attributes on local functions نام دارد و این توانایی را به ما می‌دهد تا بر روی متد‌های محلی که درون متدها تعریف می‌شوند، Attributes قرار دهیم. قابلیت local functions در نسخه 7 سی شارپ اضافه شده‌است و با استفاده از این قابلیت می‌توانیم درون یک متد، تابع دیگری را تعریف کنیم و در همان متد، از آن تابع درونی استفاده کنیم. در واقع تابع درونی، لوکال متد بیرونی است و در خارج از متد بیرونی، قابل دسترسی نیست. مانند مثال زیر:

    // Main method 
    public static void Main()
    {
        // Local Function 
        void AddValue(int a, int b)
        {
            Console.WriteLine("Value of a is: " + a);
            Console.WriteLine("Value of b is: " + b);
            Console.WriteLine("Sum of a and b is: {0}", a + b);
            Console.WriteLine();
        }

        // Calling Local function 
        AddValue(20, 40);
        AddValue(40, 60);
    }

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

        static void Main(string[] args)
        {
            static void DoAction()
            {
                // DoAction

                Console.WriteLine("DoAction...");
            }

#if DEBUG
            DoAction();
#endif
        }

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

        static void Main(string[] args)
        {
            [Conditional("DEBUG")]
            static void DoAction()
            {
                // Do Action Here

                Console.WriteLine("Do Action...");
            }

            DoAction();
        }

اگر بر روی متدهای محلی C# 8.0 از Attribute استفاده کنیم، با خطای زیر روبرو می‌شویم:

ErrorCS8400Feature 'local function attributes' is not available in C# 8.0. Please use language version 9.0 or greater.
پرسش‌ها
چگونه میتوانم یک پلاک خودرو را در سی شارپ بخوانم

درود بر همه عزیزان؛ زبان سی شارپ. اندازه پروژه کوچک.

برای خواندن متن (پلاک ماشین) از داخل تصویری که با OpenCVsharp گرفتم. (کیفیت تصاویر خوبه)

راه حل های طی شده :

1 :

با IronOCR و Tesseract کار کردم جواب نداد یا شاید بشه گفت من بلد نیستم چجوری از اینا استفاده کنم.

void Test1()
{
    var Ocr = new IronTesseract();
    Ocr.Language = OcrLanguage.Persian;
    using (var Input = new OcrInput("car-plate-iran.jpg"))
    {
       var Result = Ocr.Read(Input);
       var AllText = Result.Text;
       Console.WriteLine(AllText);
    }
}

2 :

امکانات آنلاین مثل api.ocr.space رو نمیتونم استفاده کنم

راه های مختلفی رو تست کردم، چت جی پی تی هم یسری کد میده همشون بدون خطا هستن ولی هیچ کدوم خروجی نمیدن. شاید قبلش کانفیگی چیزی لازمه و من بلد نیستم یا اینکه کلا سورس در رابطه با OCR بسیار کم هست؟! چیزی نمیدونم. ممنون میشم راهنمایی کنید

مطالب
بومی سازی منابع در پروژه‌های ASP.NET Core Web API
اگر پروژه‌ی ما فقط از یک Web API تشکیل شده و نیاز است در قسمت‌های مختلف آن، مانند کنترلرها، سرویس‌ها، اعتبارسنج‌ها و غیره از منابع بومی شده استفاده شود، می‌توان از یک راه حل ساده‌ی «SharedResource» استفاده کرد؛ با این مزایا و شرایط:
 - تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار می‌گیرند. این فایل نیز در یک اسمبلی مستقل از برنامه‌ی اصلی اضافه می‌شود.
 - با استفاده از تزریق سرویس IStringLocalizer می‌توان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامه‌ی Web API دسترسی یافت.
 - در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ می‌شود.
 - کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx می‌باشند.
 
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی می‌کنیم.
 
 
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources

پس از ایجاد پروژه‌ی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشه‌ی جدید Resources را تعریف می‌کنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه می‌کنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.

کار با ین فایل (و یا فایل‌های دیگری مانند SharedResource.en.resx) همانند تمام فایل‌های منبع استاندارد است و نکته‌ی خاصی را به همراه ندارد.


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

پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی می‌کنیم:
    public static class LocalizationConfig
    {
        public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services)
        {
            mvcBuilder.AddDataAnnotationsLocalization(options =>
                    {
                        const string resourcesPath = "Resources";
                        string baseName = $"{resourcesPath}.{nameof(SharedResource)}";
                        var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name;

                        options.DataAnnotationLocalizerProvider = (type, factory) =>
                        {
                            // to use `SharedResource.fa.resx` file
                            return factory.Create(baseName, location);
                        };
                    });

            services.AddLocalization();
            services.AddScoped<IStringLocalizer>(provider =>
                            provider.GetRequiredService<IStringLocalizer<SharedResource>>());

            services.AddScoped<ISharedResourceService, SharedResourceService>();
            return mvcBuilder;
        }
    }
- در اینجا در ابتدا توسط متد AddDataAnnotationsLocalization، کار معرفی اسمبلی ثالثی که باید تعاریف بومی سازی را از آن دریافت کرد، صورت گرفته‌است.
- سپس با استفاده از متد AddLocalization، سرویس‌های پایه‌ی بومی سازی ASP.NET Core به برنامه اضافه می‌شوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، می‌توان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کرده‌ایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت می‌کند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانه‌هایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده می‌کنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده می‌کنید که در ادامه‌ی بحث تکمیل خواهد شد.

هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار می‌گیرد:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddControllers().AddCustomLocalization(services);
        }

پس از آن نیاز است میان‌افزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام می‌دهد:
    public static class LocalizationConfig
    {
        public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app)
        {
            var requestLocalizationOptions = new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
                SupportedCultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                },
                SupportedUICultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                }
            };
            app.UseRequestLocalization(requestLocalizationOptions);
            return app;
        }
    }
محل قرارگیری متد UseCustomRequestLocalization فوق در فایل آغازین برنامه، باید به صورت زیر باید باشد:
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseCustomRequestLocalization();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }


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

در اینجا تعریف RegisterModel را مشاهده می‌کنید که ErrorMessage‌های آن هرچند به ظاهر یک رشته‌ی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده می‌شوند:
using System.ComponentModel.DataAnnotations;

namespace Core3xSharedResource.Models.Account
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources
        [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources
        public string Email { get; set; }
    }
}

فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="&lt;b&gt;Hello&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;" xml:space="preserve">
    <value>&lt;b&gt;سلام&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;</value>
  </data>
  <data name="About Title" xml:space="preserve">
    <value>درباره</value>
  </data>
  <data name="DNT" xml:space="preserve">
    <value>.NET Tips</value>
  </data>
  <data name="SiteName" xml:space="preserve">
    <value>DNT</value>
  </data>
  <data name="Please enter an email address" xml:space="preserve">
    <value>لطفا ایمیلی را وارد کنید</value>
  </data>
  <data name="Please enter a valid email address" xml:space="preserve">
    <value>لطفا ایمیل معتبری را وارد کنید</value>
  </data>
</root>
یک نکته: در اینجا بهتر است کلیدها را به صورت جملات کامل انگلیسی وارد کرد، تا اگر منبع فارسی معادل آن‌ها یافت نشدند، دقیقا از همان کلید، به عنوان مقدار خروجی سیستم بومی سازی استفاده کند.


آزمایش برنامه

اکنون برنامه‌ی Web API، ‌برای آزمایش آماده‌است. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازنده‌ی کلاس تزریق شده‌است و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شده‌ی مدل برنامه را نیز بررسی می‌کنیم:
using Core3xSharedResource.Models.Account;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Core3xSharedResource.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class NormalIStringLocalizerController : ControllerBase
    {
        private readonly IStringLocalizer _localizer;

        public NormalIStringLocalizerController(IStringLocalizer localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public ActionResult<string> Get()
        {
            var localizedString = _localizer["About Title"];
            if (localizedString.ResourceNotFound)
            {
                return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`.");
            }
            return localizedString.Value;
        }

        [HttpPost]
        public ActionResult<RegisterModel> Post(RegisterModel model)
        {
            return model;
        }
    }
}


حالت get را در تصویر فوق مشاهده می‌کنید. در Web API برای تنظیم زبان مورد استفاده می‌توان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شده‌است و نتیجه‌ی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیش‌فرض آن همان fa-IR خواهد بود.

حالت post را نیز در تصویر زیر می‌توان مشاهده کرد:


در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شده‌ی در مدل برنامه را دریافت کرده‌ایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار می‌کند.


تعریف یک سرویس عمومی برای محصور سازی قابلیت‌های بومی سازی، در برنامه‌های Web API

در ادامه تعریف سرویس SharedResourceService را مشاهده می‌کنید که ثبت آن‌را پیشتر انجام دادیم:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;

namespace Core3xSharedResource.Services
{
    public interface ISharedResourceService
    {
        string this[string index] { get; }

        IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
        string GetString(string name, params object[] arguments);
        string GetString(string name);
    }

    public class SharedResourceService : ISharedResourceService
    {
        private readonly IStringLocalizer _sharedLocalizer;
        private readonly ILogger<SharedResourceService> _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public SharedResourceService(
            IStringLocalizer sharedHtmlLocalizer,
            IHttpContextAccessor httpContextAccessor,
            ILogger<SharedResourceService> logger
            )
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            return _sharedLocalizer.GetAllStrings(includeParentCultures);
        }

        public string this[string index] => GetString(index);

        public string GetString(string name, params object[] arguments)
        {
            var result = _sharedLocalizer.GetString(name, arguments);
            logError(name, result);
            return result;
        }

        private void logError(string name, LocalizedString result)
        {
            if (result.ResourceNotFound)
            {
                var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"];
                _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`.");
            }
        }

        public string GetString(string name)
        {
            var result = _sharedLocalizer.GetString(name);
            logError(name, result);
            return result;
        }
    }
}
این سرویس نه فقط دسترسی به IStringLocalizer را محصور می‌کند، بلکه در متد logError آن اینبار خطای بسیار مفیدی جهت دیباگ کردن سیستم بومی سازی لاگ خواهد شد. اگر کلیدی یافت نشود، فایلی یافت نشود و یا زبان ارسالی تنظیمی یافت نشود، خطای آن‌را در لاگ‌های برنامه می‌توانید مشاهده کنید که در حالت عادی کار با IStringLocalizer، لاگ نمی‌شوند و همچنین هیچ خطا و یا استثنائی را نیز سبب نمی‌شوند. به همین جهت دیباگ کردن سیستم بومی سازی بدون این لاگ‌ها، تقریبا غیرممکن است. برای مثال مقدار baseNameهایی را که در کدهای این مطلب مشاهده می‌کنید، بر اساس همین لاگ‌ها تشخیص داده شدند و بدون آن‌ها تشکیل این مقادیر غیرممکن بودند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Core3xSharedResource.zip