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

آیا می‌دانید آخرین باری که از یک دیتابیس مفروض بک‌آپ گرفته شده، چه زمانی بوده است؟ (یا اینکه اصلا چه اهمیتی داره؟!)

USE master;
SELECT B.name AS Database_Name,
ISNULL(STR(ABS(DATEDIFF(day, GetDate(),
MAX(Backup_finish_date)))), 'NEVER') AS DaysSinceLastBackup,
ISNULL(CONVERT(char(10), MAX(backup_finish_date), 101), 'NEVER') AS
LastBackupDate
FROM master.dbo.sysdatabases B
LEFT OUTER JOIN msdb.dbo.backupset A
ON A.database_name = B.name
AND A.type = 'D'
GROUP BY
B.Name
ORDER BY
B.name


همانطور که مطلع هستید در SQL Server 2008 امکان فشرده سازی خودکار بک آپ‌ها هم فراهم شده است. با استفاده از اسکریپت زیر می‌توان از درصد فشرده سازی بک‌آپ‌ها گزارش گرفت:
SELECT bs.server_name,
bs.database_name AS 'Database Name',
CONVERT (BIGINT, bs.backup_size / 1048576 ) AS
'Uncompressed Backup Size (MB)',
CONVERT (BIGINT, bs.compressed_backup_size / 1048576 ) AS
'Compressed Backup Size (MB)',
CONVERT (NUMERIC (20,2), (CONVERT (FLOAT, bs.backup_size) /
CONVERT (FLOAT, bs.compressed_backup_size))) AS 'Compression Ratio',
DATEDIFF (SECOND, bs.backup_start_date, bs.backup_finish_date) AS
'Backup Elapsed Time (sec)',
bs.backup_finish_date AS 'Backup Finish Date'
FROM msdb.dbo.backupset AS bs
WHERE DATEDIFF (SECOND, bs.backup_start_date, bs.backup_finish_date) > 0
AND bs.backup_size > 0
AND bs.type = 'D' -- Change to L if you want Log backups
ORDER BY
bs.backup_finish_date DESC




برای فعال سازی گزینه فشرده سازی بک‌آپ‌ها با استفاده از دستورات T-SQL می‌توان به صورت زیر عمل کرد (در SQL server 2008):
USE master;
EXEC sp_configure 'backup compression default', '1';
RECONFIGURE WITH OVERRIDE;



ماخذ مورد استفاده:
http://www.sqlservercentral.com

پاسخ به بازخورد‌های پروژه‌ها
ایجاد گزارش با داده های ثابت و متغیر
ممنون از جوابتون.کلاس من به فرض مثال به شکل زیر است :
public class Documents
    {
        [Key]
        public int Id { set; get; }

        [Display(Name = "تاریخ")]
        public string Date { set; get; }

        [Display(Name = "تاریخ میلادی")]
        public DateTime? EnDate { set; get; }

        [Display(Name = "روز هفته")]
        public string Day { set; get; }

        [Display(Name = "بهداشتی نقدی ")]
        public string BehdashtiNaghdi { set; get; }

        [Display(Name = "بهداشتی کارتخوان")]
        public string BehdashtiPose { set; get; }

        [Display(Name = "فیش بستری نقدی")]
        public string FishBastariNaghdi { set; get; }

        [Display(Name = "فیش بستری کارتخوان")]
        public string FishBastariPose { set; get; }

        [Display(Name = "تعداد فیش")]
        public string FishCount { set; get; }

        [Display(Name = "سرپایی نقدی")]
        public string SarepayiNaghdi { set; get; }

        [Display(Name = "سرپایی کارتخوان")]
        public string SarepayiPose { set; get; }

        [Display(Name = "تعداد نسخه")]
        public string Noskhecount { set; get; }

        [Display(Name = "صندوق شماره 1")]
        public string SandoghNumberOne { set; get; }

        [Display(Name = "صندوق شماره 2")]
        public string SandoghNumberTow { set; get; }

        [Display(Name = "تعداد فیش‌های کارتخوان")]
        public string PoseResidCount { set; get; }

        [Display(Name = "نمره دکتر")]
        public string DoctosrRate { set; get; }

        [Display(Name = "توضیحات")]
        public string Description { set; get; }
        
        [Display(Name = "توسط کاربر")]
        [ForeignKey("User")]
        public virtual Users Users { get; set; }
        public int User { get; set; }

        [Display(Name = "پزشک شیفت")]
        [ForeignKey("Doctor")]
        public virtual Doctors Doctors { get; set; }
        public int Doctor { get; set; }

        [Display(Name = "شیفت")]
        [ForeignKey("Shift")]
        public virtual Shift ShiftsShift { get; set; }
        public int Shift { get; set; }

        [Display(Name = "نام بیمارستان")]
        [ForeignKey("HospitalId")]
        public virtual Hospitals Hospitals { get; set; }
        public int? HospitalId { get; set; }

    }
طبق فرموده‌ی شما من باید برای هر فیلد، یک فیلد دیگر با عنوان Title تعریف کنم که بشود در گزارش استفاده کرد. چون داخل تک تک فیلد‌های بالا فقط مقادیر ریخته میشوند. چیزی با این شکل که عنوانی برای هر فیلد قرار داشته باشد نیست.
اما راهی نیست که این موارد که همان عناوین است به شکل تصویر بالا رو به صورت دستی در گزارش وارد کنم؟
مطالب
ایجاد بارکد به همراه یک متن
در این مقاله یک بارکد را به همراه یک متن در پایین آن، مشابه تصویر زیر ایجاد میکنیم. سورس کامل این مطلب در این آدرس قرار دارد. جهت تست بارکد از این آدرس استفاده کنید.

- طول عکس خروجی نهایی 250 پیکسل است.

- فونت متن 10 پیکسل هست و عرض هر خط 17 پیکسل.

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

- عرض بارکد 50 پیکسل است.

- فاصله بین بارکد و متن 5 پیکسل است.

public static class BarcodeHelper
{
    public static string GenerateBarcodeWithText(string input, string textBelow)
    {
        // barcode: 50 pixels
        // margin: top 5 pixels
        // height of each text line is 17 pixels
        // text: maximum 3 lines
        // each 30 letters is: 1 line

        var eachLineHeight = 17;

        var eachLineLetters = 30;

        var maximumLines = 3;

        var maximumTextHeight = eachLineHeight * maximumLines;

        var resultWidth = 250;

        var barcodeHeight = 50;

        var textY = barcodeHeight + 5;

        // each 30 letters is: 1 line for example input length is 150 letters and for show 100 letters we need (150 / 30) 5 lines
        // each line is 17 pixels and text height will be (17 * 5) 102 pixels
        var textHeight = (textBelow.Length / eachLineLetters) * eachLineHeight;

        // if height of text be greater than (eachLineHeight * maximumLines) we use maximum text height (eachLineHeight * maximumLines)
        textHeight = textHeight > maximumTextHeight ? maximumTextHeight : textHeight;

        // if text height be less than 1 line we set 1 line height (17 pixels) to the text height
        // text height minimum is equal 1 linle (17 pixels)
        textHeight = textHeight < eachLineHeight ? eachLineHeight : textHeight;

        var resultHeight = textY + textHeight;
    }
}


چون ما از Bitmap و Image استفاده میکنیم، پس به پکیچ System.Drawing.Common نیاز داریم:

<ItemGroup>
    <PackageReference Include="System.Drawing.Common" Version="6.0.0" />
</ItemGroup>

اولین کاری که انجام میدهیم، یک Bitmap را ایجاد میکنیم و بعد یک مستطیل را به اندازه‌ی خود Bitmap ایجاد میکنیم و با کلاس Graphics، به نارنجی، رنگش میکنیم و داخل Bitmap میریزیم و در نهایت عکس ایجاد شده را در حافظه‌ی رم ذخیره میکنیم.

- Bitmap فضایی را در اختیار ما قرار میدهد که داخلش هر چیزی را ترسیم کنیم.

- Graphics به ما کمک میکند که عملیات گرافیکی را نظیر رنگ آمیزی، ترسیم عکس و ... روی یک شیء انجام دهیم.

- MemoryStream برای ذخیره سازی موقت در حافظه‌ی رم به کار میاد؛ عکس ایجاد شده‌ی تا این لحظه را که یک مستطیل نارنجی رنگ هست، در داخل رم ذخیره میکنیم.

#region MainBitmap

var mainBitmap = new Bitmap(resultWidth, resultHeight);
using var rectangleGraphics = Graphics.FromImage(mainBitmap);
{
    var rectangle = new Rectangle(0, 0, resultWidth, resultHeight);
    rectangleGraphics.FillRectangle(Brushes.OrangeRed, rectangle);
}

using var rectangleStream = new MemoryStream();
{
    mainBitmap.Save(rectangleStream, ImageFormat.Png);
}

#endregion

خروجی تا این لحظه:

حالا باید بارکد را ایجاد کنیم و عکس خروجی بارکد را داخل این مستطیل بریزیم؛ برای اینکار از کتابخانه BarcodeLib استفاده میکنیم:

private static Bitmap GenerateBarcodeImage(string input, int width, int height)
{
    var barcodeInstance = new Barcode();
    var barcodeImage = barcodeInstance.Encode(BarcodeLib.TYPE.CODE39, input, Color.Black,
        Color.OrangeRed, width, height);
    using var barcodeStream = new MemoryStream();
    {
        barcodeImage.Save(barcodeStream, ImageFormat.Png);
    }
    return (Bitmap)Image.FromStream(barcodeStream);
}

و الان این عکس بارکد را داخل مستطیل اصلی میریزیم و هر دو را Merge میکنیم:

#region Barcode

var barcodeImage = GenerateBarcodeImage(input, resultWidth, barcodeHeight);

#endregion

#region MergedRectangleAndBarcode

var newMainBitmap = (Bitmap)Image.FromStream(rectangleStream);
var newBarcodeBitmap = barcodeImage;
using var newRectangleGraphics = Graphics.FromImage(newMainBitmap);
{
    newRectangleGraphics.DrawImage(newBarcodeBitmap, 0, 0);
}

using var mergedRectangleAndBarcodeStream = new MemoryStream();
{
    newMainBitmap.Save(mergedRectangleAndBarcodeStream, ImageFormat.Png);
}

#endregion

خروجی تا این لحظه :

حالا باید 5 پیکسل از پایین بارکد فاصله بگیریم و متن را بنویسیم.

برای اینکار از یک مستطیل کمک میگیریم. یعنی یک مستطیل بدون هیچ رنگ و Border ـی را پایین این بارکد ایجاد میکنیم، چرا؟ دلیل این است که میخواهیم متن‌مان را به صورت وسط چین، از راست و چپ، و وسط از بالا و پایین قرار بدیم و برای اینکار میگیم این نسبت وسط چین بودن از راست و چپ، وسط بودن از بالا و پایین را از مستطیل پایین بارکد کمک بگیر، خلاصه‌اش می‌شود اینکه از مستطیلِ پایینِ بارکد برای وسط چین بودن متن از راست و چپ و وسط بودن از بالا و پایین استفاده میکنیم.

#region WriteText

var barcodeBitmap = (Bitmap)Image.FromStream(mergedRectangleAndBarcodeStream);
using var graphics = Graphics.FromImage(barcodeBitmap);
{
    using var font = new Font("Tahoma", 10);
    {
        var rect = new Rectangle(0, textY, resultWidth, textHeight);
        var sf = new StringFormat();
        sf.Alignment = StringAlignment.Center;
        sf.Trimming = StringTrimming.EllipsisCharacter;
        sf.FormatFlags = StringFormatFlags.DirectionRightToLeft;
        sf.LineAlignment = StringAlignment.Center;
        graphics.DrawString(textBelow, font, Brushes.Black, rect, sf);
        //graphics.DrawRectangle(Pens.Green, rect);
    }
}

using var finalStream = new MemoryStream();
{
    barcodeBitmap.Save(finalStream, ImageFormat.Png);
}

#endregion

graphics.DrawString می‌گوید textBelow را با font تاهوما و با رنگ سیاه، داخل rect (مستطیل) و با این تنظیماتِ متن بریز.

Alignment متن را وسط چین می‌کند (این وسط چین شدن نسبت به مستطیل پایین بارکد است که هیچ رنگ و Border ـی ندارد) .

LineAlignment متن را از بالا و پایین میارد وسط (این وسط شدن نسبت به مستطیل پایین بارکد است که هیچ رنگ و Border ـی ندارد).

EllipsisCharacter اگر متن طولانی باشد، اضافه متن را به صورت سه نقطه نمایش می‌دهد.

DirectionRightToLeft متن را RTL می‌کند.

خروجی نهایی:

عکس نهایی به صورت Stream ذخیره شده‌است، آن‌را به فرمت Base64 تبدیل میکنیم و برگشت میزنیم.

return Convert.ToBase64String(finalStream.ToArray());

برای نمایش یک آرایه بایتی که به فرمت Base64 تبدیل شده، به این روش عمل میکنیم:

<img src="data:image/png;base64, @BarcodeHelper.GenerateBarcodeWithText("barcode text", "below text")" />

چون برای ایجاد بارکد از تایپ 39 استفاده کرده‌ایم و تایپ 39 فقط حروف بزرگ انگلیسی را پشتیبانی میکند، پس برای اینکه دچار خطا نشویم، میتوانیم ابتدای متدمان، از این کد استفاده کنیم:

// Type 39 doesn't support lower case letters, for prevent exception, we convert all input letters to upper case
// more details: https://www.dntips.ir/newsarchive/details/18019
input = input.ToUpperInvariant();

همچنین جهت تشخیص خودکار راست به چک بودن متن پایین بارکد، میتوان از متد ContainsFarsi در پکیج DNTPersianUtils.Core استفاده کرد:

if (textBelow.ContainsFarsi())
    sf.FormatFlags = StringFormatFlags.DirectionRightToLeft;
مطالب دوره‌ها
مدل سازی داده‌ها در RavenDB
در مطلب جاری، به صورت اختصاصی، مبحث مدل سازی اطلاعات و رسیدن به مدل ذهنی مرسوم در طراحی‌های NoSQL سندگرا را در مقایسه با دنیای Relational، بررسی خواهیم کرد.


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

- دنیای بانک‌های اطلاعاتی رابطه‌ای برای Write بهینه سازی شده‌اند؛ از این جهت که تاریخچه پیدایش آن‌ها به دهه 70 میلادی بر می‌گردد، زمانیکه برای تهیه سخت دیسک‌ها باید هزینه‌های گزافی پرداخت می‌شد. به همین جهت الگوریتم‌ها و روش‌های بسیاری در آن دوره ابداع شدند تا ذخیره سازی اطلاعات، حجم کمتری را به خود اختصاص دهند. اینجا است که مباحثی مانند Normalization بوجود آمدند تا تضمین شود که داده‌ها تنها یکبار ذخیره شده و دوبار در جاهای مختلفی ذخیره نگردند. جهت اطلاع در سال 1980 میلادی، یک سخت دیسک 10 مگابایتی حدود 4000 دلار قیمت داشته است.
- تفاوت مهم دیگر دوره ما با دهه‌های 70 و 80 میلادی، پدیدار شدن UI و روابط کاربری بسیار پیچیده، در مقایسه با برنامه‌های خط فرمان یا حداکثر فرم‌های بسیار ساده ورود اطلاعات در آن زمان است. برای مثال در دهه 70 میلادی تصور UI ایی مانند صفحه ابتدایی سایت Stack overflow احتمالا به ذهن هم خطور نمی‌کرده است.


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


معرفی مفهوم Unit of change

همین پیچیدگی‌ها سبب شدند تا جهت ساده‌سازی حل اینگونه مسایل، حرکتی به سمت دنیای NoSQL شروع شود. ایده اصلی مدل سازی داده‌ها در اینجا کم کردن تعداد اعمالی است که باید جهت رسیدن به یک نتیجه واحد انجام داد. اگر قرار است یک سؤال به همراه تگ‌ها، اطلاعات کاربر، رای‌ها و غیره واکشی شوند، چرا باید تعداد اعمال قابل توجهی جهت مراجعه به جداول مختلف مرتبط صورت گیرد؟ چرا تمام این اطلاعات را یکجا نداشته باشیم تا بتوان همگی را در طی یک واکشی به دست آورد و به این ترتیب دیگر نیازی نباشد انواع و اقسام JOIN‌ها را به چند ده جدول موجود نوشت؟
اینجا است که مفهومی به نام Unit of change مطرح می‌شود. در هر واحد تغییر، کلیه اطلاعات مورد نیاز برای رندر یک شیء قرار می‌گیرند. برای مثال اگر قرار است با شیء محصول کار کنیم، تمام اطلاعات مورد نیاز آن‌‌را اعم از گروه‌ها، نوع‌ها، رنگ‌ها و غیره را در طی یک سند بانک اطلاعاتی NoSQL سندگرا، ذخیره می‌کنیم.


محدود‌ه‌های تراکنشی یا Transactional boundaries

محدوده‌های تراکنشی در Domain driven design به Aggregate root نیز معروف است. هر محدود تراکنشی حاوی یک Unit of change قرار گرفته داخل یک سند است. ابتدا بررسی می‌کنیم که در یک Read به چه نوع اطلاعاتی نیاز داریم و سپس کل اطلاعات مورد نیاز را بدون نوشتن JOIN ایی از جداول دیگر، داخل یک سند قرار می‌دهیم.
هر محدوده تراکنشی می‌تواند به محدوده تراکنشی دیگری نیز ارجاع داده باشد. برای مثال در RavenDB شماره‌های اسناد، یک سری رشته هستند؛ برخلاف بانک‌های اطلاعاتی رابطه‌ای که بیشتر از اعداد برای مشخص سازی Id استفاده می‌کنند. در این حالت برای ارجاع به یک کاربر فقط کافی است برای مثال مقدار خاصیت کاربر یک سند به "users/1" تنظیم شود. "users/1" نیز یک Id تعریف شده در RavenDB است.
مزیت این روش، سرعت واکشی بسیار بالای دریافت اطلاعات آن است؛ دیگر در اینجا نیازی به JOINهای سنگین به جداول دیگر برای تامین اطلاعات مورد نیاز نیست و همچنین در ساختار‌های پیچیده‌تری مانند ساختارهای تو در تو، دیگر نیازی به تهیه کوئری‌های بازگشتی و استفاده از روش‌های پیچیده مرتبط با آن‌ها نیز وجود ندارد و کلیه اطلاعات مورد نظر، به شکل یک شیء JSON داخل یک سند حاضر و آماده برای واکشی در طی یک Read هستند.
به این ترتیب می‌توان به سیستم‌های مقیاس پذیری رسید. سیستم‌هایی که با بالا رفتن حجم اطلاعات در حین واکشی‌های داده‌های مورد نیاز، کند نبوده و بسیار سریع پاسخ می‌دهند.


Denormalization داده‌ها

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


ملاحظات اندازه‌های داده‌ها

زمانیکه سند‌ها بسیار بزرگ می‌شوند چه رخ خواهد داد؟ از لحاظ اندازه داده‌ها سه نوع سند را می‌توان متصور بود:
الف) سندهای محدود، مانند اغلب اطلاعاتی که تعداد فیلدهای مشخصی دارند با تعداد اشیاء مشخصی.
ب) سندهای نامحدود اما با محدودیت طبیعی. برای مثال اطلاعات فرزندان یک شخص را درنظر بگیرید. هرچند این اطلاعات نامحدود هستند، اما به صورت طبیعی می‌توان فرض کرد که سقف بالایی آن عموما به 20 نمی‌رسد!
ج) سندهای نامحدود، مانند سندهایی که آرایه‌ای از اطلاعات را ذخیره می‌کنند. برای مثال در یک سایت فروشگاه، اطلاعات فروش یک گروه از اجناس خاص را درنظر بگیرید که عموما نامحدود است. اینجا است که باید به اندازه اسناد نیز دقت داشت. برای مدیریت این مساله حداقل از دو روش استفاده می‌شود:
- محدود کردن تعداد اشیاء. برای مثال در هر سند حداکثر 100 اطلاعات فروش یک محصول بیشتر ثبت نشود. زمانیکه به این حد رسیدیم، یک سند جدید ایجاد شده و Id سند قبلی مثلا "products/1" در سند دوم ذکر خواهد شد.
- محدود کردن تعداد اطلاعات ذخیره شده بر اساس زمان
RavenDB برای مدیریت این مساله، مفهوم Includes را معرفی کرده است. در اینجا با استفاده از متد الحاقی Include، کار زنجیر کردن سندهای مرتبط صورت خواهد گرفت.



یک مثال عملی: مدل سازی داده‌های یک بلاگ در RavenDB

پس از این بحث مقدماتی که جهت معرفی ذهنیت مدل سازی داده‌ها در دنیای غیر رابطه‌ای NoSQL ضروری بود، در ادامه قصد داریم مدل‌های داده‌های یک بلاگ را سازگار با ساختار بانک اطلاعاتی NoSQL سندگرای RavenDB طراحی کنیم.
در یک بلاگ، تعدادی مطلب، نظر، برچسب (گروه‌های مطالب) و امثال آن وجود دارند. اگر بخواهیم این اطلاعات را به صورت رابطه‌ای مدل کنیم، به ازای هر کدام از این موجودیت‌ها یک جدول نیاز خواهد بود و برای رندر صفحه اصلی بلاگ، چندین و چند کوئری برای نمایش اطلاعات مطالب، نویسنده(ها)، برچسب‌ها و غیره باید به بانک اطلاعاتی ارسال گردد، که تعدادی از آن‌ها مستقیما بر روی یک جدول اجرا می‌شوند و تعدادی دیگر نیاز به JOIN دارند.
مشکلاتی که روش رابطه‌ای دارد:
- تعداد اعمالی که باید برای نمایش صفحه اول سایت صورت گیرد، بسیار زیاد است و این مساله با تعداد بالای کاربران از دید مقیاس پذیری سیستم مشکل ساز است.
- داده‌های مرتبط در جداول مختلفی پراکنده‌اند.
- این سیستم برای Write بهینه سازی شده است و نه برای Read. (همان بحث گران بودن سخت دیسک‌ها در دهه‌های قبل که در ابتدای بحث به آن اشاره شد)

مدل سازی سازگار با دنیای NoSQL یک بلاگ

در اینجا چند کلاس مقدماتی را مشاهده می‌کنید که تعریف آن‌ها به همین نحو صحیح است و نیاز به جزئیات و یا روابط بیشتری ندارند.
namespace RavenDBSample01.BlogModels
{
    public class BlogConfig
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Description { set; get; }
        // ... more items here
    }

    public class User
    {
        public string Id { set; get; }
        public string FullName { set; get; }
        public string Email { set; get; }
        // ... more items here
    }
}
اما کلاس مطالب بلاگ را به چه صورتی طراحی کنیم؟ هر مطلب، دارای تعدادی نظر خواهد بود. اینجا است که بحث unit of change مطرح می‌شود و درج اطلاعاتی که در طی یک read نیاز است از بانک اطلاعاتی جهت رندر UI واکشی شوند. به این ترتیب به این نتیجه می‌رسیم که بهتر است کلیه کامنت‌های یک مطلب را داخل همان شیء مطلب مرتبط قرار دهیم. از این جهت که یک نظر، خارج از یک مطلب بلاگ دارای مفهوم نیست.
اما این طراحی نیز یک مشکل دارد. درست است که ساختار یک صفحه مطلب، از مطالب وبلاگ به همین نحوی است که توضیح داده شد؛ اما در صفحه اول سایت، هیچگاه کامنت‌های مطالب درج نمی‌شوند. بنابراین نیازی نیست تا تمام کامنت‌ها را داخل یک مطلب ذخیره کرد. به این ترتیب برای نمایش صفحه اول سایت، حجم کمتری از اطلاعات واکشی خواهند شد.
    public class Post
    {
        public string Id { set; get; }
        public string Title { set; get; }
        public string Body { set; get; }

        public ICollection<string> Tags { set; get; }

        public string AuthorId { set; get; }

        public string PostCommentsId { set; get; }
        public int CommentsCount { set; get; }
    }

    public class Comment
    {
        public string Id { set; get; }
        public string Body { set; get; }
        public string AuthorName { set; get; }
        public DateTime CreatedAt { set; get; }
    }

    public class PostComments
    {
        public List<Comment> Comments { set; get; }
        public string LastCommentId { set; get; }
    }
در اینجا ساختار Post و Commentهای بلاگ را مشاهده می‌کنید. جایی که ذخیره سازی اصلی کامنت‌ها صورت می‌گیرد در شیء PostComments است. یعنی PostCommentsId شیء Post به یک وهله از شیء PostComments که حاوی کلیه کامنت‌های آن مطلب است، اشاره می‌کند.
به این ترتیب برای نمایش صفحه اول سایت، فقط یک کوئری صادر می‌شود. برای نمایش یک مطلب و کلیه کامنت‌های متناظر با آن دو کوئری صادر خواهند شد.

بنابراین همانطور که مشاهده می‌کنید، در دنیای NoSQL، طراحی مدل‌های داده‌ای بر اساس «سناریوهای Read» صورت می‌گیرد و نه صرفا طراحی یک مدل رابطه‌ای بهینه سازی شده برای حالت Write.

سورس کامل ASP.NET MVC این بلاگ‌را که «راکن بلاگ» نام دارد، از GitHub نویسندگان اصلی RavenDB می‌توانید دریافت کنید.
مطالب
پیاده سازی یک تامین کننده MySQL برای ASP.NET Identity
در این مقاله جایگزینی پیاده سازی پیش فرض ASP.NET Identity را بررسی می‌کنیم. در ادامه خواهید خواند:

  • جزئیات نحوه پیاده سازی یک Storage Provider برای ASP.NET Identity
  • تشریح اینترفیس هایی که باید پیاده سازی شوند، و نحوه استفاده از آنها در ASP.NET Identity
  • ایجاد یک دیتابیس MySQL روی Windows Azure
  • نحوه استفاده از یک ابزار کلاینت (MySQL Workbench) برای مدیریت دیتابیس مذکور
  • نحوه جایگزینی پیاده سازی سفارشی با نسخه پیش فرض در یک اپلیکیشن ASP.NET MVC
در انتهای این مقاله یک اپلیکیشن ASP.NET MVC خواهیم داشت که از ASP.NET Identity و تامین کننده سفارشی جدید استفاده می‌کند. دیتابیس اپلیکیشن MySQL خواهد بود و روی Windows Azure میزبانی می‌شود. سورس کد کامل این مثال را هم می‌توانید از این لینک دریافت کنید.


پیاده سازی یک Storage Provider سفارشی برای ASP.NET Identity

ASP.NET Identity سیستم توسعه پذیری است که می‌توانید بخش‌های مختلف آن را جایگزین کنید.در این سیستم بناهای سطح بالایی مانند Managers و Stores وجود دارند.
Managers کلاس‌های سطح بالایی هستند که توسعه دهندگان از آنها برای اجرای عملیات مختلف روی ASP.NET Identity استفاده می‌کنند. مدیریت کننده‌های موجود عبارتند از UserManager و RoleManager. کلاس UserManager برای اجرای عملیات مختلف روی کاربران استفاده می‌شود، مثلا ایجاد کاربر جدید یا حذف آنها. کلاس RoleManager هم برای اجرای عملیات مختلف روی نقش‌ها استفاده می‌شود.

Stores کلاس‌های سطح پایین‌تری هستند که جزئیات پیاده سازی را در بر می‌گیرند، مثلا اینکه موجودیت‌های کاربران و نقش‌ها چگونه باید ذخیره و بازیابی شوند. این کلاس‌ها با مکانیزم ذخیره و بازیابی تلفیق شده اند. مثلا Microsoft.AspNet.Identity.EntityFramework کلاسی با نام UserStore دارد که برای ذخیره و بازیابی User‌ها و داده‌های مربوطه توسط EntityFramework استفاده می‌شود.

Managers از Stores تفکیک شده اند و هیچ وابستگی ای به یکدیگر ندارند. این تفکیک بدین منظور انجام شده که بتوانید مکانیزم ذخیره و بازیابی را جایگزین کنید، بدون اینکه اپلیکیشن شما از کار بیافتد یا نیاز به توسعه بیشتر داشته باشد. کلاس‌های Manager می‌توانند با هر Store ای ارتباط برقرار کنند. از آنجا که شما از API‌های سطح بالای UserManager برای انجام عملیات CRUD روی کاربران استفاده می‌کنید، اگر UserStore را با پیاده سازی دیگری جایگزین کنید، مثلا AzureTable Storage یا MySql، نیازی به بازنویسی اپلیکیشن نیست.

در مثال جاری پیاده سازی پیش فرض Entity Framework را با یک  تامین کننده MySQL جایگزین می‌کنیم.

پیاده سازی کلاس‌های Storage
برای پیاده سازی تامین کننده‌های سفارشی، باید کلاس هایی را پیاده سازی کنید که همتای آنها در Microsoft.AspNet.Identity.EntityFramework وجود دارند:
  • <UserStore<TUser
  • IdentityUser
  • <RoleStore<TRole
  • IdentityRole
پیاده سازی پیش فرض Entity Framework را در تصاویر زیر مشاهده می‌کنید.
Users

Roles

در مخزن پیش فرض ASP.NET Identity EntityFramework کلاس‌های بیشتری برای موجودیت‌ها مشاهده می‌کنید.

  • IdentityUserClaim
  • IdentityUserLogin
  • IdentityUserRole
همانطور که از نام این کلاس‌ها مشخص است، اختیارات، نقش‌ها و اطلاعات ورود کاربران توسط این کلاس‌ها معرفی می‌شوند. در مثال جاری این کلاس‌ها را پیاده سازی نخواهیم کرد، چرا که بارگذاری اینگونه رکوردها از دیتابیس به حافظه برای انجام عملیات پایه (مانند افزودن و حذف اختیارات کاربران) سنگین است. در عوض کلاس‌های backend store اینگونه عملیات را بصورت مستقیم روی دیتابیس اجرا خواهند کرد. بعنوان نمونه متد ()UserStore.GetClaimsAsync را در نظر بگیرید. این متد به نوبه خود متد (userClaimTable.FindByUserId(user.Id را فراخوانی می‌کند که یک کوئری روی جدول مربوطه اجرا می‌کند و لیستی از اختیارات کاربر را بر می‌گرداند.
public Task<IList<Claim>> GetClaimsAsync(IdentityUser user)
{
    ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id);
    return Task.FromResult<IList<Claim>>(identity.Claims.ToList());
}
برای پیاده سازی یک تامین کننده سفارشی MySQL مراحل زیر را دنبال کنید.
1. کلاس کاربر را ایجاد کنید، که اینترفیس IUser را پیاده سازی می‌کند.
public class IdentityUser : IUser
{
    public IdentityUser(){...}

    public IdentityUser(string userName) (){...}

    public string Id { get; set; }

    public string UserName { get; set; }

    public string PasswordHash { get; set; }

    public string SecurityStamp { get; set; }
}
2. کلاس User Store را ایجاد کنید، که اینترفیس‌های IUserStore, IUserClaimStore, IUserLoginStore, IUserRoleStore و IUserPasswordStore را پیاده سازی می‌کند. توجه کنید که تنها اینترفیس IUserStore را باید پیاده سازی کنید، مگر آنکه بخواهید از امکاناتی که دیگر اینترفیس‌ها ارائه می‌کنند هم استفاده کنید.
public class UserStore : IUserStore<IdentityUser>,
                         IUserClaimStore<IdentityUser>,
                         IUserLoginStore<IdentityUser>,
                         IUserRoleStore<IdentityUser>,
                         IUserPasswordStore<IdentityUser>
{
    public UserStore(){...}

    public Task CreateAsync(IdentityUser user){...}

    public Task<IdentityUser> FindByIdAsync(string userId){...}   
...
}
3. کلاس Role را ایجاد کنید که اینترفیس IRole را پیاده سازی می‌کند.
public class IdentityRole : IRole
{
    public IdentityRole(){...}

    public IdentityRole(string roleName) (){...}

    public string Id { get; set; }

    public string Name { get; set; }
}
4. کلاس Role Store را ایجاد کنید که اینترفیس IRoleStore را پیاده سازی می‌کند. توجه داشته باشید که پیاده سازی این مخزن اختیاری است و در صورتی لازم است که بخواهید از نقش‌ها در سیستم خود استفاده کنید.
public class RoleStore : IRoleStore<IdentityRole>                        
{
    public RoleStore(){...}

    public Task CreateAsync(IdentityRole role){...}

    public Task<IdentityRole> FindByIdAsync(string roleId){...}   
....
}
کلاس‌های بیشتری هم وجود دارند که مختص پیاده سازی مثال جاری هستند.
  • MySQLDatabase: این کلاس اتصال دیتابیس MySql و کوئری‌ها را کپسوله می‌کند. کلاس‌های UserStore و RoleStore توسط نمونه ای از این کلاس وهله سازی می‌شوند.
  • RoleTable: این کلاس جدول Roles و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserClaimsTable: این کلاس جدول UserClaims و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserLoginsTable: این کلاس جدول UserLogins و عملیات CRUD مربوط به آن را کپسوله می‌کند.
  • UserRolesTable: این کلاس جدول UserRoles و عملیات CRUD مربوطه به آن را کپسوله می‌کند.
  • UserTable: این کلاس جدول Users و عملیات CRUD مربوط به آن را کپسوله می‌کند.

ایجاد یک دیتابیس MySQL روی Windows Azure

1. به پورتال مدیریتی Windows Azure وارد شوید.
2. در پایین صفحه روی NEW+ کلیک کنید و گزینه STORE را انتخاب نمایید.

در ویزارد Choose Add-on به سمت پایین اسکرول کنید و گزینه ClearDB MySQL Database را انتخاب کنید. سپس به مرحله بعد بروید.

4. راهکار Free بصورت پیش فرض انتخاب شده، همین گزینه را انتخاب کنید و نام دیتابیس را به IdentityMySQLDatabase تغییر دهید. نزدیک‌ترین ناحیه (region) به خود را انتخاب کنید و به مرحله بعد بروید.

5. روی علامت checkmark کلیک کنید تا دیتابیس شما ایجاد شود. پس از آنکه دیتابیس شما ساخته شد می‌توانید از قسمت ADD-ONS آن را مدیریت کنید.

6. همانطور که در تصویر بالا می‌بینید، می‌توانید اطلاعات اتصال دیتابیس (connection info) را از پایین صفحه دریافت کنید.

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


ایجاد جداول ASP.NET Identity در یک دیتابیس MySQL

ابتدا ابزار MySQL Workbench را نصب کنید.
1. ابزار مذکور را از اینجا دانلود کنید.
2. هنگام نصب، گزینه Setup Type: Custom را انتخاب کنید.
3. در قسمت انتخاب قابلیت ها، گزینه‌های Applications و MySQLWorkbench را انتخاب کنید و مراحل نصب را به اتمام برسانید.
4. اپلیکیشن را اجرا کرده و روی MySQLConnection کلیک کنید تا رشته اتصال جدیدی تعریف کنید. رشته اتصالی که در مراحل قبل از Azure MySQL Database کپی کردید را اینجا استفاده کنید. بعنوان مثال:
 Connection Name: AzureDB; Host Name: us-cdbr-azure-west-b.cleardb.com; Username: <username>; Password: <password>; Default Schema: IdentityMySQLDatabase 
5. پس از برقراری ارتباط با دیتابیس، یک برگ Query جدید باز کنید. فرامین زیر را برای ایجاد جداول مورد نیاز کپی کنید.
CREATE TABLE `IdentityMySQLDatabase`.`users` (
  `Id` VARCHAR(45) NOT NULL,
  `UserName` VARCHAR(45) NULL,
  `PasswordHash` VARCHAR(100) NULL,
  `SecurityStamp` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));

CREATE TABLE `IdentityMySQLDatabase`.`roles` (
  `Id` VARCHAR(45) NOT NULL,
  `Name` VARCHAR(45) NULL,
  PRIMARY KEY (`Id`));

CREATE TABLE `IdentityMySQLDatabase`.`userclaims` (
  `Id` INT NOT NULL AUTO_INCREMENT,
  `UserId` VARCHAR(45) NULL,
  `ClaimType` VARCHAR(100) NULL,
  `ClaimValue` VARCHAR(100) NULL,
  PRIMARY KEY (`Id`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userlogins` (
  `UserId` VARCHAR(45) NOT NULL,
  `ProviderKey` VARCHAR(100) NULL,
  `LoginProvider` VARCHAR(100) NULL,
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) on delete cascade);

CREATE TABLE `IdentityMySQLDatabase`.`userroles` (
  `UserId` VARCHAR(45) NOT NULL,
  `RoleId` VARCHAR(45) NOT NULL,
  PRIMARY KEY (`UserId`, `RoleId`),
  FOREIGN KEY (`UserId`)
    REFERENCES `IdentityMySQLDatabase`.`users` (`Id`) 
on delete cascade
on update cascade,
  FOREIGN KEY (`RoleId`)
    REFERENCES `IdentityMySQLDatabase`.`roles` (`Id`)
on delete cascade
on update cascade);
6. حالا تمام جداول لازم برای ASP.NET Identity را در اختیار دارید، دیتابیس ما MySQL است و روی Windows Azure میزبانی شده.


ایجاد یک اپلیکیشن ASP.NET MVC و پیکربندی آن برای استفاده از MySQL Provider

2. در گوشه سمت راست پایین صفحه روی دکمه Download Zip کلیک کنید تا کل پروژه را دریافت کنید.
3. محتوای فایل دریافتی را در یک پوشه محلی استخراج کنید.
4. پروژه AspNet.Identity.MySQL را باز کرده و آن را کامپایل (build) کنید.
5. روی نام پروژه کلیک راست کنید و گزینه Add, New Project را انتخاب نمایید. پروژه جدیدی از نوع ASP.NET Web Application بسازید و نام آن را به IdentityMySQLDemo تغییر دهید.

6. در پنجره New ASP.NET Project قالب MVC را انتخاب کنید و تنظیمات پیش فرض را بپذیرید.

7. در پنجره Solution Explorer روی پروژه IdentityMySQLDemo کلیک راست کرده و Manage NuGet Packages را انتخاب کنید. در قسمت جستجوی دیالوگ باز شده عبارت "Identity.EntityFramework" را وارد کنید. در لیست نتایج این پکیج را انتخاب کرده و آن را حذف (Uninstall) کنید. پیغامی مبنی بر حذف وابستگی‌ها باید دریافت کنید که مربوط به پکیج EntityFramework است، گزینه Yes را انتخاب کنید. از آنجا که کاری با پیاده سازی فرض نخواهیم داشت، این پکیج‌ها را حذف می‌کنیم.

8. روی پروژه IdentityMySQLDemo کلیک راست کرده و Add, Reference, Solution, Projects را انتخاب کنید. در دیالوگ باز شده پروژه AspNet.Identity.MySQL را انتخاب کرده و OK کنید.

9. در پروژه IdentityMySQLDemo پوشه Models را پیدا کرده و کلاس IdentityModels.cs را حذف کنید.

10. در پروژه IdentityMySQLDemo تمام ارجاعات ";using Microsoft.AspNet.Identity.EntityFramework" را با ";using AspNet.Identity.MySQL" جایگزین کنید.

11. در پروژه IdentityMySQLDemo تمام ارجاعات به کلاس "ApplicationUser" را با "IdentityUser" جایگزین کنید.

12. کنترلر Account را باز کنید و متد سازنده آنرا مطابق لیست زیر تغییر دهید.

public AccountController() : this(new UserManager<IdentityUser>(new UserStore(new MySQLDatabase())))
{

}

13. فایل web.config را باز کنید و رشته اتصال DefaultConnection را مطابق لیست زیر تغییر دهید.

<add name="DefaultConnection" connectionString="Database=IdentityMySQLDatabase;Data Source=<DataSource>;User Id=<UserID>;Password=<Password>" providerName="MySql.Data.MySqlClient" />

مقادیر <DataSource>, <UserId> و <Password> را با اطلاعات دیتابیس خود جایگزین کنید.


اجرای اپلیکیشن و اتصال به دیتابیس MySQL

1. روی پروژه IdentityMySQLDemo کلیک راست کرده و Set as Startup Project را انتخاب کنید.
2. اپلیکیشن را با Ctrl + F5 کامپایل و اجرا کنید.
3. در بالای صفحه روی Register کلیک کنید.
4. حساب کاربری جدیدی بسازید.

5. در این مرحله کاربر جدید باید ایجاد شده و وارد سایت شود.

6. به ابزار MySQL Workbench بروید و محتوای جداول IdentityMySQLDatabase را بررسی کنید. جدول users را باز کنید و اطلاعات کاربر جدید را بررسی نمایید.

برای ساده نگاه داشتن این مقاله از بررسی تمام کدهای لازم خودداری شده، اما اگر مراحل را دنبال کنید و سورس کد نمونه را دریافت و بررسی کنید خواهید دید که پیاده سازی تامین کنندگان سفارشی برای ASP.NET Identity کار نسبتا ساده ای است.

نظرات مطالب
C# 7 - Ref Returns and Ref Locals
یک نکته‌ی تکمیلی: امکان تعریف خروجی از نوع ref readonly در C# 7.2

modifier جدیدی در C# 7.2 به نام ref readonly جهت تعریف نوع خروجی متدها نیز معرفی شده‌است. به این ترتیب یک متد می‌تواند بازگشت ارجاعی به اطلاعاتی موجود را بیان و همچنین فراخوان را از تغییر آن منع کند.
البته فراخوان می‌تواند تصمیم گیری کند که آیا یک کپی و یا یک ارجاع فقط خواندنی را از این متد ویژه دریافت کند. به این معنا که خروجی از نوع ref readonly، فراخوان را ملزم به تعریف یک متغیر محلی از نوع ref readonly نمی‌کند.

در مثال زیر، متد ReturnBiggestA یک خروجی کپی را باز می‌گرداند و متد ReturnBiggestARefReadonly دقیقا ارجاعی را به DataInfo اصلی بازگشت می‌دهد و با آن یکی است:
namespace CS72Tests
{
    public struct DataInfo
    {
        public double A;
    }

    public class RefReadonlyExamples
    {
        public DataInfo ReturnBiggestA(in DataInfo data1, in DataInfo data2)
        {
            return data1.A > data2.A ? data1 : data2;
        }

        public ref readonly DataInfo ReturnBiggestARefReadonly(in DataInfo data1, in DataInfo data2)
        {
            if (data1.A > data2.A)
            {
                return ref data1;
            }
            return ref data2;
        }

        public void TestingRefReadonly()
        {
            var data1 = new DataInfo { A = 0 };
            var data2 = new DataInfo { A = 100 };

            var biggest = ReturnBiggestA(data1, data2);
            biggest.A = 42;


            var biggest2 = ReturnBiggestARefReadonly(data1, data2);
            biggest2.A = 99;


            ref readonly var biggest3 = ref ReturnBiggestARefReadonly(data1, data2);
            biggest3.A = 99; // ERROR: The left-hand side of an assignment must be a variable, property or indexer
        }
    }
}
- در این فراخوانی‌ها، biggest یک کپی از data2 را باز می‌گرداند. به همین جهت می‌توان A آن‌را تغییر داد.
- در اولین فراخوانی ReturnBiggestARefReadonly، با تعریف خروجی به صورت var biggest2، یک کپی از data2 را دریافت کرده‌ایم. به همین جهت A آن قابل تغییر است.
- اما در دومین فراخوانی ReturnBiggestARefReadonly، چون خروجی آن‌را از نوع ref readonly var دریافت کرده‌ایم، این خروجی به data2 اصلی اشاره می‌کند و همچنین فقط خواندنی است. بنابراین سطر بعدی آن که A را تغییر می‌دهد، مجاز نیست.

پ.ن
در ابتدا قصد داشتند ref readonly را برای تعریف پارامترهای value type نیز بکار برند، اما این تصمیم با معرفی پارامترهای از نوع in جایگزین شد. به همین جهت ممکن است مقالات قدیمی‌تر C# 7.2 را با تعریف متدهایی مانند ذیل نیز مشاهده کنید که در نگارش آخر C# 7.2، تمام این‌ها به in تغییر کرده‌اند:
public static void Add(ref readonly int x, ref readonly int y, ref int z)
{
   z = x + y + z;
}
نظرات مطالب
تعریف نوع جنریک به صورت متغیر
سلام آقای نصیری .
ممنون از مطلب مفیدتون. من این کد رو نوشتم برای اینکه یک کلاس رو به متد جنریک بفرستم ولی نمیدونم چطور میتونم T رو به کلاسی که میخوام Cast کنم. ممنون میشم من رو راهنمایی بفرمایید:

var clsType = typeof(MyClass);var method = typeof(TestGenerics).GetMethods().First(x => x.Name == "Print" && (x.GetParameters()[0]).ParameterType.IsGenericParameter);genericMethod = method.MakeGenericMethod(new[] { clsType });genericMethod.Invoke(null, new object[] { new MyClass{P1=100,P2="Name"} });Console.Read();var clsType = typeof(MyClass);var method = typeof(TestGenerics).GetMethods().First(x => x.Name == "Print" && (x.GetParameters()[0]).ParameterType.IsGenericParameter);genericMethod = method.MakeGenericMethod(new[] { clsType });genericMethod.Invoke(null, new object[] { new MyClass{P1=100,P2="Name"} });Console.Read();var method = typeof(TestGenerics).GetMethods().First(x => x.Name == "Print" && (x.GetParameters()[0]).ParameterType.IsGenericParameter);genericMethod = method.MakeGenericMethod(new[] { clsType });genericMethod.Invoke(null, new object[] { new MyClass{P1=100,P2="Name"} });Console.Read();new[] { clsType });genericMethod.Invoke(null, new object[] { new MyClass{P1=100,P2="Name"} });Console.Read();null, new object[] { new MyClass{P1=100,P2="Name"} });Console.Read();Console.Read();و  public static void Print(T data) { if (typeof(T) == typeof(MyClass)){var cls = (MyClass)data;}} که از خط آخر ایراد میگیره static void Print(T data) { if (typeof(T) == typeof(MyClass)){var cls = (MyClass)data;}} که از خط آخر ایراد میگیرهif (typeof(T) == typeof(MyClass)){var cls = (MyClass)data;}} که از خط آخر ایراد میگیرهvar cls = (MyClass)data;}} که از خط آخر ایراد میگیره
مطالب
ابزارهای سراسری در NET Core 2.1.
مفهوم «ابزارها» و یا «project tools» از نگارش اول NET Core. به همراه آن است؛ مانند تنظیم زیر در فایل csproj برنامه‌ها:
<ItemGroup>
   <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.0" />
</ItemGroup>
که سبب فعالسازی ابزار dotnet ef می‌شود و توسط آن می‌توان دستورات dotnet ef database update و یا dotnet ef migrations add را بر روی پروژه‌ی جاری اجرا کرد. در این حالت برنامه dotnet.exe، هاست اجرایی این ابزارهای محلی و مختص به یک پروژه است.
این ایده نیز از npm و ابزارهای محلی و مختص به یک پروژه‌ی آن گرفته شده‌است. اما npm امکان نصب این ابزارها را به صورت سراسری نیز دارد که امکان وجود linters ، test runners و یا  development web servers را میسر کرده‌است و در این حالت نیازی نیست یک چنین ابزارهایی را به ازای هر پروژه نیز یکبار نصب کرد.


معرفی ابزارهای سراسری در NET Core 2.1.

اگر SDK جدید NET Core 2.1 را نصب کرده باشید، پس از Build یک پروژه‌ی مبتنی بر NET Core 2.0. (که توسط فایل global.json، شماره SDK آن محدود و مقید نشده‌است) یک چنین پیام‌های اخطاری را مشاهده خواهید کرد:
warning : Using DotNetCliToolReference to reference 'Microsoft.EntityFrameworkCore.Tools.DotNet' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK.
warning : Using DotNetCliToolReference to reference 'Microsoft.DotNet.Watcher.Tools' is obsolete and can be removed from this project. This tool is bundled by default in the .NET Core SDK.
یکی از ویژگی‌های جدید NET Core 2.1 معرفی global tools یا ابزارهای سراسری آن است. هدف از آن، تهیه برنامه‌های کنسول مبتنی بر NET Core. است که توسط NuGet توزیع و به روز رسانی می‌شوند. توسعه دهندگان جاوا اسکریپت با یک چنین مفهومی تحت عنوان ابزارهای سراسری NPM آشنا هستند (NPM global tools)؛ همان سوئیچ g- که یک ابزار جاوا اسکریپتی را به صورت سراسری نصب می‌کند؛ مانند کامپایلر TypeScript.
پیام‌های اخطار فوق نیز به این معنا هستند که دیگر نیازی نیست تا برای اجرای دستور dotnet watch run، حتما ابزار پروژه‌ی Microsoft.DotNet.Watcher.Tools را به صورت دستی به تمام فایل‌های csproj خود اضافه کنید. ابزار Watcher و یا EntityFrameworkCore.Tools اکنون جزو ابزارهای سراسری NET Core 2.1. هستند و بدون نیازی به افزودن ارجاع خاصی به آن‌ها، هم اکنون در تمام پروژه‌های NET Core. شما قابل دسترسی و استفاده هستند. بنابراین ارجاعات مستقیم به آن‌ها را حذف کنید؛ چون غیرضروری می‌باشند.


روش نصب ابزارهای سراسری در NET Core.

روش نصب ابزارهای سراسری NET Core. به صورت زیر است:
 dotnet tool install -g example
با این دستور، برنامه‌ی فرضی example از نیوگت دریافت شده و ابتدا در یکی از دو پوشه‌ی زیر، فایل فشرده شده‌ی آن باز خواهد شد:
 %USERPROFILE%\.dotnet\toolspkgs (Windows)
 $HOME/.dotnet/toolspkgs (macOS/Linux)
و سپس در ویندوز، در مسیر زیر قرار خواهد گرفت (محل نصب نهایی):
 %USERPROFILE%\.dotnet\tools
این مسیر در لینوکس به صورت زیر است:
 ~/.dotnet/tools

در حال حاضر برای عزل این برنامه‌ها باید به یکی از این مسیرها مراجعه و آن‌ها را دستی حذف کرد (در هر دو مسیر toolspkgs و tools باید حذف شوند).


یک نمونه از این ابزارها را که dotnet-dev-certs نام دارد، پس از نصب SDK جدید، در مکان‌های یاد شده، خواهید یافت. کار این ابزار سراسری، تولید یک self signed certificate مخصوص برنامه‌های ASP.NET Core 2.1 است که پیشتر در مطلب «اجبار به استفاده‌ی از HTTPS در حین توسعه‌ی برنامه‌های ASP.NET Core 2.1» آن‌را بررسی کردیم.

نکته 1: بر اساس تصویر فوق، در خط فرمان، دستور dotnet-dev-certs را صادر کنید. اگر پیام یافت نشدن این دستور یا ابزار را مشاهده کردید، به معنای این است که مسیر نصب آن‌ها به PATH سیستم اضافه نشده‌است. با استفاده از دستورات ذیل می‌توانید این مسیر را به PATH سیستم اضافه کنید:
Windows PowerShell:
setx PATH "$env:PATH;$env:USERPROFILE/.dotnet/tools"
Linux/macOS:
echo "export PATH=\"\$PATH:\$HOME/.dotnet/tools\"" >> ~/.bash_profile

نکته 2: اگر به این مسیرها دقت کنید، این ابزارها صرفا برای کاربر جاری سیستم نصب می‌شوند و مختص به او هستند؛ به عبارتی user-specific هستند و نه machine global.


روش ایجاد ابزارهای سراسری NET Core.

همانطور که عنوان شد، ابزارهای سراسری NET Core. در اصل برنامه‌های کنسول آن هستند. به همین جهت پس از نصب SDK جدید، در یک پوشه‌ی جدید، دستور dotnet new console را اجرا کنید تا یک برنامه‌ی کنسول جدید مطابق آن ایجاد شود. سپس فایل csproj آن‌را به صورت زیر ویرایش کنید:
<Project Sdk="Microsoft.NET.Sdk">  
   <PropertyGroup>
     <OutputType>Exe</OutputType>
     <IsPackable>true</IsPackable>
     <PackAsTool>true</PackAsTool>
     <TargetFramework>netcoreapp2.1</TargetFramework>
   </PropertyGroup>  
</Project>
در اینجا دو خاصیت IsPackable و PackAsTool جدید بوده و مختص به ابزارهای سراسری NET Core. هستند. تنظیم همین دو خاصیت برای تبدیل یک برنامه‌ی کنسول معمولی به ابزار سراسری کافی است.
پس از آن برای تهیه‌ی یک بسته‌ی نیوگت از آن، دستور زیر را اجرا کنید:
 dotnet pack -c Release
پس از ارسال فایل nupkg حاصل به سایت نیوگت، کاربران آن می‌توانند توسط دستور زیر آن‌را نصب کنند:
 dotnet tool install -g package-name
مطالب
Pipeها در Angular 2 - بخش اول
برای تغییر نحوه نمایش یک عبارت در رابط کاربری، از Pipe استفاده می‌شود. مثلا ممکن است تاریخ تولد به صورت میلادی از سرور دریافت شده باشد، می‌خواهیم بدون تغییری در متغیر حامل تاریخ میلادی و فقط در لایه رابط کاربری، کاربر تاریخ را به صورت شمسی مشاهده کند. به عبارت دیگر برای تغییر نحوه نمایش مقدار نمایشی (display-value) در صفحات HTML خود، از Pipe استفاده می‌شود. 

نحوه استفاده از Pipe

Pipe یک متغیر یا عبارت را به عنوان ورودی دریافت کرده و آن‌را به شکل دیگری برای نمایش تغییر می‌دهد. Pipeها معمولا در صفحات HTML مورد استفاده قرار می‌گیرند. با استفاده از عملگر Pipe (|) به شکل زیر می‌توانید Pipe مورد نظر خود را اعمال کنید.

import { Component } from '@angular/core';

@Component({
  selector: 'hero-birthday',
  template: `<p>The hero's birthday is {{ birthday | date }}</p>`
})
export class HeroBirthdayComponent {
  birthday = new Date(1988, 3, 15); // April 15, 1988
}

در این مثال Pipe از قبل ساخته شده date را بر روی متغییر birthday اعمال می‌کنیم. خروجی کار به شکل زیر خواهد بود.

The hero's birthday is April 15, 1988

لازم به ذکر است Pipe هیچگونه اثری بر روی متغییر birthday نداشته و فقط نحوه نمایش آن را تغییر می‌دهد.

 

Pipeهای از پیش ساخته شده

در انگیولار ۲ یکسری Pipe از پیش ساخته شده مانند DatePipe، UpperCasePipe، LowerCasePipe، CurrencyPipe و PercentPipe وجود دارند که شما در تمامی صفحات HTML می‌توانید بدون هیچ تنظیم اضافه‌ای از آنها استفاده کنید. لیست Pipeهای از پیش ساخته شده را اینجا مشاهده کنید. 


ارسال پارامتر به Pipe

  Pipeها در انگیولار ۲ می‌توانند تعدادی پارامتر ورودی را برای ارائه خروجی مدنظر دریافت کنند. برای فرستادن پارامتر به Pipe، بلافاصله بعد از نام Pipe با استفاده از دو نقطه (:) پارامتر مورد نظر را ارسال می‌کنیم (برای مثال 'currency:'EUR). درصورتیکه Pipe چند پارامتر را دریافت می‌کند، هر پارامتر با یک دو نقطه (:) از هم جدا می‌شوند؛ برای مثال slice:1:5.
  برای نمونه اگر بخواهیم تاریخ موجود در متغیر birthday در مثال قبل را به صورت «04/15/88» نمایش دهیم کافی است پارامتر «MM/dd/yy» را به datePipe ارسال کنیم.
<p>The hero's birthday is {{ birthday | date:"MM/dd/yy" }} </p>

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

 

Template expressions: عباراتی (expressions) که بعد از اجرا توسط انگیولار، تبدیل به یک مقدار جهت نمایش در HTML، کامپوننت یا دایرکتیو می‌شود.


 

استفاده زنجیره‌ای از Pipeها

برای اعمال چند Pipe بر روی یک عبارت، می‌توان از Pipeها به صورت پشت سر هم استفاده کرد. برای مثال در ادامه می‌خواهیم علاوه بر اعمال DatePipe با پارامتر fullDate جهت نمایش تاریخ به صورت Friday, April 15, 1988، حروف را نیز به صورت UpperCase نمایش دهیم. لازم به ذکر است برای نمایش حروف به صورت UpperCase از Pipe به همین نام استفاده می‌کنیم. 

<p>The hero's birthday is {{ birthday | date:"fullDate" | uppercase}} </p>

خروجی کار به شکل زیر خواهد بود. 

The hero's birthday is FRIDAY, APRIL 15, 1988


استفاده از Pipe در TypeScript

برای کسانیکه با انگیولار یک آشنایی دارند، Pipe در انگولار ۲ معادل filter در انگولار یک است. در انگیولار یک با تزریق سرویس filter$ به کنترلرها یا سرویس‌ها، می‌توانستیم filterهای تعریف شده شخصی و از پیش ساخته شده را جهت اعمال بر روی متغیر‌های خود، استفاده کنیم. در انگیولار دو نیز این امکان فراهم شده‌است؛ ولی به سادگی تزریق filter$ نیست. یعنی لازم است علاوه بر تزریق Pipe به سرویس یا کامپوننت‌های خود، Pipe مورد نظر خود را در لیست providers ماژول خود نیز اضافه کنید. برای نمونه اگر بخواهیم DatePipe را در component خود (نه در template) مورد استفاده قرار دهیم به شکل زیر عمل می‌کنیم:

import { Component } from '@angular/core';

// اضافه کردن DatePipe از @angular/common
import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'app works!';
  birthDay = new Date(1988, 3, 15);
  strBirthDay = "";

// تزریق DatePipe
  constructor(private datePipe: DatePipe) {
    this.strBirthDay = this.datePipe.transform(this.birthDay, 'yyyy-MM-dd');
  }
}

پس از وارد کردن DatePipe از angular/common@ به کامپوننت و تزریق آن از طریق سازنده کامپوننت، برای اعمال Pipe بر روی عبارت مورد نظر خود، از متد transform استفاده می‌کنیم.

  تمامی Pipeها به واسطه پیاده سازی PipeTransform دارای متد transform هستند. این متد در اولین پارامتر خود، عبارتی را که قرار است Pipe بر روی آن اعمال شود، دریافت می‌کند. در صورتیکه Pipe مورد نظر دارای پارامتر باشد از طریق پارامتر دوم به بعد آنرا دریافت می‌کند.
همانطور که قبلا اشاره شد، علاوه بر تزریق Pipe در کامپوننت یا سرویس، Pipe باید در لیست providers ماژول نیز اضافه شود.
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { DatePipe } from '@angular/common'
import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  // افزودن Pipe به Providers
  providers: [DatePipe],
  bootstrap: [AppComponent]
})
export class AppModule { }

نکته‌ای در مورد DatePipe و CurrencyPipe

Pipeهای Date و Currency به دلیل استفاده از شی Intl در داخل خود نیاز به ECMAScript Internationalization API دارند. مرورگرهای قدیمی و همچنین مرورگر Safari به دلیل عدم پشتیبانی از این قضیه به هنگام استفاده از این Pipeها دچار مشکل می‌شوند. برای حل این مشکل کافی است اسکریپت زیر را به صفحه خود اضافه کنید. 

<script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=Intl.~locale.en"></script>

در بخش‌های بعدی نحوه ساخت Pipe‌های سفارشی و همچنین نکات تکمیلی در مورد Pipeها را بررسی خواهیم کرد.

مطالب
Angular Material 6x - قسمت چهارم - نمایش پویای اطلاعات تماس‌ها
در قسمت قبل، یک لیست ثابت item 1/item 2/… را در sidenav نمایش دادیم. در این قسمت می‌خواهیم این لیست را با اطلاعات دریافت شده‌ی از سرور، پویا کنیم و همچنین با کلیک بر روی هر کدام، جزئیات آن‌ها را نیز در قسمت main-content نمایش دهیم.



تهیه سرویس اطلاعات پویای برنامه

سرویس Web API ارائه شده‌ی توسط ASP.NET Core در این برنامه، لیست کاربران را به همراه یادداشت‌های آن‌ها به سمت کلاینت باز می‌گرداند و ساختار موجودیت‌های آن‌ها به صورت زیر است:

موجودیت کاربر که یک رابطه‌ی one-to-many را با UserNotes دارد:
using System;
using System.Collections.Generic;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class User
    {
        public User()
        {
            UserNotes = new HashSet<UserNote>();
        }

        public int Id { set; get; }
        public DateTimeOffset BirthDate { set; get; }
        public string Name { set; get; }
        public string Avatar { set; get; }
        public string Bio { set; get; }

        public ICollection<UserNote> UserNotes { set; get; }
    }
}
و موجودیت یادداشت‌های کاربر که سر دیگر رابطه را تشکیل می‌دهد:
using System;

namespace MaterialAspNetCoreBackend.DomainClasses
{
    public class UserNote
    {
        public int Id { set; get; }
        public DateTimeOffset Date { set; get; }
        public string Title { set; get; }

        public User User { set; get; }
        public int UserId { set; get; }
    }
}
در نهایت اطلاعات ذخیره شده‌ی در بانک اطلاعاتی توسط سرویس کاربران:
    public interface IUsersService
    {
        Task<List<User>> GetAllUsersIncludeNotesAsync();
        Task<User> GetUserIncludeNotesAsync(int id);
    }
در اختیار کنترلر Web API برنامه، برای ارائه‌ی به سمت کلاینت، قرار می‌گیرد:
namespace MaterialAspNetCoreBackend.WebApp.Controllers
{
    [Route("api/[controller]")]
    public class UsersController : Controller
    {
        private readonly IUsersService _usersService;

        public UsersController(IUsersService usersService)
        {
            _usersService = usersService ?? throw new ArgumentNullException(nameof(usersService));
        }

        [HttpGet]
        public async Task<IActionResult> Get()
        {
            return Ok(await _usersService.GetAllUsersIncludeNotesAsync());
        }

        [HttpGet("{id:int}")]
        public async Task<IActionResult> Get(int id)
        {
            return Ok(await _usersService.GetUserIncludeNotesAsync(id));
        }
    }
}
کدهای کامل لایه سرویس، تنظیمات EF Core و تنظیمات ASP.NET Core این قسمت را از پروژه‌ی پیوستی انتهای بحث می‌توانید دریافت کنید.
در این حالت اگر برنامه را اجرا کنیم، در مسیر زیر
 https://localhost:5001/api/users
یک چنین خروجی قابل مشاهده خواهد بود:


و آدرس https://localhost:5001/api/users/1 صرفا مشخصات اولین کاربر را بازگشت می‌دهد.


تنظیم محل تولید خروجی Angular CLI

ساختار پوشه بندی پروژه‌ی جاری به صورت زیر است:


همانطور که ملاحظه می‌کنید، کلاینت Angular در یک پوشه‌است و برنامه‌ی سمت سرور ASP.NET Core در پوشه‌ای دیگر. برای اینکه خروجی نهایی Angular CLI را به پوشه‌ی wwwroot پروژه‌ی وب کپی کنیم، فایل angular.json کلاینت Angular را به صورت زیر ویرایش می‌کنیم:
"build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "../MaterialAspNetCoreBackend/MaterialAspNetCoreBackend.WebApp/wwwroot",
تنظیم این outputPath به wwwroot پروژه‌ی وب سبب خواهد شد تا با صدور فرمان زیر:
 ng build --no-delete-output-path --watch
برنامه‌ی Angular در حالت watch (گوش فرا دادان به تغییرات فایل‌ها) کامپایل شده و سپس به صورت خودکار در پوشه‌ی MaterialAspNetCoreBackend.WebApp/wwwroot کپی شود. به این ترتیب پس از اجرای برنامه‌ی ASP.NET Core توسط دستور زیر:
 dotnet watch run
 این برنامه‌ی سمت سرور، در همان لحظه هم API خود را ارائه خواهد داد و هم هاست برنامه‌ی Angular می‌شود.
بنابراین دو صفحه‌ی کنسول مجزا را باز کنید. در اولی ng build (را با پارامترهای یاد شده در پوشه‌ی MaterialAngularClient) و در دومی dotnet watch run را در پوشه‌ی MaterialAspNetCoreBackend.WebApp اجرا نمائید.
هر دو دستور در حالت watch اجرا می‌شوند. مزیت مهم آن این است که اگر تغییر کوچکی را در هر کدام از پروژه‌ها ایجاد کردید، صرفا همان قسمت کامپایل می‌شود و در نهایت سرعت کامپایل نهایی برنامه به شدت افزایش خواهد یافت.


تعریف معادل‌های کلاس‌های موجودیت‌های سمت سرور، در برنامه‌ی Angular

در ادامه پیش از تکمیل سرویس دریافت اطلاعات از سرور، نیاز است معادل‌های کلاس‌های موجودیت‌های سمت سرور خود را به صورت اینترفیس‌هایی تایپ‌اسکریپتی تعریف کنیم:
ng g i contact-manager/models/user
ng g i contact-manager/models/user-note
این دستورات دو اینترفیس خالی کاربر و یادداشت‌های او را در پوشه‌ی جدید models ایجاد می‌کنند. سپس آن‌ها را به صورت زیر و بر اساس تعاریف سمت سرور آن‌ها، تکمیل می‌کنیم:
محتویات فایل contact-manager\models\user-note.ts :
export interface UserNote {
  id: number;
  title: string;
  date: Date;
  userId: number;
}
محتویات فایل contact-manager\models\user.ts :
import { UserNote } from "./user-note";

export interface User {
  id: number;
  birthDate: Date;
  name: string;
  avatar: string;
  bio: string;

  userNotes: UserNote[];
}


ایجاد سرویس Angular دریافت اطلاعات از سرور

ساختار ابتدایی سرویس دریافت اطلاعات از سرور را توسط دستور زیر ایجاد می‌کنیم:
 ng g s contact-manager/services/user --no-spec
که سبب ایجاد فایل user.service.ts در پوشه‌ی جدید services خواهد شد:
import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor() { }
}
قسمت providedIn آن مخصوص Angular 6x است و هدف از آن کم حجم‌تر کردن خروجی نهایی برنامه‌است؛ اگر از سرویسی که تعریف شده، در برنامه جائی استفاده نشده‌است. به این ترتیب دیگر نیازی نیست تا آن‌را به صورت دستی در قسمت providers ماژول جاری ثبت و معرفی کرد.
کدهای تکمیل شده‌ی UserService را در ذیل مشاهده می‌کنید:
import { HttpClient, HttpErrorResponse } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { Observable, throwError } from "rxjs";
import { catchError, map } from "rxjs/operators";

import { User } from "../models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {

  constructor(private http: HttpClient) { }

  getAllUsersIncludeNotes(): Observable<User[]> {
    return this.http
      .get<User[]>("/api/users").pipe(
        map(response => response || []),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }

  getUserIncludeNotes(id: number): Observable<User> {
    return this.http
      .get<User>(`/api/users/${id}`).pipe(
        map(response => response || {} as User),
        catchError((error: HttpErrorResponse) => throwError(error))
      );
  }
}
در اینجا از pipe-able operators مخصوص RxJS 6x استفاده شده که در مطلب «ارتقاء به Angular 6: بررسی تغییرات RxJS» بیشتر در مورد آن‌ها بحث شده‌است.
- متد getAllUsersIncludeNotes، لیست تمام کاربران را به همراه یادداشت‌های آن‌ها از سرور واکشی می‌کند.
- متد getUserIncludeNotes صرفا اطلاعات یک کاربر را به همراه یادداشت‌های او از سرور دریافت می‌کند.


بارگذاری و معرفی فایل svg نمایش avatars کاربران به Angular Material

بسته‌ی Angular Material و کامپوننت mat-icon آن به همراه یک MatIconRegistry نیز هست که قصد داریم از آن برای نمایش avatars کاربران استفاده کنیم.
در قسمت اول، نحوه‌ی «افزودن آیکن‌های متریال به برنامه» را بررسی کردیم که در آنجا آیکن‌های مرتبط، از فایل‌های قلم، دریافت و نمایش داده می‌شوند. این کامپوننت، علاوه بر قلم آیکن‌ها، از فایل‌های svg حاوی آیکن‌ها نیز پشتیبانی می‌کند که یک نمونه از این فایل‌ها در مسیر wwwroot\assets\avatars.svg فایل پیوستی انتهای مطلب کپی شده‌است (چون برنامه‌ی وب ASP.NET Core، هاست برنامه است، این فایل را در آنجا کپی کردیم).
ساختار این فایل svg نیز به صورت زیر است:
<?xml version="1.0" encoding="utf-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink">
    <defs>
    <svg viewBox="0 0 128 128" height="100%" width="100%" 
             pointer-events="none" display="block" id="user1" >
هر svg تعریف شده‌ی در آن دارای یک id است. از این id به عنوان نام avatar کاربرها استفاده خواهیم کرد. نحوه‌ی فعالسازی آن نیز به صورت زیر است:
ابتدا به فایل contact-manager-app.component.ts مراجعه و سپس این کامپوننت آغازین ماژول مدیریت تماس‌ها را با صورت زیر تکمیل می‌کنیم:
import { Component } from "@angular/core";
import { MatIconRegistry } from "@angular/material";
import { DomSanitizer } from "@angular/platform-browser";

@Component()
export class ContactManagerAppComponent {

  constructor(iconRegistry: MatIconRegistry, sanitizer: DomSanitizer) {
    iconRegistry.addSvgIconSet(sanitizer.bypassSecurityTrustResourceUrl("assets/avatars.svg"));
  }
  
}
MatIconRegistry جزئی از بسته‌ی angular/material است که در ابتدای کار import شده‌است. متد addSvgIconSet آن، مسیر یک فایل svg حاوی آیکن‌های مختلف را دریافت می‌کند. این مسیر نیز باید توسط سرویس DomSanitizer در اختیار آن قرار گیرد که در کدهای فوق روش انجام آن‌را ملاحظه می‌کنید. در مورد سرویس DomSanitizer در مطلب «نمایش HTML در برنامه‌های Angular» بیشتر بحث شده‌است.
در اینجا در صورتیکه فایل svg شما دارای یک تک آیکن است، روش ثبت آن به صورت زیر است:
iconRegistry.addSvgIcon(
      "unicorn",
      this.domSanitizer.bypassSecurityTrustResourceUrl("assets/unicorn_icon.svg")
    );
که در نهایت کامپوننت mat-icon، این آیکن را به صورت زیر می‌تواند نمایش دهد:
 <mat-icon svgIcon="unicorn"></mat-icon>

یک نکته: پوشه‌ی node_modules\material-design-icons به همراه تعداد قابل ملاحظه‌ای فایل svg نیز هست.


نمایش لیست کاربران در sidenav

در ادامه به فایل sidenav\sidenav.component.ts مراجعه کرده و سرویس فوق را به آن جهت دریافت لیست کاربران، تزریق می‌کنیم:
import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component()
export class SidenavComponent implements OnInit {

  users: User[] = [];

  constructor(private userService: UserService) {  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => this.users = data);
  }
}
به این ترتیب با اجرای برنامه و بارگذاری sidenav، در رخ‌داد OnInit آن، کار دریافت اطلاعات کاربران و انتساب آن به خاصیت عمومی users صورت می‌گیرد.

اکنون می‌خواهیم از این اطلاعات جهت نمایش پویای آن‌ها در sidenav استفاده کنیم. در قسمت قبل، جای آن‌ها را در منوی سمت چپ صفحه به صورت زیر با اطلاعات ایستا مشخص کردیم:
    <mat-list>
      <mat-list-item>Item 1</mat-list-item>
      <mat-list-item>Item 2</mat-list-item>
      <mat-list-item>Item 3</mat-list-item>
    </mat-list>
اگر به مستندات mat-list مراجعه کنیم، در میانه‌ی صفحه، navigation lists نیز ذکر شده‌است که می‌تواند لیستی پویا را به همراه لینک به آیتم‌های آن نمایش دهد و این مورد دقیقا کامپوننتی است که در اینجا به آن نیاز داریم. بنابراین فایل sidenav\sidenav.component.html را گشوده و mat-list فوق را با mat-nav-list تعویض می‌کنیم:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine href="#">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
اکنون اگر برنامه را اجرا کنیم، یک چنین شکلی قابل مشاهده است:


که در اینجا علاوه بر لیست کاربران که از سرویس Users دریافت شده، آیکن avatar آن‌ها که از فایل assets/avatars.svg بارگذاری شده نیز قابل مشاهده است.


اتصال کاربران به صفحه‌ی نمایش جزئیات آن‌ها

در mat-nav-list فوق، فعلا هر کاربر به آدرس # لینک شده‌است. در ادامه می‌خواهیم با کمک سیستم مسیریابی، با کلیک بر روی نام هر کاربر، در سمت راست صفحه جزئیات او نیز نمایش داده شود:
    <mat-nav-list>
      <mat-list-item *ngFor="let user of users">
        <a matLine [routerLink]="['/contactmanager', user.id]">
          <mat-icon svgIcon="{{user.avatar}}"></mat-icon> {{ user.name }}
        </a>
      </mat-list-item>
    </mat-nav-list>
در اینجا با استفاده از routerLink، هر کاربر را بر اساس Id او، به صفحه‌ی جزئیات آن شخص، متصل کرده‌ایم. البته این مسیریابی برای اینکه کار کند باید به صورت زیر به فایل contact-manager-routing.module.ts اضافه شود:
const routes: Routes = [
  {
    path: "", component: ContactManagerAppComponent,
    children: [
      { path: ":id", component: MainContentComponent },
      { path: "", component: MainContentComponent }
    ]
  },
  { path: "**", redirectTo: "" }
];
البته اگر تا اینجا برنامه را اجرا کنید، با نزدیک کردن اشاره‌گر ماوس به نام هر کاربر، آدرسی مانند https://localhost:5001/contactmanager/1 در status bar مرورگر ظاهر خواهد شد، اما با کلیک بر روی آن، اتفاقی رخ نمی‌دهد.
این مشکل دو علت دارد:
الف) چون ContactManagerModule را به صورت lazy load تعریف کرده‌ایم، دیگر نباید در لیست imports فایل AppModule ظاهر شود. بنابراین فایل app.module.ts را گشوده و سپس تعریف ContactManagerModule را هم از قسمت imports بالای صفحه و هم از قسمت imports ماژول حذف کنید؛ چون نیازی به آن نیست.
ب) برای مدیریت خواندن id کاربر، فایل main-content\main-content.component.ts را گشوده و به صورت زیر تکمیل می‌کنیم:
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router";

import { User } from "../../models/user";
import { UserService } from "../../services/user.service";

@Component({
  selector: "app-main-content",
  templateUrl: "./main-content.component.html",
  styleUrls: ["./main-content.component.css"]
})
export class MainContentComponent implements OnInit {

  user: User;
  constructor(private route: ActivatedRoute, private userService: UserService) { }

  ngOnInit() {
    this.route.params.subscribe(params => {
      this.user = null;

      const id = params["id"];
      if (!id) {
        return;
      }

      this.userService.getUserIncludeNotes(id)
        .subscribe(data => this.user = data);
    });
  }
}
در اینجا به کمک سرویس ActivatedRoute و گوش فرادادن به تغییرات params آن در ngOnInit، مقدار id مسیر دریافت می‌شود. سپس بر اساس این id، با کمک سرویس کاربران، اطلاعات این تک کاربر از سرور دریافت و به خاصیت عمومی user نسبت داده خواهد شد.
اکنون می‌توان از اطلاعات این user دریافتی، در قالب این کامپوننت و یا همان فایل main-content.component.html استفاده کرد:
<div *ngIf="!user">
  <mat-spinner></mat-spinner>
</div>
<div *ngIf="user">
  <mat-card>
    <mat-card-header>
      <mat-icon mat-card-avatar svgIcon="{{user.avatar}}"></mat-icon>
      <mat-card-title>
        <h2>{{ user.name }}</h2>
      </mat-card-title>
      <mat-card-subtitle>
        Birthday {{ user.birthDate | date:'d LLLL' }}
      </mat-card-subtitle>
    </mat-card-header>
    <mat-card-content>
      <mat-tab-group>
        <mat-tab label="Bio">
          <p>
            {{user.bio}}
          </p>
        </mat-tab>
        <!-- <mat-tab label="Notes"></mat-tab> -->
      </mat-tab-group>
    </mat-card-content>
  </mat-card>
</div>
در اینجا از کامپوننت mat-spinner برای نمایش حالت منتظر بمانید استفاده کرده‌ایم. اگر user نال باشد، این spinner نمایش داده می‌شود و برعکس.


همچنین mat-card را هم بر اساس مثال مستندات آن، ابتدا کپی و سپس سفارشی سازی کرده‌ایم (اگر دقت کنید، هر کامپوننت آن سه برگه‌ی overview، سپس API و در آخر Example را به همراه دارد). این روشی است که همواره می‌توان با کامپوننت‌های این مجموعه انجام داد. ابتدا مثالی را در مستندات آن پیدا می‌کنیم که مناسب کار ما باشد. سپس سورس آن‌را از همانجا کپی و در برنامه قرار می‌دهیم و در آخر آن‌را بر اساس اطلاعات خود سفارشی سازی می‌کنیم.



نمایش جزئیات اولین کاربر در حین بارگذاری اولیه‌ی برنامه

تا اینجای کار اگر برنامه را از ابتدا بارگذاری کنیم، mat-spinner قسمت نمایش جزئیات تماس‌ها ظاهر می‌شود و همانطور باقی می‌ماند، با اینکه هنوز موردی انتخاب نشده‌است. برای رفع آن به کامپوننت sidnav مراجعه کرده و در لحظه‌ی بارگذاری اطلاعات، اولین مورد را به صورت دستی نمایش می‌دهیم:
import { Router } from "@angular/router";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  users: User[] = [];
  
  constructor(private userService: UserService, private router: Router) {
  }

  ngOnInit() {
    this.userService.getAllUsersIncludeNotes()
      .subscribe(data => {
        this.users = data;
        if (data && data.length > 0 && !this.router.navigated) {
          this.router.navigate(["/contactmanager", data[0].id]);
        }
      });
  }
}
در اینجا ابتدا سرویس Router به سازنده‌ی کلاس تزریق شده‌است و سپس زمانیکه کار دریافت اطلاعات تماس‌ها پایان یافت و this.router.navigated نبود (یعنی پیشتر هدایت به آدرسی صورت نگرفته بود؛ برای مثال کاربر آدرس id داری را ریفرش نکرده بود)، اولین مورد را توسط متد this.router.navigate فعال می‌کنیم که سبب تغییر آدرس صفحه از https://localhost:5001/contactmanager به https://localhost:5001/contactmanager/1 و باعث نمایش جزئیات آن می‌شود.

البته روش دیگر مدیریت این حالت، حذف کدهای فوق و تبدیل کدهای کامپوننت main-content به صورت زیر است:
let id = params['id'];
if (!id) id = 1;
در اینجا اگر id انتخاب نشده باشد، یعنی اولین بار نمایش برنامه است و خودمان id مساوی 1 را برای آن در نظر می‌گیریم.


بستن خودکار sidenav در حالت نمایش موبایل

اگر اندازه‌ی صفحه‌ی نمایشی را کوچکتر کنیم، قسمت sidenav در حالت over نمایان خواهد شد. در این حالت اگر آیتم‌های آن‌را انتخاب کنیم، هرچند آن‌ها نمایش داده می‌شوند، اما زیر این sidenav مخفی باقی خواهند ماند:


بنابراین در جهت بهبود کاربری این قسمت بهتر است با کلیک کاربر بر روی sidenav و گزینه‌های آن، این قسمت بسته شده و ناحیه‌ی زیر آن نمایش داده شود.
در کدهای قالب sidenav، یک template reference variable برای آن به نام sidenav درنظر گرفته شده‌است:
<mat-sidenav #sidenav
برای دسترسی به آن در کدهای کامپوننت خواهیم داشت:
import { MatSidenav } from "@angular/material";

@Component()
export class SidenavComponent implements OnInit, OnDestroy {

  @ViewChild(MatSidenav) sidenav: MatSidenav;
اکنون که به این ViewChild دسترسی داریم، می‌توانیم در حالت نمایشی موبایل، متد close آن‌را فراخوانی کنیم:
  ngOnInit() {
    this.router.events.subscribe(() => {
      if (this.isScreenSmall) {
        this.sidenav.close();
      }
    });
  }
در اینجا با مشترک this.router.events شدن، متوجه‌ی کلیک کاربر و نمایش صفحه‌ی جزئیات آن می‌شویم. در قسمت سوم این مجموعه نیز خاصیت isScreenSmall را بر اساس ObservableMedia مقدار دهی کردیم. بنابراین اگر کاربر بر روی گزینه‌ای کلیک کرده بود و همچنین اندازه‌ی صفحه در حالت موبایل قرار داشت، sidenav را خواهیم بست تا بتوان محتوای زیر آن‌را مشاهده کرد:



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MaterialAngularClient-03.zip
برای اجرای آن:
الف) ابتدا به پوشه‌ی src\MaterialAngularClient وارد شده و فایل‌های restore.bat و ng-build-dev.bat را اجرا کنید.
ب) سپس به پوشه‌ی src\MaterialAspNetCoreBackend\MaterialAspNetCoreBackend.WebApp وارد شده و فایل‌های restore.bat و dotnet_run.bat را اجرا کنید.
اکنون برنامه در آدرس https://localhost:5001 قابل دسترسی است.