نظرات مطالب
آشنایی با مفاهیم نوع داده Enum و توسعه آن - قسمت دوم
با سلام خدمت شما دوست عزیز.  همانطور که آقای نصیری اشاره کردند هدف این مطلب و البته این سری از مطالب آشنایی قدم به قدم با مفاهیم این نوع داده بوده است.
انقیاد کنترل‌ها با اشیاء از نوع Enum موضوع بعدی این سری از مطالب بود. البته باز هم از شما تشکر می‌کنم بخاطر این نظر.
اگر این قابلیت در سایت ایجاد شود که بتوان به نظرات لینک داد بهتر است.
مطالب
intern pool جدول نگهداری رشته‌ها در دات‌نت
کد زیر را در نظر بگیرید :
object text1 = "test";
object text2 = "test";

object num1 = 1;
object num2 = 1;

Console.WriteLine("text1 == text2 : " + (text1 == text2));
Console.WriteLine("num1 == num2 : " + (num1 == num2));

به نظر شما چه چیزی در خروجی نمایش داده میشود؟

هر چهار متغییر text1  و text2 و num1 و num2 از نوع object هستند. با اینکه مقدار text1 و text2 یکی و مقدار num1 و num2 هم یکی است، نتیجه text1==text2 برابر true است اما num1==num2 برابر false.

خطی که text2 تعریف شده است را تغییر میدهیم :
object text2 = "test".ToLower();

اینبار با این که باز مقدار text1 و text2 یکی و هر دو "test" است، اما نتیجه text1==text2 برابر false است. انتظار ما هم همین است. دو object ایجاد شده است و یکی نیستند. تنها در صورتی باید نتیجه == آنها true باشد که هر دو به یک object اشاره کنند.

اما چرا در کد اولی اینگونه نبود؟

دلیل این کار برمیگردد به رفتار دات‌نت نسبت به رشتههایی که به صورت صریح در برنامه تعریف میشوند. CLR یک جدول برای ذخیره رشتهها به نام intern pool برای برنامه میسازد. هر رشتهای تعریف میشود، اگر در intern pool رشتهای با همان مقدار وجود نداشته باشد، یک رشته جدید ایجاد و به جدول اضافه میشود، و اگر موجود باشد متغییر جدید فقط به آن اشاره میکند. در واقع اگر 100 جای برنامه حتی در کلاسهای مختلف، رشتههایی با مقادیر یکسان وجود داشته باشند، برای همه آنها یک نمونه وجود دارد.

بنابراین text1 و text2 در کد اولی واقعا یکی هستند و یک نمونه برای آنها ایجاد شده است.

البته چند نکته در اینجا هست :
اگر text1 و text2 به صورت string تعریف شوند، نتیجه text1==text2 در هر دو حالت فوق برابر true است. چون عملگر == در کلاس string یکبار دیگر overload شده است:
public sealed class String : ...
{
    ...
    public static bool operator ==(string a, string b)
    {
      return string.Equals(a, b);
    }
   ...
}

این که کدام یک از overload‌ها اجرا شوند (کلاس پایه، کلاس اصلی، ...) به نوع دو متغییر اطراف == بستگی دارد. مثلا در کد زیر :
string text1 = "test";
string text2 = "test".ToLower();

Console.WriteLine("text1 == text2 (string) : " + (text1 == text2));
Console.WriteLine("text1 == text2 (object) : " + ((object)text1 == (object)text2));

اولین نتیجه true و دومی false است. چون در اولی عملگر == تعریف شده در کلاس string مورد استفاده قرار می‏‏گیرد اما در دومی عملگر == تعریف شده در کلاس object.

اگر دقت نشود این رفتار مشکلزا میشود. مثلا حالتی را در نظر بگیرید که text1 ورودی کاربر است و text2 از بانک اطلاعاتی خوانده شده است و با اینکه مقادیر یکسان دارند نتیجه == آنها false است. اگر تعریف عملگرها در کلاس object به صورت virtual بود و در کلاس‌های دیگر override می‌شد، این تغییر نوع‌ها تاثیری نداشت. اما عملگرها به صورت static تعریف می‌شوند و امکان override شدن ندارند. به همین خاطر کلاس object متدی به اسم Equals در اختیار گذاشته که کلاس‌ها آنرا override می‌کنند و معمولا از این متد برای سنجش برابری دو کلاس استفاده می‌شود :
object text1 = "test";
object text2 = "test".ToLower();

Console.WriteLine("text1 Equals text2 : " + text1.Equals(text2));
Console.WriteLine("text1 Equals text2 : " + object.Equals(text1, text2));

البته یادآور می‌شوم که فقط رشته‌هایی که به صورت صریح در برنامه تعریف شده‌اند، در intern pool قرار می‌گیرند و این فهرست شامل رشته‌هایی که از فایل یا بانک اطلاعاتی خوانده می‌شوند یا در برنامه تولید می‌شوند، نیست. این کار منطقی است وگرنه حافظه زیادی مصرف خواهد شد.

با استفاده از متد string.Intern می‌توان یک رشته را که در intern pool وجود ندارد، به فهرست آن افزود. اگر رشته در intern pool وجود داشته باشد، reference آنرا بر می‌گرداند در غیر اینصورت یک reference به رشته جدید به intern pool اضافه می‌کند و آنرا برمی‌گرداند.

یک مورد استفاده آن هنگام lock روی رشته‌هاست. برای مثال در کد زیر DeviceId یک رشته است که از بانک اطلاعاتی خوانده می‌شود و باعث می‌شود که چند job همزمان به یک دستگاه وصل نشوند :
lock (job.DeviceId)
{
    job.Execute();
}

اگر یک job با DeviceId برابر COM1 در حال اجرا باشد، این lock جلوی اجرای همزمان job دیگری با همین DeviceId را نمی‌گیرد. زیرا هر چند مقدار DeviceId دو job یکی است ولی به یک نمونه اشاره نمی‌کنند.

می‌توان lock را اینگونه اصلاح کرد :
lock (string.Intern(job.DeviceId))
{
    job.Execute();
}

مطالب
آشنایی با آزمایش واحد (unit testing) در دات نت، قسمت 5

ادامه آشنایی با NUnit

حالت‌های مختلف Assert :

NUnit framework حالت‌های مختلفی از دستور Assert را پشتیبانی می‌کند که در ادامه با آنها آشنا خواهیم شد.

کلاس Assertion :
این کلاس دارای متدهای زیر است:
public static void Assert(bool condition)
public static void Assert(string message, bool condition)

تنها در حالتی این بررسی موفقیت آمیز گزارش خواهد شد که condition مساوی true باشد
public static void AssertEquals(string message, object expected, object actual)
public static void AssertEquals(string message, float expected, float actual, float delta)
public static void AssertEquals(string message, double expected, double actual, double delta)
public static void AssertEquals(string message, int expected, int actual)
public static void AssertEquals(int expected, int actual)
public static void AssertEquals(object expected, object actual)
public static void AssertEquals(float expected, float actual, float delta)
public static void AssertEquals(double expected, double actual, double delta)

تنها در صورتی این بررسی به اثبات خواهد رسید که اشیاء actual و expected یکسان باشند. (دلتا در اینجا به عنوان تلرانس آزمایش درنظر گرفته می‌شود)

public static void AssertNotNull(string message, object anObject)
public static void AssertNotNull(object anObject)

این بررسی تنها در صورتی موفقیت آمیز گزارش می‌شود که شیء مورد نظر نال نباشد.

public static void AssertNull(string message, object anObject)
public static void AssertNull(object anObject)

این بررسی تنها در صورتی موفقیت آمیز گزارش می‌شود که شیء مورد نظر نال باشد.

public static void AssertSame(string message, object expected, object actual)
public static void AssertSame(object expected, object actual)

تنها در صورتی این بررسی به اثبات خواهد رسید که اشیاء actual و expected یکسان باشند.

public static void Fail(string message)
public static void Fail()

همواره Fail خواهد شد. (در مورد کاربرد آن در قسمت بعد توضیح داده خواهد شد)


نکته:
در یک متد آزمایش واحد شما مجازید به هرتعدادی که لازم است از متدهای Assertion استفاده نمائید. در این حالت اگر تنها یکی از متدهای assertion با شکست روبرو شود، کل متد آزمایش واحد شما مردود گزارش شده و همچنین عبارات بعدی Assertion بررسی نخواهند شد. بنابراین توصیه می‌شود به ازای هر متد آزمایش واحد، تنها از یک Assertion استفاده نمائید.

مهم!
کلاس Assertion منسوخ شده است و توصیه می‌شود بجای آن از کلاس Assert استفاده گردد.

آشنایی با کلاس Assert :
این کلاس از متدهای زیر تشکیل شده است:

الف) بررسی حالت‌های تساوی

Assert.AreEqual( object expected, object actual );

جهت بررسی تساوی دو شیء مورد بررسی و شیء مورد انتظار بکار می‌رود.

Assert.AreNotEqual( object expected, object actual );

جهت بررسی عدم تساوی دو شیء مورد بررسی و شیء مورد انتظار بکار می‌رود.
برای مشاهده انواع و اقسام overload های آن‌ها می‌توانید به راهنمای NUnit که پس از نصب، در پوشه doc آن قرار می‌گیرد مراجعه نمائید.

همچنین دو متد زیر و انواع overload های آن‌ها جهت برسی اختصاصی حالت تساوی دو شیء بکار می‌روند:

Assert.AreSame( object expected, object actual );
Assert.AreNotSame( object expected, object actual );

بعلاوه اگر نیاز بود بررسی کنیم که آیا شیء مورد نظر حاوی یک آرایه یا لیست بخصوصی است می‌توان از متد زیر و oveload های آن استفاده نمود:

Assert.Contains( object anObject, IList collection );

ب) بررسی حالت‌های شرطی:

Assert.IsTrue( bool condition );

تنها در حالتی این بررسی موفقیت آمیز گزارش خواهد شد که condition مساوی true باشد

Assert.IsFalse( bool condition);

تنها در حالتی این بررسی موفقیت آمیز گزارش خواهد شد که condition مساوی false باشد
Assert.IsNull( object anObject );

این بررسی تنها در صورتی موفقیت آمیز گزارش می‌شود که شیء مورد نظر نال باشد.
Assert.IsNotNull( object anObject );

این بررسی تنها در صورتی موفقیت آمیز گزارش می‌شود که شیء مورد نظر نال نباشد.

Assert.IsNaN( double aDouble );

این بررسی تنها در صورتی موفقیت آمیز گزارش می‌شود که شیء مورد نظر عددی نباشد (اگر با جاوا اسکریپت کار کرده باشید حتما با isNan آشنا هستید، is not a numeric ).

Assert.IsEmpty( string aString );
Assert.IsEmpty( ICollection collection );

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

Assert.IsNotEmpty( string aString );
Assert.IsNotEmpty( ICollection collection );

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

ج) بررسی حالت‌های مقایسه‌ای

Assert.Greater( double arg1, double arg2 );
Assert.GreaterOrEqual( int arg1, int arg2 );
Assert.Less( int arg1, int arg2 );
Assert.LessOrEqual( int arg1, int arg2 );

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

د) بررسی نوع اشیاء

Assert.IsInstanceOfType( Type expected, object actual );
Assert.IsNotInstanceOfType( Type expected, object actual );
Assert.IsAssignableFrom( Type expected, object actual );
Assert.IsNotAssignableFrom( Type expected, object actual );

این توابع و Overload های آن‌ها امکان بررسی نوع شیء مورد نظر را میسر می‌سازند.

ه) متدهای کمکی
Assert.Fail();
Assert.Ignore();

در حالت استفاده از ignore ، آزمایش واحد شما در حین اجرا ندید گرفته خواهد شد. از متد fail برای طراحی یک متد assertion سفارشی می‌توان استفاده کرد. برای مثال:
طراحی متدی که بررسی کند آیا یک رشته مورد نظر حاوی عبارتی خاص می‌باشد یا خیر:

public void AssertStringContains( string expected, string actual,
string message )
{
if ( actual.IndexOf( expected ) < 0 )
Assert.Fail( message );
}

و) متدهای ویژه‌ی بررسی رشته‌ها

StringAssert.Contains( string expected, string actual );
StringAssert.StartsWith( string expected, string actual );
StringAssert.EndsWith( string expected, string actual );
StringAssert.AreEqualIgnoringCase( string expected, string actual );
StringAssert.IsMatch( string expected, string actual );

این متدها و انواع overload های آن‌ها جهت بررسی‌های ویژه رشته‌ها بکار می‌روند. برای مثال آیا رشته مورد نظر حاوی عبارتی خاص است؟ آیا با عبارتی خاص شروع می‌شود یا با عبارتی ویژه، پایان می‌پذیرد و امثال آن.

ز) بررسی فایل‌ها

FileAssert.AreEqual( Stream expected, Stream actual );
FileAssert.AreEqual( FileInfo expected, FileInfo actual );
FileAssert.AreEqual( string expected, string actual );

FileAssert.AreNotEqual( Stream expected, Stream actual );
FileAssert.AreNotEqual( FileInfo expected, FileInfo actual );
FileAssert.AreNotEqual( string expected, string actual );

این متدها جهت مقایسه دو فایل بکار می‌روند و ورودی‌های آن‌ها می‌تواند از نوع stream ، شیء FileInfo و یا مسیر فایل‌ها باشد.

ح) بررسی collections

CollectionAssert.AllItemsAreInstancesOfType( IEnumerable collection, Type expectedType );
CollectionAssert.AllItemsAreNotNull( IEnumerable collection );
CollectionAssert.AllItemsAreUnique( IEnumerable collection );
CollectionAssert.AreEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreEquivalent( IEnumerable expected, IEnumerable actual);
CollectionAssert.AreNotEqual( IEnumerable expected, IEnumerable actual );
CollectionAssert.AreNotEquivalent( IEnumerable expected,IEnumerable actual );
CollectionAssert.Contains( IEnumerable expected, object actual );
CollectionAssert.DoesNotContain( IEnumerable expected, object actual );
CollectionAssert.IsSubsetOf( IEnumerable subset, IEnumerable superset );
CollectionAssert.IsNotSubsetOf( IEnumerable subset, IEnumerable superset);
CollectionAssert.IsEmpty( IEnumerable collection );
CollectionAssert.IsNotEmpty( IEnumerable collection );

به صورت اختصاصی و ویژه نیز می‌توان بررسی مقایسه‌ای را بر روی اشیایی از نوع IEnumerable انجام داد. برای مثال آیا معادل هستند، آیا شیء مورد نظر نال نیست و امثال آن.

نکته: در تمامی overload های این توابع، آرگومان message نیز وجود دارد. از این آرگومان زمانیکه آزمایش با شکست مواجه شد، جهت ارائه اطلاعات بیشتری استفاده می‌گردد.

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

مطالب
CoffeeScript #13

بخش‌های بد

در ادامه‌ی قسمت قبل، به مواردی که توسط CoffeeScript اصلاح شده‌اند، می‌پردازیم.

Reserved words

کلمات کلیدی خاصی در جاوااسکریپت وجود دارد مانند class، enum و const که برای نسخه‌های بعدی جاوااسکریپت در آینده رزرو شده‌اند. استفاده از این کلمات در برنامه‌های جاوااسکریپت می‌تواند نتایج غیرقابل پیش بینی داشته باشد. برخی از مرورگرهای به خوبی از عهده‌ی این کار برمی‌آیند و بعضی دیگر به طور کامل جلوی استفاده از این‌ها را گرفته‌اند. CoffeeScript بعد از تشخیص استفاده از یک کلمه‌ی کلیدی، با یک راه کار خاص، از این موضوع می‌گریزد.

به عنوان مثال، فرض کنید می‌خواهیم از کلمه کلیدی class به عنوان یک خصوصیت در یک شیء استفاده کنیم:

myObj = {
  delete: "I am a keyword!"
}
myObj.class = ->
پس از کامپایل، پارسر CoffeeScript متوجه استفاده شما از کلمه کلیدی رزرو شده می‌شود و آنها را در بین "" قرار می‌دهد.
var myObj;
myObj = {
  "delete": "I am a keyword!"
};
myObj["class"] = function() {};

Equality comparisons

مقایسه برابری ضعف دیگری است که در جاوااسکریپت باعث ایجاد رفتاری گیج کننده و اغلب باعث ایجاد اشکالاتی در کد نوشته شده می‌شود. به مثال زیر توجه کنید:

""           ==   "0"// false
0            ==   ""// true
0            ==   "0"// true
false        ==   "false"// false
false        ==   "0"// true
false        ==   undefined// false
false        ==   null// false
null         ==   undefined// true
" \t\r\n"    ==   0// true
مطمئنم که شما هم با من موافقید که همه‌ی مقایسه‌های بالا بسیار مبهم هستند و استفاده از آن‌های می‌توانند منجر به نتایج غیر منتظره شوند و همچنین مشکلاتی را پیش بیاورند.
راه حل این کار استفاده از عملگر برابری سختگیرانه است، که از 3 مساوی تشکیل شده است: === عملگر برابر سخت گیرانه دقیقا مانند عملگر برابری عادی عمل می‌کند و تنها نوع داده‌ها را بررسی می‌کند که با هم برابر باشند.
توصیه می‌شود که همیشه از عملگر برابری سختگیرانه استفاده کنید و هرجا لازم بود قبل مقایسه عمل تبدیل نوع داده‌ها را انجام دهید.
CoffeeScript این مشکل را به صورت کامل حل کرده است؛ یعنی هر جایی که عمل مقایسه == انجام شود به === تبدیل می‌شود. شما باید به صورت صریح نوع داده‌ها را قبل از مقایسه تبدیل کرده باشید.
نکته: در مقایسه‌ها رشته خالی ""، null ،undefined و عدد 0 همگی false برمی گردانند.
alert "Empty Array"  unless [].length
alert "Empty String" unless ""
alert "Number 0"      unless 0
که پس از کامپایل می‌شود:
if (![].length) {
  alert("Empty Array");
}

if (!"") {
  alert("Empty String");
}

if (!0) {
  alert("Number 0");
}
در صورتیکه می‌خواهید به صورت صریح null و یا undefined را بررسی کنید، می‌توانید از عملگر ? CoffeeScript استفاده کنید:
alert "This is not called" unless ""?
پس از کامپایل می‌شود:
if ("" == null) {
  alert("This is not called");
}
با اجرای مثال بالا alert اجرای نمی‌شود چون رشته خالی با null برابر نیست.

Function definition


خیلی جالب است که در جاوااسکریپت می‌توانید تابعی را بعد از اینکه فراخوانی کردید، تعریف کنید. به عنوان مثال، کد زیر به صورت کامل اجرا می‌شود:
wem();
function wem() {alert("hi");}
این به دلیل دامنه (scope) تابع است. تمام توابع قبل از اجرای برنامه، به بالا برده می‌شوند و در همه جا در دامنه‌ای که در آن تعریف شده‌اند، قابل دسترسی می‌باشند؛ حتی اگر قبل از تعریف واقعی در منبع، فراخوانی شده باشد. مشکل اینجاست که عمل بالابردن توابع در مرورگرها با یکدیگر متفاوت است. به مثال زیر توجه کنید:
if (true) {
  function declaration() {
    return "first";
  }
} else {
  function declaration() {
    return "second";
  }
}
declaration();
در بعضی از مرورگرها مانند Firefox ، تابع ()declaration مقدار "first" را برگشت خواهد داد و در دیگر مرورگرها مانند Chrome، مقدار "second" برگشت داده خواهد شد. در حالیکه به نظر می‌رسد که قسمت else هیچگاه اجرا نخواهد شد.
در صورتیکه علاقمند به کسب اطلاعات بیشتری درباره‌ی نحوه تعریف توابع، هستید باید راهنمای آقای Juriy Zaytsev را مطالعه کنید. به صورت خلاصه، رفتار نسبتا مبهم مرورگرها می‌تواند منجر به ایجاد مشکلاتی در مسیر نوشتن یک پروژه شوند.
همه چیز در CoffeeScript در نظر گرفته شده است و بهترین روش برای حل این مشکل، حذف کلمه function و به جای آن استفاده از عبارت (expression) تابع است.

Number property lookups

نقصی که در پارسر جاوااسکریپت در مواجه با نماد نقطه (dot notation) بر روی اعداد وجود دارد، تفسیر آن به ممیز شناور، بجای مراجعه به ویژگی‌های آن است. برای مثال کد جاوااسکریپت زیر باعث ایجاد خطای نحوی می‌شود:
5.toString();
پارسر جاوااسکریپت بعد از نقطه به دنبال یک عدد دیگر می‌گردد و با برخورد با ()toString، باعث ایجاد یک Unexpected token می‌شود. راه حل این مشکل، استفاده از پرانتز یا اضافه کردن یک نقطه دیگر است.
(5).toString();
5..toString();
خوشبختانه پارسر CoffeeScript به اندازه‌ی کافی هوشمندانه با این مسئله برخورد می‌کند و هر زمانی که شما دسترسی به ویژگی‌های اعداد را داشته باشید، به صورت خودکار با اضافه کردن دوتا نقطه (همانند مثال بالا) جلوی ایجاد خطا را می‌گیرد.
نظرات اشتراک‌ها
همکاری مایکروسافت با تیم Mono در مورد portable class libraries
مدتی است بدنبال برنامه نویسی اندروید هستم و بین انتخاب زبان سی شارپ و جاوا مردد هستم . کدام گزینه‌ی بهتری برای کار هست استفاده از جاوا یا سی شارپ ؟ آیا ممکنه بلایی که سر سیلورلایت اومد سر برنامه نویسان اندروید (سی شارپ) هم بیاد ؟ اگر بخواهیم آینده نگری کنیم کار رو با کدوم پیش ببریم و ابزارها و امکانات کدوم بهتر هست ؟

مطالب
فعالسازی Windows Authentication در برنامه‌های ASP.NET Core 2.0
اعتبارسنجی مبتنی بر ویندوز، بر اساس قابلیت‌های توکار ویندوز و اختیارات اعطا شده‌ی به کاربر وارد شده‌ی به آن، کار می‌کند. عموما محل استفاده‌ی از آن، در اینترانت داخلی شرکت‌ها است که بر اساس وارد شدن افراد به دومین و اکتیودایرکتوری آن، مجوز استفاده‌ی از گروه‌های کاربری خاص و یا سطوح دسترسی خاصی را پیدا می‌کنند. میان‌افزار اعتبارسنجی ASP.NET Core، علاوه بر پشتیبانی از روش‌های اعتبارسنجی مبتنی بر کوکی‌‌ها و یا توکن‌ها، قابلیت استفاده‌ی از اطلاعات کاربر وارد شده‌ی به ویندوز را نیز جهت اعتبارسنجی او به همراه دارد.


فعالسازی Windows Authentication در IIS

پس از publish برنامه و رعایت مواردی که در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 22 - توزیع برنامه توسط IIS» بحث شد، باید به قسمت Authentication برنامه‌ی مدنظر، در کنسول مدیریتی IIS رجوع کرد:


و سپس Windows Authentication را با کلیک راست بر روی آن و انتخاب گزینه‌ی Enable، فعال نمود:


این تنظیم دقیقا معادل افزودن تنظیمات ذیل به فایل web.config برنامه است:
  <system.webServer>
    <security>
      <authentication>
        <anonymousAuthentication enabled="true" />
        <windowsAuthentication enabled="true" />
      </authentication>
    </security>
  </system.webServer>
اما اگر این تنظیمات را به فایل web.config اضافه کنید، پیام و خطای قفل بودن تغییرات مدخل windowsAuthentication را مشاهده خواهید کرد. به همین جهت بهترین راه تغییر آن، همان مراجعه‌ی مستقیم به کنسول مدیریتی IIS است.


فعالسازی Windows Authentication در IIS Express

اگر برای آزمایش می‌خواهید از IIS Express به همراه ویژوال استودیو استفاده کنید، نیاز است فایلی را به نام Properties\launchSettings.json با محتوای ذیل در ریشه‌ی پروژه‌ی خود ایجاد کنید (و یا تغییر دهید):
{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": true,
    "iisExpress": {
      "applicationUrl": "http://localhost:3381/",
      "sslPort": 0
    }
  }
}
در اینجا الزامی به خاموش کردن anonymousAuthentication نیست. اگر برنامه‌ی شما قرار است هم توسط کاربران ویندوزی و هم توسط کاربران وارد شده‌ی از طریق اینترنت (و نه صرفا اینترانت داخلی) به برنامه، قابلیت دسترسی داشته باشد، نیاز است anonymousAuthentication به true تنظیم شده باشد (همانند تنظیمی که برای IIS اصلی ذکر شد).


تغییر مهم فایل web.config برنامه جهت هدایت اطلاعات ویندوز به آن

اگر پروژه‌ی شما فایل web.config ندارد، باید آن‌را اضافه کنید؛ با حداقل محتوای ذیل:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    <handlers>
      <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified"/>
    </handlers>
    <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" 
            stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" 
            forwardWindowsAuthToken="true"/>
  </system.webServer>
</configuration>
که در آن خاصیت forwardWindowsAuthToken، حتما به true تنظیم شده باشد. این مورد است که کار اعتبارسنجی و یکی‌سازی اطلاعات کاربر وارد شده به ویندوز و ارسال آن‌را به میان‌افزار IIS برنامه‌ی ASP.NET Core انجام می‌دهد. بدون تنظیم آن، با مراجعه‌ی به سایت، شاهد نمایش صفحه‌ی login ویندوز خواهید بود.


تنظیمات برنامه‌ی ASP.NET Core جهت فعالسازی Windows Authentication

پس از فعالسازی windowsAuthentication در IIS و همچنین تنظیم forwardWindowsAuthToken به true در فایل web.config برنامه، اکنون جهت استفاده‌ی از windowsAuthentication دو روش وجود دارد:

الف) تنظیمات مخصوص برنامه‌های Self host
اگر برنامه‌ی وب شما قرار است به صورت self host ارائه شود (بدون استفاده از IIS)، تنها کافی است در تنظیمات ابتدای برنامه در فایل Program.cs، استفاده‌ی از میان‌افزار HttpSys را ذکر کنید:
namespace ASPNETCore2WindowsAuthentication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                                     .UseKestrel()
                                     .UseContentRoot(Directory.GetCurrentDirectory())
                                     .UseStartup<Startup>()
                                     .UseHttpSys(options => // Just for local tests without IIS, Or self-hosted scenarios on Windows ...
                                     {
                                         options.Authentication.Schemes =
                                              AuthenticationSchemes.Negotiate | AuthenticationSchemes.NTLM;
                                         options.Authentication.AllowAnonymous = true;
                                         //options.UrlPrefixes.Add("http://+:80/");
                                     })
                                     .Build();
            host.Run();
        }
    }
}
در اینجا باید دقت داشت که استفاده‌ی از UseHttpSys با تنظیمات فوق، اعتبارسنجی یکپارچه‌ی با ویندوز را برای برنامه‌های self host خارج از IIS مهیا می‌کند. اگر قرار است برنامه‌ی شما در IIS هاست شود، نیازی به تنظیم فوق ندارید و کاملا اضافی است.


ب) تنظیمات مخصوص برنامه‌هایی که قرار است در IIS هاست شوند

در این‌حالت تنها کافی است UseIISIntegration در تنظیمات ابتدایی برنامه ذکر شود و همانطور که عنوان شد، نیازی به UseHttpSys در این حالت نیست:
namespace ASPNETCore2WindowsAuthentication
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                                     .UseKestrel()
                                     .UseContentRoot(Directory.GetCurrentDirectory())
                                     .UseIISIntegration()
                                     .UseDefaultServiceProvider((context, options) =>
                                     {
                                         options.ValidateScopes = context.HostingEnvironment.IsDevelopment();
                                     })
                                     .UseStartup<Startup>()
                                     .Build();
            host.Run();
        }
    }
}


فعالسازی میان‌افزار اعتبارسنجی ASP.NET Core جهت یکپارچه شدن با Windows Authentication

در پایان تنظیمات فعالسازی Windows Authentication نیاز است به فایل Startup.cs برنامه مراجعه کرد و یکبار AddAuthentication را به همراه تنظیم ChallengeScheme آن به IISDefaults افزود:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.Configure<IISOptions>(options =>
            {
                // Sets the HttpContext.User
                // Note: Windows Authentication must also be enabled in IIS for this to work.
                options.AutomaticAuthentication = true;
                options.ForwardClientCertificate = true;
            });
            
            services.AddAuthentication(options =>
            {
                // for both windows and anonymous authentication
                options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
            });
        }
برای مثال اگر از ASP.NET Core Identity استفاده می‌کنید، سطر services.AddAuthentication فوق، پس از تنظیمات ابتدایی آن باید ذکر شود؛ هرچند روش فوق کاملا مستقل است از ASP.NET Core Identity و اطلاعات کاربر را از سیستم عامل و اکتیودایرکتوری (در صورت وجود) دریافت می‌کند.


آزمایش برنامه با تدارک یک کنترلر محافظت شده

در اینجا قصد داریم اطلاعات ذیل را توسط تعدادی اکشن متد، نمایش دهیم:
        private string authInfo()
        {
            var claims = new StringBuilder();
            if (User.Identity is ClaimsIdentity claimsIdentity)
            {
                claims.Append("Your claims: \n");
                foreach (var claim in claimsIdentity.Claims)
                {
                    claims.Append(claim.Type + ", ");
                    claims.Append(claim.Value + "\n");
                }
            }

            return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}";
        }
کار آن نمایش نام کاربر، وضعیت لاگین او و همچنین لیست تمام Claims متعلق به او می‌باشد:
namespace ASPNETCore2WindowsAuthentication.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }

        [Authorize]
        public IActionResult Windows()
        {
            return Content(authInfo());
        }

        private string authInfo()
        {
            var claims = new StringBuilder();
            if (User.Identity is ClaimsIdentity claimsIdentity)
            {
                claims.Append("Your claims: \n");
                foreach (var claim in claimsIdentity.Claims)
                {
                    claims.Append(claim.Type + ", ");
                    claims.Append(claim.Value + "\n");
                }
            }

            return $"IsAuthenticated: {User.Identity.IsAuthenticated}; Identity.Name: {User.Identity.Name}; WindowsPrincipal: {(User is WindowsPrincipal)}\n{claims}";
        }

        [AllowAnonymous]
        public IActionResult Anonymous()
        {
            return Content(authInfo());
        }

        [Authorize(Roles = "Domain Admins")]
        public IActionResult ForAdmins()
        {
            return Content(authInfo());
        }

        [Authorize(Roles = "Domain Users")]
        public IActionResult ForUsers()
        {
            return Content(authInfo());
        }
    }
}
برای آزمایش برنامه، ابتدا برنامه را توسط دستور ذیل publish می‌کنیم:
 dotnet publish
سپس تنظیمات مخصوص IIS را که در ابتدای بحث عنوان شد، بر روی پوشه‌ی bin\Debug\netcoreapp2.0\publish که محل قرارگیری پیش‌فرض خروجی برنامه است، اعمال می‌کنیم.
اکنون اگر برنامه را در مرورگر مشاهده کنیم، یک چنین خروجی قابل دریافت است:


در اینجا نام کاربر وارد شده‌ی به ویندوز و همچنین لیست تمام Claims او مشاهده می‌شوند. مسیر Home/Windows نیز توسط ویژگی Authorize محافظت شده‌است.
برای محدود کردن دسترسی کاربران به اکشن متدها، توسط گروه‌های دومین و اکتیودایرکتوری، می‌توان به نحو ذیل عمل کرد:
[Authorize(Roles = @"<domain>\<group>")]
//or
[Authorize(Roles = @"<domain>\<group1>,<domain>\<group2>")]
و یا می‌توان بر اساس این نقش‌ها، یک سیاست دسترسی جدید را تعریف کرد:
services.AddAuthorization(options =>
{
   options.AddPolicy("RequireWindowsGroupMembership", policy =>
   {
     policy.RequireAuthenticatedUser();
     policy.RequireRole(@"<domain>\<group>"));  
   }
});
و در آخر از این سیاست دسترسی استفاده نمود:
 [Authorize(Policy = "RequireWindowsGroupMembership")]
و یا با برنامه نویسی نیز می‌توان به صورت ذیل عمل کرد:
[HttpGet("[action]")]
public IActionResult SomeValue()
{
    if (!User.IsInRole(@"Domain\Group")) return StatusCode(403);
    return Ok("Some Value");
}


افزودن Claims سفارشی به Claims پیش‌فرض کاربر سیستم

همانطور که در شکل فوق ملاحظه می‌کنید، یک سری Claims حاصل از Windows Authentication در اینجا به شیء User اضافه شده‌اند؛ بدون اینکه برنامه، صفحه‌ی لاگینی داشته باشد و همینقدر که کاربر به ویندوز وارد شده‌است، می‌تواند از برنامه استفاده کند.
اگر نیاز باشد تا Claims خاصی به لیست Claims کاربر جاری اضافه شود، می‌توان از پیاده سازی یک IClaimsTransformation سفارشی استفاده کرد:
    public class ApplicationClaimsTransformation : IClaimsTransformation
    {
        private readonly ILogger<ApplicationClaimsTransformation> _logger;

        public ApplicationClaimsTransformation(ILogger<ApplicationClaimsTransformation> logger)
        {
            _logger = logger;
        }

        public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
        {
            if (!(principal.Identity is ClaimsIdentity identity))
            {
                return Task.FromResult(principal);
            }

            var claims = addExistingUserClaims(identity);
            identity.AddClaims(claims);

            return Task.FromResult(principal);
        }

        private IEnumerable<Claim> addExistingUserClaims(IIdentity identity)
        {
            var claims = new List<Claim>();
            var user = @"VahidPC\Vahid";
            if (identity.Name != user)
            {
                _logger.LogError($"Couldn't find {identity.Name}.");
                return claims;
            }

            claims.Add(new Claim(ClaimTypes.GivenName, user));
            return claims;
        }
    }
و روش ثبت آن نیز در متد ConfigureServices فایل آغازین برنامه به صورت ذیل است:
            services.AddScoped<IClaimsTransformation, ApplicationClaimsTransformation>();
            services.AddAuthentication(options =>
            {
                // for both windows and anonymous authentication
                options.DefaultChallengeScheme = IISDefaults.AuthenticationScheme;
            });
هر زمانیکه کاربری به برنامه وارد شود و متد HttpContext.AuthenticateAsync فراخوانی گردد، متد TransformAsync به صورت خودکار اجرا می‌شود. در اینجا چون forwardWindowsAuthToken به true تنظیم شده‌است، میان‌افزار IIS کار فراخوانی HttpContext.AuthenticateAsync و مقدار دهی شیء User را به صورت خودکار انجام می‌دهد. بنابراین همینقدر که برنامه را اجرا کنیم، شاهد اضافه شدن یک Claim سفارشی جدید به نام ClaimTypes.GivenName که در متد addExistingUserClaims فوق آن‌را اضافه کردیم، خواهیم بود:


به این ترتیب می‌توان لیست Claims ثبت شده‌ی یک کاربر را در یک بانک اطلاعاتی استخراج و به لیست Claims فعلی آن افزود و دسترسی‌های بیشتری را به او اعطاء کرد (فراتر از دسترسی‌های پیش‌فرض سیستم عامل).

برای دسترسی به مقادیر این Claims نیز می‌توان به صورت ذیل عمل کرد:
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var userName = User.FindFirstValue(ClaimTypes.Name);
var userName = User.FindFirstValue(ClaimTypes.GivenName);


کدهای کامل این برنامه را از اینجا می‌توانید دریافت کنید: ASPNETCore2WindowsAuthentication.zip
مطالب
تزریق وابستگی‌ها در ASP.NET Core - بخش 6 - Implementation Factory

در بعضی از شرایط پیش رفته، ممکن است نمونه سازی از یک Implementation Type، نیاز به دخالت مستقیم ما را داشته باشد. Implementation Factory کنترل بیشتری بر چگونگی استفاده‌ی از Implementation Type‌ها را  به ما ارائه می‌دهد. در هنگام ثبت سرویسی که Implementation Factory را در اختیار ما قرار می‌دهد، ما یک Delegate را برای فراخوانی سرویس استفاده می‌کنیم. این Delegate مسئول ساخت یک نمونه از Service Type است. برای مثال وقتی از الگوهای builder یا factory برای ساخت یک شیء استفاده می‌کنید، شاید نیاز باشد که Implementation Factory را به صورت دستی پیاده سازی کنید. اولین قدم این است که کدتان را در صورت امکان چنان refactor کنید تا DI Container بتواند آن را به صورت خودکار بسازد؛ ولی اینکار همیشه ممکن نیست. برای مثال بعضی از برنامه نویسان ترجیح می‌دهند یک Config را مستقیما از IOptionMonitor   بگیرند و بعد در هر جائیکه خواستند، بجای تزریق IOptionMonitor به سرویس، مستقیما از همان سرویس ثبت شده استفاده کنند:

services.AddSingleton<ILiteDbConfig>(sp =>sp.GetRequiredService<IOptionsMonitor<LiteDbConfig>>().CurrentValue);

پیاده سازیComposite Pattern 

یک کاربرد بالقوه‌ی دیگر برای Implementation Factory ، استفاده از Composite Pattern است. هر چند Microsoft DI Container به صورت پیش فرض از Composite Pattern پشتیبانی نمی‌کند، ولی ما می‌توانیم آن‌را پیاده سازی کنیم. فرض کنید که قبلا به ازای انجام کاری، به کاربر یک ایمیل را می‌فرستادیم؛ ولی حالا مالک محصول می‌آید و می‌گوید که علاوه بر ایمیل، باید پیامک هم بفرستید و ما یا این سرویس پیامک را از قبل داریم و یا باید آن را بسازیم که فرض می‌کنیم از قبل آن را داریم. برای این کار ما یک اینترفیس کلی‌تر به نام INotificationService می‌سازیم که دو سرویس IEmailNotificationService و ISmsNotificaitonService از آن ارث بری می‌کنند:

public interface INotificationService
{
      void SendMessage(string msg, int userId);
}
حالا CompositeNotificationService را به این صورت تعریف می‌کنیم:
    public class CompositeNotificationService : INotificationService
    {
        private readonly IEnumerable<INotificationService> _notificationServices;
        public CompositeNotificationService(IEnumerable<INotificationService> notificationServices)
        {
            _notificationServices = notificationServices;
        }

        public void SendMessage(string msg, int userId)
        {
            foreach (var notificationServicei in _notificationServices) 
            {
                notificationServicei.SendMessage(msg, userId);
            }
        }
    }
و این سرویس‌ها را به این صورت در DI Container ثبت می‌کنیم: 
services.AddScoped<IEmailNotificationService, EmailNotificationService>();
services.AddScoped<ISMSNotificationService, SMSNotificationService>();

services.AddSingleton<INotificationService>(sp => new CompositeNotificationService(
      new INotificationService[] { 
          sp.GetRequiredService<IEmailNotificationService>() , 
          sp.GetRequiredService<ISMSNotificationService>()
      }
));
حالا هر زمانیکه بخواهیم همزمان، هم ایمیل و هم پیامک بفرستیم، کافی است که سرویس INotificationService را در سازنده‌ی کلاس مورد نظر تزریق کرده و از آن در مکان‌ها و شرایط مختلفی استفاده کنیم. اگر هر کدام از سرویس‌های ارسال ایمیل و سرویس‌های پیامک را به صورت جداگانه بخواهیم، می‌توانیم آنها را به صورت تکی ثبت و استفاده کنیم.  


وهله سازی سفارشی

در مثال بعدی نشان می‌دهیم که چطور می‌توانیم از Implementation Factory برای برگرداندن پیاده‌سازی سرویس‌هایی که Service Provider امکان ساخت خودکار آنها را ندارد، استفاده کنیم. فرض کنید یک کلاس Account داریم که از IAccount ارث بری می‌کند و برای ساخت آن باید از متدهای IAccountBuilder که فرآیند ساخت را انجام می‌دهند، استفاده کنیم. بنابراین امکان ساخت مستقیم یک شیء از IAccount وجود ندارد. در این حالت بدین صورت عمل می‌کنیم: 

services.AddTransient<IAccountBuilder, AccountBuilder>();

services.AddScoped<IAccount>(sp => {
    var builder = sp.GetService<IAccountBuilder>();
    builder.WithAccountType(AccountType.Guest);
    return builder.Build();
});


ثبت یک نمونه برای چندین اینترفیس

ممکن است بنا به دلایلی مجبور باشیم یک implementation Type را برای چند سرویس (اینترفیس) به ثبت برسانیم. در این حالت نمونه‌ی شیء ساخته شده‌، توسط هر کدام از اینترفیس‌ها قابل استفاده است. فرض کنید یک سرویس Greeting داریم که پیش از این فقط اینترفیس IHomeGreetingService را پیاده سازی می‌کرد؛ ولی بنا به دلایلی تصمیم گرفتیم که سرویسی جامع‌تر را با نیازمندی‌های دیگری نیز تعریف کنیم و GreetingService آن را پیاده سازی کند: 

public class GreetingService : IHomeGreetingService , IGreetingService
{ // code here }

احتمالا اولین چیزی که به ذهنمان می‌رسد این است: 

services.TryAddSingleton<IHomeGreetingService, GreetingService>();
services.TryAddSingleton<IGreetingService, GreetingService>();

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

var greetingService = new GreetingService(Environment);
services.TryAddSingleton<IHomeGreetingService>(greetingService);
services.TryAddSingleton<IGreetingService>(greetingService);

در اینجا سازنده‌ی کلاس GreetingService فقط به  environment نیاز داشت که یکی از سرویس‌های پایه‌ی فریم ورک هست و در این مرحله در دسترس است. به صورت کلی مشکل روش بالا این است که ما مسئول نمونه سازی از سرویس GreetingService هستیم! اگر GreetingService برای ساخته شدن به سرویس‌ها یا ورودی هایی نیاز داشته باشد که فقط در زمان اجرا در دسترس باشند، ما امکان نمونه سازی آن‌ها را نداریم؛ علاوه بر این نمی‌توان از روش‌های بالای برای حالت‌های Scoped یا Transient  استفاده کرد.

روش بعدی همان روش استفاده از Implementation Factory است که در ادامه آن را می‌بینید: 

services.TryAddSingleton<GreetingService>();
services.TryAddSingleton<IHomeGreetingService>(sp => sp.GetRequiredService<GreetingService>());
services.TryAddSingleton<IGreetingService>(sp => sp.GetRequiredService<GreetingService>());

در این روش خود DI Container مسئول نمونه سازی از GreetingService است. علاوه بر این می‌توان با استفاده از روش فوق از طول حیات‌های Scoped و Transient هم استفاده کرد؛ در حالیکه در روش قبلی این کار امکان پذیر نبود.

 

Open Generics Service

گاهی از اوقات می‌خواهید سرویس‌هایی را ثبت کنید که از اینترفیسی جنریک ارث بری می‌کنند. هر نوع جنریک در زمان اجرا، نوع مخصوص به خود را واکشی می‌کند. ثبت کردن دستی این سرویس‌ها می‌تواند خسته کننده باشد. برای همین مایکروسافت در DI Container خود قابلیت ثبت و واکشی سرویس‌های جنریک را نیز در اختیار ما گذاشته‌است. بیایید نگاهی به سرویس ILogger<T>  بیندازیم. این یک سرویس درونی فریمورک است و می‌تواند به ازای هر نوع، کارهای مربوط به ثبت log را انجام بدهد و در پروژه‌ها معمولا از این اینترفیس برای ثبت لاگ‌ها در سطح کنترلر و سرویس‌ها استفاده می‌شود: 

public interface ILogger<out TCategoryName> : ILogger
{
}

در حالت عادی اگر سرویسی مشابه سرویس فوق را داشته باشیم، برای ثبت کردن هر سرویس با نوع جنریک اختصاصی آن، مجبوریم به صورت دستی آن را درون DI Container ثبت کنیم؛ مثلا باید به این صورت عمل کنیم: 

services.TryAddScoped<ILogger<HomeController>,Logger<HomeController>>();
این کاری طاقت فرساست. به همین جهت مایکروسافت قابلیت Open Generics Service را در اختیار ما گذاشته تا بتوانیم اینگونه سرویس‌ها را فقط و فقط یکبار ثبت کنیم: 
services.TryAddScoped(typeof(ILogger<>) , typeof(Logger<>));
و اینگونه می‌توانیم نمونه‌های مختلف از ILogger<T> را به هر جایی که خواستیم، تزریق کنیم.

 

دسته بندی سرویس‌ها درون متدهای مختلف و پاکسازی  متد ConfigurationService

 تا اینجای کار ما سرویس‌های مختلفی را به روش‌های مختلفی ثبت کرده‌ایم. حتی در همین آموزش ساده، تعداد زیاد سرویس‌های ثبت شده، باعث شلوغی و در هم ریختگی کدهای ما می‌شوند که خوانایی و در ادامه اشکال زدایی و توسعه‌ی کدها را برای ما سخت‌تر می‌کنند.  ساده‌ترین کار برای دسته بندی کدها، استفاده از متدهای private محلی یا استفاده از متدهای توسعه‌ای(الحاقی) است که در اینجا مثالی از استفاده از متدهای توسعه‌ای را آورده‌ام:
namespace AspNetCoreDependencyInjection.Extensions
{
    public static class DICRegisterationExetnsion
    {
        /// <summary>
        /// مثال ثبت برای اپن جنریت
        /// </summary>
        /// <param name="services"></param>
        public static void OpenGenericRegisterationExample(this IServiceCollection services)
        {
            services.TryAddScoped<ILogger<HomeController>, Logger<HomeController>>();
            services.TryAddScoped(typeof(ILogger<>), typeof(Logger<>));
        }


        /// <summary>
        /// ثبت تنظیمات به روش‌های مختلف 
        /// </summary>
        public static void RegisterConfiguration(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddSingleton(services => new AppConfig
            {
                ApplicationName = configuration["ApplicationName"],
                GreetingMessage = configuration["GreetingMessage"],
                AllowedHosts = configuration["AllowedHosts"]
            });

            services.AddSingleton(services => new AccountTypeBalanceConfig(
                    new List<(AccountType, long)> {
                        (AccountType.Guest , Convert.ToInt64 (configuration["AccountInitialBalance.Guest"]) ) ,
                        (AccountType.Silver , Convert.ToInt64 (configuration["AccountInitialBalance.Silver"]) ) ,
                        (AccountType.Gold , Convert.ToInt64 (configuration["AccountInitialBalance.Gold"]) ) ,
                        (AccountType.Platinum , Convert.ToInt64 (configuration["AccountInitialBalance.Platinum"]) ) ,
                        (AccountType.Titanium , Convert.ToInt64 (configuration["AccountInitialBalance.Titanium"]) ) ,
                    })
            );

            services.AddSingleton(services => new LiteDbConfig
            {
                ConnectionString = configuration["LiteDbConfig:ConnectionString"],
            });

            services.Configure<UserOptionConfig>(configuration.GetSection("UserOptionConfig"));
        }
    }
}

حالا در کلاس ConfigureServices ، درون متدStartup ، به این صورت از این متدهای توسعه‌ای استفاده می‌کنیم:
services.RegisterConfiguration(this.Configuration);
services.OpenGenericRegisterationExample();

می‌توانید کد منبع این آموزش را در اینجا  ببینید.
مطالب
MongoDb در سی شارپ (بخش دوم)
در قسمت پیشین عملیات درج و واکشی را در مونگو مورد بررسی قرار دادیم. در این مقاله به عملیات CRUD، پایان داده و عملیات ایندکس گذاری را نیز مورد بررسی قرار می‌دهیم.
در مقاله قبلی از بیلدر برای فیلترگذاری و مرتب سازی نتایج استفاده کردیم و در این مقاله هم برای به روزرسانی و ایندکس گذاری استفاده میکنیم.

به روزرسانی
کد زیر، تاریخ آخرین ورود کتب به انبار را به روزرسانی میکند؛ بدین صورت که اگر تاریخ آخرین به روزرسانی انبار کمتر یا مساوی ده روز است، آن‌را برابر 5 روز قبل قرار دهد.
var update = Builders<Book>.Update.Set("LastStock", new DateTime().AddDays((-5)));
var filter = Builders<Book>.Filter.Lte("LastStock", new DateTime().AddDays(-10));
collection.UpdateManyAsync(filter, update);
در خط اول ابتدا یک فرمان به روزرسانی را می‌سازیم و مقدار جدید را انتساب می‌دهیم. در خط بعدی، از گزینه فیلتر برای تعیین شرط استفاده میکنیم که در قسمت قبلی به آن پرداخته‌ایم. حال هم شرط را داریم و عمل به روزرسانی و از کالکشن میخواهیم که با استفاده از این دو شیء عملیات به روزرسانی را روی اسناد آغاز کند. توجه داشته باشید که دستور UpdateOne که به دو حالت همزمان و غیرهمزمان وجود دارد، وقتی به اولین مورد که با شرط همخوانی داشته باشد برسد و عملیات به روزرسانی را روی آن سند انجام دهد، دیگر باقی عملیات را ادامه نمی‌دهد. پس اگر میخواهید که کلیه اسناد در این فرمان شرکت کنند حتما از متد UpdateMany که به دو شکل همزمان و غیرهمزمان وجود دارد، استفاده کنید.

در بسیاری از موارد نیاز است که به روزرسانی یک فیلد، بسته به مقدار فعلی‌اش صورت بگیرد. برای مثال در کد زیر ما قصد داریم کتبی را که قیمت آن‌ها بالای دویست هزار ریال است، سی هزار ریال از قیمت آن‌ها کم کنیم:
var update = Builders<Book>.Update.Inc("Price", -30000);
var filter = Builders<Book>.Filter.Gt("Price", 200000);
collection.UpdateMany(filter, update);
اگر به متدهای خصوصیت Update نگاهی بیندازید، میتوانید خصوصیات مختلف و ترکیبات مختلفی از آن را نیز ببینید.

حذف
جهت حذف یک سند از سیستم با توجه به مواردی که تا به الان داشتیم باید این حدس را زده باشید که برای حذف، باید از متد Delete استفاده کنیم و یک فیلتر را باید به عنوان شرط گذاری به عنوان پارامتر آن، در نظر بگیریم. کد زیر کتاب‌هایی را که به زبان انگلیسی هستند، از کالکشن حذف میکند:
var filter = Builders<Book>.Filter.Eq("Language.Name", "English");
collection.DeleteMany(filter);
دستور Delete هم همانند آپدیت میتواند اولین سند برابر با شرط و همچنین تمامی اسناد برابر شرط را حذف کند. نکته‌ای که در این کد به نسبت کدهای دیگر وجود دارد این است که اگر سند توکار (Embed) در سند شما وجود دارد، باید ابتدا نام خصوصیت اول و بعد از علامت «.» نام خصوصیت درونی نوشته شود و اگر باز سند توکار دیگری نیز بود، این رویه ادامه پیدا کند. در اینجا برای دسترسی به خاصیت نام زبان ابتدا نام خصوصیت Language آورده شده است و سپس نام خصوصیت Name آن ذکر شده است:
Language.Name
در صورتی که قصد شما حذف یک کالکشن به طور کلی باشد، از خصوصیت DropCollection در شیء دیتابیس استفاده میکنیم:
var client = new MongoClient();
var db = client.GetDatabase("publisher");
db.DropCollection("books");
برای حذف دیتابیس هم از دستور DropDatebase در شیء کلاینت استفاده میکنیم:
var client = new MongoClient();
client.DropDatabase("publisher");

ایندکس گذاری Indexing
ایندکسها باعث میشوند که دسترسی سریعتری را به داده‌ها داشته باشیم و فیلدهایی را که در پرس و جوها مورد استفاده قرار میگیرند، سریعتر پیدا کنیم. به عنوان مثال فیلد Year فیلدی است که به احتمال زیاد در پرس و جوهایمان مرتبا استفاده خواهیم کرد تا به لیست کتب آن سال دسترسی سریعتری داشته باشیم. برای ایندکس گذاری هم از کلاس Builders استفاده میکنیم:
var index = Builders<Book>.IndexKeys.Ascending("Year");
collection.Indexes.CreateOneAsync(index);

ایندکس‌ها ترکیبی Compound Index

حتی میتوانید این ایندکس گذاری را بر روی دو فیلد، یعنی به حالت ترکیبی هم انجام دهید:
var index = Builders<Book>.IndexKeys.Ascending("Year").Descending("LastStock");
collection.Indexes.CreateOneAsync(index);
در اولین نگاه ممکن از است بپرسید که چرا این مرتب سازی بر روی اندیس‌ها مهم است؟ منظور از این صعودی و نزولی چیست؟ در اینجا باید ساختاری را که مونگو از آن برای یافتن آیتم‌ها استفاده میکند، بررسی کنیم.

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

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

ایندکس جغرافیایی GeoSpatial Index
برای داده‌های جرافیای می‌توان این نوع ایندکس را پیاده سازی کرد. موقعیکه شما با مختصات جغرافیایی کار میکنید، این ایندکس‌ها میتوانند به شما در بازیابی سریعتر مکان‌ها و مختصات کمک کنند. از آنجا که مطالب این نوع ایندکس‌ها تخصصی‌تر است، آن را به زمان دیگری موکول می‌کنیم.
var index2 = Builders<Book>.IndexKeys.Geo2D("Location");
var index3 = Builders<Book>.IndexKeys.Geo2DSphere("Location");
var index4 = Builders<Book>.IndexKeys.GeoHaystack("Location");

ایندکس متنی Text Index

این نوع ایندکس‌ها بر روی فیلدهای نوع رشته String قرار داده میشوند تا جست و جوی عبارت‌ها بین آن‌ها ساده‌تر گردد. زمانیکه ما در مورد جست و جو در بین مطالب یک سایت صحبت میکنیم، سریعا عبارات Full Text Search یا Elastic و لوسین را به یاد می‌آوریم. در مونگو هم برای این نوع جست و جو‌ها ایندکس Text را داریم که از این پس بتوانیم جست و جوی سریعتری بر روی آن داشته باشیم.
var index5 = Builders<Book>.IndexKeys.Text("Content");


قابلیت‌های ایندکس گذاری این نوع دیتابیس حتی به مواردی چون Sharding ، SparseIndex ,TTL Indexes و ... هم میرسد که از حوصله مقاله فعلی خارج است و باید به زمان دیگری و در یک مقاله تخصصی‌تر آنها را بررسی کرد.
 
در قسمت بعدی، ذخیره و بازیابی فایل‌ها در سطح دیتابیس را مورد بررسی قرار می‌دهیم.
مطالب
Blazor 5x - قسمت سوم - مبانی Razor
پیش از شروع به کار توسعه‌ی برنامه‌های مبتنی بر Blazor، باید با مبانی Razor آشنایی داشت. Razor امکان ترکیب کدهای #C و HTML را در یک فایل میسر می‌کند. دستور زبان آن از @ برای سوئیچ بین کدهای #C و HTML استفاده می‌کند. کدهای Razor را می‌توان در فایل‌های cshtml. نوشت که عموما مخصوص صفحات و Viewها هستند و یا در فایل‌های razor. که برای توسعه‌ی کامپوننت‌های Balzor بکار گرفته می‌شوند. در اینجا مهم نیست که پسوند فایل مورد استفاده چیست؛ چون اصول razor بکار گرفته شده در آن‌ها یکی است. البته در اینجا تاکید ما بیشتر بر روی فایل‌های razor. است که در برنامه‌های مبتنی بر Blazor بکار گرفته می‌شوند.


ایجاد یک پروژه‌ی جدید Blazor WASM

برای پیاده سازی و اجرای مثال‌های این قسمت، نیاز به یک پروژه‌ی جدید Blazor WASM را داریم که می‌توان آن‌را با اجرای دستور dotnet new blazorwasm --hosted در یک پوشه‌ی خالی، ایجاد کرد.

یک نکته: دستور فوق به همراه یک سری پارامتر اختیاری مانند hosted-- نیز هست. برای مشاهده‌ی لیست آن‌ها دستور dotnet new blazorwasm --help را صادر کنید. برای مثال ذکر پارامتر hosted-- سبب می‌شود تا یک ASP.NET Core host نیز برای Blazor WebAssembly app ایجاد شده تولید شود.

حالت hosted-- آن یک چنین ساختاری را دارد که از سه پروژه و پوشه‌ی Client ،Server و Shared تشکیل می‌شود:


در اینجا یک پروژه‌ی خالی WASM ایجاد شده که برخلاف حالت معمولی dotnet new blazorwasm که در قسمت قبل آن‌را بررسی کردیم، دیگر از فایل استاتیک wwwroot\sample-data\weather.json در آن خبری نیست. بجای آن، یک پروژه‌ی استاندارد ASP.NET Core Web API را در پوشه‌ی جدید Server ایجاد کرده که کار ارائه‌ی اطلاعات این سرویس آب و هوا را انجام می‌دهد و برنامه‌ی WASM ایجاد شده، این اطلاعات را توسط HTTP Client خود، از سرور Web API دریافت می‌کند.

بنابراین اگر مدل برنامه‌ای که قصد دارید تهیه کنید، ترکیبی از یک Web API و WASM است، روش hosted--، آغاز آن‌را بسیار ساده می‌کند.

نکته: روش اجرای این نوع برنامه‌ها با اجرای دستور dotnet run در داخل پوشه‌ی Server پروژه، انجام می‌شود. با اینکار هم سرور ASP.NET Core آغاز می‌شود و هم برنامه‌ی WASM توسط آن ارائه می‌گردد. در این حالت اگر آدرس https://localhost:5001 را در مرورگر باز کنیم، هم قسمت‌های بدون نیاز به سرور پروژه‌ی WASM قابل دسترسی است (مانند کار با شمارشگر آن) و هم قسمت دریافت اطلاعات از سرور آن، در منوی Fetch Data.


شروع به کار با Razor

پس از ایجاد یک پروژه‌ی جدید WASM، به فایل Client\Pages\Index.razor آن مراجعه کرده و محتوای پیش‌فرض آن‌را بجز سطر اول زیر، حذف می‌کنیم:
@page "/"
این سطر، بیانگر مسیریابی منتهی به کامپوننت جاری است. یعنی با گشودن برنامه‌ی WASM در مرورگر و مراجعه به ریشه‌ی سایت، محتوای این کامپوننت را مشاهده خواهیم کرد.
در فایل‌های razor. می‌توان ترکیبی از کدهای #C و HTML را نوشت. برای مثال:
@page "/"

<p>Hello, @name</p>

@code
{
    string name = "Vahid N.";
}
در اینجا قصد داریم مقدار یک متغیر را در یک پاراگراف درج کنیم. به همین جهت برای تعریف آن و شروع به کدنویسی می‌توان با تعریف یک قطعه کد که در فایل‌های razor با code@ شروع می‌شود، اینکار را انجام داد. در این قطعه کد، نوشتن هر نوع کد #C ای مجاز است که نمونه‌ای از آن‌را در اینجا با تعریف یک متغیر مشاهده می‌کنید. اکنون برای درج مقدار این متغیر در بین کدهای HTML از حرف @ استفاده می‌کنیم؛ مانند name@ در اینجا. نمونه‌ای از خروجی تغییرات فوق را در تصویر زیر مشاهده می‌کنید:


یک نکته: با توجه به اینکه تغییرات زیادی را در فایل جاری اعمال خواهیم کرد، بهتر است برنامه را با دستور dotnet watch run اجرا کرد، تا این تغییرات را تحت نظر قرار داده و آن‌ها را به صورت خودکار کامپایل کند. به این صورت دیگر نیازی نخواهد بود به ازای هر تغییر، یکبار دستور dotnet run اجرا شود.

در زمان درج متغیرهای #C در بین کدهای HTML توسط razor، استفاده از تمام متدهای الحاقی زبان #C نیز مجاز هستند؛ مانند:
 <p>Hello, @name.ToUpper()</p>
بنابراین درج حرف @ در بین کدهای HTML به این معنا است که به کامپایلر razor اعلام می‌کنیم، پس از این حرف، هر عبارتی که قرار می‌گیرد، یک عبارت معتبر #C است.

یا حتی می‌توان یک متد جدید را مانند CustomToUpper در قطعه کد razor، تعریف کرد و از آن به صورت زیر استفاده نمود:
@page "/"

<p>Hello, @name.ToUpper()</p>
<p>Hello, @CustomToUpper(name)</p>

@code
{
    string name = "Vahid N.";

    string CustomToUpper(string value) => value.ToUpper();
}
در این مثال‌ها، ابتدای عبارت #C تعریف شده با حرف @ شروع می‌شود و انتهای آن‌را خود کامپایلر razor بر اساس بسته شدن تگ p تعریف شده، تشخیص می‌دهد. اما اگر قصد داشته باشیم برای مثال جمع دو عدد را در اینجا محاسبه کنیم چطور؟
<p>Let's add 2 + 2 : @2 + 2 </p>
در این حالت امکان تشخیص ابتدا و انتهای عبارت #C توسط کامپایلر میسر نیست. برای رفع این مشکل می‌توان از پرانتزها استفاده کرد:
<p>Let's add 2 + 2 : @(2 + 2) </p>
نمونه‌ی دیگر نیاز به تعریف ابتدا و انتهای یک قطعه کد، در حین تعریف مدیریت کنندگان رویدادها است:
<button @onclick="@(()=>Console.WriteLine("Test"))">Click me</button>
در اینجا onclick@ مشخص می‌کند که با کلیک بر روی این دکمه قرار است قطعه کد #C ای اجرا شود. سپس با استفاده از ()@ محدوده‌ی این قطعه کد، مشخص می‌شود و اکنون در داخل آن می‌توان یک anonymous function را تعریف کرد که خروجی آن را در قسمت console ابزارهای توسعه دهندگان مرورگر می‌توان مشاهده کرد:


در اینجا اگر از Console.WriteLine("Test")@ استفاده می‌شد، به معنای انتساب یک رشته‌ی محاسبه شده به رویداد onclick بود که مجاز نیست.
روش دیگر انجام اینکار به صورت زیر است:
@page "/"

<button @onclick="@WriteLog">Click me 2</button>

@code
{
    void WriteLog()
    {
        Console.WriteLine("Test");
    }
}
می‌توان یک متد void را تعریف کرد و سپس فقط نام آن‌را توسط @ به onlick انتساب داد. ذکر این نام، اشاره‌گری خواهد بود به متد اجرا نشده‌ی WriteLog. در این حالت اگر نیاز به ارسال پارامتری به متد WriteLog بود، چطور؟
@page "/"

<button @onclick="@(()=>WriteLogWithParam("Test 3"))">Click me 3</button>

@code
{
    void WriteLogWithParam(string value)
    {
        Console.WriteLine(value);
    }
}
در این حالت نیز می‌توان از روش بکارگیری anonymous function‌ها برای تعریف پارامتر استفاده کرد.

یک نکته: اگر به اشتباه بجای WriteLogWithParam، همان WriteLog قبلی را بنویسیم، کامپایلر (در حال اجرای توسط دستور dotnet watch run) خطای زیر را نمایش می‌دهد؛ پیش از اینکه برنامه در مرورگر اجرا شود:
BlazorRazorSample\Client\Pages\Index.razor(12,25): error CS1501: No overload for method 'WriteLog' takes 1 arguments


امکان تعریف کلاس‌ها در فایل‌های razor.

در فایل‌های razor.، محدود به تعریف یک سری متدها و متغیرهای ساده نیستیم. در اینجا امکان تعریف کلاس‌ها نیز وجود دارد و همچنین می‌توان از کلاس‌های خارجی (کلاس‌هایی که خارج از فایل razor جاری تعریف شده‌اند) نیز استفاده کرد.
@page "/"

<p>Hello, @StringUtils.MyCustomToUpper(name)</p>

@code
{
    public class StringUtils
    {
        public static string MyCustomToUpper(string value) => value.ToUpper();
    }
}
برای نمونه در اینجا یک کلاس کمکی را جهت تعریف متد MyCustomToUpper، اضافه کرده‌ایم. در ادامه نحوه‌ی استفاده از این متد را در پاراگراف تعریف شده، مشاهده می‌کنید که همانند کار با کلاس و متدهای متداول #C است.
البته این کلاس را تنها می‌توان داخل همین کامپوننت استفاده کرد. برای اینکه بتوان از امکانات این کلاس، در سایر کامپوننت‌ها نیز استفاده کرد، می‌توان آن‌را در پروژه‌ی Shared قرار داد. اگر به تصویر ابتدای مطلب جاری دقت کنید، سه پروژه ایجاد شده‌است:
الف) پروژه‌ی کلاینت: که همان WASM است.
ب) پروژه‌ی سرور: که یک پروژه‌ی ASP.NET Core Web API ارائه کننده‌ی سرویس و API آب و هوا است و همچنین هاست کننده‌ی WASM ما.
ج) پروژه‌ی Shared: کدهای این پروژه، بین هر دو پروژه به اشتراک گذاشته می‌شوند و برای مثال محل مناسبی است برای تعریف DTO ها. برای نمونه WeatherForecast.cs قرار گرفته‌ی در آن، DTO یا data transfer object سرویس API برنامه است که قرار است به کلاینت بازگشت داده شود. به این ترتیب دیگر نیازی نخواهد بود تا این تعاریف را در پروژه‌های سرور و کلاینت تکرار کنیم و می‌توان کدهای اینگونه را به اشتراک گذاشت.
کاربرد دیگر آن تعریف کلاس‌های کمکی است؛ مانند StringUtils فوق. به همین به پروژه‌ی Shared مراجعه کرده و کلاس StringUtils را به صورت زیر در آن تعریف می‌کنیم (و یا حتی می‌توان این قطعه کد را داخل یک پوشه‌ی جدید، در همان پروژه‌ی WASM نیز قرار داد):
namespace BlazorRazorSample.Shared
{
    public class StringUtils
    {
        public static string MyNewCustomToUpper(string value) => value.ToUpper();
    }
}
اگر به فایل‌های csproj دو پروژه‌ی سرور و کلاینت جاری مراجعه کنیم، از پیش، مدخلی را به فایل Shared\BlazorRazorSample.Shared.csproj دارند. بنابراین جهت معرفی این اسمبلی به آن‌ها، نیاز به کار خاصی نیست و از پیش، ارجاعی به آن تعریف شده‌است.

پس از آن روش استفاده‌ی از این کلاس کمکی خارجی اشتراکی به صورت زیر است:
@page "/"

@using BlazorRazorSample.Shared

<p>Hello, @StringUtils.MyNewCustomToUpper(name)</p>
ابتدا فضای نام این کلاس را با استفاده از using@ مشخص می‌کنیم و سپس امکان دسترسی به امکانات آن میسر می‌شود.

یک نکته: می‌توان به فایل Client\_Imports.razor مراجعه و مدخل زیر را به انتهای آن اضافه کرد:
@using BlazorRazorSample.Shared
به این ترتیب دیگر نیازی به ذکر این using@ تکراری، در هیچکدام از فایل‌های razor. پروژه‌ی کلاینت نخواهد بود؛ چون تعاریف درج شده‌ی در فایل Client\_Imports.razor سراسری هستند.


کار با حلقه‌ها در فایل‌های razor.

همانطور که عنوان شد، یکی از کاربردهای پروژه‌ی Shared، امکان به اشتراک گذاشتن مدل‌ها، در برنامه‌های کلاینت و سرور است. برای مثال یک پوشه‌ی جدید Models را در این پروژه ایجاد کرده و کلاس MovieDto را به صورت زیر در آن تعریف می‌کنیم:
using System;

namespace BlazorRazorSample.Shared.Models
{
    public class MovieDto
    {
        public string Title { set; get; }

        public DateTime ReleaseDate { set; get; }
    }
}
سپس به فایل Client\_Imports.razor مراجعه کرده و فضای نام این پوشه را اضافه می‌کنیم؛ تا دیگر نیازی به تکرار آن در تمام فایل‌های razor. برنامه‌ی کلاینت نباشد:
@using BlazorRazorSample.Shared.Models
اکنون می‌خواهیم لیستی از فیلم‌ها را در فایل Client\Pages\Index.razor نمایش دهیم:
@page "/"

<div>
    <h3>Movies</h3>
    @foreach(var movie in movies)
    {
        <p>Title: <b>@movie.Title</b></p>
        <p>ReleaseDate: @movie.ReleaseDate.ToString("dd MMM yyyy")</p>
    }
</div>

@code
{
    List<MovieDto> movies = new List<MovieDto>
    {
        new MovieDto
        {
            Title = "Movie 1",
            ReleaseDate = DateTime.Now.AddYears(-1)
        },
        new MovieDto
        {
            Title = "Movie 2",
            ReleaseDate = DateTime.Now.AddYears(-2)
        },
        new MovieDto
        {
            Title = "Movie 3",
            ReleaseDate = DateTime.Now.AddYears(-3)
        }
    };
}
در اینجا در ابتدا لیستی از MovieDto‌ها در قسمت code@ تعریف شده و سپس روش استفاده‌ی از یک حلقه‌ی foreach سی‌شارپ را در کدهای razor نوشته شده، مشاهده می‌کنید که این خروجی را ایجاد می‌کند:


یک نکته: در حین تعریف فیلدهای code@، امکان استفاده‌ی از var وجود ندارد؛ مگر اینکه از آن بخواهیم در داخل بدنه‌ی یک متد استفاده کنیم.

و یا نمونه‌ی دیگری از حلقه‌های #‍C مانند for را می‌توان به صورت زیر تعریف کرد:
    @for(var i = 0; i < movies.Count; i++)
    {
        <div style="background-color: @(i % 2 == 0 ? "blue" : "red")">
            <p>Title: <b>@movies[i].Title</b></p>
            <p>ReleaseDate: @movies[i].ReleaseDate.ToString("dd MMM yyyy")</p>
        </div>
    }
در اینجا روش تغییر پویای background-color هر ردیف را نیز به کمک کدهای razor، مشاهده می‌کنید. اگر شماره‌ی ردیفی زوج بود، با آبی نمایش داده می‌شود؛ در غیراینصورت با قرمز. در اینجا نیز از ()@ برای تعیین محدوده‌ی کدهای #C نوشته شده، کمک گرفته‌ایم.


نمایش شرطی عبارات در فایل‌های razor.

اگر به مثال توکار Client\Pages\FetchData.razor مراجعه کنیم (مربوط به حالت host-- که در ابتدای مطلب عنوان شد)، کدهای زیر قابل مشاهده هستند:
@page "/fetchdata"
@using BlazorRazorSample.Shared
@inject HttpClient Http

<h1>Weather forecast</h1>

<p>This component demonstrates fetching data from the server.</p>

@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private WeatherForecast[] forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
    }

}
در این مثال، روش کار با یک سرویس تزریق شده‌ی async که قرار است از Web API اطلاعاتی را دریافت کند، مشاهده می‌کنید. در اینجا برخلاف مثال قبلی ما، از روال رویدادگردان OnInitializedAsync برای مقدار دهی لیست یا آرایه‌ای از اطلاعات وضعیت هوا استفاده شده‌است (و نه به صورت مستقیم در یک فیلد قسمت code@). این مورد جزو life-cycle‌های کامپوننت‌های razor است که در قسمت‌های بعد بیشتر بررسی خواهد شد. متد OnInitializedAsync برای بارگذاری اطلاعات یک سرویس از راه دور استفاده می‌شود و در اولین بار اجرای کامپوننت فراخوانی خواهد شد. نکته‌ی مهمی که در اینجا وجود دارد، نال بودن فیلد forecasts در زمان رندر اولیه‌ی کامپوننت جاری است؛ از این جهت که کار دریافت اطلاعات از سرور زمان‌بر است ولی رندر کامپوننت، به صورت آنی صورت می‌گیرد. در این حالت زمانیکه نوبت به اجرای foreach (var forecast in forecasts)@ می‌رسد، برنامه با یک استثنای نال بودن forecasts، متوقف خواهد شد؛ چون هنوز کار OnInitializedAsync به پایان نرسیده‌است:


 برای رفع این مشکل، ابتدا یک if@ مشاهده می‌شود، تا نال بودن forecasts را بررسی کند:
@if (forecasts == null)
{
    <p><em>Loading...</em></p>
}
و همچنین عبارت در حال بارگذاری را نمایش می‌دهد. سپس در قسمت else آن، نمایش اطلاعات دریافت شده را توسط یک حلقه‌ی foreach مشاهده می‌کنید. با مقدار دهی forecasts در متد OnInitializedAsync، مجددا کار رندر جدول انجام خواهد شد.


روش نمایش عبارات HTML در فایل‌های razor.

فرض کنید عنوان اول فیلم مثال جاری، به همراه یک تگ HTML هم هست:
new MovieDto
{
   Title = "<i>Movie 1</i>",
   ReleaseDate = DateTime.Now.AddYears(-1)
},
در این حالت اگر برنامه را اجرا کنیم، خروجی آن دقیقا به صورت <Title: <i>Movie 1</i خواهد بود. این مورد به دلایل امنیتی انجام شده‌است. اگر پیشتر تگ‌های HTML را تمیز کرده‌اید و مطمئن هستید که خطری را ایجاد نمی‌کنند، می‌توانید با استفاده از روش زیر، آن‌ها را رندر کرد:
<p>Title: <b>@((MarkupString)movie.Title)</b></p>


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-03.zip
برای اجرای آن وارد پوشه‌ی Server شده و دستور dotnet run را اجرا کنید.
مطالب
ارتقاء به HTTP Client در Angular 4.3
عموما در برنامه‌های SPA، اطلاعات از طریق HTTP و از طرف سرور دریافت می‌شوند. از نگارش‌های ابتدایی Angular، اینکار از طریق HTTP Module آن مسیر بود و هست. در Angular 4.3 روش بهبودیافته‌ای نسبت به این روش متداول ارائه شده‌است که در ادامه تعدادی از ویژگی‌های مقدماتی آن‌را بررسی می‌کنیم.
هرچند ارتقاء به HttpClient الزامی نیست و کدهای پیشین، هنوز هم به خوبی کار می‌کنند؛ اما طراحی جدید آن شامل ویژگی‌های توکاری است که به سختی توسط HTTP Module پیشین قابل پیاده سازی هستند.


به روز رسانی وابستگی‌های پروژه

پیش از هرکاری نیاز است وابستگی‌های پروژه را به روز رسانی کرد و یکی از روش‌های ساده‌ی یافتن شماره نگارش‌های جدید بسته‌های تعریف شده‌ی در فایل package.json برنامه، استفاده از بسته‌ی npm-check-updates است:
npm install npm-check-updates -g
ncu
دستور اول، این بسته را به صورت عمومی نصب کرده و صدور دستور دوم در ریشه‌ی پروژه، سبب می‌شود تا گزارشی از آخرین به روز رسانی‌ها، نمایش داده شود (بدون هیچگونه تغییری در پروژه):


در اینجا شماره نگارش‌های جدید مشخص شده‌اند و همچنین روش سریع ارتقاء به آن‌ها نیز ذکر شده‌است. فقط کافی است دستورات ذیل را صادر کنیم تا این به روز رسانی‌ها توسط ncu انجام شوند:
ncu -a
npm update
دستور اول صرفا شماره نگارش‌های جدید را در فایل package.json، به صورت خودکار اصلاح می‌کند و دستور دوم سبب دریافت، نصب و اعمال آن‌ها خواهد گردید.


تغییرات مورد نیاز جهت معرفی ماژول HttpClient

این ماژول جدید از طریق اینترفیس HttpClientModule ارائه می‌شود. بنابراین اولین تغییر در جهت ارتقاء به نگارش 4.3، اصلاح importهای لازم است:
از:
 import { HttpModule } from '@angular/http';
به:
 import { HttpClientModule } from '@angular/common/http';

پس از آن، این HttpClientModule را به لیست imports ماژول اصلی برنامه اضافه می‌کنیم؛ تا در کل برنامه قابل دسترسی شود:
@NgModule({
    imports: [
        // ...
        HttpClientModule,
        // ...
    ],
declarations: [ ... ],
providers: [ ... ],
exports: [ ... ]
})
export class AppModule { }


تغییرات مورد نیاز در سازنده‌ها و تزریق وابستگی‌ها

پس از تغییرات فوق، دیگر دسترسی به HttpModule پیشین را نداریم. بنابراین نیاز است هر جائی را که سرویس Http به سازنده‌ی کلاسی تزریق شده‌است، یافته و به صورت ذیل تغییر دهیم:
از:
 constructor(private http: Http) { }
به:
import { HttpClient } from '@angular/common/http';
// ...
constructor(private http: HttpClient) { }


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Verbs

یکی از اهداف HTTP Client جدید، سادگی کار با اطلاعات دریافتی از سرور است. برای مثال در HTTP Module پیشین، روش دریافت اطلاعات از سرور به صورت ذیل است:
public get(): Observable<MyType> => {
    return this.http.get(url)
        .map((response: Response) => <MyType>response.json());
}
ابتدا درخواستی ارسال شده و سپس نتیجه‌ی آن به JSON تبدیل گشته و در آخر به نوع بازگشتی متد تبدیل می‌شود.
در HTTP Client جدید دیگر نیازی نیست تا متد ()json. فراخوانی شود. در اینجا به صورت پیش‌فرض نوع بازگشتی از سرور JSON فرض می‌شود. همچنین اکنون متدهای get/put/post و امثال آن برخلاف HTTP Client قبلی، جنریک هستند. یعنی در همینجا می‌توان نوع بازگشتی را هم مشخص کرد. به این ترتیب، قطعه کد قدیمی فوق، به کد ساده‌ی ذیل تبدیل می‌شود که در آن خبری از map و همچنین یک cast اضافی نیست:
get<T>(url: string): Observable<T> {
    return this.http.get<T>(url);
}
برای نمونه شبیه به همین نکته برای post نیز صادق است:
post<T>(url: string, body: string): Observable<T> {
   return this.http.post<T>(url, body);
}

نکته 1: در اینجا اگر خروجی از سرور، نوع دیگری را داشت، نیاز است responseType را به صورت صریحی به شکل ذیل مشخص کرد:
 getData() {
  this.http.get(this.url, { responseType: 'text' }).subscribe(res => {
       this.data = res;
  });
}
در این‌حالت خروجی متنی <Observable<string را دریافت می‌کنیم و نیازی به ذکر <get<string نیست.

نکته 2: ممکن است اطلاعات بازگشتی از سمت سرور، داخل یک فیلد محصور شده باشند:
{
  "results": [
    "Item 1",
    "Item 2",
  ]
}
در این حالت برای دسترسی به اطلاعات این فیلد می‌توان از حالت key/value بودن اشیاء جاوا اسکریپتی به شکل زیر برای دسترسی به خاصیت results استفاده کرد:
this.http.get('/api/items').subscribe(data => {
   this.results = data['results'];
});


نکاتی را که باید حین کار با یک RxJS Observable-based API در نظر داشت

این API جدید نیز همانند قبل مبتنی بر RxJS Observables است. بنابراین نکات ذیل در مورد آن نیز صادق است:
- اگر متد subscribe بر روی این observables فراخوانی نشود، اتفاقی رخ نخواهد داد.
- اگر چندین بار مشترک این observables شویم، چندین درخواست HTTP صادر می‌شوند.
- این نوع خاص از observables، تنها یک مقدار را بازگشت می‌دهند. اگر درخواست HTTP موفقیت آمیز باشد، این observables یک نتیجه را بازگشت داده و سپس خاتمه پیدا می‌کنند.
- این observables اگر در حین درخواست HTTP با خطایی مواجه شوند، سبب صدور استثنایی می‌شوند.


تغییرات مورد نیاز در کدهای سرویس‌ها جهت کار با HTTP Headers

در اینجا برای تعریف headers می‌توان به صورت ذیل عمل کرد:
import { HttpHeaders } from "@angular/common/http";

const headers = new HttpHeaders({ "Content-Type": "application/json" });
و یا به صورت fluent به شکل زیر:
 const headers = new HttpHeaders().set("Accept", "application/json").set('Content-Type', 'application/json');

سپس آن‌را به عنوان پارامتر سوم، به متدهای http ارسال می‌کنیم. یک مثال:
  updateAppProduct(id: number, item: AppProduct): Observable<AppProduct> {
    const header = new HttpHeaders({ "Content-Type": "application/json" });
    return this.http
      .put<AppProduct>(
        `${this.baseUrl}/UpdateProduct/${id}`,
        JSON.stringify(item),
        { headers: header }
      )
      .map(response => response || {});
  }

تعریف پارامتر options اینبار به صورت یک شیء دارای چندین خاصیت درآمده‌است. به همین جهت است که در اینجا یک {} را نیز مشاهده می‌کنید:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>

یک نکته: شیء HttpHeaders به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const headers = new HttpHeaders();
headers = headers.set('Content-Type', 'application/json');
headers = headers.set('Accept', 'application/json');
headers تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
 const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Accept', 'application/json')
;


امکان تعریف HttpParams

اگر به شیء options در تعریف فوق دقت کنید، دارای خاصیت اختیاری params نیز هست. از آن می‌توان جهت تعریف کوئری استرینگ‌ها استفاده کرد. برای مثال درخواست ذیل:
http
  .post('/api/items/add', body, {
      params: new HttpParams().set('id', '3'),
  })
  .subscribe();
سبب تولید یک چنین URL ایی می‌گردد:
  /api/items/add?id=3

یک نکته: شیء HttpParams به صورت immutable طراحی شده‌است. یعنی اگر آن‌را به صورت ذیل فراخوانی کنیم:
const params = new HttpParams();
params.set('orderBy', '"$key"')
params.set('limitToFirst', "1");
params تولیدی ... خالی خواهد بود. به همین جهت روش صحیح تشکیل آن به صورت ذیل و زنجیروار است:
const params = new HttpParams()
.set('orderBy', '"$key"')
.set('limitToFirst', "1");
به علاوه روش تعریف ذیل نیز برای کار با HttpParams مجاز است:
 const params = new HttpParams({fromString: 'orderBy="$key"&limitToFirst=1'});


تغییرات مورد نیاز در کدهای سرویس‌ها جهت مدیریت خطاها

در اینجا اینبار خطای بازگشتی، از نوع ویژه‌ی HttpErrorResponse است که شامل اطلاعات شماره کد و متن خطای حاصل می‌باشد:
import { HttpClient, HttpHeaders, HttpErrorResponse } from "@angular/common/http";

postData() { 
  this.http.post(this.url, this.payload).subscribe( 
    res => { 
      console.log(res); 
    }, 
    (err: HttpErrorResponse) => { 
      console.log(err.error); 
      console.log(err.name); 
      console.log(err.message); 
      console.log(err.status); 

        if (err.error instanceof Error) { 
          console.log("Client-side error occured."); 
        } else { 
          console.log("Server-side error occured."); 
        }
    } 
  ); 
}


امکان سعی مجدد در اتصال توسط HTTP Client

ممکن است در اولین سعی در اتصال به سرور، خطایی رخ دهد و یا سرور در دسترس نباشد. در اینجا توسط متد retry می‌توان درخواست سعی مجدد در اتصال را صادر کرد.
برای این منظور ابتدا عملگر retry مربوط به RxJS را import می‌کنیم:
 import 'rxjs/add/operator/retry';
سپس:
http
  .get<ItemsResponse>('/api/items')
  .retry(3)
  .subscribe(...);
این کد در صورت بروز خطایی، این عملیات را سه بار تکرار می‌کند. در انتها اگر بازهم خطایی دریافت شد، این خطا را به برنامه بازگشت می‌دهد.


امکان درخواست کل Response بجای Body

اگر به امضای پارامتر اختیاری options دقت کنید، خاصیت observe آن به صورت پیش فرض به body تنظیم شده‌است. به این معنا که تنها body یک response را تبدیل به یک شیء JSON می‌کند:
(method) HttpClient.post(url: string, body: any, options?: {
          headers?: HttpHeaders;
          observe?: "body";
          params?: HttpParams;
          reportProgress?: boolean;
          responseType?: "json";
          withCredentials?: boolean;
}): Observable<Object>
اما گاهی از اوقات نیاز است تا به کل Response دسترسی داشته باشیم. در این حالت باید نوع observe را به response تنظیم کرد:
http
  .get<MyJsonData>('/data.json', {observe: 'response'})
  .subscribe(resp => {
    console.log(resp.headers.get('X-Custom-Header'));
    console.log(resp.body.someField);
  });
به این ترتیب اینبار resp از نوع <HttpResponse<MyJsonData خواهد بود که توسط آن می‌توان به خواص headers و یا body، به صورت جداگانه‌ای دسترسی یافت.


یک نکته‌ی تکمیلی: کدهای سری کار با فرم‌ها در Angular را اگر به HttpClient ارتقاء دهیم، خلاصه‌ی تغییرات آن‌ها به این صورت خواهند بود.