نظرات مطالب
iTextSharp و نمایش صحیح تاریخ در متنی راست به چپ
سلام، می خواستم اگر ممکن باشه من رو راهنمایی کنید، من می خوام با استفاده از itextsharp محتوای یک فایل پی دی اف رو به صورت txt ذخیره کنم، با زبانهای چپ به راست خیلی آسون هست ولی در زبان راست به چپ، کلمات از اخر به اول نوشته می شن مثلا کلمه ارزیابی به صورت "یبایزرا" خوانده می شه، ممنون می شم من رو راهنمایی بکنید.
        private static string ParsPDFToString()
        {
            PdfReader reader = new PdfReader("c:/2v.pdf");
            PdfReaderContentParser parser = new PdfReaderContentParser(reader);
            StringBuilder sb = new StringBuilder();
            Console.WriteLine("reader.NumberOfPages : " + reader.NumberOfPages.ToString());
            Console.ReadKey();
            for (int i = 1; i <= reader.NumberOfPages; i++)
            {
                ITextExtractionStrategy strategy = parser.ProcessContent(
                  i , new SimpleTextExtractionStrategy()
                );
                sb.Append(strategy.GetResultantText());
            }
            return sb.ToString();
        }
اشتراک‌ها
مروری بر ابزارهای SQL بر روی هدوپ و کلان داده
«... اس کیو ال به عنوان روش استاندارد کار با داده‌های رابطه ای، سالهاست که در تمام تار و پود دنیای فناوری اطلاعات نفوذ کرده است و بنابراین دور از انتظار نیست که برای داده‌های بدون ساختار یا نیمه ساختیافته در حوزه کلان داده هم به فکر استفاده از این زبان رایج باشیم تا توسعه گران و تحلیل گران حوزه داده به راحتی و با حداقل آموزش بتوانند به پردازش داده‌ها بپردازند ... »
مروری بر ابزارهای SQL بر روی هدوپ و کلان داده
مطالب
MVVM و رویدادگردانی

در دو قسمت قبل به اینجا رسیدیم که بجای شروع به کدنویسی مستقیم در code behind یک View (یک پنجره، یک user control ...)، کلاس مجزای دیگری را به نام ViewModel به برنامه اضافه خواهیم کرد و این کلاس از وجود هیچ فرمی در برنامه مطلع نیست.
بنابراین جهت انتقال رخدادها به ViewModel، بجای روش متداول تعریف روال‌های رخدادگردان در Code behind:
<Button  Click="btnClick_Event">Last</Button>

آن‌ها را با Commands به ViewModel ارسال خواهیم کرد:
<Button Command="{Binding GoLast}">Last</Button>  


همچنین بجای اینکه مستقیما بخواهیم از طریق نام یک شیء به مثلا خاصیت متنی آن دسترسی پیدا کنیم:
<TextBox Name="txtName" />  

از طریق Binding، اطلاعات مثلا متنی آن‌را به ViewModel منتقل خواهیم کرد:
<TextBox Text="{Binding Name}" />  


و همینجا است که 99 درصد آموزش‌های MVVM موجود در وب به پایان می‌رسند؛ البته پس از مشاهده 10 تا 20 ویدیو و خواندن بیشتر از 30 تا مقاله! و اینجا است که خواهید گفت: فقط همین؟! با این‌ها میشه یک برنامه رو مدیریت کرد؟!
البته همین‌ها برای مدیریت قسمت عمده‌ای از اکثر برنامه‌ها کفایت می‌کنند؛ اما خیلی از ریزه‌ کاری‌ها وجود دارند که به این سادگی‌ها قابل حل نیستند و در طی چند مقاله به آن‌ها خواهیم پرداخت.

سؤال: در همین مثال فوق، اگر متن ورودی در TextBox تغییر کرد، چگونه می‌توان بلافاصله از تغییرات آن در ViewModel مطلع شد؟ قدیم‌ترها می‌شد نوشت:
<TextBox TextChanged="TextBox_TextChanged" />


اما الان که قرار نیست در code behind کد بنویسیم (تا حد امکان البته)، باید چکار کرد؟
پاسخ: امکان Binding به TextChanged وجود ندارد، پس آن‌را فراموش می‌کنیم. اما همان Binding معمولی را به این صورت هم می‌شود نوشت (همان مثال قسمت قبل):
<TextBox Text="{Binding 
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged}" />


و نکته مهم آن UpdateSourceTrigger است. اگر روی حالت پیش فرض باشد، ViewModel پس از تغییر focus از این TextBox به کنترلی دیگر، از تغییرات آگاه خواهد شد. اگر آن‌را صریحا ذکر کرده و مساوی PropertyChanged قرار دهیم (این مورد در سیلورلایت 5 جدید است؛ هر چند از روز نخست WPF وجود داشته است)، با هر تغییری در محتوای TextBox، خاصیت MainPageModelData.Name به روز رسانی خواهد شد.
اگر هم بخواهیم این تغییرات آنی‌را در ViewModel تحت نظر قرار دهیم، می‌توان نوشت:

using System.ComponentModel;

namespace SL5Tests
{
public class MainPageViewModel
{
public MainPageModel MainPageModelData { set; get; }
public MainPageViewModel()
{
MainPageModelData = new MainPageModel();
MainPageModelData.Name = "Test1";
MainPageModelData.PropertyChanged += MainPageModelDataPropertyChanged;
}

void MainPageModelDataPropertyChanged(object sender, PropertyChangedEventArgs e)
{
switch (e.PropertyName)
{
case "Name":
//do something
break;
}
}
}
}

تعریف MainPageModel را در قسمت قبل مشاهده کرده‌اید و این کلاس اینترفیس INotifyPropertyChanged را پیاده سازی می‌کند. بنابراین می‌توان از رویدادگردان PropertyChanged آن در ViewModel هم استفاده کرد.
به این ترتیب همان کار رودیدادگردان TextChanged را اینطرف هم می‌توان شبیه سازی کرد و تفاوتی نمی‌کند. البته با این تفاوت که در ViewModel فقط به اطلاعات به روز موجود در MainPageModelData.Name دسترسی داریم، اما نمی‌دانیم و نمی‌خواهیم هم بدانیم که منبع آن دقیقا کدام شیء رابط کاربری برنامه است.

سؤال: ما قبلا مثلا می‌توانستیم بررسی کنیم که اگر کاربر حین تایپ در یک TextBox بر روی دکمه‌ی Enter کلیک کرد، آن‌گاه برای نمونه، جستجویی بر اساس اطلاعات وارد شده صورت گیرد. الان این فشرده شدن دکمه‌ی Enter را چگونه دریافت و چگونه به ViewModel ارسال کنیم؟
این مورد کمی پیشرفته‌تر از حالت‌های قبلی است. برای حل این مساله ابتدا باید UpdateSourceTrigger یاد شده را مساوی Explicit قرار داد. یعنی اینبار می‌خواهیم نحوه ی به روز رسانی خاصیت MainPageModelData.Name را از طریق Binding خودمان مدیریت کنیم. این مدیریت کردن هم با استفاده از امکاناتی به نام Attached properties قابل انجام است که به آن‌ها Behaviors هم می‌گویند. مثلا:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace SL5Tests
{
public static class InputBindingsManager
{
public static readonly DependencyProperty UpdatePropertySourceWhenEnterPressedProperty
= DependencyProperty.RegisterAttached(
"UpdatePropertySourceWhenEnterPressed",
typeof(bool),
typeof(InputBindingsManager),
new PropertyMetadata(false, OnUpdatePropertySourceWhenEnterPressedPropertyChanged));

static InputBindingsManager()
{ }

public static void SetUpdatePropertySourceWhenEnterPressed(DependencyObject dp, bool value)
{
dp.SetValue(UpdatePropertySourceWhenEnterPressedProperty, value);
}

public static bool GetUpdatePropertySourceWhenEnterPressed(DependencyObject dp)
{
return (bool)dp.GetValue(UpdatePropertySourceWhenEnterPressedProperty);
}

private static void OnUpdatePropertySourceWhenEnterPressedPropertyChanged(DependencyObject dp,
DependencyPropertyChangedEventArgs e)
{
var txt = dp as TextBox;
if (txt == null)
return;

if ((bool)e.NewValue)
{
txt.KeyDown += HandlePreviewKeyDown;
}
else
{
txt.KeyDown -= HandlePreviewKeyDown;
}
}

static void HandlePreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Enter) return;

var txt = sender as TextBox;
if (txt == null)
return;

var binding = txt.GetBindingExpression(TextBox.TextProperty);
if (binding == null) return;
binding.UpdateSource();
}
}
}

تعریف Attached properties یک قالب استاندارد دارد که آن را در کد فوق ملاحظه می‌کنید. یک تعریف به صورت static و سپس تعریف متدهای Get و Set آن. با تغییر مقدار آن که اینجا از نوع bool تعریف شده، متد OnUpdatePropertySourceWhenEnterPressedPropertyChanged به صورت خودکار فراخوانی می‌شود. اینجا است که ما از طریق آرگومان dp به textBox جاری دسترسی کاملی پیدا می‌کنیم. مثلا در اینجا بررسی شده که آیا کلید فشرده شده enter است یا خیر. اگر بله، یک سری فرامین را انجام بده. به عبارتی ما توانستیم، قطعه کدی را به درون شیءایی موجود تزریق کنیم. Txt تعریف شده در اینجا، واقعا همان کنترل TextBox ایی است که به آن متصل شده‌ایم.

و برای استفاده از آن خواهیم داشت:

<UserControl x:Class="SL5Tests.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:VM="clr-namespace:SL5Tests"
mc:Ignorable="d" Language="fa"
d:DesignHeight="300" d:DesignWidth="400">
<UserControl.Resources>
<VM:MainPageViewModel x:Name="vmMainPageViewModel" />
</UserControl.Resources>
<Grid DataContext="{Binding Source={StaticResource vmMainPageViewModel}}"
x:Name="LayoutRoot"
Background="White">
<TextBox Text="{Binding
MainPageModelData.Name,
Mode=TwoWay,
UpdateSourceTrigger=Explicit}"
VerticalAlignment="Top"
VM:InputBindingsManager.UpdatePropertySourceWhenEnterPressed="True" />
</Grid>
</UserControl>

همانطور که مشاهده می‌کنید، UpdateSourceTrigger به Explicit تنظیم شده و سپس InputBindingsManager.UpdatePropertySourceWhenEnterPressed به این کنترل متصل گردیده است. یعنی تنها زمانیکه در متد HandlePreviewKeyDown ذکر شده، متد UpdateSource فراخوانی گردد، خاصیت MainPageModelData.Name به روز رسانی خواهد شد (کنترل آن‌را خودمان در دست گرفته‌ایم نه حالت‌های از پیش تعریف شده).

این روش، روش متداولی است برای تبدیل اکثر حالاتی که Binding و Commanding متداول در مورد آن‌ها وجود ندارد. مثلا نیاز است focus را به آخرین سطر یک ListView از داخل ViewModel انتقال داد. در حالت متداول چنین امری میسر نیست، اما با تعریف یک Attached properties می‌توان به امکانات شیء ListView مورد نظر دسترسی یافت (به آن متصل شد، یا نوعی تزریق)، آخرین عنصر آن‌را یافته و سپس focus را به آن منتقل کرد یا به هر اندیسی مشخص که بعدا در ViewModel به این Behavior از طریق Binding ارسال خواهد شد.

مطالب
با ASP.MVC چه مزایایی را به دست خواهیم آورد
عموما در اکثر مطالب مقایسه‌ای بین وب فرم‌ها و ASP.NET MVC به جداسازی بهتر منطق کدها از فرم‌ها و قابلیت بهتر تهیه آزمون‌های واحد اشاره می‌شود. در این مطلب از دیدگاهی دیگر به این مساله خواهیم پرداخت؛ از لحاظ فنی و جدای از مسایل یاد شده، چه مزایای دیگری را می‌توان با استفاده از ASP.NET MVC نسبت به وب فرم‌ها به دست آورد؟

1- آدرس‌های تمیزتر
در ASP.NET MVC به صورت پیش فرض از سیستم Routing موجود در زیر ساخت ASP.NET برای نمایش Urlهایی بدون پسوند استفاده می‌شود. همچنین این سیستم امکان تهیه آدرس‌هایی با سازگاری بهتر با موتورهای جستجو را نیز از ابتدا مدنظر داشته است.
بله. این زیر ساخت در اختیار وب فرم‌ها نیز هست؛ اما فرق است بین حالتی که از ابتدا مجبور شویم تمیزتر کار کنیم با زمانیکه این انتخاب را داریم و ... عموما هم از آن در وب فرم‌ها استفاده نمی‌شود.

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

3- امکان مدیریت بهتر قسمت‌های مختلف سایت در پوشه‌های جداگانه
اگر به سورس اکثر سایت‌های مبتنی بر ASP.NET Web forms توجه کنیم، تمام فایل‌های آن‌ها در ریشه سایت قرار دارند منهای فایل‌های CSS و JS و تصاویر. در ASP.NET MVC از ابتدای کار، هر قسمت از سایت در پوشه‌های جداگانه‌ای قرار می‌گیرد و به این ترتیب مدیریت فایل‌ها و نظم دهی به آن‌ها ساده‌تر خواهد بود.

4- امکان تعریف تمام اجزای یک فرم یا view به صورت strongly typed
در ASP.NET MVC می‌توان یک کلاس را به یک فرم یا View نسبت داد. به این ترتیب کنترل‌های web forms تبدیل به خواص این کلاس در MVC خواهند شد. مزیت این امر امکان کنترل تمام اجزای فرم‌های سایت توسط کامپایلر است.
به این ترتیب اگر در طی یک حلقه، جدولی را ایجاد کنیم، تمام عناصر تشکیل دهنده این حلقه (چه کدهای آن و چه المان‌هایی که اطلاعات را در صفحه نمایش می‌دهند) نیز توسط کامپایلر قابل بررسی و خطایابی هستند.

5- مقدار دهی خودکار مدل متناظر با یک فرم یا View در ASP.NET MVC
روال متداول کار با وب فرم‌ها، قرار دادن تعدادی کنترل در صفحه و سپس دریافت دستی مقادیر آن‌ها در فایل code behind است. در MVC دیگر نیازی نیست تا این کارها را دستی انجام دهید. به یک فرم یا View کلاسی را انتساب خواهید داد. فریم ورک خواص آن‌را به صورت خودکار در حین ارسال به سرور مقدار دهی خواهد کرد. این مورد حتی در حین کار با jQuery Ajax نیز صادق  است.
این مساله کار با ORMها را نیز ساده‌تر می‌کند. از این جهت که تمام آن‌ها نهایتا با یک سری مدل و کلاس کار خواهند کرد و تمام فیلدها و جداول به تعدادی کلاس و خاصیت تعریف شده در آن‌ها نگاشت می‌شوند.
به این ترتیب چون دیگر نیازی به ارجاع مستقیم به اشیاء بصری در فایل‌های code behind که در اینجا کنترلر نام گرفته‌اند نیست، نوشتن آزمون واحد برای این کلاس‌ها نیز به شدت ساده‌تر شده است.

6- ASP.NET MVC به همراه یک فرم ساز توکار ارائه می‌شود
اگر کسی به شما گفته است که سرعت کار با ASP.NET MVC پایین است به طور قطع دو فصل اول یک کتاب MVC را بیشتر مطالعه نکرده است. در MVC یک کلاس متناظر با فرمی را طراحی می‌کنید. توسط ابزار scaffolding همراه با VS.NET از روی این کلاس و مدل، با چند کلیک یک فرم تولید خواهد شد. فرمی که حتی مقدار دهی و انتساب عناصر بصری آن به کلاس متناظر با آن نیز خودکار است.
سرعت پیاده سازی یک برنامه با ASP.NET MVC به مراتب بیشتر است از کار با وب فرم‌ها.

7- حذف View State در MVC
از آنجائیکه فرم‌های ASP.NET Web forms از نوع strongly typed نیستند (در دات نت 4 و نیم اندکی بهبود در حد گرید‌های آن حاصل شده البته)، برای اینکه پس از ارسال یک فرم به سرور باز هم کنترل‌های نمایش داده شده در صفحه همان مقادیر قبلی را نمایش دهند، مکانیزم View State به همراه ذخیره سازی اطلاعات فرم در فیلدهای مخفی فرم جاری طراحی شد.
در MVC نیازی به این مکانیزم نیست. زیرا فقط کافی است که اطلاعات مدل را مجددا به View ارسال کنیم. نمایش و انتساب نهایی آن در اینجا خودکار است. بنابراین نیازی به View State وجود ندارد.

8- کنترل بهتر بر روی اعتبار سنجی اطلاعات دریافتی
در وب فرم‌ها اگر بخواهیم سیستم اعتبارسنجی آن‌را غیرفعال کنیم، مثلا برای دریافت html از کاربر، نیاز است کلا آن‌را از کار بیندازیم (یا در سطح فرم یا در سطح کل برنامه). در MVC می‌توان این اعتبار سنجی را تنها در سطح یک خاصیت که قرار است اطلاعات HTML ایی را دریافت کند، غیرفعال کرد؛ نه برای کل فرم و نه در سطح کل برنامه.

9- امکان استفاده از فرم‌های و Viewهای Razor بجای موتور وب فرم‌ها
در وب فرم‌ها تا این زمان فقط محدود به تنها موتور نمایشی مخصوص به آن هستیم. اما در MVC این محدودیت برداشته شده و تا به حال چندین و چند View engine در این بین توسط مایکروسافت و سایر برنامه نویس‌ها طراحی شده است. مهم‌ترین آن‌ها Razor است که تمام برنامه نویس‌های MVC پس از مدتی به روان بودن و طراحی طبیعی و عالی آن اعتراف دارند.

10- امکان تعریف بیش از یک فرم در صفحه
طراحی ASP.NET Web forms از روز اول آن محدود به یک فرم در صفحه بوده است. این محدودیت در MVC برداشته شده و مزیت آن امکان ارسال اطلاعات قسمت‌های مختلف یک فرم به کنترلرهای مختلف و جداسازی بهتر کدهای قسمت‌های مختلف برنامه است.

11- امکان Refactoring بهتر کدهای تکراری در ASP.NET MVC به کمک مفهوم فیلترها
فیلترها در MVC یک سری attribute هستند که می‌توان آن‌ها را به متدهای کنترلرها اعمال کرد و به صورت خودکار توسط فریم ورک پیش یا پس از اجرای یک متد اجرا می‌شوند. به این ترتیب حجم قابل ملاحظه‌ای از if و else‌ها را می‌توان در این فیلترها کپسوله کرد و کدهای متدهای کنترلرها را تمیزتر و زیباتر نمود.

12- سازگاری کامل با jQuery و jQuery Ajax و کلا انواع و اقسام فریم‌ورک‌های جاوا اسکریپتی
در MVC وب کنترل‌ها کنار گذاشته شده‌اند و سعی شده است با وب به همان نحو که هست برخورد شود. به این ترتیب اگر نیاز داشتید، کل دکمه‌های فرم را با spanها جایگزین کرده و توسط فریم ورک‌های CSS ایی تزئین کنید، بدون نیاز به نگارش جدیدی از ASP.NET MVC، باز هم برنامه کار خواهد کرد.
یا برای کار با اجزای مختلف فرم از ده‌ها و صدها افزونه موجود برای jQuery به سادگی می‌توان استفاده کرد. برای نمونه کل سیستم اعتبار سنجی توکار MVC با اعتبار سنجی jQuery یکپارچه و جایگزین شده است.
یا برای کار با jQuery Ajax نیازی نیست تا متدی را static تعریف کنید و به این ترتیب از مزایای امنیتی توکار ASP.NET محروم شوید (مثلا دسترسی به شیء User اعتبار سنجی مبتنی بر فرم‌ها). یا اگر فرم شما 10 فیلد دارد، کل این فیلدها به صورت خودکار به خواص متناظر با آن‌ها نگاشت خواهد شد و نیازی نیست برای این مورد کد بنویسید. به علاوه باید درنظر داشت که jQuery Ajax نسبت به فریم ورک Ajax همراه با ASP.NET web forms بسیار سبک‌تر و سریعتر عمل می‌کند چون نیازی ندارد تا هر بار View state را نیز به سرور ارسال کند.
همچنین در اینجا دیگر ID کنترل‌های مورد استفاده در اسکریپت‌ها به صورت خودکار تولید نمی‌شوند و برنامه نویس از ابتدای امر کنترل کاملی را روی این مساله دارد.

13- امکانات فشرده سازی css و js بهتر
در MVC 4 سیستم bundling آن از نمونه مشابه موجود در وب فرم‌ها کامل‌تر است و جهت فشرده سازی و یکی کردن هر دو مورد فایل‌های css و js می‌تواند بکارگرفته شود؛ به همراه تنظیمات کش مرورگر و gzip خودکار حاصل. به علاوه این سیستم را سفارشی سازی نیز می‌توان ساخت و بهینه سازی عملکرد آن مطابق نیاز میسر است.

14- یکپارچگی بهتر EF Code first با MVC
عنوان شد که وجود مدل‌ها و فرم‌های strongly typed یکی از مزایای کار با MVC است و ORMها نیز نهایتا با همین کلاس‌ها هستند که کار می‌کنند. در MVC سیستم کد سازی به نام scaffolding وجود دارد (تهیه شده توسط خود مایکروسافت) که می‌تواند بر اساس مدل‌های EF code first شما، قسمت عمده‌ای از کدهای یک برنامه ASP.NET MVC را تولید کند. سپس می‌توانید به سفارشی سازی آن مشغول شوید.

15- تزریق وابستگی‌ها در MVC ساده‌تر است
در هر دو فریم ورک وب فرم‌ها و MVC امکان تزریق وابستگی‌ها وجود دارد. اما در MVC می‌توان در میانه کار وهله سازی کنترلرها، دخالت کرد و کنترل آن را کاملا در دست گرفت. همین امر سبب می‌شود حین کار با کتابخانه‌های تزریق وابستگی‌ها در ASP.NET MVC حجم کد نویسی به شدت کاهش پیدا کند.

16- امکانات امنیتی MVC بیشتر است
عنوان شد که در MVC می‌توان اعتبار سنجی را تنها در حد یک خاصیت غیرفعال کرد. فیلتر مبارزه با حملات CSRF جزئی از فریم ورک MVC است. به همراه فیلتر Authorize آن که باز هم اعمال سفارشی سیستم اعتبار سنجی مبتنی بر فرم‌ها را ساده‌تر می‌کند با امکان یکپارچگی بهتر با Role providerهای سفارشی.
و یا برای نمونه Razor به صورت پیش فرض امن طراحی شده است. خروجی Razor همواره و در بدو امر، html encoded است مگر اینکه برنامه نویس آگاهانه آن‌را تغییر دهد. این مورد مقاومت در برابر حملات XSS را بالا خواهد برد.
امکان استفاده از فیلترهای سفارشی که عنوان شد، جهت مسایل امنیتی بسیار کاربرد دارند. برای مثال بررسی referrer فرم ارسال به سرور را درنظر بگیرید. در وب فرم‌ها می‌توان این‌کار را با یک http module که روی کل برنامه تاثیر گذار است انجام داد. اما در MVC این فیلتر را تنها می‌توان بر روی یک فرم خاص عمومی برای مثال اعمال کرد و نه کل برنامه.
مطالب
شروع به کار با DNTFrameworkCore - قسمت 3 - پیاده‌سازی سرویس‌های موجودیت‌ها
در قسمت قبل سناریوهای مختلف مرتبط با طراحی موجودیت‌های سیستم را بررسی کردیم. در این قسمت به طراحی DTO‌های متناظر با موجودیت‌ها به همراه اعتبارسنج‌های مرتبط و در نهایت به پیاده سازی سرویس‌های CRUD آنها خواهیم پرداخت. 
قراردادها، مفاهیم و نکات اولیه
  1. برخلاف بسیاری از طراحی‌های موجود، بر فراز هر موجودیت اصلی (منظور AggregateRoot) باید یک DTO که از این پس با عنوان Model از آنها یاد خواهیم کرد، تعریف شود. 
  2. هیچ تراکنشی برای موجودیت‌های فرعی یا همان Detailها نخواهیم داشت. این موجودیت‌ها در تراکنش موجودیت اصلی مرتبط به آن مدیریت خواهند شد.
  3. هر Commandای که قرار است مرتبط با یک موجودیت اصلی در سیستم انجام پذیرد، باید از منطق تجاری آن موجودیت عبور کند و نباید با دور زدن منطق تجاری، از طرق مختلف تغییراتی بر آن موجودیت اعمال شود. (موضوع مهمی که در ادامه مطلب جاری تشریح خواهد شد)
  4. ویوهای مختلفی از یک موجودیت می‌توان انتظار داشت که ویو پیش‌فرض آن در CrudService تدارک دیده شده است. برای سایر موارد نیاز است در سرویس مرتبط، متدهای Read مختلفی را پیاده‌سازی کنید.
  5. با اعمال اصل CQS، متدهای ثبت و ویرایش در کلاس سرویس پایه CRUD، بعد از انجام عملیات مربوطه، Id و RowVersion مدل ورودی و هچنین Id و TrackingState موجودیت‌های فرعی وابسته، مقداردهی خواهند شد و نیاز به انجام یک Query دیگر و بازگشت آن به عنوان خروجی متدها نبوده است. به همین دلیل خروجی این متدها صرفا Result ای می‌باشد که نشان از امکان Failure بودن انجام آنها می‌باشد که با اصل مذکور در تضاد نمی‌باشد.
  6. ورودی متدهای Read شما که در اکثر موارد نیاز به مهیا کردن خروجی صفحه‌بندی شده دارند، باید از نوع PagedQueryModel و یا اگر همچنین نیاز به جستجوی پویا براساس فیلدهایی موجود در ReadModel مرتبط دارید، باید از نوع FilteredPagedQueryModel باشد. متدهای الحاقی برای اعمال خودکار این صفحه‌‌بندی و جستجوی پویا در نظر گرفته شده است. همچنین خروجی آنها در اکثر موارد از نوع IPagedQueryResult خواهد بود. اگر نیاز است تا جستجوی خاصی داشته باشید که خصوصیتی متناظر با آن فیلد در مدل Read وجود ندارد، لازم است تا از این QueryModel‌های مطرح شده، ارث‌بری کرده و خصوصیت اضافی مدنظر خود را تعریف کنید. بدیهی است که اعمال جستجوی این موارد خاص به عهده توسعه دهنده می‌باشد.
  7. عملیات ثبت، ویرایش و حذف، برای کار بر روی لیستی از وهله‌های Model، طراحی شده‌اند. این موضوع در بسیاری از دومین‌ها قابلیت مورد توجهی می‌باشد. 
  8. رخداد متناظر با عملیات CUD مرتبط با هر موجودیت اصلی، به عنوان یکسری نقاط قابل گسترش (Extensibility Point) در اختیار سایر بخش‌های سیستم می‌باشد. این رخدادها درون تراکنش جاری Raise خواهند شد؛ از این جهت امکان اعمال یکسری Rule جدید از سمت سایر موءلفه‌های سیستم موجود می‌باشد.
  9. برخلاف بسیاری از طراحی‌های موجود، قصد ایجاد لایه انتزاعی برفراز EF Core  به منظور رسیدن به Persistence Ignorance را ندارم. بنابراین امروز بسته DNTFrameworkCore.EntityFramework آن آماده می‌باشد. اگر توسعه دهنده‌ای قصد یکپارچه کردن این زیرساخت را با سایر ORMها یا Micro ORMها داشته باشد، می‌تواند Pull Request خود را ارسال کند.
  10. خبر خوب اینکه هیچ وابستگی به AutoMapper به منظور نگاشت مابین موجودیت‌ها و مدل‌های متناظر آنها، در این زیرساخت وجود ندارد. با پیاده سازی متدهای MapToModel و MapToEntity می‌توانید از کتابخانه Mapper مورد نظر خودتان استفاده کنید؛ یا به صورت دستی این کار را انجام دهید. بعد از چند سال استفاده از AutoMapper، این روزها خیلی اعتقادی به استفاده از آن ندارم.
  11. هیچ وابستگی به FluentValidation به منظور اعتبارسنجی ورودی متدها یا پیاده‌سازی قواعد تجاری، در این زیرساخت وجود ندارد. شما امکان استفاده از Attributeهای اعتبارسنجی توکار، پیاده سازی IValidatableObject توسط مدل یا در موارد خاص به منظور پیاده سازی قواعد تجاری پیچیده، پیاده سازی IModelValidator را دارید. با این حال برای یکپارچگی با این کتابخانه محبوب، می‌توانید بسته نیوگت DNTFrameworkCore.FluentValidation را نصب کرده و استفاده کنید.
  12. با اعمال الگوی Template Method در پیاده سازی سرویس CRUD پایه، از طریق تعدادی متد با پیشوندهای Before و After متناظر با عملیات CUD می‌توانید در فرآیند انجام آنها نیز دخالت داشته باشید؛ به عنوان مثال: BeforeEditAsync یا AfterCreateAsync
  13. باتوجه به اینکه در فرآیند انجام متدهای CUD، یکسری Event هم Raise خواهند شد و همچنین در خیلی از موراد شاید نیاز به فراخوانی SaveChange مرتبط با UnitOfWork جاری باشد، لذا مطمئن‌ترین راه حل برای این قضیه و حفظ ثبات سیستم، همان استفاده از تراکنش محیطی می‌باشد. از این جهت متدهای مذکور با TransactionAttribute نیز تزئین شده‌اند که برای فعال سازی این مکانیزم نیاز است تا TransactionInterceptor مربوطه را به سیستم معرفی کنید.
  14. ValidationInterceptor موجود در زیرساخت، در صورتیکه خروجی متد از نوع Result باشد، خطاهای ممکن را در قالب یک شی Result بازگشت خواهد داد؛ در غیر این صورت یک استثنای ValidationException پرتاب می‌شود که این مورد هم توسط GlobalExceptionFilter مدیریت خواهند شد و در قالب یک BadRequest به کلاینت ارسال خواهد شد.
  15. در سناریوهای Master-Detail، قرارداد این است که Detailها به همراه Master متناظر واکشی خواهند شد و در زمان ثبت و یا ویرایش هم همه آنها به همراه Master متناظر خود به سرور ارسال خواهند شد. 
نکته مهم:  همانطور که اشاره شد، در سناریوهای Master-Detail باید تمامی Detailها به سمت سرور ارسال شوند. در این صورت سناریویی را در نظر بگیرید که قرار است کاربر در front-office سیستم امکان حذف یک قلم از اقلام فاکتور را داشته باشد؛ این درحالی است که در back-office و در منطق تجاری اصلی، ما جایی برای حذف یک تک قلم ندیده‌ایم و کلا منطق و قواعد تجاری حاکم بر فاکتور را زیر سوال می‌برد. چرا که ممکن است یکسری قواعد تجاری متناسب با دومین، بر روی لیست اقلام یک فاکتور در زمان ذخیره سازی وجود داشته باشند که با حذف یک تک قلم از یک مسیر فرعی، کل فاکتور را در حالت نامعتبری برای ذخیره سازی‌های بعدی قرار دهد. در این موارد باید API شما یک DTO سفارشی را دریافت کند که شامل شناسه قلم فاکتور و شناسه فاکتور می‌باشد. سپس با استفاده از شناسه فاکتور و سرویس متناظر، آن را واکشی کرده و از لیست قلم‌های InvoiceModel، آن قلم را با TrackingState.Deleted علامت‌گذاری کنید. همچنین باید توجه داشته باشید که برروی فیلدهای موجود در جداول مرتبط با موجودیت‌های Detail، محدودیت‌های دیتابیسی از جمله Unique Constraint و ... را اعمال نکنید؛ مگر اینکه میدانید و دقیقا مطمئن باشید عملیات حذف اقلام، قبل از عملیات ثبت اقلام جدید رخ می‎دهد (این موضوع نیاز به توضیح و شبیه سازی شرایط خاص آن را دارد که در صورت نیاز می‌توان در مطلب جدایی به آن پرداخت).
‌پیاده سازی و بررسی تعدادی سرویس فرضی
برای شروع لازم است بسته‌های نیوگت زیر را نصب کنید:
PM> Install-Package DNTFrameworkCore -Version 1.0.0
PM> Install-Package DNTFrameworkCore.EntityFramework -Version 1.0.0

مثال اول: پیاده‌سازی سرویس یک موجودیت ساده بدون نیاز به ReadModel 
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class BlogModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Maximum length is 50")]
    public string Url { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "BlogTitle")
        {
            yield return new ValidationResult("IValidatableObject Message", new[] {nameof(Title)});
        }
    }
}
مدل متناظر با موجودیت‌های اصلی باید از کلاس جنریک MasterModel ارث‌بری کرده باشد. همانطور که ملاحظه می‌کنید، برای نشان دادن مکانیزم اعتبارسنجی، از DataAnnotationها و IValidatableObject استفاده شده‌است. LocalizationResource برای مشخص کردن نام و محل فایل Resource متناظر برای خواندن پیغام‌های اعتبارسنجی استفاده می‌شود. این مورد برای سناریوهای ماژولار و کامپوننت محور بیشتر می‌تواند مدنظر باشد. 
گام دوم: پیاده‌سازی اعتبارسنج مستقل
در صورت نیاز به اعتبارسنجی پیچیده برای مدل متناظر، می‌توانید با استفاده از دو روش زیر به این هدف برسید:
1- استفاده از کتابخانه DNTFrameworkCore.FluentValidation
public class BlogValidator : FluentModelValidator<BlogModel>
{
    public BlogValidator(IMessageLocalizer localizer)
    {
        RuleFor(b => b.Title).NotEmpty()
            .WithMessage(localizer["Blog.Fields.Title.Required"]);
    }
}
2- پیاده‌سازی IModelValidator یا ارث‌بری از کلاس ModelValidator پایه
public class BlogValidator : ModelValidator<BlogkModel>
{
    public override IEnumerable<ModelValidationResult> Validate(BlogModel model)
    {
        yield return new ModelValidationResult(nameof(BlogkModel.Title), "Validation from IModelValidator");
    }
}

گام سوم: پیاده‌سازی سرویس متناظر
public interface IBlogService : ICrudService<int, BlogModel>
{
}
پیاده سازی واسط بالا
public class BlogService : CrudService<Blog, int, BlogModel>, IBlogService
{
    public BlogService(CrudServiceDependency dependency) : base(dependency)
    {
    }

    protected override IQueryable<BlogModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(b => new BlogModel
            {Id = b.Id, RowVersion = b.RowVersion, Url = b.Url, Title = b.Title});
    }

    protected override Blog MapToEntity(BlogModel model)
    {
        return new Blog
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            Url = model.Url,
            Title = model.Title,
            NormalizedTitle = model.Title.ToUpperInvariant() //todo: normalize based on your requirement 
        };
    }

    protected override BlogModel MapToModel(Blog entity)
    {
        return new BlogModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            Url = entity.Url,
            Title = entity.Title
        };
    }
}
برای این چنین موجودیت‌هایی، بازنویسی همین 3 متد کفایت می‌کند؛ دو متد MapToModel و MapToEntity برای نگاشت مابین مدل و موجودیت مورد نظر و متد BuildReadQuery نیز برای تعیین نحوه ساخت کوئری ReadPagedListAsync پیش‌فرض موجود در CrudService به عنوان متد Read پیش‌فرض این موجودیت. باکمترین مقدار کدنویسی و با کیفیت قابل قبول، عملیات CRUD یک موجودیت ساده، تکمیل شد. 
مثال دوم: پیاده سازی سرویس یک موجودیت ساده با ReadModel و  FilteredPagedQueryModel متمایز
گام اول: طراحی Model متناظر
[LocalizationResource(Name = "SharedResource", Location = "DNTFrameworkCore.TestAPI")]
public class TaskModel : MasterModel<int>, IValidatableObject
{
    public string Title { get; set; }

    [MaxLength(50, ErrorMessage = "Validation from DataAnnotations")]
    public string Number { get; set; }

    public string Description { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (Title == "IValidatableObject")
        {
            yield return new ValidationResult("Validation from IValidatableObject");
        }
    }
}
public class TaskReadModel : MasterModel<int>
{
    public string Title { get; set; }
    public string Number { get; set; }
    public TaskState State { get; set; } = TaskState.Todo;
    public DateTimeOffset CreationDateTime { get; set; }
    public string CreatorUserDisplayName { get; set; }
}
به عنوان مثال خصوصیاتی برای نمایش داریم که در زمان ثبت و ویرایش، انتظار دریافت آنها را از کاربر نیز نداریم. 
گام دوم: پیاده‌سازی اعتبارسنج  مستقل 
public class TaskValidator : ModelValidator<TaskModel>
{
    public override IEnumerable<ModelValidationResult> Validate(TaskModel model)
    {
        if (!Enum.IsDefined(typeof(TaskState), model.State))
        {
            yield return new ModelValidationResult(nameof(TaskModel.State), "Validation from IModelValidator");
        }
    }
}
 گام سوم: پیاده‌سازی سرویس متناظر
public interface ITaskService : ICrudService<int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>
{
}
همانطور که ملاحظه می‌کنید، از ICrudService استفاده شده است که امکان تعیین نوع پارامتر جنریک TReadModel و TFilteredPagedQueryModel را هم دارد.
مدل جستجو و صفحه‌بندی سفارشی 
public class TaskFilteredPagedQueryModel : FilteredPagedQueryModel
{
    public TaskState? State { get; set; }
}


پیاده سازی واسط ITaskService با استفاده از AutoMapper

public class TaskService : CrudService<Task, int, TaskReadModel, TaskModel, TaskFilteredPagedQueryModel>,
  ITaskService
{
    private readonly IMapper _mapper;

    public TaskService(CrudServiceDependency dependency, IMapper mapper) : base(dependency)
    {
        _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
    }

    protected override IQueryable<TaskReadModel> BuildReadQuery(TaskFilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking()
                    .WhereIf(model.State.HasValue, t => t.State == model.State)
                    .ProjectTo<TaskReadModel>(_mapper.ConfigurationProvider);
    }

    protected override Task MapToEntity(TaskModel model)
    {
        return _mapper.Map<Task>(model);
    }

    protected override TaskModel MapToModel(Task entity)
    {
        return _mapper.Map<TaskModel>(entity);
    }
}

به عنوان مثال در کلاس بالا برای نگاشت مابین مدل و موجودیت، از واسط IMapper کتابخانه AutoMapper استفاده شده‌است و همچنین عملیات جستجوی سفارشی در همان متد BuildReadQuery برای تولید کوئری متد Read پیش‌فرض، قابل ملاحظه می‌باشد.

مثال سوم: پیاده‌سازی سرویس یک موجودیت اصلی به همراه تعدادی موجودیت فرعی وابسته (سناریوهای Master-Detail) 

گام اول: طراحی Modelهای متناظر

    public class UserModel : MasterModel
    {
        public string UserName { get; set; }
        public string DisplayName { get; set; }
        public string Password { get; set; }
        public bool IsActive { get; set; }
        public ICollection<UserRoleModel> Roles { get; set; } = new HashSet<UserRoleModel>();
        public ICollection<PermissionModel> Permissions { get; set; } = new HashSet<PermissionModel>();
        public ICollection<PermissionModel> IgnoredPermissions { get; set; } = new HashSet<PermissionModel>();
    }

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

public class PermissionModel : DetailModel<int>
{
    public string Name { get; set; }
}

به عنوان مثال PermissionModel بالا از DetailModel جنریک‌ای ارث‌بری کرده است که دارای Id و TrackingState نیز می‌باشد. 

public class UserRoleModel : DetailModel<int>
{
    public long RoleId { get; set; }
}

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

گام دوم: پیاده سازی اعتبارسنج مستقل

public class UserValidator : FluentModelValidator<UserModel>
{
    private readonly IUnitOfWork _uow;

    public UserValidator(IUnitOfWork uow, IMessageLocalizer localizer)
    {
        _uow = uow ?? throw new ArgumentNullException(nameof(uow));

        RuleFor(m => m.DisplayName).NotEmpty()
            .WithMessage(localizer["User.Fields.DisplayName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.DisplayName.MinimumLength"])
            .MaximumLength(User.MaxDisplayNameLength)
            .WithMessage(localizer["User.Fields.DisplayName.MaximumLength"])
            .Matches(@"^[\u0600-\u06FF,\u0590-\u05FF,0-9\s]*$")
            .WithMessage(localizer["User.Fields.DisplayName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateDisplayName(model.DisplayName, model.Id))
                    .WithMessage(localizer["User.Fields.DisplayName.Unique"])
                    .OverridePropertyName(nameof(UserModel.DisplayName));
            });

        RuleFor(m => m.UserName).NotEmpty()
            .WithMessage(localizer["User.Fields.UserName.Required"])
            .MinimumLength(3)
            .WithMessage(localizer["User.Fields.UserName.MinimumLength"])
            .MaximumLength(User.MaxUserNameLength)
            .WithMessage(localizer["User.Fields.UserName.MaximumLength"])
            .Matches("^[a-zA-Z0-9_]*$")
            .WithMessage(localizer["User.Fields.UserName.RegularExpression"])
            .DependentRules(() =>
            {
                RuleFor(m => m).Must(model =>
                     !CheckDuplicateUserName(model.UserName, model.Id))
                    .WithMessage(localizer["User.Fields.UserName.Unique"])
                    .OverridePropertyName(nameof(UserModel.UserName));
            });

        RuleFor(m => m.Password).NotEmpty()
            .WithMessage(localizer["User.Fields.Password.Required"])
            .When(m => m.IsNew, ApplyConditionTo.CurrentValidator)
            .MinimumLength(6)
            .WithMessage(localizer["User.Fields.Password.MinimumLength"])
            .MaximumLength(User.MaxPasswordLength)
            .WithMessage(localizer["User.Fields.Password.MaximumLength"]);

        RuleFor(m => m).Must(model => !CheckDuplicateRoles(model))
            .WithMessage(localizer["User.Fields.Roles.Unique"])
            .When(m => m.Roles != null && m.Roles.Any(r => !r.IsDeleted));
    }

    private bool CheckDuplicateUserName(string userName, long id)
    {
        var normalizedUserName = userName.ToUpperInvariant();
        return _uow.Set<User>().Any(u => u.NormalizedUserName == normalizedUserName && u.Id != id);
    }

    private bool CheckDuplicateDisplayName(string displayName, long id)
    {
        var normalizedDisplayName = displayName.NormalizePersianTitle();
        return _uow.Set<User>().Any(u => u.NormalizedDisplayName == normalizedDisplayName && u.Id != id);
    }

    private bool CheckDuplicateRoles(UserModel model)
    {
        var roles = model.Roles.Where(a => !a.IsDeleted);
        return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
    }
}

به عنوان مثال در این اعتبارسنج بالا، قواعدی از جمله بررسی تکراری بودن نام‌کاربری و از این دست اعتبارسنجی‌ها نیز انجام شده است. نکته حائز اهمیت آن متد CheckDuplicateRoles می‌باشد:

private bool CheckDuplicateRoles(UserModel model)
{
    var roles = model.Roles.Where(a => !a.IsDeleted);
    return roles.GroupBy(r => r.RoleId).Any(r => r.Count() > 1);
}

با توجه به «نکته مهم» ابتدای بحث، model.Roles، شامل تمام گروه‌های کاربری متصل شده به کاربر می‌باشند که در این لیست برخی از آنها با TrackingState.Deleted، برخی دیگر با TrackingState.Added و ... علامت‌گذاری شده‌اند. لذا برای بررسی یکتایی و عدم تکرار در این سناریوها نیاز به اجری پرس‌و‌جویی بر روی دیتابیس نمی‌باشد. بدین منظور، با اعمال یک شرط، گروه‌های حذف شده را از بررسی خارج کرده‌ایم؛ چرا که آنها بعد از عبور از منطق تجاری، حذف خواهند شد. 


گام سوم: پیاده‌سازی سرویس متناظر

public interface IUserService : ICrudService<long, UserReadModel, UserModel>
{
}
public class UserService : CrudService<User, long, UserReadModel, UserModel>, IUserService
{
    private readonly IUserManager _manager;

    public UserService(CrudServiceDependency dependency, IUserManager manager) : base(dependency)
    {
        _manager = manager ?? throw new ArgumentNullException(nameof(manager));
    }

    protected override IQueryable<User> BuildFindQuery()
    {
        return base.BuildFindQuery()
            .Include(u => u.Roles)
            .Include(u => u.Permissions);
    }

    protected override IQueryable<UserReadModel> BuildReadQuery(FilteredPagedQueryModel model)
    {
        return EntitySet.AsNoTracking().Select(u => new UserReadModel
        {
            Id = u.Id,
            RowVersion = u.RowVersion,
            IsActive = u.IsActive,
            UserName = u.UserName,
            DisplayName = u.DisplayName,
            LastLoggedInDateTime = u.LastLoggedInDateTime
        });
    }

    protected override User MapToEntity(UserModel model)
    {
        return new User
        {
            Id = model.Id,
            RowVersion = model.RowVersion,
            IsActive = model.IsActive,
            DisplayName = model.DisplayName,
            UserName = model.UserName,
            NormalizedUserName = model.UserName.ToUpperInvariant(),
            NormalizedDisplayName = model.DisplayName.NormalizePersianTitle(),
            Roles = model.Roles.Select(r => new UserRole
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = model.Permissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = true,
                Name = p.Name
            }).Union(model.IgnoredPermissions.Select(p => new UserPermission
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                IsGranted = false,
                Name = p.Name
            })).ToList()
        };
    }

    protected override UserModel MapToModel(User entity)
    {
        return new UserModel
        {
            Id = entity.Id,
            RowVersion = entity.RowVersion,
            IsActive = entity.IsActive,
            DisplayName = entity.DisplayName,
            UserName = entity.UserName,
            Roles = entity.Roles.Select(r => new UserRoleModel
                {Id = r.Id, RoleId = r.RoleId, TrackingState = r.TrackingState}).ToList(),
            Permissions = entity.Permissions.Where(p => p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList(),
            IgnoredPermissions = entity.Permissions.Where(p => !p.IsGranted).Select(p => new PermissionModel
            {
                Id = p.Id,
                TrackingState = p.TrackingState,
                Name = p.Name
            }).ToList()
        };
    }

    protected override Task BeforeSaveAsync(IReadOnlyList<User> entities, List<UserModel> models)
    {
        ApplyPasswordHash(entities, models);
        ApplySerialNumber(entities, models);
        return base.BeforeSaveAsync(entities, models);
    }

    private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];

            if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
                model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
                model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
                model.Permissions.Any(p => p.IsDeleted || p.IsNew))
            {
                entity.SerialNumber = _manager.NewSerialNumber();
            }
            else
            {
                //prevent include SerialNumber in update query
                UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
            }
        }
    }

    private void ApplyPasswordHash(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
    {
        var i = 0;
        foreach (var entity in entities)
        {
            var model = models[i++];
            if (model.IsNew || !model.Password.IsEmpty())
            {
                entity.PasswordHash = _manager.HashPassword(model.Password);
            }
            else
            {
                //prevent include PasswordHash in update query
                UnitOfWork.Entry(entity).Property(a => a.PasswordHash).IsModified = false;
            }
        }
    }
}

در سناریوهای Master-Detail نیاز است متد دیگری تحت عنوان BuildFindQuery را نیز بازنویسی کنید. این متد برای بقیه حالات نیاز به بازنویسی نداشت؛ چرا که یک تک موجودیت واکشی می‌شد و خبری از موجودیت‌های Detail نبود. در اینجا لازم است تا روش تولید کوئری FindAsyn رو بازنویسی کنیم تا جزئیات دیگری را نیز واکشی کنیم. به عنوان مثال در اینجا Roles و Permissions کاربر نیز Include شده‌اند.

نکته: بازنویسی BuildFindQuery را شاید بتوان با روش‌های دیگری هم مانند تزئین موجودیت‌های وابسته با یک DetailOfAttribute و مشخص کردن نوع موجودیت اصلی، نیز جایگزین کرد.

متدهای MapToModel و MapToEntity هم به مانند قبل پیاده‌سازی شده‌اند. موضوع دیگری که در برخی از سناریوها پیش خواهد آمد، مربوط است به خصوصیتی که در زمان ثبت ضروری می‌باشد، ولی در زمان ویرایش اگر مقدار داشت باید با اطلاعات موجود در دیتابیس جایگزین شود؛ مانند Password و SerialNumber در موجودیت کاربر. برای این حالت می‌توان از متد BeforeSaveAsync بهره برد؛ به عنوان مثال برای SerialNumber:

private void ApplySerialNumber(IEnumerable<User> entities, IReadOnlyList<UserModel> models)
{
    var i = 0;
    foreach (var entity in entities)
    {
        var model = models[i++];

        if (model.IsNew || !model.IsActive || !model.Password.IsEmpty() ||
            model.Roles.Any(a => a.IsNew || a.IsDeleted) ||
            model.IgnoredPermissions.Any(p => p.IsDeleted || p.IsNew) ||
            model.Permissions.Any(p => p.IsDeleted || p.IsNew))
        {
            entity.SerialNumber = _manager.NewSerialNumber();
        }
        else
        {
            //prevent include SerialNumber in update query
            UnitOfWork.Entry(entity).Property(a => a.SerialNumber).IsModified = false;
        }
    }
}

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

نکته: متد BeforeSaveAsync دقیقا بعد از ردیابی شدن وهله‌های موجودیت توسط Context برنامه و دقیقا قبل از UnitOfWork.SaveChange فراخوانی خواهد شد.


برای بررسی بیشتر، پیشنهاد می‌کنم پروژه DNTFrameworkCore.TestAPI موجود در مخزن این زیرساخت را بازبینی کنید.
اشتراک‌ها
چالش 10 سال پیش برای سایت های معروف دنیا!

شاید دیدن تغییرات ظاهری سایت‌های معروف دنیا طی 10 سال گذشته برایتان جالب باشد

نکته ای که داخل همه شان دیده می‌شود، سادگی هست. 

ولی سوال اینجاست، آیا باید از ابتدا روی سادگی تمرکز کنیم؟ یا باید فرایند ریدیزاین، سیر رشد طبیعی خودش را طی کند؟ 

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

چالش 10 سال پیش برای سایت های معروف دنیا!
نظرات مطالب
Ajax.BeginForm و ارسال فایل به سرور در ASP.NET MVC

با سلام؛ در صورتی که قصد ذخیره سازی اطلاعات یک محصول را داشته باشم و یکی از Property‌های کلاس محصول ImageName باشد، باید نام عکس ارسالی را دریافت و دخیره کنیم، اکشن متد UpoadFiles نام عکس ذخیره شده را برگشت می‌دهد، حال به چه طریقی میتوان به این نام در اکشن متد Index دسترسی داشت؟ نام برگشتی از اکشن متد UploadFiles را در یک HiddenField قرار می‌دهم ولی باز هم در اکشن Index در دسترس نیست.

نظرات مطالب
UrlRewriter توسط Intelligencia.UrlRewriter
واقعا ممنونم. من دقیقا دنبال این یه خط می‌گشتم:
string.Format("Details/{0}-{1}", Eval("Name").ToString().Replace(" ", "-"),Eval("id")
که البته من اون P رو حذف کردم. اگه بخوام id رو هم نشون نده فقط همون اسم محصول رو نشون بده باید چی کار کنم؟ یا مثلا بخوام فیلدهای دیگه محصول رو هم نشون بده. 
مطالب
آشنایی با ساختار IIS قسمت یازدهم
در این مطلب و همینطور مطلب بعدی قرار است به مبحث لاگ فایل‌ها Logfile بپردازیم. همانطور که می‌دانید سیستم IIS مثل هر سیستم دیگری لاگ هایی دارد که به مرور زمان این لاگ‌ها میتوانند مقدار زیادی از ظرفیت دیسک سخت را به خود اختصاص بدهند و این عمل میتواند موجب بروز مشکلاتی در سرور شود. به خوبی به یاد دارم که برای یکی از مشتریانم VPS تهیه نموده بودیم و بعد از یک سال با من تماس گرفت که سایت بالا نمی‌آید و وقتی بررسی شد، دیدم که از فضای دیسک سخت چند گیگابایتی، تنها چند مگابایت به طور ناچیز فضا برایش باقی مانده است و باعث شده است سرور از کار بیفتد. پس از بررسی متوجه شدیم تمام این فضاها توسط لاگ فایل‌ها پر شده است و از آنجا که سرویس دهنده تا مبلغی را به عنوان مدیریت سرور، ماهانه دریافت نکند، مدیریت این سرور مجازی را به عهده نداشته اند که البته بعدها با انتقال به یک سرور دیگر از یک سرویس دهنده دیگر مشکلات ما در مورد سرور برای همیشه حل شد.
پس این داستان به خوبی روشن می‌کند که مدیریت این لاگ‌ها چقدر میتواند مهم و حیاتی باشد. آقای تیم وَن از تیم تحریریه مایکروسافت در بخش IIS موارد زیر را برای مدیریت لاگ‌ها بر می‌شمارد:
  • فعال سازی فشرده سازی
  • انتقال لاگ‌ها به یک سیستم راه دور
  • حذف لاگ فایل‌های قدیمی از طریق اسکریپت نویسی
  • حذف لاگ فایل‌های قدیمی توسط IIS Log File Cleaner

فشرده سازی دایرکتوری لاگ فایل ها
مسیر ذخیره لاگ فایل‌ها در آدرس زیر می‌باشد. به این آدرس رفته و Properties دایرکتوری مورد نظر  را باز کنید و در برگه‌ی General آن، گزینه‌ی Advanced را زده تا در کادر جدیدی که باز می‌شود، گزینه‌ی Compress contents to save disk space را انتخاب کنید تا محتویات در هنگام ذخیره روی دیسک سخت فشرده شوند.
%SystemDrive%\inetpub\logs\LogFiles

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


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

همانطور که در بالا اشاره کردیم محل پیش فرض ذخیره سازی لاگ‌ها درمسیر

%SystemDrive%\inetpub\logs\LogFiles

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

در IIS وب سایتی را که میخواهید لاگ آن انتقال یابد، انتخاب کنید؛ یا اگر لاگ کل سیستم IIS را میخواهید انتقال بدهید نام سرور را در لیست درختی انتخاب کنید و از ماژول‌های سمت راست، ماژول Logging را انتخاب کنید و در قسمت Directory که محل ذخیره سازی فعلی لاگ‌ها را نوشته شده است، به صورت UNC آدرس دهی کنید. در آدرس زیر اولی نام سرور است Contoso-server1\\ و دومی هم Logs نام پوشه‌ای که به اشتراک گذاشته شده است.


حذف لاگ فایل‌های قدیمی با استفاده از اسکریپت

با این روش میتوانید لاگ فایل هایی را که بعد از مدتی معین که دلخواه شما هست، از سیستم حذف نمایید و اگر این اسکریپت را زمان بندی خودکار نمایید، می‌توانید از مراقبت مداوم و ثابت این کار نیز رها شوید.

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

sLogFolder = "c:\inetpub\logs\LogFiles"
iMaxAge = 30   'in days
Set objFSO = CreateObject("Scripting.FileSystemObject")
set colFolder = objFSO.GetFolder(sLogFolder)
For Each colSubfolder in colFolder.SubFolders
        Set objFolder = objFSO.GetFolder(colSubfolder.Path)
        Set colFiles = objFolder.Files
        For Each objFile in colFiles
                iFileAge = now-objFile.DateCreated
                if iFileAge > (iMaxAge+1)  then
                        objFSO.deletefile objFile, True
                end if
        Next
Next

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

برای اجرای دستی اسکریپت در cmd تایپ کنید:

cscript.exe c:\scripts\retentionscript.vbs

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


زمان بندی اجرای اسکریپت

server manager (قابل تست در ویندوزهای سرور)  را باز کرده و از منوی Tools گزینه Task Scheduler را انتخاب کنید و در قسمت Actions گزینه Create Task را انتخاب نمایید. در کادر باز شده نام "Delete Log Files " را برای مثال برگزینید و در قسمت Security هم کاربری که اجازه اجرای اسکریپت را دارد مشخص کنید.

برگه Triggers را انتخاب کرده و گزینه New را انتخاب کنید و عملیات زمان بندی را تنظیم کنید و حتما بعد از زمان بندی مطمئن باشید که تیک Enabled فعال است.

در برگه Actions هم گزینه New را انتخاب کنید؛ در کادر باز شده از لیست Start a program را انتخاب کرده و در قسمت Program\script، دستور cscript را ذکر نمایید و به عنوان آرگومان ورودی Add arguments  هم مسیر اسکریپت خود را ذکر نمایید و کادر را تایید کنید.

برای آغاز زمان بندی در لیست وظیفه‌های فعال active task pane، وظیفه ای که الان ساخته اید را اجرا کرده و به مسیر ذخیره لاگ‌ها رفته و می‌بینید که لاگ‌های مورد نظر حذف شده‌اند؛ پس از صحت اجرای اسکریپت مطمئن می‌شویم. دوباره به لیست وظایف رفته و گزینه End را بزنید تا وظیفه، در حالت Ready قرار گیرد تا از همین الان فرایند زمان بندی اجرای اسکریپت آغاز شود.


حذف لاگ فایل‌ها با استفاده از IIS Log Cleaner Tools

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

ابتدا برنامه را از اینجا  دانلود کنید. موقعیکه برنامه را اجرا کنید، در نوتیفیکیشن taskbar می‌نشیند و برنامه با یک پیغام به شما اعلام می‌کند، این اولین بار است که برنامه را باز کرده‌اید. پس یک سر به setting آن بزنید؛ با انتخاب گزینه‌ی settings برنامه بسته شده و فایل Settings.txt برای شما باز می‌شود که مدت زمان عمر لاگ فایل و مسیر ذخیره آن‌ها، از شما پرسیده می‌شود که مقدار عمر هر لاگ فایل به طور پیش فرض 30 روز و مسیر ذخیره‌ی لاگ‌ها همان مسیر پیش فرض IIS است که اگر شما دستی آن را تغییر داده اید، با پرسیدن آن، از محل لاگ‌ها اطمینان کسب می‌کند. در صورتی که قصد تغییری را در فایل، دارید آن را تغییر داده و ذخیره کنید و برنامه را مجددا اجرا کنید.

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

  • اگر از ابزار IIS Cleaner Tool استفاده می‌کنید باید دستی سطل بازیافت را هم پاک کنید و هم اینکه میتوانید یک محدودیت حجمی برای Recycle Bin قرار دهید که اگر به یک حدی رسید، خودکار پاک کند تا مشکلی برای سیستم عامل ایجاد نشود که البته به طور پیش فرض چنین است.
  • برنامه بالا به طور پیش فرض ریشه‌ی لاگ‌ها را حذف می‌کند. پس اگر میخواهید فقط سایت خاصی را مد نظر داشته باشد، آدرس دایرکتوری آن را اضافه کنید. البته چون این برنامه فقط روی یک دایرکتوری کار می‌کند و شما چند وب سایت دارید و مثلا میخواهید سه تای آن‌ها را پاکسازی کنید، چاره‌ی جز استفاده از اسکریپت‌های با زمان بندی ندارید.
  • برنامه‌ی بالا فقط فایل هایی با پسوند log را به سطل بازیافت انتقال می‌دهد.
  • برنامه‌ی بالا یک سرویس نیست و باید به طور دستی توسط کاربر اجرا گردد. پس اگر ریست هم شد باید دستی اجرا شود یا آن را به داخل پوشه startup بکشید.
  • برنامه برای اجرایش نیاز به لاگین کاربر و مجوز نوشتن در آن پوشه را دارد تا به درستی کار کند.
در قسمت بعدی مبحث لاگ‌ها را ادامه خواهیم داد و با ماژول Logging در IIS و تنظیماتش آشنا خواهیم شد.
مطالب
بررسی دقیق‌تر صفحات آبی ویندوز

حدود یک سال قبل کامپیوتری را که داشتم (اینتل پنتیوم 4) به یک AMD دوهسته‌ای ارتقاء دادم و هفته‌ی اول پس از ارتقاء، روزگار من سیاه شد! روزهای اول 2 بار کرش ویندوز و مشاهده صفحه آبی و روزهای بعد تا 7 بار این اتفاق تکرار می‌شد. حتی تا تعویض مادربرد جدید هم پیش رفتم ولی تاثیری نداشت. تست رم و غیره هم انجام شد، مشکلی نبود. خلاصه اینجا بود که از سر ناچاری به این فکر افتادم که آیا این پیغام‌های صفحه‌ی آبی ویندوز را می‌شود تفسیر کرد؟ مشکل دقیقا از کجاست؟ چون در این موارد به هر کسی که مراجعه کنید بر اساس تجربه قبلی یک نسخه برای شما خواهد پیچید. رمت خرابه! بایوست رو ارتقاء بده! (این مورد تاثیر داشت! ولی تعداد کرش‌ها صفر نشد) مادربردت مشکل داره و ...

تمام این‌ها بر اساس تجربیات قبلی این افراد است و ارزشمند. ولی آیا این جواب‌ها قانع کننده هستند؟ چرا باید رم را عوض کرد؟ از کجا فهمیدید مادربرد مشکل داره؟



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

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



اکنون بعد از هر کرش و صفحه آبی ویندوز یک فایل دامپ در دایرکتوری C:\WINDOWS\Minidump تشکیل می‌شود. برای آنالیز این فایلها به صورت زیر می‌شود عمل کرد:
ابتدا برنامه زیر را دانلود کنید:
Debugging Tools for Windows

پس از نصب، Debugging Tools for Windows را خواهید داشت که جهت دیباگ کردن سیستم و آنالیز فایلهای دامپ و غیره کاربرد دارد.

سپس مطالعه مقاله زیر در مورد نحوه استفاده از این ابزار بسیار مفید است:
http://support.microsoft.com/kb/315263

به صورت خلاصه :
یک فایل bat درست کنید با محتویات زیر و دقیقا به همین شکل:
c:\windbg\kd -y srv*c:\symbols*http://msdl.microsoft.com/download/symbols -i c:\windows\i386 -z %1


در این دستور سه مورد قابل ملاحظه است:
الف) مسیر فایل kd.exe که توسط پکیج Debugging Tools for Windows نصب می‌شود. (مطابق سیستم خودتان آنرا اصلاح کنید)
ب) مسیر c:\windows\i386 بدین معنا است که دایرکتوری i386 سی دی ویندوز را در این مسیر کپی کرده‌اید یا خواهید کرد (نیاز به یک ویندوز تر و تازه و نصب نشده خواهد بود).
ج) مسیر c:\symbols خودبخود ایجاد خواهد شد و فایلهای مربوطه از سایت مایکروسافت توسط برنامه kd.exe دانلود می‌شود (بنابراین باید دسترسی به اینترنت نیز داشت).

فرض کنید نام این فایل را test.bat گذاشته‌اید.
برای آنالیز فایل Mini102607-07.dmp در دایرکتوری مینی دامپ ویندوز (07 در اینجا یعنی هفتمین کرش روز مربوطه!) دستور زیر را در خط فرمان صادر کنید:
test.bat C:\WINDOWS\Minidump\Mini102607-07.dmp
پس از مدتی این برنامه کار آنالیز را تمام خواهد کرد و گزارشی را ارائه می‌دهد (یک فایل log متنی تشکیل خواهد شد).
نتیجه یک نمونه از این آنالیزهای سیستم من به صورت زیر بود:
BAD_POOL_CALLER (c2)
The current thread is making a bad pool request. Typically this is at a bad IRQL level or double freeing the same allocation, etc.
Arguments:
Arg1: 00000007, Attempt to free pool which was already freed
Arg2: 00000cd4, (reserved)
Arg3: 02060008, Memory contents of the pool block
Arg4: 88b4a118, Address of the block of pool being deallocated

Debugging Details:
------------------

POOL_ADDRESS: 88b4a118

FREED_POOL_TAG: TCPc

BUGCHECK_STR: 0xc2_7_TCPc

CUSTOMER_CRASH_COUNT: 4

DEFAULT_BUCKET_ID: COMMON_SYSTEM_FAULT

PROCESS_NAME: System

LAST_CONTROL_TRANSFER: from 8054a583 to 804f9deb

STACK_TEXT:
ba4f3874 8054a583 000000c2 00000007 00000cd4 nt!KeBugCheckEx+0x1b
ba4f38c4 b043d3ff 88b4a118 00000000 ba4f390c nt!ExFreePoolWithTag+0x2a3
ba4f38d4 b043cca3 883ae760 883ae7f4 883ae7f4 tcpip!TCPClose+0x16
ba4f390c b02f3161 8a74fe20 883ae760 b02f2a6d tcpip!TCPDispatch+0x101
WARNING: Stack unwind information not available. Following frames may be wrong.
ba4f3984 b03e2046 00000001 00000000 ba4f39d8 vsdatant+0x45161
ba4f39d8 b03e921c 00000008 ba4f3aac 00000000 ipnat!NatpRedirectQueryHandler+0x250
ba4f3a70 00000000 8837d8e8 0000000d 000005ee ipnat!NatpDirectPacket+0xd2

STACK_COMMAND: kb

FOLLOWUP_IP:
vsdatant+45161
b02f3161 ?? ???

SYMBOL_STACK_INDEX: 4

SYMBOL_NAME: vsdatant+45161

FOLLOWUP_NAME: MachineOwner

MODULE_NAME: vsdatant

IMAGE_NAME: vsdatant.sys

DEBUG_FLR_IMAGE_TIMESTAMP: 46e0766a

FAILURE_BUCKET_ID: 0xc2_7_TCPc_vsdatant+45161

BUCKET_ID: 0xc2_7_TCPc_vsdatant+45161

Followup: MachineOwner

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

خوب! تا اینجا مشخص شد که دلیل کرش، درایور vsdatant.sys است. با جستجو در اینترنت مشخص شد که این درایور مربوط به فایروال زون آلارم است! (همین عبارت بالا یا نام درایور ذکر شده را مستقیما در گوگل جستجو کنید)
پس از آن زون آلارم را با outpost firewall جایگزین کردم و تا الان کرشی حاصل نشده است (حتی یکبار از سال قبل تا به امروز). جدا زندگی من مختل شده بود. تصور کنید سیستم شما روزی 7 بار کرش کند!! و چه تصورات نامربوطی را نسبت به فروشنده سخت افزار در ذهن خود مرور کرده باشید!

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

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