مطالب
آشنایی با ساختار IIS قسمت دوازدهم
پیکربندی قسمت لاگ‌ها، میتواند برای یک سرور و یا وب سایت خاص از طریق فایل کانفیگ یا از طریق خود IIS انجام گیرد. برای اینکه به بیشتر این قابلیت‌ها در IIS دسترسی داشت، باید یکی از نسخه‌های ویندوز سرور 2012 و ویندوز 8 را نصب کرده باشید. لاگ‌ها به ثبت خطاها و درخواست‌های HTTP می‌پردازند و با تحلیل آن‌ها میتوان عملیات بهینه سازی را بر روی سرو اجرا کرد. تمامی ثبت لاگ‌ها توسط Http.sys انجام می‌گیرد.

نحوه‌ی ذخیره سازی لاگها
در این بخش نحوه‌ی ذخیره سازی و فرمت ذخیره‌ی لاگ‌ها را در دو سطح سایت و سرور به طور جداگانه بررسی می‌کنیم. در IIS ماژول Logging را باز کنید و در لیست One log file per می‌توانید مشخص کنید که لاگ‌ها در چه سطحی اجرا شوند. اگر گزینه‌ی server باشد، تمامی خطاها و درخواست‌های رسیده به سرور در یک فایل لاگ ثبت می‌شوند. ولی اگر سطح سایت باشد، برای هر سایت بر روی IIS لاگ‌ها، جداگانه بررسی می‌شوند. به طور پیش فرض سطح سایت انتخاب شده است.

سطح سایت
موقعی‌که در لیست، سایت را انتخاب کنید، در لیست format می‌توانید تعیین کنید که لاگ‌ها به چه صورتی باید ذخیره شوند. مواردی که در این حالت لیست می‌شوند گزینه‌های W3C,IIS,NCSA,Custom می‌باشند که در زیر یکایک آن‌ها را بررسی می‌کنیم:

فرمت IIS: این فرمت توسط مایکروسافت ارائه شده و در این حالت لاگ‌های همه‌ی وب سایت‌ها ذخیره می‌شوند. به این فرمت  Fixed ASCII Based Text نیز می‌گویند؛ چرا که اجازه‌ی خصوصی سازی ندارد و نمی‌توانید بگویید چه فیلدهایی در لاگ قرار داشته باشند. لاگ فایل‌های این فرمت با ، (کاما) از هم جدا می‌شوند و مقدار زمانی که برای هر فیلد ثبت می‌شود، به صورت محلی local Time می‌باشد.
فیلدهایی که در لاگ این نوع فرمت خواهند آمد، به شرح زیر است:
  • Client IP address 
  • User name 
  • Date 
  • Time 
  • Service and instance 
  • Server name 
  • Server IP address 
  • Time taken 
  • Client bytes sent 
  • Server bytes sent 
  • (Service status code (A value of 200 indicates that the request was fulfilled successfully 
  • (Windows status code (A value of 0 indicates that the request was fulfilled successfully. 
  • Request type 
  • Target of operation 
  • (Parameters (the parameters that are passed to a script 
احتمال این وجود دارد که بعضی از فیلدها در بعضی رکوردها، شامل اطلاعاتی نباشند که به جای مقدار آن علامت -  ثبت می‌گردد و برای کاراکترهایی که قابل نمایش نیستند یا کاراکتر نمایشی ندارند، از علامت + استفاده می‌شود. دلیل اینکار هم این است که ممکن است یک کاربر مهاجم، به ارسال اطلاعات کلیدهای کنترلی چون Carriage return اختصارا CR یا Line Feed به اختصار LF کند، که باعث شکسته شدن خط لاگ فایل می‌شود و در نتیجه از استاندارد خارج خواهد شد و هنگام خواندن آن هم با خطا روبرو می‌شویم؛ در نتیجه با جایگزینی چنین کاراکترهایی با + از این اتفاق جلوگیری می‌شود.
شکل زیر نمونه ای از یک خط لاگ در این فرمت است:
192.168.114.201, -, 03/20/01, 7:55:20, W3SVC2, SERVER, 172.21.13.45, 4502, 163, 3223, 200, 0, GET, /DeptLogo.gif, -,
 نام فیلد نوع حالت مقداردهی  توضیح اتفاقات افتاده
 Client IP address   192.168.114.201   آی پی کلاینت
 User name   -  کاربر ناشناس است
 Date  03/20/01   تاریخ فعالیت
 Time  7:55:20  ساعت فعالیت 
 Service and instance  W3SVC2  لاگی که مربوط به سایت خاصی می‌شود به صورت  #W3SVC  نمایش داده می‌شود که علامت # شماره سایت می‌باشد که در اینجا این لاگ مربوط به سایت شماره 2 است 
 Server name   SERVER   نام سرور
 Server IP   172.21.13.45   آی پی سرور
 Time taken  4502   چقدر انجام عملیات این درخواست به طول انجامیده است که بر حسب میلی ثانیه است.
 Client bytes sent   163   تعداد بایت هایی که از طرف کلاینت به سرور ارسال شده است
 Server bytes sent   3223   تعداد بایت هایی که از طرف سرور به سمت کلاینت ارسال شده است
 Service status code   200   درخواست کاملا موفقیت آمیز بوده است
 Windows status code  0  درخواست کاملا موفقیت آمیز بوده است
 Request type  GET   نوع درخواست کاربر
 Target of operation   /DeptLogo.gif   کاربر قصد دانلود یک فایل تصویری GIF داشته است که نامش Deptlogo است
 Parameters   -  پارامتری ارسال نشده است

فرمت NCSA:
 این فرمت توسط مرکز علمی کاربردهای ابرمحاسباتی National Center for Supercomputing Applications ایجاد شده و دقیقا مانند قبلی نمیتوان در آن نوع فیلدها را مشخص کرد و برای جدا سازی، از فاصله space استفاده می‌کند و ثبت مقدار زمان در آن هم به صورت محلی و هم UTC می‌باشد.
این فیلدها در لاگ آن نمایش داده می‌شوند:
  • Remote host address 
  • (Remote log name (This value is always a hyphen 
  • User name 
  • Date, time, and Greenwich mean time (GMT) offset 
  • Request and protocol version 
  • (Service status code (A value of 200 indicates that the request was fulfilled successfully   
  • Bytes sen 

نمونه ای از یک لاگ ثبت شده:
172.21.13.45 - Microsoft\JohnDoe [08/Apr/2001:17:39:04 -0800] "GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0" 200 3401
نام فیلد  مقدار ثبت شده  توضیح اتفاق افتاده 
 Remote host address  172.21.13.45  آی پی کلاینت
 Remote log name  - نامی وجود ندارد 
 User name  Microsoft\JohnDoe  نام کاربری
 Date, time, and GMT offset  [08/Apr/2001:17:39:04 -0800]  تاریخ و ساعت فعالیت به صورت محلی که 8 ساعت از مبدا گرینویچ بیشتر است
 Request and protocol version  GET /scripts/iisadmin/ism.dll?http/serv HTTP/1.0  کاربر با متد GET و Http نسخه‌ی یک، درخواست فایل ism.dll را کرده است.
 Service status code  200  عملیات کاملا موفقیت آمیز بود.
 Bytes sent  3401  تعداد بایت‌های ارسال شده به سمت کاربر

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


فرمت W3C: توسط W3C توسط کنسرسیوم جهانی وب ارائه شده است و یک فرمت customizable ASCII text-based است. به این معنی که میتوان فیلدهایی که در گزارش نهایی می‌آید را خودتان مشخص کنید، که برای اینکار در کنار لیست، دکمه‌ی Select وجود دارد که میتوانید هر کدام از فیلد‌هایی را که خواستید، انتخاب کنید تا به ترتیب در خط لاگ ظاهر شوند. تاریخ ثبت به صورت UTC است.

نام فیلد   توضیح به طور پیش فرض انتخاب شده است 
 Date  تاریخ رخ دادن فعالیت  بله
 Time  ساعت زخ دادن فعالیت بر اساس UTC  بله
 Client IP Address آی پی کلاینت  بله
 User Name نام کاربری که هویت آن تایید شده و در صورتی که هویت تایید شده نباشد و کاربر ناشناس باشد، جای آن - قرار می‌گیرد   بله
 Service Name and Instance Number  نام و شماره سایتی که درخواست در آن صورت گرفته است  خیر
 Server Name  نام سروری که لاگ روی آن ثبت می‌شود  خیر
 Server IP Address  آی پی سرور که لاگ روی آن ثبت می‌شود  بله
 Server Port  شمره پورتی که سرویس مورد نظر روی آن پورت اعمال می‌شود.  بله
 Method  متد درخواست مثل GET  بله
 URI Stem   هدف درخواست یا Target مثل index.htm  بله
 URI Query  کوئری ارسال شده برای صفحات داینامیک  بله
 HTTP Status  کد وضیعینی HTTP status  بله
 Win32 Status  کد وضعیتی ویندوز  خیر
 Bytes Sent  تعداد بایت‌های ارسال شده به سمت کلاینت  خیر
 Bytes Received  تعداد بایت‌های دریافت شده از سمت کلاینت  خیر
 Time Taken  زمان به طول انجامیدن درخواست بر حسب میلی ثانیه  خیر
 Protocol Version درخواست با چه نسخه‌ای از پروتکل http یا ftp ارسال شده است  خیر
 Host  اگر در هدر درخواست ارسالی این گزینه بوده باشد، نوشته خواهد شد.  خیر
 User Agent  اطلاعات را از هدر درخواست می‌گیرد.  بله
 Cookie اگر کوکی رد و بدل شده باشد، محتویات کوکی ارسالی یا دریافت شده  خیر
 Referrer  کاربر از چه سایتی به سمت سایت ما آمده است.  خیر
 Protocol Substatus 
 در صورت رخ دادن خطا در IIS ، کد خطا بازگردانده میشود. در IIS به منظور امنیت بیشتر و کاهش حملات، محتوای خطاهای رخ داده در IIS به صورت متنی نمایش داده نمی‌شوند و شامل کد خطایی به اسم Substatus Code هستند تا مدیران شبکه با ردیابی لاگ‌ها پی به دلیل خطا و درخواست‌های ناموفق ببرند. برای مثال Error 404.2 به این معنی است که فایل درخواستی به دلیل قوانین محدود کنده، قفل شده و قابل ارائه نیست. ولی هکر تنها با خطای 404 یعنی وجود نداشتن فایل روبرو می‌شود. در حالت substatus code، کد شماره 2 را هم خواهید داشت که در لاگ ثبت می‌شود.
هر شخصی که در سرور توانایی دسترسی به لاگ‌ها را داشته باشد، می‌تواند کد دوم خطا را نیز مشاهده کند. برای مثال مدیر سرور متوجه میشود که یکی از فایل‌های مورد نظر به کاربران، خطای 404 نمایش میدهد و با بررسی لاگ‌ها متوجه می‌شود که کد خطا 404.9 هست. از آنجا که ما همه‌ی کدها را حفظ نیستیم به این صفحه رجوع می‌کنیم و متوجه میشویم تعداد کاربرانی که برای این فایل، اتصال connection ایجاد کرده‌اند بیش از مقدار مجاز است و مدیر میتواند این وضع را کنترل کند. برای مثال تعداد اتصالات مجاز را نامحدود unlimited تعیین کند.
 بله
حروف - و + برای موارد بالا هم صدق می‌کند. در ضمن گزینه‌های زیر در حالتی که درخواست از پروتکل FTP باشد مقداری نخواهند گرفت:
  • uri-query
  • host
  • (User-Agent)
  • Cookie
  • Referrer
  • substatus

گزینه Custom
 : موقعی که شما این گزینه را انتخاب کنید ماژول logging غیرفعال خواهد شد. زیرا این امکان در IIS قابل پیکربندی نیست و نوشتن ماژول آن بر عهده شما خواهد بود؛ با استفاده از اینترفیس های ILogPlugin ، ILogPluginEx و  ILogUIPlugin  آن را پیاده سازی کنید.

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

ادامه‌ی تنظیمات
موارد بعدی که در تنظیمات لاگ‌ها کاملا مشخص و واضح است، عملیات زمان بندی  است که برای ساخت یک فایل لاگ جدید به کار می‌رود؛ برای مثال هر ساعت یک لاگ فایل جدید بسازد و فعالیت‌های موجود در هر ساعت در یک لاگ ذخیره می‌شوند.
گزینه‌ی بعدی حداکثر حجم هر فایل لاگ است که به صورت بایت مشخص می‌شود. اگر مقداری که تعیین میکنید کمتر از 1048576 بایت باشد، خودش به طور پیش فرض همان 1048576 بایت را در نظر خواهد گرفت.
گزینه بعدی do not create a new logfile بدین معناست که همه‌ی لاگ‌ها در یک فایل ذخیره می‌شوند و فایل جدیدی برای لاگ‌ها ایجاد نمی‌شود.
گزینه آخری به اسم use local time for filenaming and rollover است که اگر انتخاب شود، نامگذاری هر فایل لاگ بر اساس زمان محلی ساخت فایل لاگ خواهد بود. در صورتیکه انتخاب نشود، نامگذاری با زمان  UTC درج خواهد شد.

سطح سرور
لاگ‌ها فقط در سمت سرور انجام می‌گیرد و لاگ هر سایت در یک فایل لاگ ثبت می‌شود. اگر بخواهید لاگ‌ها را در سطح سرور انجام دهید، گزینه‌ی binary هم اضافه خواهد شد.
Binary: در این گزینه دیگر از قالب بندی یا فرمت بندی لاگ‌ها خبری نیست و لاگ هر وب سایت به صورت اختصاصی صورت نمی‌گیرد. عملیات ذخیره سازی و ثبت هر لاگ می‌تواند از منابع یک سرور از قبیل حافظه و CPU و ... استفاده کند و اگر تعداد این وب سایت‌ها بالا باشد، باقی روش‌ها باعث فشار به سرور می‌شوند. برای همین ایجاد یک فایل خام از لاگ‌ها در این مواقع می‌تواند راهگشا باشد. برای همه یک فایل لاگ ایجاد شده  و بدون قالب بندی ذخیره می‌کند. پسوند این نوع لاگ‌ها ibl است که مخفف Internet Binary Log می‌باشد. دلیل این تغییر پسوند این است که اطمینان کسب شود کاربر، با برنامه‌های متنی چون notepad یا امثال آن که به Text Utilities معروفند فایل را باز نمی‌کند. برای خواندن این فایل‌های میتوان از برنامه‌ی Log parser استفاده کرد. پروتکل‌های FTP,NNTP و SMTP در این حالت لاگشان ثبت نمی‌شود.
مطالب دوره‌ها
تبدیلگر تاریخ شمسی برای AutoMapper
فرض کنید مدل معادل با جدول بانک اطلاعاتی ما چنین ساختاری را دارد:
public class User
{
    public int Id { set; get; }
    public string Name { set; get; }
    public DateTime RegistrationDate { set; get; }
}
و ViewModel ایی که قرار است به کاربر نمایش داده شود این ساختار را دارد:
public class UserViewModel
{
    public int Id { set; get; }
    public string Name { set; get; }
    public string RegistrationDate { set; get; }
}
در اینجا می‌خواهیم حین تبدیل User به UserViewModel، تاریخ میلادی به صورت خودکار، تبدیل به یک رشته‌ی شمسی شود. برای مدیریت یک چنین سناریوهایی توسط AutoMapper، امکان نوشتن تبدیلگرهای سفارشی نیز پیش بینی شده‌است.


تبدیلگر سفارشی تاریخ میلادی به شمسی مخصوص AutoMapper

در ذیل یک تبدیلگر سفارشی مخصوص AutoMapper را با پیاده سازی اینترفیس ITypeConverter آن ملاحظه می‌کنید:
public class DateTimeToPersianDateTimeConverter : ITypeConverter<DateTime, string>
{
    private readonly string _separator;
    private readonly bool _includeHourMinute;
 
    public DateTimeToPersianDateTimeConverter(string separator = "/", bool includeHourMinute = true)
    {
        _separator = separator;
        _includeHourMinute = includeHourMinute;
    }
 
    public string Convert(ResolutionContext context)
    {
        var objDateTime = context.SourceValue;
        return objDateTime == null ? string.Empty : toShamsiDateTime((DateTime)context.SourceValue);
    }
 
    private string toShamsiDateTime(DateTime info)
    {
        var year = info.Year;
        var month = info.Month;
        var day = info.Day;
        var persianCalendar = new PersianCalendar();
        var pYear = persianCalendar.GetYear(new DateTime(year, month, day, new GregorianCalendar()));
        var pMonth = persianCalendar.GetMonth(new DateTime(year, month, day, new GregorianCalendar()));
        var pDay = persianCalendar.GetDayOfMonth(new DateTime(year, month, day, new GregorianCalendar()));
        return _includeHourMinute ?
            string.Format("{0}{1}{2}{1}{3} {4}:{5}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture), info.Hour.ToString("00"), info.Minute.ToString("00"))
            : string.Format("{0}{1}{2}{1}{3}", pYear, _separator, pMonth.ToString("00", CultureInfo.InvariantCulture), pDay.ToString("00", CultureInfo.InvariantCulture));
    } 
}
ITypeConverter دو پارامتر جنریک را قبول می‌کند. پارامتر اول نوع ورودی و پارامتر دوم، نوع خروجی مورد انتظار است. در اینجا باید خروجی متد Convert را بر اساس آرگومان دوم ITypeConverter مشخص کرد. توسط ResolutionContext می‌توان به برای مثال context.SourceValue که معادل DateTime دریافتی است، دسترسی یافت. سپس این DateTime را بر اساس متد toShamsiDateTime تبدیل کرده و بازگشت می‌دهیم.


ثبت و معرفی تبدیلگرهای سفارشی AutoMapper

پس از تعریف یک تبدیلگر سفارشی AutoMapper، اکنون نیاز است آن‌را به AutoMapper معرفی کنیم:
public class TestProfile1 : Profile
{
    protected override void Configure()
    {
        // این تنظیم سراسری هست و به تمام خواص زمانی اعمال می‌شود
        this.CreateMap<DateTime, string>().ConvertUsing(new DateTimeToPersianDateTimeConverter()); 
        this.CreateMap<User, UserViewModel>();
     }
 
    public override string ProfileName
    {
        get { return this.GetType().Name; }
    }
}
جهت مدیریت بهتر نگاشت‌های AutoMapper ابتدا یک کلاس Profile را آغاز خواهیم کرد و سپس توسط متدهای CreateMap، کار معرفی نگاشت‌ها را آغاز می‌کنیم.
همانطور که مشاهده می‌کنید در اینجا دو نگاشت تعریف شده‌اند. یکی برای تبدیل User به UserViewModel و دیگری، معرفی نحوه‌ی نگاشت DateTime به string، توسط تبدیلگر سفارشی DateTimeToPersianDateTimeConverter است که به کمک متد الحاقی ConvertUsing صورت گرفته‌است.
باید دقت داشت که تنظیمات تبدیلگرهای سفارشی سراسری هستند و در کل برنامه و به تمام پروفایل‌ها اعمال می‌شوند.


بررسی خروجی تبدیلگر سفارشی تاریخ

اکنون کار استفاده از تنظیمات AutoMapper با ثبت پروفایل تعریف شده آغاز می‌شود:
Mapper.Initialize(cfg => // In Application_Start()
{
     cfg.AddProfile<TestProfile1>();
});
سپس نحوه‌ی استفاده از متد Mapper.Map همانند قبل خواهد بود:
var dbUser1 = new User
{
    Id = 1,
    Name = "Test",
    RegistrationDate = DateTime.Now.AddDays(-10)
};
 
var uiUser = new UserViewModel();

Mapper.Map(source: dbUser1, destination: uiUser);
در اینجا در حین کار تبدیل و نگاشت dbUser به uiUser، زمانیکه AutoMapper به هر خاصیت DateTime ایی می‌رسد، مقدار آن‌را با توجه به تبدیلگر سفارشی تاریخی که به آن معرفی کردیم، تبدیل به معادل رشته‌ای شمسی می‌کند.


نوشتن تبدیلگرهای غیر سراسری

همانطور که عنوان شد، معرفی تبدیلگرها به AutoMapper سراسری است و در کل برنامه اعمال می‌شود. اگر نیاز است فقط برای یک مدل خاص و یک خاصیت خاص آن تبدیلگر نوشته شود، باید نگاشت مورد نظر را به صورت ذیل تعریف کرد:
this.CreateMap<User, UserViewModel>()
             .ForMember(userViewModel => userViewModel.RegistrationDate,
                        opt => opt.ResolveUsing(src =>
                        {
                             var dt = src.RegistrationDate;
                             return dt.ToShortDateString();
                        }));
اینبار در همان کلاس پروفایل ابتدای بحث، نگاشت User به ViewModel آن با کمک متد ForMember، سفارشی سازی شده‌است. در اینجا عنوان شده‌است که اگر به خاصیت ویژه‌ی RegistrationDate رسیدی، مقدار آن‌را با توجه به فرمولی که مشخص شده، محاسبه کرده و بازگشت بده. این تنظیم خصوصی است و به کل برنامه اعمال نمی‌شود.


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

اگر می‌خواهید تنظیمات TestProfile1 به کل برنامه اعمال نشود، نیاز است یک MappingEngine جدید و مجزای از MappingEngine سراسری AutoMapper را ایجاد کرد:
var configurationStore = new ConfigurationStore(new TypeMapFactory(), MapperRegistry.Mappers);
configurationStore.AddProfile<TestProfile1>();
var mapper = new MappingEngine(configurationStore);
mapper.Map(source: dbUser1, destination: uiUser);
به صورت پیش فرض و در پشت صحنه، متد Mapper.Map از یک MappingEngine سراسری استفاده می‌کند. اما می‌توان در یک برنامه چندین MappingEngine مجزا داشت که نمونه‌ای از آن‌را در اینجا مشاهده می‌کنید.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید:
AM_Sample02.zip
نظرات مطالب
ASP.NET MVC #11
با سلام 
هنگامی که از ViewModel‌ها برای ساختن یک view که ترکیبی از چند DomainModel هست استفاده میکنیم، چگونه باید بین فیلدهای درون یک ViewModel با فیلد‌های DomainModel‌ها تناظر یک به یک بر قرار کرد.
در چند مثال دیدم که از یک تابع جداگانه برای نگاشت بین اطلاعات این کلاس‌ها  استفاده میکردند ولی نحوه این نگاشت رو توضیح نداده بودند (مانند http://blogs.msdn.com/b/simonince/archive/2010/01/26/view-models-in-asp-net-mvc.aspx )
مطالب
شمسی سازی Date-Picker توکار Angular Material 6x
Angular Material به همراه یک کامپوننت Date-Picker بسیار شکیل و حرفه‌ای است اما ... از تقویم شمسی پشتیبانی نمی‌کند. در این مطلب می‌خواهیم با تدارک یک DateAdapter سفارشی، این مشکل را برطرف کنیم تا در نهایت به یک چنین Date-Picker شمسی برسیم:



تاریخچه‌ی تغییرات کامپوننت Date-Picker

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

DateAdapter شمسی تهیه شده از کتابخانه‌ی jalali-moment برای تبدیل تاریخ‌ها استفاده می‌کند. بنابراین ابتدا نیاز است این وابستگی را نصب کرد:
 npm install jalali-moment --save


افزودن DateAdapter شمسی به پروژه

برای افزودن DateAdapter شمسی تهیه شده، فایل جدید app\shared\material.persian-date.adapter.ts را به برنامه اضافه کرده و به صورت زیر تکمیل کنید:
import { DateAdapter } from "@angular/material";
import * as jalaliMoment from "jalali-moment";

export const PERSIAN_DATE_FORMATS = {
  parse: {
    dateInput: "jYYYY/jMM/jDD"
  },
  display: {
    dateInput: "jYYYY/jMM/jDD",
    monthYearLabel: "jYYYY jMMMM",
    dateA11yLabel: "jYYYY/jMM/jDD",
    monthYearA11yLabel: "jYYYY jMMMM"
  }
};

export class MaterialPersianDateAdapter extends DateAdapter<jalaliMoment.Moment> {

  constructor() {
    super();
    super.setLocale("fa");
  }

  getYear(date: jalaliMoment.Moment): number {
    return this.clone(date).jYear();
  }

  getMonth(date: jalaliMoment.Moment): number {
    return this.clone(date).jMonth();
  }

  getDate(date: jalaliMoment.Moment): number {
    return this.clone(date).jDate();
  }

  getDayOfWeek(date: jalaliMoment.Moment): number {
    return this.clone(date).day();
  }

  getMonthNames(style: "long" | "short" | "narrow"): string[] {
    switch (style) {
      case "long":
      case "short":
        return jalaliMoment.localeData("fa").jMonths().slice(0);
      case "narrow":
        return jalaliMoment.localeData("fa").jMonthsShort().slice(0);
    }
  }

  getDateNames(): string[] {
    const valuesArray = Array(31);
    for (let i = 0; i < 31; i++) {
      valuesArray[i] = String(i + 1);
    }
    return valuesArray;
  }

  getDayOfWeekNames(style: "long" | "short" | "narrow"): string[] {
    switch (style) {
      case "long":
        return jalaliMoment.localeData("fa").weekdays().slice(0);
      case "short":
        return jalaliMoment.localeData("fa").weekdaysShort().slice(0);
      case "narrow":
        return ["ی", "د", "س", "چ", "پ", "ج", "ش"];
    }
  }

  getYearName(date: jalaliMoment.Moment): string {
    return this.clone(date).jYear().toString();
  }

  getFirstDayOfWeek(): number {
    return jalaliMoment.localeData("fa").firstDayOfWeek();
  }

  getNumDaysInMonth(date: jalaliMoment.Moment): number {
    return this.clone(date).jDaysInMonth();
  }

  clone(date: jalaliMoment.Moment): jalaliMoment.Moment {
    return date.clone().locale("fa");
  }

  createDate(year: number, month: number, date: number): jalaliMoment.Moment {
    if (month < 0 || month > 11) {
      throw Error(
        `Invalid month index "${month}". Month index has to be between 0 and 11.`
      );
    }
    if (date < 1) {
      throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
    }
    const result = jalaliMoment()
      .jYear(year).jMonth(month).jDate(date)
      .hours(0).minutes(0).seconds(0).milliseconds(0)
      .locale("fa");

    if (this.getMonth(result) !== month) {
      throw Error(`Invalid date ${date} for month with index ${month}.`);
    }
    if (!result.isValid()) {
      throw Error(`Invalid date "${date}" for month with index "${month}".`);
    }
    return result;
  }

  today(): jalaliMoment.Moment {
    return jalaliMoment().locale("fa");
  }

  parse(value: any, parseFormat: string | string[]): jalaliMoment.Moment | null {
    if (value && typeof value === "string") {
      return jalaliMoment(value, parseFormat, "fa");
    }
    return value ? jalaliMoment(value).locale("fa") : null;
  }

  format(date: jalaliMoment.Moment, displayFormat: string): string {
    date = this.clone(date);
    if (!this.isValid(date)) {
      throw Error("JalaliMomentDateAdapter: Cannot format invalid date.");
    }
    return date.format(displayFormat);
  }

  addCalendarYears(date: jalaliMoment.Moment, years: number): jalaliMoment.Moment {
    return this.clone(date).add(years, "jYear");
  }

  addCalendarMonths(date: jalaliMoment.Moment, months: number): jalaliMoment.Moment {
    return this.clone(date).add(months, "jmonth");
  }

  addCalendarDays(date: jalaliMoment.Moment, days: number): jalaliMoment.Moment {
    return this.clone(date).add(days, "jDay");
  }

  toIso8601(date: jalaliMoment.Moment): string {
    return this.clone(date).format();
  }

  isDateInstance(obj: any): boolean {
    return jalaliMoment.isMoment(obj);
  }

  isValid(date: jalaliMoment.Moment): boolean {
    return this.clone(date).isValid();
  }

  invalid(): jalaliMoment.Moment {
    return jalaliMoment.invalid();
  }

  deserialize(value: any): jalaliMoment.Moment | null {
    let date;
    if (value instanceof Date) {
      date = jalaliMoment(value);
    }
    if (typeof value === "string") {
      if (!value) {
        return null;
      }
      date = jalaliMoment(value).locale("fa");
    }
    if (date && this.isValid(date)) {
      return date;
    }
    return super.deserialize(value);
  }
}
کار این Adapter و یا «وفق دهنده» این است که مشخص می‌کند، هفته‌ی ایرانی از چه روزی شروع می‌شود. نام روزهای هفته‌ی ایرانی چیست؟ برچسب‌های نام ماه‌های ایرانی چگونه باید تامین شوند و در کل جهت وفق دادن تقویم میلادی اصلی با تقویم شمسی، چه اجزایی باید به سیستم معرفی شوند تا این تقویم توکار بدون مشکل مانند قبل کار کند.
 
معرفی وفق دهنده‌ی شمسی به پروژه

پس از تعریف MaterialPersianDateAdapter و همچنین PERSIAN_DATE_FORMATS، برای معرفی آن‌ها به برنامه، فایل app\shared\material.module.ts را گشوده و به صورت زیر تغییر دهید:
import { NgModule } from "@angular/core";
import {  DateAdapter,  MAT_DATE_FORMATS,  MAT_DATE_LOCALE } from "@angular/material";

import { MaterialPersianDateAdapter, PERSIAN_DATE_FORMATS } from "./material.persian-date.adapter";

@NgModule({
  providers: [
    { provide: DateAdapter, useClass: MaterialPersianDateAdapter, deps: [MAT_DATE_LOCALE] },
    { provide: MAT_DATE_FORMATS, useValue: PERSIAN_DATE_FORMATS }
  ]
})
export class MaterialModule {
}
کار این تعاریف، تعویض DateAdapter اصلی میلادی، با نمونه‌ی شمسی است. همچنین فرمت نمایشی برچسب‌ها را نیز جایگزین می‌کند.

پس از آن اگر mat-datepicker را به نحو متداولی به صفحه اضافه کنیم:
<mat-form-field>
    <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl">
    <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle>
    <mat-datepicker #picker6></mat-datepicker>
</mat-form-field>
یک چنین خروجی حاصل خواهد شد:




چند مثال تکمیلی از کاربردهای کامپوننت mat-datepicker

1) استفاده از تاریخ میلادی رسیده‌ی از سمت سرور و نمایش آن
<mat-form-field>
    <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl">
    <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle>
    <mat-datepicker #picker6></mat-datepicker>
</mat-form-field>
با این کدها:
@Component()
export class PersianDatepickerComponent {

  jsonDate = "2018-01-08T20:21:29.4674496";
  dateControl = this.jsonDate;
}
در اینجا jsonDate همان رشته‌ی تاریخی است که از سمت سرور دریافت شده و میلادی است. با انتساب آن به ngModel، به صورت خودکار شمسی نمایش داده خواهد شد:




2) تعیین تاریخ آغاز تقویم و نمایش آن در حالت انتخاب سال
<mat-form-field>
    <input matInput [matDatepicker]="picker2" placeholder="startAt 2017-01-01 and startView=year">
    <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
    <mat-datepicker #picker2 startView="year" [startAt]="startDate"></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent  {

  startDate = moment("2017-01-01", "YYYY-MM-DD"); // = moment.from("2017-01-01", "en");
  
}
در این مثال خاصیت startAt را به یک تاریخ میلادی متصل کرده‌ایم و همچنین خاصیت startView به year تنظیم شده‌است که یک چنین خروجی را در بار اول نمایش تقویم ایجاد می‌کند:




3) تعیین باز‌ه‌ی تاریخی قابل انتخاب توسط کاربر
<mat-form-field>
    <input matInput [matDatepicker]="picker3" [min]="minDate" [max]="maxDate" placeholder="min: 2017-10-02 and max: 1396-07-29">
    <mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
    <mat-datepicker #picker3></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent  {

  minDate = moment.from("2017-10-02", "en"); // = moment('2017-10-02', 'YYYY-MM-DD');
  maxDate = moment.from("1396-07-29", "fa"); // = moment('1396-07-29', 'jYYYY-jMM-jDD');
}
همانطور که ملاحظه می‌کنید کتابخانه‌ی jalali-moment می‌تواند تاریخ شمسی و یا میلادی را توسط متد from آن دریافت کند و هر دو حالت در اینجا پس از انتساب به خواص min و max تقویم، به خوبی کار کرده و سبب محدود ساختن بازه‌ی قابل انتخاب توسط کاربر می‌شوند.



در این تصویر روزهای خاکستری، قابل انتخاب نیستند و غیرفعال شده‌اند (چون min به 10 مهر و max به 29 مهر تنظیم شده‌است).


4) غیرفعال کردن روزهای قابل انتخاب بر اساس یک منطق سفارشی
<mat-form-field>
    <input matInput [matDatepicker]="picker4" [matDatepickerFilter]="myFilter" placeholder="Date validation - Datepicker Filter">
    <mat-datepicker-toggle matSuffix [for]="picker4"></mat-datepicker-toggle>
    <mat-datepicker #picker4></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent {

  myFilter = (d: moment.Moment): boolean => {
    const day: number = d.day();
    // Prevent Thursday and Friday from being selected.
    return day !== 5 && day !== 4;
  }
}
در اینجا روزهای پنج‌شنبه و جمعه در تقویم نمایش داده شده، بر اساس تعریف matDatepickerFilter سفارشی، دیگر قابل انتخاب نیستند:



5) کار با رخ‌دادهای تقویم
<mat-form-field>
    <input matInput [matDatepicker]="picker5" (dateInput)="onInput($event)" (dateChange)="onChange($event)"
        placeholder="dateInput and dateChange events">
    <mat-datepicker-toggle matSuffix [for]="picker5"></mat-datepicker-toggle>
    <mat-datepicker #picker5></mat-datepicker>
</mat-form-field>
با این کدها:
import { MatDatepickerInputEvent } from "@angular/material";
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent {

  onInput(event: MatDatepickerInputEvent<moment.Moment>) {
    console.log("OnInput: ", event.value);
  }

  onChange(event: MatDatepickerInputEvent<moment.Moment>) {
    const x = moment(event.value).format("jYYYY/jMM/jDD");
    console.log("OnChange: ", x);
  }
}
در اینجا نحوه‌ی واکنش نشان دادن به رخ‌دادهای dateInput و dateChange کامپوننت mat-datepicker را ملاحظه می‌کنید:


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


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
تاریخ شمسی با Extension Method برای DateTime
سلام خیلی خوبه منم از کلاس زیر استفاده می‌کنم
اما تو سیلورلایت چطور استفاده کنم؟

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reflection;
using System.Text;


public class PersianCulture : CultureInfo
{
    private readonly Calendar cal;
    private readonly Calendar[] optionals;


    public PersianCulture(): this("FA-IR", true)
    {

    }

    public PersianCulture(string cultureName, bool useUserOverride): base(cultureName, useUserOverride)
    {
        //Temporary Value for cal.
        cal = base.OptionalCalendars[0];

        //populating new list of optional calendars.
        var optionalCalendars = new List<Calendar>();
        optionalCalendars.AddRange(base.OptionalCalendars);
        optionalCalendars.Insert(0, new PersianCalendar());


        Type formatType = typeof(DateTimeFormatInfo);
        Type calendarType = typeof(Calendar);


        PropertyInfo idProperty = calendarType.GetProperty("ID", BindingFlags.Instance | BindingFlags.NonPublic);
        FieldInfo optionalCalendarfield = formatType.GetField("optionalCalendars",
                                                                BindingFlags.Instance | BindingFlags.NonPublic);

        //populating new list of optional calendar ids
        var newOptionalCalendarIDs = new Int32[optionalCalendars.Count];
        for (int i = 0; i < newOptionalCalendarIDs.Length; i++)
            newOptionalCalendarIDs[i] = (Int32)idProperty.GetValue(optionalCalendars[i], null);

        optionalCalendarfield.SetValue(DateTimeFormat, newOptionalCalendarIDs);

        optionals = optionalCalendars.ToArray();
        cal = optionals[0];
        DateTimeFormat.Calendar = optionals[0];

        DateTimeFormat.MonthNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty };
        DateTimeFormat.MonthGenitiveNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty };
        DateTimeFormat.AbbreviatedMonthNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty };
        DateTimeFormat.AbbreviatedMonthGenitiveNames = new[] { "فروردین", "اردیبهشت", "خرداد", "تیر", "مرداد", "شهریور", "مهر", "آبان", "آذر", "دی", "بهمن", "اسفند", string.Empty };

        DateTimeFormat.AbbreviatedDayNames = new string[] { "ی", "د", "س", "چ", "پ", "ج", "ش" };
        DateTimeFormat.ShortestDayNames = new string[] { "ی", "د", "س", "چ", "پ", "ج", "ش" };
        DateTimeFormat.DayNames = new string[] { "یکشنبه", "دوشنبه", "ﺳﻪشنبه", "چهارشنبه", "پنجشنبه", "جمعه", "شنبه" };

        DateTimeFormat.AMDesignator = "ق.ظ";
        DateTimeFormat.PMDesignator = "ب.ظ";

        
        DateTimeFormat.ShortDatePattern = "yyyy/MM/dd";
        DateTimeFormat.LongDatePattern = "yyyy/MM/dd";
             
        DateTimeFormat.SetAllDateTimePatterns(new[] {"yyyy/MM/dd"}, 'd');
        //DateTimeFormat.SetAllDateTimePatterns(new[] {"dddd, dd MMMM yyyy"}, 'D');
        //DateTimeFormat.SetAllDateTimePatterns(new[] {"yyyy MMMM"}, 'y');
        //DateTimeFormat.SetAllDateTimePatterns(new[] {"yyyy MMMM"}, 'Y');
        

    }

    public override Calendar Calendar
    {
        get { return cal; }
    }

    public override Calendar[] OptionalCalendars
    {
        get { return optionals; }
    }
}


اشتراک‌ها
وبینار تولید واسط کاربری Future-proof با استفاده از Web Components

دوستان، همکاران سیستم یک وبینار رایگان در مورد Web Components برگزار می‌کند که من ارائه می‌کنم 🙂. اگر علاقه‌مند بودید ثبت نام کنید.


Web Components مجموعه‌ای از تکنولوژی‌هایی هستند که امکان ساخت المان‌های سفارشی استاندارد با قابلیت استفاده‌ی مجدد و به همراه کپسوله‌سازی ساختار، استایل و عملکرد را در اختیار ما قرار می‌دهند. این المان‌های استاندارد، وابستگی به فریم‌ورک خاصی ندارند.
در این وبینار به بررسی مسائلی خواهیم پرداخت که با استفاده از Web Components قابل حل هستند و در ادامه روش تولید آن‌ها با استفاده از JavaScript/HTML/CSS ارائه خواهد شد. در نهایت ابزارهایی مانند StencilJS و Polymer برای توسعه ساده‌تر و بهینه‌تر این کامپوننت‌ها معرفی می‌شوند.


محورهای اصلی این وبینار: 
  • چرا Web Components؟
  • وضعیت پشتیبانی و به کار گیری Web Components
  • توسعه چند کامپوننت نمونه با استفاده از Web Components
  • بررسی ابزارهای جانبی
  • استفاده به شکل Framework agnostic
 📌تاریخ برگزاری: شنبه 20 دی، ساعت 18:30 تا 20:00 
وبینار تولید واسط کاربری Future-proof با استفاده از Web Components
مطالب
زیر نویس فارسی ویدیوهای ساخت برنامه‌های مترو توسط سی شارپ و XAML - قسمت سوم

زیرنویس‌های فارسی قسمت سوم «Building Windows 8 Metro Apps in C# and XAML» را از اینجا و یا اینجا می‌تونید دریافت کنید.

لیست سرفصل‌های قسمت سوم به شرح زیر است:

Controls  00:48:08 
WinRT provides numerous XAML controls, which act as the building blocks of your applications.
Some of these will be familiar, while some are new to Metro.
Introduction
Command Controls
Demo: Application Bar
Context Menus
Demo: Context Menus
Application Settings
CheckBox, RadioButton, and ToggleSwitch
Progress Controls
Demo: Progress Controls
Displaying Text
Demo: Displaying Text
Text Entry
Sliders
Styling
WebView
UserControl
Summary

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

مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت اول - تزریق وابستگی‌ها در برنامه‌های کنسول
پیشتر با مقدمات تزریق وابستگی‌ها در برنامه‌های ASP.NET Core آشنا شده‌ایم:
در ادامه در طی چند مطلب می‌خواهیم نکات و سناریوهای تکمیلی مرتبط با امکانات تزریق وابستگی‌های توکار برنامه‌های مبتنی بر NET Core. را بررسی کنیم.


تزریق وابستگی‌ها در برنامه‌های کنسول مبتنی بر NET Core.

تزریق وابستگی‌ها، یکی از پرکاربردترین الگوهای طراحی برنامه‌های مدرن است. در نگارش‌های قبلی ASP.NET، به کمک DependencyResolver آن، کتابخانه‌های ثالث کمکی تزریق وابستگی‌ها می‌توانستند خودشان را به سیستم متصل کنند. اینبار ASP.NET Core به همراه IoC Container توکار خودش ارائه شده‌است که این کتابخانه، در خارج از آن، مانند برنامه‌های کنسول نیز قابل استفاده است.


سرویس نمونه‌‌ای برای تزریق آن در یک برنامه‌ی کنسول NET Core.

در پوشه‌ی جدید CoreIocServices، دستور dotnet new classlib را صادر می‌کنیم تا یک پروژه‌ی class library جدید را ایجاد کند. سپس اینترفیس ITestService و یک نمونه پیاده سازی آن‌را به این پروژه اضافه می‌کنیم تا در ادامه بتوانیم تنظیمات تزریق وابستگی‌های آن‌را در یک پروژه‌ی کنسول، ایجاد کنیم:
using System;
using Microsoft.Extensions.Logging;

namespace CoreIocServices
{
    public interface ITestService
    {
        void Run();
    }

    public class TestService : ITestService
    {
        private readonly ILogger<TestService> _logger;

        public TestService(ILogger<TestService> logger)
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public void Run()
        {
            _logger.LogWarning("A Warning from the TestService!");
        }
    }
}
در اینجا این سرویس نمونه نیز دارای یک وابستگی تزریق شده‌ی در سازنده‌ی آن است. این وابستگی، همان امکانات توکار logging مربوط به ASP.NET Core است که در برنامه‌های کنسول نیز قابل استفاده است. برای اینکه پروژه قابل کامپایل باشد، نیاز است وابستگی Microsoft.Extensions.Logging را نیز به آن افزود:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
  </ItemGroup>

</Project>


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

در ادامه، یک پوشه‌ی جدید را به نام CoreIocSample01 ایجاد کرده و دستور dotnet new console را در آن اجرا می‌کنیم تا یک برنامه‌ی کنسول جدید را ایجاد کند.
سپس اولین قدم برای استفاده‌ی از سرویس TestService از طریق تزریق وابستگی‌ها، افزودن وابستگی‌های مورد نیاز آن است:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\CoreIocServices\CoreIocServices.csproj" />
  </ItemGroup>

</Project>
در اینجا بسته‌ی Microsoft.Extensions.DependencyInjection جهت دسترسی به امکانات تزریق وابستگی‌های NET Core. به پروژه اضافه شده و همچنین ارجاعی نیز به پروژه‌ی class library که پیشتر ایجاد کردیم، افزوده شده‌است.
اکنون می‌توانیم همان روشی را که در یک برنامه‌ی ASP.NET Core با ارائه‌ی متد ConfigureServices به صورت از پیش آماده شده برای ما مهیا است، در اینجا نیز پیاده سازی کنیم:
using CoreIocServices;
using Microsoft.Extensions.DependencyInjection;

namespace CoreIocSample01
{
    class Program
    {
        static void Main(string[] args)
        {
            var serviceCollection = new ServiceCollection();
            ConfigureServices(serviceCollection);
            var serviceProvider = serviceCollection.BuildServiceProvider();

            var testService = serviceProvider.GetService<ITestService>();
            testService.Run();
        }

        private static void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<ITestService, TestService>();
        }
    }
}
کار با تعریف یک ServiceCollection جدید شروع می‌شود. سپس در متد ConfigureServices، همانند کاری که در برنامه‌های ASP.NET Core انجام می‌دهیم، ارتباطات اینترفیس‌ها و پیاده سازی‌های آن‌ها، به همراه طول عمر آن‌ها را تعریف می‌کنیم.
سپس نیاز است بر روی این ServiceCollection، متد BuildServiceProvider فراخوانی شود تا بتوانیم به IServiceProvider دسترسی پیدا کنیم. به آن Dependency Management Container نیز می‌گویند. این Container است که امکان دسترسی به وهله‌ای از ITestService و سپس فراخوانی متد Run آن‌را میسر می‌کند.


مشکل! برنامه‌ی کنسول اجرا نمی‌شود!

اگر سعی کنیم مثال فوق را اجرا کنیم، با استثنای زیر برنامه خاتمه می‌یابد:
Exception has occurred: CLR/System.InvalidOperationException
An unhandled exception of type 'System.InvalidOperationException' occurred in Microsoft.Extensions.DependencyInjection.dll:
'Unable to resolve service for type 'Microsoft.Extensions.Logging.ILogger`1[CoreIocServices.TestService]'
while attempting to activate 'CoreIocServices.TestService'.'
عنوان می‌کند که وابستگی تزریق شده‌ی در سازنده‌ی کلاس TestService را نمی‌تواند پیدا کند. علت اینجا است که هرچند ILogger را به سازنده‌ی کلاس سرویس خود اضافه کرده‌ایم، اما هنوز پیاده سازی کننده‌ی آن‌را مشخص نکرده‌ایم. به همین جهت امکان وهله سازی از این کلاس وجود ندارد. عموما در برنامه‌های ASP.NET Core نیازی به تنظیم زیر ساخت logging آن نیست؛ چون این مورد نیز به صورت پیش‌فرض انجام شده‌است. اما در اینجا خیر. به همین جهت دو وابستگی جدید Microsoft.Extensions.Logging.Console و Microsoft.Extensions.Logging.Debug را به پروژه‌ی کنسول اضافه می‌کنیم:
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="2.2.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\CoreIocServices\CoreIocServices.csproj" />
  </ItemGroup>

</Project>
پس از آن متد ConfigureServices ما جهت تعریف logging در دو حالت دیباگ و کنسول، به صورت زیر تغییر می‌کند:
private static void ConfigureServices(IServiceCollection services)
{
   services.AddLogging(configure => configure.AddConsole().AddDebug());
   services.AddTransient<ITestService, TestService>();
}
اکنون اگر برنامه را اجرا کنیم، خروجی زیر قابل مشاهده خواهد بود:
 CoreIocServices.TestService:Warning: A Warning from the TestService!


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید: CoreDependencyInjectionSamples-01.zip
مطالب
استفاده از Sparse Columns در SQL Server 2012
مقدمه
مقدار null به معنی پوچ و هیچ می‌باشد اما زمانی که در مقدار دهی جداول از آن استفاده می‌نمایم با توجه به نوع آن ستون فضای متفاوتی اشتغال می‌نماید. شاید در پایگاه داده‌های کوچک زیاد مطرح نباشد اما زمانی که حداقل چند گیگ حجم آن باشد و فرضا 20 تا 30 درصد آن از مقادیر null پر شده باشد فضای زیای از پوچ گرفته شده است این در حالی است که خیلی از توسعه دهندگان اصلا به اهمیت استفاده از null توجهی نمی‌کنند و از مقادیری غیر معتبری مثل 0 یا 1- در آن ستون به جای null استفاده می‌کنند.
SQL Server Sparce Columns
sparse column یا ستون‌های تنک قابلیتی از که از SQL Server 2008 اضافه شده و به ستون‌های عادی امکان استفاده بهینه از فضای ذخیره شده برای مقادیر null را می‌دهد. در واقع sparse column فضای مورد نیاز برای مقادیر null نسبت به مقادیر غیر null را کاهش می‌دهد. با استفاده از sparse column فضای ذخیره شده حداقل 20 تا 40 درصد کمتر خواهد شد.

ویژگی‌های Sparse Columns
  • SQL Server Database Engine از کلمه کلیدی SPARSE برای تعریف یک ستون که مقادیر آن می‌بایست بهینه شود استفاده می‌نماید.
  • نمای Catalog  جداول با ستون sparse شبیه جداول معمولی می‌باشد.
  • مقدار برگشتی از تابع COLUMNS_UPDATED با ستون sparce متفاوت از ستون معمولی است.
در نوع داده‌های زیر امکان استفاده از sparce columns  را ندارند:
 geography  text
 geometry   timestamp 
 image   user-defined data types 
ntext  
sparse column فضای بیشتری برای ذخیره داده‌های غیر null نسبت به داده‌های نشانه گذاری نشده با SPARSE لازم دارد و این فضا4 بایت بیشتر از ستون معمولی است. برآورد فضای ذخیره شده براساس نوع داده با طول ثابت در جدول زیر آورده شده است:
 نوع داده  بایت بدون sparse  بایت sparse  درصد null
 bit   0.125   5  98%
 tinyint   1   5  86%
 smallint   2  6  76%
 int   4  8  64%
bigint  8  12  52%
 real   4  8  64%
 float   8  12  52%
 smallmoney   4  8  64%
 money   8  12  52%
 smalldatetime   4  8  64%
 datetime   8  12  52%
 uniqueidentifier   16  20  43%
 date   3  7  69%
نوع داده با دقت - وابسته به طول
 
 نوع داده  بایت بدون sparse  یابت sparse  درصد null 
 (datetime(2   6  10  57%
 (datetime(2   8  12  52%
 (time(0   3  7  69%
 (time(7   5  9  60%
 (datetimetoffset(0   8  12  52%
 (datetimetoffset (7   10  14  49%
 (decimal/numeric(1,s   5  9  60%
 (decimal/numeric(38,s   17  21  42%
 (vardecimal(p,s      
نوع داده - داده وابسته به طول
نوع داده 
بایت بدون sparse   یابت sparse  درصد null
 sql_variant   2*   2*   60% 
 varchar or char   2*  4*+   60% 
 nvarchar or nchar   2*   4*   60% 
 varbinary or binary   2*   4*   60% 
 xml   2*   4*   60% 
 hierarchyid   2*  4*   60% 

محدویت‌های استفاده از Sparse columns
  • sparse column می‌ بایست nullable باشد و نمی‌تواند ROWGUIDCOL یا IDENTITY باشد. 
  • sparse column مقدار پیش فرض نمی‌تواند داشته باشد 
  • ستون محاسبه ای نمی‌تواند sparse باشد
  • sparse column نمی‌تواند بخشی از clustered index یا  unique primary key index باشد
  • sparse column  نمی تواند بخشی از  user-defined table باشد

مثالی از کاربرد Sparse columns
CREATE TABLE Employees_sparse (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) SPARSE NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) SPARSE NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) SPARSE NULL)
GO
CREATE TABLE Employees (
   EMP_ID INT IDENTITY(5001,1) PRIMARY KEY, 
   SSN CHAR(9) NOT NULL, 
   TITLE CHAR(10) NULL, 
   FIRSTNAME VARCHAR(50) NOT NULL, 
   MIDDLEINIT CHAR(1) NULL, 
   LASTNAME VARCHAR(50) NOT NULL, 
   EMAIL CHAR(50) NULL)
GO
در این دو جدول یکی با سه ستون Sparse  و دیگری بدون Sparse ایجاد شده و با  50000 ردیف داده پر شده است حال با رویه ذخیره شده sp_spaceused می‌توان فضای ذخیره شده دو جدول را باهم مقایسه نمود.
sp_spaceused 'Employees'
GO
sp_spaceused 'Employees_sparse' 

البته ذکر این نکته گفتی است که بهتر است از این تکنیک برای جداولی که تعداد زیادی ستون null دارند استفاده شود. 
مطالب
نحوه ایجاد یک نقشه‌ی سایت پویا با استفاده از قابلیت Reflection
طبق این استاندارد قالب نقشه‌ی سایت به فرم زیر می‌باشد:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
   <url>
      <loc>http://www.example.com/</loc>
      <lastmod>2005-01-01</lastmod>
      <changefreq>monthly</changefreq>
      <priority>0.8</priority>
   </url>
</urlset>

که یک فایل XML متشکل از یک تگ urlset  است و این تگ نیز حاوی یک یا چند تگ url می‌باشد. با توجه به تعاریف بالا به یک چنین کلاسی خواهیم رسید: 

public enum ChangeFreq
        {
            Always,
            Hourly,
            Daily,
            Weekly,
            Monthly,
            Yearly,
            Never
        }

        [XmlElement("loc")]
        public string Url { get; set; }

        [XmlElement("lastmod")]
        public DateTime? LastModified { get; set; }
        public bool ShouldSerializeLastModified()
        {
            return LastModified.HasValue;
        }

        [XmlElement("changefreq")]
        public ChangeFreq? ChangeFrequency { get; set; }
        public bool ShouldSerializeChangeFrequency()
        {
            return ChangeFrequency.HasValue;
        }
        [XmlElement("priority")]
        public float? Priority { get; set; }
        public bool ShouldSerializePriority()
        {
            return Priority.HasValue;
        }
    }

دقت داشته باشید که چون پروپرتی‌های LastModified ، ChangeFrequency و Priority از نوع Nullable تعریف شده‌اند، پس باید کاری کنیم در صورتیکه این پروپرتی‌ها نال بودند سریالیز نشوند. بدین منظور از تابع ShouldSerialize[MemberName] استفاده می‌شود. این تابع  جزئی از دات نت است. کافی است بعد از ShouldSerialize نام پروپرتی را ذکر کنید. حال به کلاس دیگری نیاز داریم تا لیستی از کلاس فوق را دربر داشته باشد. 

[XmlRoot("urlset",Namespace = "http://www.sitemaps.org/schemas/sitemap/0.9")]
    public class SiteMp
    {
        private readonly List<Location> _locations;

        public SiteMp()
        {
            _locations = new List<Location>();
        }

        [XmlElement("url")]
        public List<Location> Locations
        {
            get { return _locations; }
            set
            {
                foreach (var location in value)
                {
                    Add(location);
                }
            } 
            
        }

        public void Add(Location location)
        {
            _locations.Add(location);
        }
    }

حال برای پردازش کلاس بالا لازم است ActionResultی را طراحی کنیم تا خروجی Response را به فرمت XML پردازش کند:

public class XmlResult : ActionResult
    {
        private readonly object _objectToSerialize;

        public XmlResult(object objectToSerialize)
        {
            _objectToSerialize = objectToSerialize;
        }
        public override void ExecuteResult(ControllerContext context)
        {
            if (_objectToSerialize == null)
               return;
             context.HttpContext.Response.Clear();
             var xmlSerializer = new XmlSerializer(_objectToSerialize.GetType());
             context.HttpContext.Response.ContentType = "text/xml";
             xmlSerializer.Serialize(context.HttpContext.Response.Output, _objectToSerialize);            
        }
    }

و در آخر یک کنترلر ساخته و به صورت زیر از آن استفاده می‌کنیم: 

public class SiteMapController : Controller
    {
        // GET: SiteMap
        public ActionResult Index()
        {
            SiteMp siteMap = new SiteMp();
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/Index"
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/NewRequest",
                ChangeFrequency = Location.ChangeFreq.Always,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/Home/FindRequest",
                ChangeFrequency = Location.ChangeFreq.Always,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            siteMap.Add(new Location
            {
                Url = Request.Url.GetLeftPart(UriPartial.Authority) + "/ContactUs/Index",
                ChangeFrequency = Location.ChangeFreq.Daily,
                LastModified = DateTime.UtcNow,
                Priority = 0.5f
            });
            return new XmlResult(siteMap);
        }

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

public class ControllerScanner
    {
       public static List<string> ScanAllControllers(HttpRequestBase requestBase)
        {
            Assembly asm = Assembly.GetAssembly(typeof(MvcApplication));

            var controllerActionlist = asm.GetTypes()
                .Where(type => typeof (Controller).IsAssignableFrom(type))
                .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public))
                .Where((returnType => returnType.ReturnType == (typeof(ViewResult)) || returnType.ReturnType==(typeof(ActionResult))))
                .Select(
                    x =>
                        new
                        {
                            Controller = x.DeclaringType.Name,
                            Action = x.Name,
                            ReturnType = x.ReturnType.Name

                        })
                .OrderBy(x => x.Controller).ThenBy(x => x.Action).Distinct().ToList();

            if (requestBase.Url == null)
                return null;

            var url = requestBase.Url.GetLeftPart(UriPartial.Authority);
            return controllerActionlist.Select(controller => $"{url}/{controller.Controller}/{controller.Action}").ToList();
        }
    }

حال از کلاس بالا در کنترلر SiteMap به صورت زیر استفاده می‌کنیم :

public class SiteMapController : Controller
    {
        // GET: SiteMap
        public ActionResult Index()
        {
            var siteMap = new SiteMap();
            var controllers = ControllerScanner.ScanAllControllers(Request);
            foreach (var controller in controllers)
            {
                siteMap.Add(new Location
                {
                    Url = controller,
                    ChangeFrequency = Location.ChangeFreq.Always,
                    LastModified = DateTime.UtcNow,
                    Priority = 0.5f
                });
            }
            return new XmlResult(siteMap);
        }        
    }

در آخر نیز سطر زیر را به سیستم مسیریابی اضافه نمایید تا در صورت درخواست فایل sitemap.xml  اکشن Index از کنترلر SiteMap فراخوانی شود.

 routes.MapRoute(
                "SiteMap", // Route name
                "sitemap.xml", // URL with parameters
                new { controller = "Sitemap", action = "Index", name = UrlParameter.Optional, area = "" }
            );