مطالب
پیاده سازی سیاست‌های دسترسی پویای سمت سرور و کلاینت در برنامه‌های Blazor WASM
فرض کنید در حال توسعه‌ی یک برنامه‌ی Blazor WASM هاست شده هستید و می‌خواهید که نیازی نباشد تا به ازای هر صفحه‌ای که به برنامه اضافه می‌کنید، یکبار منوی آن‌را به روز رسانی کنید و نمایش منو به صورت خودکار توسط برنامه صورت گیرد. همچنین در این حالت نیاز است در قسمت مدیریتی برنامه، بتوان به صورت پویا، به ازای هر کاربری، مشخص کرد که به کدامیک از صفحات برنامه دسترسی دارد و یا خیر و به علاوه اگر به صفحاتی دسترسی ندارد، مشخصات این صفحه، در منوی پویا برنامه ظاهر نشود و همچنین با تایپ آدرس آن در نوار آدرس مرورگر نیز قابل دسترسی نباشد. امن سازی پویای سمت کلاینت، یک قسمت از پروژه‌است؛ قسمت دیگر چنین پروژه‌ای، لیست کردن اکشن متدهای API سمت سرور پروژه و انتساب دسترسی‌های پویایی به این اکشن متدها، به کاربران مختلف برنامه‌است.


دریافت کدهای کامل این پروژه

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


پیشنیازها

در پروژه‌ی فوق برای شروع به کار، از اطلاعات مطرح شده‌ی در سلسله مطالب زیر استفاده شده‌است:

- «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity»
- پیاده سازی اعتبارسنجی کاربران در برنامه‌های Blazor WASM؛ قسمت‌های 31 تا 33 .
- «غنی سازی کامپایلر C# 9.0 با افزونه‌ها»
- «مدیریت مرکزی شماره نگارش‌های بسته‌های NuGet در پروژه‌های NET Core.»
- «کاهش تعداد بار تعریف using‌ها در C# 10.0 و NET 6.0.»
- «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»


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

صفحات امن سازی شده‌ی سمت کلاینت، با ویژگی Authorize مشخص می‌شوند. بنابراین قید آن الزامی است، تا صرفا جهت کاربران اعتبارسنجی شده، قابل دسترسی شوند. در اینجا می‌توان یک نمونه‌ی سفارشی سازی شده‌ی ویژگی Authorize را به نام ProtectedPageAttribute نیز مورد استفاده قرار داد. این ویژگی از AuthorizeAttribute ارث‌بری کرده و دقیقا مانند آن عمل می‌کند؛ اما این اضافات را نیز به همراه دارد:
- به همراه یک Policy از پیش تعیین شده به نام CustomPolicies.DynamicClientPermission است تا توسط قسمت‌های بررسی سطوح دسترسی پویا و همچنین منوساز برنامه، یافت شده و مورد استفاده قرار گیرد.
- به همراه خواص اضافه‌تری مانند GroupName و Title نیز هست. GroupName نام سرتیتر منوی dropdown نمایش داده شده‌ی در منوی اصلی برنامه‌است و Title همان عنوان صفحه که در این منو نمایش داده می‌شود. اگر صفحه‌ی محافظت شده‌ای به همراه GroupName نباشد، یعنی باید به صورت یک آیتم اصلی نمایش داده شود. همچنین در اینجا یک سری Order هم درنظر گرفته شده‌اند تا بتوان ترتیب نمایش صفحات را نیز به دلخواه تغییر داد.


نمونه‌ای از استفاده‌ی از ویژگی فوق را در مسیر src\Client\Pages\Feature1 می‌توانید مشاهده کنید که خلاصه‌ی آن به صورت زیر است:
 @attribute [ProtectedPage(GroupName = "Feature 1", Title = "Page 1", GlyphIcon = "bi bi-dot", GroupOrder = 1, ItemOrder = 1)]

ویژگی ProtectedPage را معادل یک ویژگی Authorize سفارشی، به همراه چند خاصیت بیشتر، جهت منوساز پویای برنامه درنظر بگیرید.


نیاز به لیست کردن صفحات علامتگذاری شده‌ی با ویژگی ProtectedPage

پس از اینکه صفحات مختلف برنامه را توسط ویژگی ProtectedPage علامتگذاری کردیم، اکنون نوبت به لیست کردن پویای آن‌ها است. اینکار توسط سرویس ProtectedPagesProvider صورت می‌گیرد. این سرویس با استفاده از Reflection، ابتدا تمام IComponentها یا همان کامپوننت‌های تعریف شده‌ی در برنامه را از اسمبلی جاری استخراج می‌کند. بنابراین اگر نیاز دارید که این جستجو در چندین اسمبلی صورت گیرد، فقط کافی است ابتدای این کدها را تغییر دهید. پس از یافت شدن IComponent ها، فقط آن‌هایی که دارای RouteAttribute هستند، پردازش می‌شوند؛ یعنی کامپوننت‌هایی که به همراه مسیریابی هستند. پس از آن بررسی می‌شود که آیا این کامپوننت دارای ProtectedPageAttribute هست یا خیر؟ اگر بله، این کامپوننت در لیست نهایی درج خواهد شد.


نیاز به یک منوساز پویا جهت نمایش خودکار صفحات امن سازی شده‌ی با ویژگی ProtectedPage

اکنون که لیست صفحات امن سازی شده‌ی توسط ویژگی ProtectedPage را در اختیار داریم، می‌توانیم آن‌ها را توسط کامپوننت سفارشی NavBarDynamicMenus به صورت خودکار نمایش دهیم. این کامپوننت لیست صفحات را توسط کامپوننت NavBarDropdownMenu نمایش می‌دهد.


تهیه‌ی جداول و سرویس‌های ثبت دسترسی‌های پویای سمت کلاینت


جداول و فیلدهای مورد استفاده‌ی در این پروژه را در تصویر فوق ملاحظه می‌کنید که در پوشه‌ی src\Server\Entities نیز قابل دسترسی هستند. در این برنامه نیاز به ذخیره سازی اطلاعات نقش‌های کاربران مانند نقش Admin، ذخیره سازی سطوح دسترسی پویای سمت کلاینت و همچنین سمت سرور است. بنابراین بجای اینکه به ازای هر کدام، یک جدول جداگانه را تعریف کنیم، می‌توان از همان طراحی ASP.NET Core Identity مایکروسافت با استفاده از جدول UserClaimها ایده گرفت. یعنی هر کدام از این موارد، یک Claim خواهند شد:


در اینجا نقش‌ها با Claim استانداردی به نام http://schemas.microsoft.com/ws/2008/06/identity/claims/role که توسط خود مایکروسافت نامگذاری شده و سیستم‌های اعتبارسنجی آن بر همین اساس کار می‌کنند، قابل مشاهده‌است. همچنین دو Claim سفارشی دیگر ::DynamicClientPermission:: برای ذخیره سازی اطلاعات صفحات محافظت شده‌ی سمت کلاینت و ::DynamicServerPermission::  جهت ذخیره سازی اطلاعات اکشن متدهای محافظت شده‌ی سمت سرور نیز تعریف شده‌اند. رابطه‌ای این اطلاعات با جدول کاربران، many-to-many است.


به این ترتیب است که مشخص می‌شود کدام کاربر، به چه claimهایی دسترسی دارد.

برای کار با این جداول، سه سرویس UsersService، UserClaimsService و UserTokensService پیش بینی شده‌اند. UserTokens اطلاعات توکن‌های صادر شده‌ی توسط برنامه را ذخیره می‌کند و توسط آن می‌توان logout سمت سرور را پیاده سازی کرد؛ از این جهت که JWTها متکی به خود هستند و تا زمانیکه منقضی نشوند، در سمت سرور پردازش خواهند شد، نیاز است بتوان به نحوی اگر کاربری غیرفعال شد، از آن ثانیه به بعد، توکن‌های او در سمت سرور پردازش نشوند که به این نکات در مطلب «اعتبارسنجی مبتنی بر JWT در ASP.NET Core 2.0 بدون استفاده از سیستم Identity» پیشتر پرداخته شده‌است.
اطلاعات این سرویس‌ها توسط اکشن متدهای UsersAccountManagerController، در اختیار برنامه‌ی کلاینت قرار می‌گیرند.


نیاز به قسمت مدیریتی ثبت دسترسی‌های پویای سمت کلاینت و سرور

قبل از اینکه بتوان قسمت‌های مختلف کامپوننت NavBarDynamicMenus را توضیح داد، نیاز است ابتدا یک قسمت مدیریتی را جهت استفاده‌ی از لیست ProtectedPageها نیز تهیه کرد:


در این برنامه، کامپوننت src\Client\Pages\Identity\UsersManager.razor کار لیست کردن کاربران، که اطلاعات آن‌را از کنترلر UsersAccountManagerController دریافت می‌کند، انجام می‌دهد. در مقابل نام هر کاربر، دو دکمه‌ی ثبت اطلاعات پویای دسترسی‌های سمت کلاینت و سمت سرور وجود دارد. سمت کلاینت آن توسط کامپوننت UserClientSidePermissions.razor مدیریت می‌شود و سمت سرور آن توسط UserServerSidePermissions.razor.
کامپوننت UserClientSidePermissions.razor، همان لیست صفحات محافظت شده‌ی توسط ویژگی ProtectedPage را به صورت گروه بندی شده و به همراه یک سری chekmark، ارائه می‌دهد. اگر در اینجا صفحه‌ای انتخاب شد، اطلاعات آن به سمت سرور ارسال می‌شود تا توسط Claim ای به نام ::DynamicClientPermission:: به کاربر انتخابی انتساب داده شود.


شبیه به همین عملکرد در مورد دسترسی سمت سرور نیز برقرار است. UserServerSidePermissions.razor، لیست اکشن متدهای محافظت شده را از کنترلر DynamicPermissionsManagerController دریافت کرده و نمایش می‌دهد. این اطلاعات توسط سرویس ApiActionsDiscoveryService جمع آوری می‌شود. همچنین این اکشن متدهای ویژه نیز باید با ویژگی Authorize(Policy = CustomPolicies.DynamicServerPermission) مزین شده باشند که نمونه مثال آن‌ها را در مسیر src\Server\Controllers\Tests می‌توانید مشاهده کنید. اگر در سمت کلاینت و قسمت مدیریتی آن، اکشن متدی جهت کاربر خاصی انتخاب شد، اطلاعات آن ذیل Claimای به نام ::DynamicServerPermission::  به کاربر انتخابی انتساب داده می‌شود.



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

تا اینجا کامپوننت‌های امن سازی شده‌ی سمت کلاینت و اکشن متدهای امن سازی شده‌ی سمت سرور را توسط صفحات مدیریتی برنامه، به کاربران مدنظر خود انتساب دادیم و توسط سرویس‌های سمت سرور، اطلاعات آن‌ها را در بانک اطلاعاتی ذخیره کردیم. اکنون نوبت به استفاده‌ی از claims تعریف شده و مرتبط با هر کاربر است. پس از یک لاگین موفقیت آمیز توسط UsersAccountManagerController، سه توکن به سمت کاربر ارسال می‌شوند:
- توکن دسترسی: اطلاعات اعتبارسنجی کاربر به همراه نام و نقش‌های او در این توکن وجود دارند.
- توکن به روز رسانی: هدف از آن، دریافت یک توکن دسترسی جدید، بدون نیاز به لاگین مجدد است. به این ترتیب کاربر مدام نیاز به لاگین مجدد نخواهد داشت و تا زمانیکه refresh token منقضی نشده‌است، برنامه می‌تواند از آن جهت دریافت یک access token جدید استفاده کند.
- توکن سطوح دسترسی پویای سمت کلاینت: در اینجا لیست ::DynamicClientPermission::ها به صورت یک توکن مجزا به سمت کاربر ارسال می‌شود. این اطلاعات به توکن دسترسی اضافه نشده‌اند تا بی‌جهت حجم آن اضافه نشود؛ از این جهت که نیازی نیست تا به ازای هر درخواست HTTP به سمت سرور، این لیست حجیم claims پویای سمت کلاینت نیز به سمت سرور ارسال شود. چون سمت سرور از claims دیگری به نام ::DynamicServerPermission:: استفاده می‌کند.


اگر دقت کنید، هم refresh-token و هم DynamicPermissions هر دو به صورت JWT ارسال شده‌اند. می‌شد هر دو را به صورت plain و ساده نیز ارسال کرد. اما مزیت refresh token ارسال شده‌ی به صورت JWT، انجام اعتبارسنجی خودکار سمت سرور اطلاعات آن است که دستکاری سمت کلاینت آن‌را مشکل می‌کند.
این سه توکن توسط سرویس BearerTokensStore، در برنامه‌ی سمت کلاینت ذخیره و بازیابی می‌شوند. توکن دسترسی یا همان access token، توسط ClientHttpInterceptorService به صورت خودکار به تمام درخواست‌های ارسالی توسط برنامه الصاق خواهد شد.


مدیریت خودکار اجرای Refresh Token در برنامه‌های Blazor WASM

دریافت refresh token از سمت سرور تنها قسمتی از مدیریت دریافت مجدد یک access token معتبر است. قسمت مهم آن شامل دو مرحله‌ی زیر است:
الف) اگر خطاهای سمت سرور 401 و یا 403 رخ دادند، ممکن است نیاز به refresh token باشد؛ چون احتمالا یا کاربر جاری به این منبع دسترسی ندارد و یا access token دریافتی که طول عمر آن کمتر از refresh token است، منقضی شده و دیگر قابل استفاده نیست.
ب) پیش از منقضی شدن access token، بهتر است با استفاده از refresh token، یک access token جدید را دریافت کرد تا حالت الف رخ ندهد.

- برای مدیریت حالت الف، یک Policy ویژه‌ی Polly طراحی شده‌است که آن‌را در کلاس ClientRefreshTokenRetryPolicy مشاهده می‌کنید. در این Policy ویژه، هرگاه خطاهای 401 و یا 403 رخ دهند، با استفاده از سرویس جدید IClientRefreshTokenService، کار به روز رسانی توکن انجام خواهد شد. این Policy در کلاس program برنامه ثبت شده‌است. مزیت کار با Policyهای Polly، عدم نیاز به try/catch نوشتن‌های تکراری، در هر جائیکه از سرویس‌های HttpClient استفاده می‌شود، می‌باشد.

- برای مدیریت حالت ب، حتما نیاز به یک تایمر سمت کلاینت است که چند ثانیه پیش از منقضی شدن access token دریافتی پس از لاگین، کار دریافت access token جدیدی را به کمک refresh token موجود، انجام دهد. پیاده سازی این تایمر را در کلاس ClientRefreshTokenTimer مشاهده می‌کنید که محل فراخوانی و راه اندازی آن یا پس از لاگین موفق در سمت کلاینت و یا با ریفرش صفحه (فشرده شدن دکمه‌ی F5) و در کلاس آغازین ClientAuthenticationStateProvider می‌باشد.



نیاز به پیاده سازی Security Trimming سمت کلاینت

از داخل DynamicPermissions دریافتی پس از لاگین، لیست claimهای دسترسی پویای سمت کلاینت کاربر لاگین شده استخراج می‌شود. بنابراین مرحله‌ی بعد، استخراج، پردازش و اعمال این سطوح دسترسی پویای دریافت شده‌ی از سرور است.
سرویس BearerTokensStore، کار ذخیره سازی توکن‌های دریافتی پس از لاگین را انجام می‌دهد و سپس با استفاده از سرویس DynamicClientPermissionsProvider، توکن سوم دریافت شده که مرتبط با لیست claims دسترسی کاربر جاری است را پردازش کرده و تبدیل به یک لیست قابل استفاده می‌کنیم تا توسط آن بتوان زمانیکه قرار است آیتم‌های منوها را به صورت پویا نمایش داد، مشخص کنیم که کاربر، به کدامیک دسترسی دارد و به کدامیک خیر. عدم نمایش قسمتی از صفحه که کاربر به آن دسترسی ندارد را security trimming گویند. برای نمونه کامپوننت ویژه‌ی SecurityTrim.razor، با استفاده از نقش‌ها و claims یک کاربر، می‌تواند تعیین کند که آیا قسمت محصور شده‌ی صفحه توسط آن قابل نمایش به کاربر است یا خیر. این کامپوننت از متدهای کمکی AuthenticationStateExtensions که کار با user claims دریافتی از طریق JWTها را ساده می‌کنند، استفاده می‌کند. یک نمونه از کاربرد کامپوننت SecurityTrim را در فایل src\Client\Shared\MainLayout.razor می‌توانید مشاهده کنید که توسط آن لینک Users Manager، فقط به کاربران دارای نقش Admin نمایش داده می‌شود.
نحوه‌ی مدیریت security trimming منوی پویای برنامه، اندکی متفاوت است. DynamicClientPermissionsProvider لیست claims متعلق به کاربر را بازگشت می‌دهد. این لیست پس از لاگین موفقیت آمیز دریافت شده‌است. سپس لیست کلی صفحاتی را که در ابتدای برنامه استخراج کردیم، در طی حلقه‌ای از سرویس ClientSecurityTrimmingService عبور می‌دهیم. یعنی مسیر صفحه و همچنین دسترسی‌های پویای کاربر، مشخص هستند. در این بین هر مسیری که در لیست claims پویای کاربر نبود، در لیست آیتم‌های منوی پویای برنامه، نمایش داده نمی‌شود.


نیاز به قطع دسترسی به مسیرهایی در سمت کلاینت که کاربر به صورت پویا به آن‌ها دسترسی ندارد

با استفاده از ClientSecurityTrimmingService، در حلقه‌ای که آیتم‌های منوی سایت را نمایش می‌دهد، موارد غیرمرتبط با کاربر جاری را حذف کردیم و نمایش ندادیم. اما این حذف، به این معنا نیست که اگر این آدرس‌ها را به صورت مستقیم در مرورگر وارد کند، به آن‌ها دسترسی نخواهد داشت. برای رفع این مشکل، نیاز به پیاده سازی یک سیاست دسترسی پویای سمت کلاینت است. روش ثبت این سیاست را در کلاس DynamicClientPermissionsPolicyExtensions مشاهده می‌کنید. کلید آن همان CustomPolicies.DynamicClientPermission که در حین تعریف ProtectedPageAttribute به عنوان مقدار Policy پیش‌فرض مقدار دهی شد. یعنی هرگاه ویژگی ProtectedPage به صفحه‌ای اعمال شد، از این سیاست دسترسی استفاده می‌کند که پردازشگر آن DynamicClientPermissionsAuthorizationHandler است. این هندلر نیز از ClientSecurityTrimmingService استفاده می‌کند. در هندلر context.User جاری مشخص است. این کاربر را به متد تعیین دسترسی مسیر جاری به سرویس ClientSecurityTrimming ارسال می‌کنیم تا مشخص شود که آیا به مسیر درخواستی دسترسی دارد یا خیر؟


نیاز به قطع دسترسی به منابعی در سمت سرور که کاربر به صورت پویا به آن‌ها دسترسی ندارد

شبیه به ClientSecurityTrimmingService سمت کلاینت را در سمت سرور نیز داریم؛ به نام ServerSecurityTrimmingService که کار آن، پردازش claimهایی از نوع ::DynamicServerPermission::  است که در صفحه‌ی مدیریتی مرتبطی در سمت کلاینت، به هر کاربر قابل انتساب است. هندلر سیاست دسترسی پویایی که از آن استفاده می‌کند نیز DynamicServerPermissionsAuthorizationHandler می‌باشد. این سیاست دسترسی پویا با کلید CustomPolicies.DynamicServerPermission در کلاس ConfigureServicesExtensions تعریف شده‌است. به همین جهت هر اکشن متدی که Policy آن با این کلید مقدار دهی شده باشد، از هندلر پویای فوق جهت تعیین دسترسی پویا عبور خواهد کرد. منطق پیاده سازی شده‌ی در اینجا، بسیار شبیه به مطلب «سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا» است؛ اما بدون استفاده‌ی از ASP.NET Core Identity.


روش اجرای برنامه

چون این برنامه از نوع Blazor WASM هاست شده‌است، نیاز است تا برنامه‌ی Server آن‌را در ابتدا اجرا کنید. با اجرای آن، بانک اطلاعاتی SQLite برنامه به صورت خودکار توسط EF-Core ساخته شده و مقدار دهی اولیه می‌شود. لیست کاربران پیش‌فرض آن‌را در اینجا می‌توانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آن‌ها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.
مطالب
Blazor 5x - قسمت دوم - بررسی ساختار اولیه‌ی پروژه‌های Blazor
پس از آشنایی با دو مدل هاستینگ برنامه‌های Blazor در قسمت قبل، اکنون می‌خواهیم ساختار ابتدایی قالب‌های این دو پروژه را بررسی کنیم.


ایجاد پروژه‌های خالی Blazor

در انتهای قسمت قبل، با روش ایجاد پروژه‌های خالی Blazor به کمک NET SDK 5x. آشنا شدیم. به همین جهت دو پوشه‌ی جدید BlazorWasmSample و BlazorServerSample را ایجاد کرده و از طریق خط فرمان و با کمک NET CLI.، در پوشه‌ی اولی دستور dotnet new blazorwasm و در پوشه‌ی دومی دستور dotnet new blazorserver را اجرا می‌کنیم.
البته اجرای این دو دستور، نیاز به اتصال به اینترنت را هم برای بار اول دارند؛ تا فایل‌های مورد نیاز و بسته‌های مرتبط را دریافت و restore کنند. بسته به سرعت اینترنت، حداقل یک ربعی را باید صبر کنید تا دریافت ابتدایی بسته‌های مرتبط به پایان برسد. برای دفعات بعدی، از کش محلی NuGet، برای restore بسته‌های blazor استفاده می‌شود که بسیار سریع است.


بررسی ساختار پروژه‌ی Blazor Server و اجرای آن

پس از اجرای دستور dotnet new blazorserver در یک پوشه‌ی خالی و ایجاد پروژه‌ی ابتدایی آن:


همانطور که مشاهده می‌کنید، ساختار این پروژه، بسیار شبیه به یک پروژه‌ی استاندارد ASP.NET Core از نوع Razor pages است.
- در پوشه‌ی properties آن، فایل launchSettings.json قرار دارد که برای نمونه، شماره پورت اجرایی برنامه را در حالت اجرای توسط دستور dotnet run و یا توسط IIS Express مشخص می‌کند.

- پوشه‌ی wwwroot آن، مخصوص ارائه‌ی فایل‌های ایستا مانند wwwroot\css\bootstrap است. در ابتدای کار، این پوشه به همراه فایل‌های CSS بوت استرپ است. در ادامه اگر نیاز باشد، فایل‌های جاوا اسکریپتی را نیز می‌توان به این قسمت اضافه کرد.

- در پوشه‌ی Data آن، سرویس تامین اطلاعاتی اتفاقی قرار دارد؛ به نام WeatherForecastService که هدف آن، تامین اطلاعات یک جدول نمایشی است که در ادامه در آدرس https://localhost:5001/fetchdata قابل مشاهده است.

- در پوشه‌ی Pages، تمام کامپوننت‌های Razor برنامه قرار می‌گیرند. یکی از مهم‌ترین صفحات آن، فایل Pages\_Host.cshtml است. کار این صفحه‌ی ریشه، افزودن تمام فایل‌های CSS و JS، به برنامه‌است. بنابراین در آینده نیز از همین صفحه برای افزودن فایل‌های CSS و JS استفاده خواهیم کرد. اگر به قسمت body این صفحه دقت کنیم، تگ جدید کامپوننت قابل مشاهده‌است:
<body>
   <component type="typeof(App)" render-mode="ServerPrerendered" />
کار آن، رندر کامپوننت App.razor واقع در ریشه‌ی پروژه‌است.
همچنین در همینجا، تگ‌های دیگری نیز قابل مشاهده هستند:
<body>
    <component type="typeof(App)" render-mode="ServerPrerendered" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="_framework/blazor.server.js"></script>
</body>
همانطور که در قسمت قبل نیز عنوان شد، اتصال برنامه‌ی در حال اجرای در مرورگر blazor server با backend، از طریق یک اتصال دائم SignalR صورت می‌گیرد. اگر در این بین خطای اتصالی رخ دهد، div با id مساوی blazor-error-ui فوق، یکی از پیام‌‌هایی را که مشاهده می‌کنید، بسته به اینکه برنامه در حالت توسعه در حال اجرا است و یا در حالت ارائه‌ی نهایی، نمایش می‌دهد. افزودن مدخل blazor.server.js نیز به همین منظور است. کار آن مدیریت اتصال دائم SignalR، به صورت خودکار است و از این لحاظ نیازی به کدنویسی خاصی از سمت ما ندارد. این اتصال، کار به روز رسانی UI و هدایت رخ‌دادها را به سمت سرور، انجام می‌دهد.

- در پوشه‌ی Shared، یکسری فایل‌های اشتراکی قرار دارند که قرار است در کامپوننت‌های واقع در پوشه‌ی Pages مورد استفاه قرار گیرند. برای نمونه فایل Shared\MainLayout.razor، شبیه به master page برنامه‌های web forms است که قالب کلی سایت را مشخص می‌کند. داخل آن Body@ را مشاهده می‌کنید که به معنای نمایش صفحات دیگر، دقیقا در همین محل است. همچنین در این پوشه فایل Shared\NavMenu.razor نیز قرار دارد که ارجاعی به آن در MainLayout.razor ذکر شده و کار آن نمایش منوی آبی‌رنگ سمت چپ صفحه‌است.

- در پوشه‌ی ریشه‌ی برنامه، فایل Imports.razor_ قابل مشاهده‌است. مزیت تعریف usingها در اینجا این است که از تکرار آن‌ها در کامپوننت‌های razor ای که در ادامه تهیه خواهیم کرد، جلوگیری می‌کند. هر using تعریف شده‌ی در اینجا، در تمام کامپوننت‌ها، قابل دسترسی است؛ به آن global imports هم گفته می‌شود.

- در پوشه‌ی ریشه‌ی برنامه، فایل App.razor نیز قابل مشاهده‌است. کار آن تعریف قالب پیش‌فرض برنامه‌است که برای مثال به Shared\MainLayout.razor اشاره می‌کند. همچنین کامپوننت مسیریابی نیز در اینجا ذکر شده‌است:
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
اگر شخصی مسیر از پیش تعریف شده‌ای را وارد کند، به قسمت Found وارد می‌شود و در غیر اینصورت به قسمت NotFound. در قسمت Found است که از قالب MainLayout برای رندر یک کامپوننت توسط کامپوننت RouteView، استفاده خواهد شد و در قسمت NotFound، فقط پیام «Sorry, there's nothing at this address» به کاربر نمایش داده می‌شود. قالب‌های هر دو نیز قابل تغییر و سفارشی سازی هستند.

- فایل appsettings.json نیز همانند برنامه‌های استاندارد ASP.NET Core در اینجا مشاهده می‌شود.

- فایل Program.cs آن که نقطه‌ی آغازین برنامه‌است و کار فراخوانی Startup.cs را انجام می‌دهد، دقیقا با یک فایل Program.cs برنامه‌ی استاندارد ASP.NET Core یکی است.

- در فایل Startup.cs آن، همانند قبل دو متد تنظیم سرویس‌ها و تنظیم میان‌افزارها قابل مشاهده‌است.
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
}
در ConfigureServices آن، سرویس‌های صفحات razor و ServerSideBlazor اضافه شده‌اند. همچنین سرویس نمونه‌ی WeatherForecastService نیز در اینجا ثبت شده‌است.
قسمت‌های جدید متد Configure آن، ثبت مسیریابی توکار BlazorHub است که مرتبط است با اتصال دائم SignalR برنامه و اگر مسیری پیدا نشد، به Host_ هدایت می‌شود:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    // ...

    app.UseEndpoints(endpoints =>
    {
       endpoints.MapBlazorHub();
       endpoints.MapFallbackToPage("/_Host");
    });
}
در ادامه در همان پوشه، دستور dotnet run را اجرا می‌کنیم تا پروژه، کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.


که به همراه 13 درخواست و نزدیک به 600 KB دریافت اطلاعات از سمت سرور است.

این برنامه‌ی نمونه، به همراه سه صفحه‌ی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شده‌است. اگر صفحه‌ی شمارشگر آن‌را باز کنیم، با کلیک بر روی دکمه‌ی آن، هرچند مقدار current count افزایش می‌یابد، اما شاهد post-back متداولی به سمت سرور نیستیم و این صفحه بسیار شبیه به صفحات برنامه‌های SPA (تک صفحه‌ای وب) به نظر می‌رسد:


همانطور که عنوان شد، مدخل blazor.server.js فایل Pages\_Host.cshtml، کار به روز رسانی UI و هدایت رخ‌دادها را به سمت سرور به صورت خودکار انجام می‌دهد. به همین جهت است که post-back ای را مشاهده نمی‌کنیم و برنامه، شبیه به یک برنامه‌ی SPA به نظر می‌رسد؛ هر چند تمام رندرهای آن سمت سرور انجام می‌شوند و توسط SignalR به سمت کلاینت بازگشت داده خواهند شد.
برای نمونه اگر بر روی دکمه‌ی شمارشگر کلیک کنیم، در برگه‌ی network مرورگر، هیچ اثری از آن مشاهده نمی‌شود (هیچ رفت و برگشتی را مشاهده نمی‌کنیم). علت اینجا است که اطلاعات متناظر با این کلیک، از طریق web socket باز شده‌ی توسط SignalR، به سمت سرور ارسال شده و نتیجه‌ی واکنش به این کلیک‌ها و رندر HTML نهایی سمت سرور آن، از همین طریق به سمت کلاینت بازگشت داده می‌شود.


بررسی ساختار پروژه‌ی Blazor WASM و اجرای آن

پس از اجرای دستور dotnet new blazorwasm در یک پوشه‌ی خالی و ایجاد پروژه‌ی ابتدایی آن:


همان صفحات پروژه‌ی خالی Blazor Server در اینجا نیز قابل مشاهده هستند. این برنامه‌ی نمونه، به همراه سه صفحه‌ی نمایش Home، نمایش یک شمارشگر و نمایش اطلاعاتی از پیش آماده شده‌است. صفحات و کامپوننت‌های پوشه‌های Pages و Shared نیز دقیقا همانند پروژه‌ی Blazor Server قابل مشاهده هستند. مفاهیمی مانند فایل‌های Imports.razor_ و App.razor نیز مانند قبل هستند.

البته در اینجا فایل Startup ای مشاهده نمی‌شود و تمام تنظیمات آغازین برنامه، داخل فایل Program.cs انجام خواهند شد:
namespace BlazorWasmSample
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

            await builder.Build().RunAsync();
        }
    }
}
در اینجا ابتدا کامپوننت App.razor را به برنامه معرفی می‌کند که ساختار آن با نمونه‌ی مشابه Blazor Server دقیقا یکی است. سپس سرویسی را برای دسترسی به HttpClient، به سیستم تزریق وابستگی‌های برنامه معرفی می‌کند. هدف از آن، دسترسی ساده‌تر به endpoint‌های یک ASP.NET Core Web API است. از این جهت که در یک برنامه‌ی سمت کلاینت، دیگر دسترسی مستقیمی به سرویس‌های سمت سرور را نداریم و برای کار با آن‌ها همانند سایر برنامه‌های SPA که از Ajax استفاده می‌کنند، در اینجا از HttpClient برای کار با Web API‌های مختلف استفاده می‌شود.

تفاوت ساختاری دیگر این پروژه‌ی WASM، با نمونه‌ی Blazor Server، ساختار پوشه‌ی wwwroot آن است:


که به همراه فایل جدید نمونه‌ی wwwroot\sample-data\weather.json است؛ بجای سرویس متناظر سمت سرور آن در برنامه‌ی blazor server. همچنین فایل جدید wwwroot\index.html نیز قابل مشاهده‌است و محتوای تگ body آن به صورت زیر است:
<body>
    <div id="app">Loading...</div>

    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="_framework/blazor.webassembly.js"></script>
</body>
- چون این برنامه، یک برنامه‌ی سمت کلاینت است، اینبار بجای فایل Host_ سمت سرور، فایل index.html سمت کلاینت را برای ارائه‌ی آغازین برنامه داریم که وابستگی به دات نت ندارد و توسط مرورگر قابل درک است.
- در ابتدای بارگذاری برنامه، یک loading نمایش داده می‌شود که در اینجا نحوه‌ی تعریف آن مشخص است. همچنین اگر خطایی رخ دهد نیز توسط div ای با id مساوی blazor-error-ui اطلاع رسانی می‌شود.
- مدخل blazor.webassembly.js در اینجا، کار دریافت وب اسمبلی و فایل‌های NET runtime. را انجام می‌دهد؛ برخلاف برنامه‌های Blazor Server که توسط فایل blazor.server.js، یک ارتباط دائم SignalR را با سرور برقرار می‌کردند تا کدهای رندر شده‌ی سمت سرور را دریافت و نمایش دهند و یا اطلاعاتی را به سمت سرور ارسال کنند: برای مثال بر روی دکمه‌ای کلیک شده‌است، اطلاعات مربوطه را در سمت سرور پردازش کن و نتیجه‌ی نهایی رندر شده را بازگشت بده. اما در اینجا همه چیز داخل مرورگر اجرا می‌شود و برای این نوع اعمال، رفت و برگشتی به سمت سرور صورت نمی‌گیرد. به همین جهت تمام کدهای #C ما به سمت کلاینت ارسال شده و داخل مرورگر به کمک فناوری وب اسمبلی، اجرا می‌شوند. در اینجا از لحاظ ارسال تمام کدهای مرتبط با UI برنامه‌ی سمت کلاینت به مرورگر کاربر، تفاوتی با فریم‌ورک‌هایی مانند Angular و یا React نیست و آن‌ها هم تمام کدهای UI برنامه را کامپایل کرده و یکجا ارسال می‌کنند.

در ادامه در همان پوشه، دستور dotnet run را اجرا می‌کنیم تا پروژه کامپایل و همچنین وب سرور آن نیز اجرا شده و برنامه در آدرس https://localhost:5001 قابل دسترسی شود.


که به همراه 205 درخواست و نزدیک به 9.6 MB دریافت اطلاعات از سمت سرور است. البته اگر همین صفحه را refresh کنیم، دیگر شاهد دریافت مجدد فایل‌های DLL مربوط به NET Runtime. نخواهیم بود و اینبار از کش مرورگر خوانده می‌شوند:


در این برنامه‌ی سمت کلاینت، ابتدا تمام فایل‌های NET Runtime. و وب اسمبلی دریافت شده و سپس اجرای تغییرات UI، در همین سمت و بدون نیاز به اتصال دائم SignalR ای به سمت سرور، پردازش و نمایش داده می‌شوند. به همین جهت زمانیکه بر روی دکمه‌ی شمارشگر آن کلیک می‌کنیم، اتفاقی در برگه‌ی network مرورگر ثبت نمی‌شود و رفت و برگشتی به سمت سرور صورت نمی‌گیرد.

عدم وجود اتصال SignalR، مزیت امکان اجرای آفلاین برنامه‌ی WASM را نیز میسر می‌کند. برای مثال یکبار دیگر همان برنامه‌ی Blazor Server را به کمک دستور dotnet run اجرا کنید. سپس آن‌را در مرورگر در آدرس https://localhost:5001 باز کنید. اکنون پنجره‌ی کنسولی که dotnet run را اجرا کرده، خاتمه دهید (قسمت اجرای سمت سرور آن‌را ببندید).


بلافاصله تصویر «سعی در اتصال مجدد» فوق را مشاهده خواهیم کرد که به دلیل قطع اتصال SignalR رخ داده‌است. یعنی یک برنامه‌ی Blazor Server، بدون این اتصال دائم، قادر به ادامه‌ی فعالیت نیست. اما چنین محدودیتی با برنامه‌های Blazor WASM وجود ندارد.
البته بدیهی است اگر یک Web API سمت سرور برای ارائه‌ی اطلاعاتی به برنامه‌ی WASM نیاز باشد، API سمت سرور برنامه نیز باید جهت کار و نمایش اطلاعات در سمت کلاینت در دسترس باشد؛ وگرنه قسمت‌های متناظر با آن، قادر به نمایش و یا ثبت اطلاعات نخواهند بود.
مطالب
MongoDb در سی شارپ (بخش اول)
MongoDb  یک دیتابیس Nosql سندگراست که توسط ++C نوشته شده است و از پشتیبانی خوبی در بسیاری از زبان‌ها برخوردار است. مونگو از ساختاری به نام Bson که ساختاری مشابه Json را دارد استفاده می‌کند؛ با این تفاوت که در Json مبحث دیتاتایپ یا نوع داده وجود ندارد، ولی در Bson دیتاتایپ‌ها تعریف می‌شوند. برای دیدن نوع‌های Bson و نحوه نوشته شدن سند آن می‌توانید مقاله MongoDb#7 را مطالعه بفرمایید.


برای آغاز به کار با این دیتابیس ابتدا باید آن را از سایت اصلی دریافت و بر روی سیستم نصب نمایید. متاسفانه سایت مونگو برای کشور ایران محدودیتی قرار داده است و باید از روش‌های دیگری آن را دریافت نمایید و بر روی سیستم خود نصب نمایید. نحوه نصب این دیتابیس را میتوانید در مقاله MongoDb#3 مشاهده نمایید.

شاید نیاز باشد بجای کار کردن با محیط کنسول این دیتابیس، با یک محیط گرافیکی شبیه آن چیزی که Raven دارد کار کنید وتغییرات را مشاهده نمایید؛ برای همین به این آدرس رفته و محیط دلخواه خود را انتخاب نمایید.

  یک پروژه از نوع کنسول را در ویژوال استادیو ایجاد کنید و سپس درایور رسمی مونگو را از این آدرس یا از طریق nuget نصب نمایید:
Install-Package mongocsharpdriver

ابتدا سه مدل را به شکل زیر ایجاد میکنیم:
  public class Author
    {
        public ObjectId Id { get; set; }
        public string Name { get; set; }
    }

public class Language
    {
        public ObjectId Id { get; set; }
        public string Name { get; set; }
    }

  public class Book
    {
        public ObjectId Id { get; set; }
        public string Title { get; set; }
        public string ISBN { get; set; }
        public int Price { get; set; }
        public List<Author> Authors { get; set; }
        public Language Language { get; set; }
    }

نوع ObjectId، نوعی است که توسط مونگو برای مشخص کردن کلید یکتای سند معرفی میشود.

در خطوط اولیه کد زیر، یک شیء از مدل بالا را ساخته و آن را مقداردهی می‌کنیم:
           var book =new Book()
           {
               Title = "Gone With Wind",
               ISBN = "43442424",
               Price = 50000,
               Language = new Language()
               {
                  Name = "Persian"
               },
               Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Margaret Mitchell"
                   },
                     new Author()
                   {
                       Name = "Ali Mahboobi (Translator)"
                   },
               }
           };

بعد از آن یک شیء کلاینت از نوع mongoClient میسازیم که نوع خروجی آن یک اینترفیس میباشد که توسط کلاسی از جنس آن مقداردهی شده است. بیشتر خروجی‌های مونگو در این کتابخانه از نوع اینترفیس هستند. شیء کلاینت وظیفه دارد تا ارتباط شما را با سرور مونگو برقرار کند:
var client = new MongoClient();
البته در این حالت سرور اتصالی مونگو، سیستم جاری و پورت شماره 27017 فرض میشود. در صورتیکه بخواهید آدرسی غیر از آن را بدهید یا حتی همین آدرس را به طور دستی تعیین کنید، از طریق زیر امکان پذیر است. پارامترهای سازنده این شیء کلاینت میتوانند به صورت رشته‌ای، رشته اتصال را دریافت کنند و یا از طریق شیء MangoClientSettings آن را پاس کنید.
 string connectionString = "mongodb://localhost:27017";
            MongoClientSettings settings = MongoClientSettings.FromUrl(new MongoUrl(connectionString));
            var client = new MongoClient(settings);

در قسمت بعد لازم است که از سرور جاری، دیتابیس خود را دریافت کنیم. در صورتیکه دیتابیس درخواستی وجود نداشته باشد، یک دیتابیس جدید با آن نام ساخته خواهد شد:
var db = client.GetDatabase("publisher");
در نمونه کدهای قدیمی مونگو، قبل از دریافت سرور بایستی شیء Server را از طریق متد GetServer نیز دریافت میکردید که از نسخه دو به بعد، آن را منسوخ اعلام کرده‌اند و همین تنظیمات بالا کفایت میکند.
در مونگو اصطلاحی به نام collection وجود دارد که اسناد در آن قرار گرفته و ارتباط با اسناد از طریق آنها انجام می‌پذیرد. پس در اینجا قبل از هر کاری باید یک collection را ایجاد کرد و در صورتیکه کالکشن درخواستی وجود نداشته باشد، آن را تولید و ارتباط با آن را برخواهد گرداند.
var collection = db.GetCollection<Book>("books");

در اینجا کالکشنی با نام books با تبدیلاتی بر اساس مدل Book ایجاد میشود. در مرحله بعد لازم است که شیء ایجاد شده بر اساس کلاس مدل را با استفاده از متدهای insert شیء کالکشن، در دیتابیس ارسال کنیم.
شی‌ءهای درج یک سند جدید به دیتابیس حالات مختلفی را دارد: افزودن تک سند، افزودن چند سند و دو مورد قبلی به صورت غیر همزمان میباشند:
collection.InsertOneAsync(book);
متد بالا سند جدید را به صورت غیرهمزمان در سیستم درج میکند. نمونه ذخیره شده این سند را که توسط برنامه Mongo Compass نمایش داده شده است، می‌توانید در زیر ببینید:

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


عملیات خواندن

برای خواندن یک یا چند سند از دیتابیس میتوانید از دو شیوه موجود Linq و queryBuilder‌ها استفاده کرد. از آنجائیکه با کار با Linq آشنایی داریم، ابتدای شیوه کوئری بیلدر را مورد بررسی قرار میدهیم و سپس نحوه کار با لینک را بررسی میکنیم.
قبل از هر چیزی برای اینکه در مانور دادن بر روی داده‌ها راحت باشیم و اطلاعات را با فیلترهای متفاوتی واکشی کنیم، 7 عدد کتاب را با مشخصات زیر اضافه میکنیم. دو فیلد سال و تاریخ آخرین موجودی انبار را هم اضافه می‌کنیم.
       var client = new MongoClient();
            var db = client.GetDatabase("publisher");
            db.DropCollection("books");
            var collection = db.GetCollection<Book>("books");
            
            var book =new Book()
           {
               Title = "Gone With Wind",
               ISBN = "43442424",
               Price = 50000,
               Year = 1936,
               LastStock = DateTime.Now.AddDays(-13),
               Language = new Language()
               {
                  Name = "Persian"
               },
               Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Margaret Mitchell"
                   },
                     new Author()
                   {
                       Name = "Ali Mahboobi (Translator)"
                   },
               }
           };

            var book2 = new Book()
            {
                Title = "Jane Eyre",
                ISBN = "87897897",
                Price = 60000,
                Year = 1847,
                LastStock = DateTime.Now.AddDays(-5),
                Language = new Language()
                {
                    Name = "English"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Charlotte Brontë"
                   },
                   
               }
            };


            var book3 = new Book()
            {
                Title = "White Fang",
                ISBN = "43442424",
                Price = 50000,
                Year = 1936,
                LastStock = DateTime.Now.AddDays(-13),
                Language = new Language()
                {
                    Name = "English"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Jack London"
                   },
                     new Author()
                   {
                       Name = "Philippe Mignon"
                   },
               }
            };

            var book4 = new Book()
            {
                Title = "The Lost Symbol",
                ISBN = "43442424",
                Price = 3500000,
                Year = 2009,
                LastStock = DateTime.Now.AddDays(-17),
                Language = new Language()
                {
                    Name = "Persian"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Dan Brown"
                   },
                     new Author()
                   {
                       Name = "Mehrdad"
                   },
               }
            };

            var book7 = new Book()
            {
                Title = "The Lost Symbol",
                ISBN = "43442424",
                Price = 47000000,
                Year = 2009,
                LastStock = DateTime.Now.AddDays(-56),
                Language = new Language()
                {
                    Name = "Persian"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Dan Brown"
                   },
                     new Author()
                   {
                       Name = "Mehrdad"
                   },
               }
            };
            var book5= new Book()
            {
                Title = "The Help",
                ISBN = "45345e3er3",
                Price = 9000000,
                Year = 2009,
                LastStock = DateTime.Now.AddDays(-2),
                Language = new Language()
                {
                    Name = "Enlish"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Kathryn Stockett"
                   },
                    
               }
            };

            var book6 = new Book()
            {
                Title = "City of Glass",
                ISBN = "454534545",
                Price = 500000,
                Year = 2009,
                LastStock = DateTime.Now,
                Language = new Language()
                {
                    Name = "Persian"
                },
                Authors = new List<Author>()
               {
                   new Author()
                   {
                       Name = "Cassandra Clare"
                   },
                   new Author()
                   {
                       Name = "Ali"
                   },

               }
            };
            var books = new List<Book> {book, book2, book3, book4, book5, book6,book7};

            collection.InsertManyAsync(books);

برای واکشی دیتاها کالکشنی از آن نوع را همانند قبل درخواست می‌کنیم. بعد از آن نیاز است که فیلتری برای واکشی اطلاعات تعریف کنیم که این فیلتر در قالب یک کلاس به نام BsonDocument ایجاد می‌شود که ما در اینجا، به دلیل اینکه میخواهیم همه اسناد را واکشی کنیم ، این سند Bson را مقداردهی نمیکنیم و توسط متد Find آن را در واکشی دیتاها شرکت میدهیم و سپس با صدا زدن متد ToList، عملیات واکشی را انجام میدهیم، برای اینکار می‌توانیم از عملیات غیرهمزمان هم استفاده کنیم.
            var client = new MongoClient();
            var db = client.GetDatabase("publisher");
            var collection = db.GetCollection<Book>("books");
            
            var filter=new BsonDocument();
            var docs = collection.Find(filter).ToList();
            foreach (var book in docs)
            {
                Console.WriteLine(book.Title + " By "+ book.Authors[0].Name);
            }

با اجرای کد بالا به نتایج زیر میرسیم:
Gone With Wind By Margaret Mitchell
Jane Eyre By Charlotte Brontë
White Fang By Jack London
The Lost Symbol By Dan Brown
The Help By Kathryn Stockett
City of Glass By Cassandra Clare
The Lost Symbol By Dan Brown

اگر بخواهید فیلتری را بر روی این واکشی قرار دهید و مثلا بخواهید کتاب‌های منتشر شده در سال 2009 را واکشی نمایید، باید این سند Bson را مقداردهی نمایید. ولی برای راحتی اینکار، این  کتابخانه شامل یک بیلدر Builder بوده که میتوان از طریق آن فیلترهای متنوعی را به صورت ساده‌تر طراحی کنید:

در خطوط بالا ابتدا یک بیلدر را برای کلاس مورد نظر ایجاد کرده و از خصوصیت Filter آن استفاده میکنیم و این خصوصیت شامل متدهای فراوانی است که میتوانید برای ایجاد شرط یا فیلتر استفاده کنید. تعدادی از متدهای پر استفاده آن همانند eq (برابری) ، gt (برزگتر از ...) ، gte (بزرگتر مساوی ...) و طبیعتا خانواده lt و ... موجود هستند.
var filter = Builders<Book>.Filter.Eq("Year", 2009);

            var docs = collection.Find(filter).ToList();
            foreach (var book in docs)
            {
                Console.WriteLine(book.Title + " By "+ book.Authors[0].Name);
            }
در کد بالا ما از متد eq برای بررسی برابر بودن استفاده کردیم و درخواست اسنادی را کردیم که دارای فیلد سال انتشار هستند و مقدار آن برابر 2009 میباشد و نتیجه آن به صورت زیر نمایش داده میشود:
The Lost Symbol By Dan Brown
The Help By Kathryn Stockett
City of Glass By Cassandra Clare
The Lost Symbol By Dan Brown
حتی می‌توانید با استفاده از شیء فیلتر، ترکیبات شرطی زیر را نیز اعمال نمایید:
   // var filter=new BsonDocument();
            var filterBuilder = Builders<Book>.Filter;
            var filter= filterBuilder.Eq("Year", 2009) | filterBuilder.Gte("Price",700000);

            var docs = collection.Find(filter).ToList();
            foreach (var book in docs)
            {
                Console.WriteLine(book.Title + " By "+ book.Authors[0].Name);
            }
در بالا ابتدا نوعی از شیء Filter را از کلاس Builder دریافت میکنیم و سپس با استفاده عملیات بیتی آن‌ها را با یکدیگر Or میکنیم. در این پرس و جو باید کتابهایی که در سال 2009 منتشر شده‌اند یا قیمتی کمتر از پنجاه هزار ریال یا برابر را دارند، نمایش داده شوند.

Gone With Wind By Margaret Mitchell
White Fang By Jack London
The Lost Symbol By Dan Brown
The Help By Kathryn Stockett
City of Glass By Cassandra Clare
The Lost Symbol By Dan Brown

برای اینکه بتوانید از linq به جای queryBuilder استفاده کنید، میتوانید از خصوصیت AsQueryable استفاده کنید. خط زیر همان شرط یا فیلتر بالا را توسط Linq اعمال میکند
var docs = collection.AsQueryable().Where(x => x.Year == 2009 || x.Price <= 50000).ToList();

Sort کردن داده‌ها
 
برای مرتب سازی اطلاعات به شیوه کوئری بیلدر، همانند فیلتر که از کلاس Builder استفاده میکردیم، از همین شیء استفاده میکنیم؛ با این تفاوت که بجای استفاده از خصوصیت Filter، از Sort استفاده میکنیم و شیء ایجاد شده را به متد Sort میدهیم:
  var sort = Builders<Book>.Sort.Ascending("Title").Descending("Price");
            var docs = collection.Find(filter).Sort(sort).ToList();
            foreach (var book in docs)
            {
                Console.WriteLine(book.Title + " By "+ book.Authors[0].Name);
            }
در نتیجه خطوط بالا کتاب‌ها ابتدا بر اساس نام به صورت صعودی و سپس بر اساس قیمت به صورت نزولی لیست می‌شوند.
City of Glass By Cassandra Clare
Gone With Wind By Margaret Mitchell
The Help By Kathryn Stockett
The Lost Symbol By Dan Brown
The Lost Symbol By Dan Brown
White Fang By Jack London

توجه داشته باشید که متد sort بعد از فیلتر گذاری، یعنی عمل Find در دسترس میباشد.

در قسمت بعدی به روزرسانی، حذف و ایندکس گذاری را مورد بررسی قرار می‌دهیم.
مطالب
شروع به کار با EF Core 1.0 - قسمت 1 - برپایی تنظیمات اولیه
در ادامه‌ی سری «ارتقاء به ASP.NET Core 1.0» اگر بخواهیم مباحث اعتبارسنجی کاربران و ASP.NET Identity مخصوص آن‌را بررسی کنیم، نیاز است ابتدا مباحث Entity framework Core 1.0 را بررسی کنیم. به همین جهت در طی چند قسمت مباحث پایه‌ای کار با EF Core 1.0 را در ASP.NET Core 1.0، بررسی خواهیم کرد. بنابراین پیشنیاز ضروری این مباحث، مطالعه‌ی سری «ارتقاء به ASP.NET Core 1.0» است و در آن از مباحثی مانند چگونگی کار با فایل‌های کانفیگ جدید، تزریق وابستگی‌ها و سرویس‌ها، فعال سازی سرویس Logging، فعال سازی صفحات مخصوص توسعه دهنده‌ها و ... در ASP.NET Core 1.0 استفاده خواهد شد.


EF Core چیست؟

EF Core یک ORM یا object-relational mapper چندسکویی است که امکان کار با بانک‌های اطلاعاتی مختلف را از طریق اشیاء دات نتی میسر می‌کند. توسط آن قسمت عمده‌ی کدهای مستقیم کار با بانک‌های اطلاعاتی حذف شده و تبدیل به کدهای دات نتی می‌شوند. مزیت این لایه‌ی Abstraction اضافی (لایه‌ای بر روی کدهای مستقیم لایه ADO.NET زیرین)، امکان تعویض بانک اطلاعاتی مورد استفاده، تنها با تغییر کدهای آغازین برنامه‌است؛ بدون نیاز به تغییری در سایر قسمت‌های برنامه. همچنین کار با اشیاء دات نتی و LINQ، مزایایی مانند تحت نظر قرار گرفتن کدها توسط کامپایلر و برخورداری از ابزارهای Refactoring پیشرفته را میسر می‌کنند. به علاوه SQL خودکار تولیدی توسط آن نیز همواره پارامتری بوده و مشکلات حملات تزریق SQL در این حالت تقریبا به صفر می‌رسند (اگر مستقیما SQL نویسی نکنید و صرفا از LINQ استفاده کنید). مزیت دیگر همواره پارامتری بودن کوئری‌ها، رفتار بسیاری از بانک‌های اطلاعاتی با آن‌ها همانند رویه‌های ذخیره شده است که به عدم تولید Query plan‌های مجزایی به ازای هر کوئری رسیده منجر می‌شود که در نهایت سبب بالا رفتن سرعت اجرای کوئری‌ها و مصرف حافظه‌ی کمتری در سمت سرور بانک اطلاعاتی می‌گردد.


تفاوت EF Core با نگارش‌های دیگر Entity framework در چیست؟

سورس باز بودن
EF از نگارش‌های آخر آن بود که سورس باز شد؛ اما EF Core از زمان نگارش‌های پیش نمایش آن به صورت سورس باز در GitHub قابل دسترسی است.

چند سکویی بودن
EF Core برخلاف EF 6.x (آخرین نگارش مبتنی بر Full Framework آن)، نه تنها چندسکویی است و قابلیت اجرای بر روی Mac و لینوکس را نیز دارا است، به علاوه امکان استفاده‌ی از آن در انواع و اقسام برنامه‌های دات نتی مانند UWP یا Universal Windows Platform و Windows phone که پیشتر با EF 6.x میسر نبود، وجود دارد. لیست این نوع سکوها و برنامه‌های مختلف به شرح زیر است:
 • All .NET application (Console, ASP.NET 4, WinForms, WPF)
 • Mac and Linux applications (Mono)
 • UWP (Universal Windows Platform)
 • ASP.NET Core applications
 • Can use EF Core in Windows phone and Windows store app

افزایش تعداد بانک‌های اطلاعاتی پشتیبانی شده
در EF Full یا EF 6.x، هدف اصلی، تنها کار با بانک‌های اطلاعاتی رابطه‌‌ای بود و همچنین مایکروسافت صرفا نگارش‌های مختلف SQL Server را به صورت رسمی پشتیبانی می‌کرد و برای سایر بانک‌های اطلاعاتی دیگر باید از پروایدرهای ثالث استفاده کرد.
در EF Core علاوه بر افزایش تعداد پروایدرهای رسمی بانک‌های اطلاعاتی رابطه‌ای، پشتیبانی از بانک‌های اطلاعاتی NoSQL هم اضافه شده‌است؛ به همراه پروایدر ویژه‌‌ای به نام In Memory جهت انجام ساده‌تر Unit testing. کاری که با نگارش‌های پیشین EF به سادگی و از روز اول پشتیبانی نمی‌شد.

حذف و یا عدم پیاده سازی تعدادی از قابلیت‌های EF 6.x
اگر موارد فوق جزو مهم‌ترین مزایای کار با EF Core باشند، باید درنظر داشت که به علت حذف و یا تقلیل یافتن یک سری از ویژگی‌ها در NET Core.، مانند Reflection (جهت پشتیبانی از دات نت در سکوهای مختلف کاری و خصوصا پشتیبانی از حالتی که کامپایلر مخصوص برنامه‌های UWP نیاز دارد تمام نوع‌ها را همانند زبان‌های C و ++C، در زمان کامپایل بداند)، یک سری از قابلیت‌های EF 6.x مانند Lazy loading هنوز در EF Core پشتیبانی نمی‌شوند. لیست کامل و به روز شده‌ی این موارد را در اینجا می‌توانید مطالعه کنید.
بنابراین امکان انتقال برنامه‌های EF 6.x به EF Core 1.0 عموما وجود نداشته و نیاز به بازنویسی کامل دارند. هرچند بسیاری از مفاهیم آن با EF Code First یکی است.


برپایی تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0

برای نصب تنظیمات اولیه‌ی EF Core 1.0 در یک برنامه‌ی ASP.NET Core 1.0، جهت کار با مشتقات SQL Server (و SQL LocalDB) نیاز است سه بسته‌ی ذیل را نصب کرد (از طریق منوی Tools -> NuGet Package Manager -> Package Manager Console):
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Pre
PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer.Design
البته در این قسمت صرفا از بسته‌ی اول که جهت اتصال به SQL Server است استفاده می‌کنیم. بسته‌های دیگر را در قسمت‌های بعد، برای به روز رسانی اسکیمای بانک اطلاعاتی (مباحث Migrations) و مباحث scaffolding استفاده خواهیم کرد.
پس از اجرای سه دستور فوق، تغییرات مداخل فایل project.json برنامه به صورت ذیل خواهند بود:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
        "Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0"
    }
}
این مداخلی که توسط نیوگت اضافه شده‌اند، نیاز به اصلاح دارند؛ به این صورت:
{
    "dependencies": {
       // same as before
        "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "type": "build"
        },
        "Microsoft.EntityFrameworkCore.SqlServer.Design": {
            "version": "1.0.0",
            "type": "build"
        }
    },

    "tools": {
       // same as before
        "Microsoft.EntityFrameworkCore.Tools": {
            "version": "1.0.0-preview2-final",
            "imports": [
                "portable-net45+win8"
            ]
        }   
   }
}
نیاز است در قسمت dependencies مشخص کنیم که ابزارهای اضافه شده مخصوص build هستند و نه اجرای برنامه. همچنین قسمت tools را باید با Microsoft.EntityFrameworkCore.Tools مقدار دهی کرد تا بتوان از این ابزار در خط فرمان، جهت اجرای فرامین migrations استفاده کرد.
بنابراین از همین ابتدای کار، بدون مراجعه‌ی به Package Manager Console، چهار تغییر فوق را به فایل project.json اعمال کرده و آن‌را ذخیره کنید؛ تا کار به روز رسانی و نصب بسته‌ها، به صورت خودکار و همچنین صحیحی انجام شود.


فعال سازی صفحات مخصوص توسعه دهنده‌های EF Core 1.0

در مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 5 - فعال سازی صفحات مخصوص توسعه دهنده‌ها» با تعدادی از اینگونه صفحات آشنا شدیم. برای EF Core نیز بسته‌ی مخصوصی به نام Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore وجود دارد که امکان فعال سازی صفحه‌ی نمایش خطاهای بانک اطلاعاتی را میسر می‌کند. بنابراین ابتدا به فایل project.json مراجعه کرده و این بسته را اضافه کنید:
{
    "dependencies": {
       // same as before
        "Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore": "1.0.0"
    }
}
سپس می‌توان متد جدید UseDatabaseErrorPage را در متد Configure کلاس آغازین برنامه، فراخوانی کرد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   if (env.IsDevelopment())
   {
      app.UseDatabaseErrorPage();
   }
با فعال سازی این صفحه، اگر در حین توسعه‌ی برنامه و اتصال به بانک اطلاعاتی، خطایی رخ دهد، بجای مشاهده‌ی یک صفحه‌ی خطای عمومی (اگر UseDeveloperExceptionPage را فعال کرده باشید)، اینبار ریز جزئیات بیشتری را به همراه توصیه‌هایی از طرف تیم EF مشاهده خواهید کرد.


تعریف اولین Context برنامه و مشخص سازی رشته‌ی اتصالی آن


در این تصویر، زیر ساخت نگاشت‌های EF Core را مشاهده می‌کنید. در سمت چپ، ظرفی را داریم به نام DB Context که در برگیرنده‌ی Db Setها است. در سمت راست که بیانگر ساختار کلی یک بانک اطلاعاتی است، معادل این‌ها را مشاهده می‌کنیم. هر Db Set به یک جدول بانک اطلاعاتی نگاشت خواهد شد و متشکل است از کلاسی به همراه یک سری خواص که این‌ها نیز به فیلدها و ستون‌های آن جدول در سمت بانک اطلاعاتی نگاشت می‌شوند.
بنابراین برای شروع کار، پوشه‌ای را به نام Entities به پروژه اضافه کرده و سپس کلاس ذیل را به آن اضافه می‌کنیم:
namespace Core1RtmEmptyTest.Entities
{
    public class Person
    {
        public int PersonId { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
}
کلاس Person بیانگر ساختار جدول اشخاص بانک اطلاعاتی است. برای اینکه این کلاس را تبدیل و نگاشت به یک جدول کنیم، نیاز است آن‌را به صورت یک DbSet در معرض دید EF Core قرار دهیم و اینکار در کلاسی که از DbContex مشتق می‌شود، صورت خواهد گرفت:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        {
        }

        public DbSet<Person> Persons { get; set; }
    }
}
بنابراین در ادامه کلاس جدید ApplicationDbContext را که از کلاس پایه DbContext مشتق می‌شود تعریف کرده و سپس کلاس Person را به صورت یک DbSet در معرض دید EF Core قرار می‌دهیم.
سازنده‌ی این کلاس نیز به نحو خاصی تعریف شده‌است. اگر به سورس‌های EF Core مراجعه کنیم، کلاس پایه‌ی DbContext دارای دو سازنده‌ی با و بدون پارامتر است:
protected DbContext()
   : this((DbContextOptions) new DbContextOptions<DbContext>())
{
}

public DbContext([NotNull] DbContextOptions options)
{
  // …
}
اگر از سازنده‌ی بدون پارامتر استفاده کنیم و برای مثال در کلاس ApplicationDbContext فوق، به طور کامل سازنده‌ی تعریف شده را حذف کنیم، باید به نحو ذیل تنظیمات بانک اطلاعاتی را مشخص کنیم:
using Microsoft.EntityFrameworkCore;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"... connection string ...");
        }
    }
}
در این حالت باید متد OnConfiguring را override و یا بازنویسی کنیم، تا بتوان از اول مشخص کرد که قرار است از پروایدر SQL Server استفاده کنیم و ثانیا رشته‌ی اتصالی به آن چیست.
اما چون در یک برنامه‌ی ASP.NET Core، کار ثبت سرویس مربوط به EF Core، در کلاس آغازین برنامه انجام می‌شود و در آنجا به سادگی می‌توان به خاصیت Configuration برنامه دسترسی یافت و توسط آن رشته‌ی اتصالی را دریافت کرد، مرسوم است از سازنده‌ی با پارامتر DbContext به نحوی که در ابتدا عنوان شد، استفاده شود.
بنابراین در ادامه، پس از مطالعه‌ی مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» به فایل appsettings.json مراجعه کرده و تنظیمات رشته‌ی اتصالی برنامه را به صورت ذیل در آن مشخص می‌کنیم:
{
    "ConnectionStrings": {
        "ApplicationDbContextConnection": "Data Source=(local);Initial Catalog=TestDbCore2016;Integrated Security = true"
    }
}
باید دقت داشت که نام این مداخل کاملا اختیاری هستند و در نهایت باید در کلاس آغازین برنامه به صورت صریحی مشخص شوند.
در اینجا به وهله‌ی پیش فرض SQL Server اشاره شده‌است؛ از حالت اعتبارسنجی ویندوزی SQL Server استفاده می‌شود و بانک اطلاعاتی جدیدی به نام TestDbCore2016 در آن مشخص گردیده‌است.

پس از تعریف رشته‌ی اتصالی، متد OnConfiguring را از کلاس ApplicationDbContext حذف کرده و از همان نگارش دارای سازنده‌ی با پارامتر آن استفاده می‌کنیم. برای اینکار به کلاس آغازین برنامه مراجعه کرده و توسط متد AddDbContext این Context را به سرویس‌های ASP.NET Core معرفی می‌کنیم:
    public class Startup
    {
        public IConfigurationRoot Configuration { set; get; }

        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                                .SetBasePath(env.ContentRootPath)
                                .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                                .AddJsonFile($"appsettings.{env}.json", optional: true);
            Configuration = builder.Build();
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(Configuration["ConnectionStrings:ApplicationDbContextConnection"]);
            });
در اینجا جهت یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config» نحوه‌ی وهله سازی خاصیت Configuration که در متد UseSqlServer مورد استفاده قرار گرفته‌است، نیز ذکر شده‌است.
بنابراین قسمت options.UseSqlServer را یا در اینجا مقدار دهی می‌کنید و یا از طریق بازنویسی متد OnConfiguring کلاس Context برنامه.


یک نکته: امکان تزریق IConfigurationRoot به کلاس Context برنامه وجود دارد

با توجه به اینکه Context برنامه را به صورت یک سرویس به ASP.NET Core معرفی کردیم، امکان تزریق وابستگی‌ها نیز در آن وجود دارد. یعنی بجای روش فوق، می‌توان IConfigurationRoot را به سازنده‌ی کلاس Context برنامه نیز تزریق کرد:
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;

namespace Core1RtmEmptyTest.Entities
{
    public class ApplicationDbContext : DbContext
    {
        private readonly IConfigurationRoot _configuration;

        public ApplicationDbContext(IConfigurationRoot configuration)
        {
            _configuration = configuration;
        }

        //public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
        //{
        //}

        public DbSet<Person> Persons { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(_configuration["ConnectionStrings:ApplicationDbContextConnection"]);
        }
    }
}
با توجه به اینکه IConfigurationRoot در کلاس ConfigureServices به صورت Singleton، به مجموعه‌ی سرویس‌های برنامه معرفی شده‌است، از آن در تمام کلاس‌های برنامه که تحت نظر سیستم تزریق وابستگی‌های توکار ASP.NET Core هستند، می‌توان استفاده کرد.
در این حالت متد ConfigureServices کلاس آغازین برنامه، چنین شکلی را پیدا می‌کند و ساده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<ApplicationDbContext>();


یک نکته: امکان تزریق ApplicationDbContext به تمام کلاس‌های برنامه وجود دارد

همینقدر که ApplicationDbContext را به عنوان سرویسی در ConfigureServices تعریف کردیم، امکان تزریق آن در اجزای مختلف یک برنامه‌ی ASP.NET Core نیز وجود دارد:
using System.Linq;
using Core1RtmEmptyTest.Entities;
using Microsoft.AspNetCore.Mvc;

namespace Core1RtmEmptyTest.Controllers
{
    public class TestDBController : Controller
    {
        private readonly ApplicationDbContext _ctx;

        public TestDBController(ApplicationDbContext ctx)
        {
            _ctx = ctx;
        }

        public IActionResult Index()
        {
            var name = _ctx.Persons.First().FirstName;
            return Json(new { firstName = name });
        }
    }
}
در اینجا نحوه‌ی تزریق DB Context برنامه را به یک کنترلر مشاهده می‌کنید. البته هرچند تزریق یک کلاس مشخص به این شکل، تزریق وابستگی‌ها نام ندارد و هنوز این کنترلر دقیقا وابسته‌است به پیاده سازی خاص کلاس ApplicationDbContext، اما ... در کل امکان آن هست.

در این حالت پس از اجرای برنامه، خطای ذیل را مشاهده خواهیم کرد:


علت اینجا است که هنوز این بانک اطلاعاتی ایجاد نشده‌است و همچنین ساختار جداول را به آن منتقل نکرده‌ایم که این موارد را در قسمت‌های بعدی مرور خواهیم کرد.
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config
یکی دیگر از تغییرات ASP.NET Core با نگارش‌های قبلی آن، تغییرات اساسی در مورد نحوه‌ی کار با تنظیمات برنامه و فایل‌های مرتبط با آن‌ها است. در ASP.NET Core می‌توانید:
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آن‌ها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شده‌ی از فایل‌های کانفیگ به کلاس‌ها پیش بینی شده‌است.
- امکان بارگذاری مجدد فایل‌های کانفیگ درصورت تغییر، بدون ری‌استارت کل برنامه وجود دارد.
- امکان تزریق وابستگی‌های تنظیمات برنامه، به قسمت‌های مختلف آن پیش بینی شده‌است.


نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON

برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بسته‌ی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:


البته همانطور که در تصویر مشاهده می‌کنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بسته‌های مرتبط با خواندن فایل‌های کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
    "dependencies": {
         //same as before  
         "Microsoft.Extensions.Configuration.Json": "1.0.0"
    },

 
افزودن یک فایل کانفیگ JSON دلخواه

بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}
در نگارش‌های پیشین ASP.NET که از web.config برای تعریف تنظیمات برنامه استفاده می‌شد، حالت پیش فرض ذکر تنظیمات برنامه می‌توانست تنها یک سطحی و با ساختار ذیل باشد (البته امکان کدنویسی و نوشتن مداخل سفارشی هم وجود داشت؛ ولی حالت پیش فرض appSettings، تنها key/valueهای یک سطحی هستند):
<appSettings>
   <add key="Logging-IncludeScopes" value="false" />
   <add key="Logging-Level-Default" value="verbose" />
   <add key="Logging-Level-System" value="Information" />
   <add key="Logging-Level-Microsoft" value="Information" />
</appSettings>
اما اکنون یک فایل JSON را با هر تعداد سطح مورد نیاز می‌توان تعریف و استفاده کرد و برای اینکار نیازی به نوشتن کدهای سفارشی تعریف مداخل خاص، وجود ندارد.
در فایل JSON فوق، نمونه‌ای از key/valueها، آرایه‌ها و اطلاعات چندین سطحی را مشاهده می‌کنید.


خواندن فایل تنظیمات appsettings.json در برنامه

پس از نصب پیشنیاز خواندن فایل‌های کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازنده‌ی جدیدی را به آن اضافه کنید:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddJsonFile("appsettings.json");
        Configuration = builder.Build();
    }
در اینجا نحوه‌ی خواندن فایل کانفیگ جدید appsettings.json را مشاهده می‌کنید. چند نکته در اینجا حائز اهمیت هستند:
الف) این خواندن، در سازنده‌ی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آن‌را به یک خاصیت عمومی انتساب داده‌ایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شده‌است. این اطلاعات را می‌توان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه می‌کنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت می‌شود.


دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot

تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شده‌ی در این اینترفیس به چه صورتی است؟


همانطور که مشاهده می‌کنید، هر سطح از سطح قبلی آن با : جدا شده‌است. همچنین اعضای آرایه، دارای ایندکس‌های 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات می‌توان نوشت:
var key1 = Configuration["Key1"];
var user1 = Configuration["Auth:Users:0"];
var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray();
var loggingIncludeScopes = Configuration["Logging:IncludeScopes"];
var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
خاصیت Configuration نیز در نهایت بر اساس key/valueها کار می‌کند و این keyها اگر چند سطحی بودند، با : از هم جدا می‌شوند و اگر نیاز به دسترسی اعضای خاصی از آرایه‌ها وجود داشت می‌توان آن ایندکس خاص را در انتهای زنجیره ذکر کرد. همچنین در اینجا نحوه‌ی استخراج تمام اعضای یک آرایه را نیز مشاهده می‌کنید.

یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن می‌توان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
 var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
در متد GetValue، آرگومان جنریک آن، یک کلاس را نیز می‌پذیرد. یعنی می‌توان خواص تو در توی مشخص شده‌ی با : را به یک کلاس نیز نگاشت کرد. در اینجا مقدار کلید معرفی شده، اولین سطحی خواهد بود که باید این اطلاعات از آن استخراج و نگاشت شوند.


سرویس IConfigurationRoot قابل تزریق است

در قسمت قبل، سرویس‌ها و تزریق وابستگی‌ها را بررسی کردیم. نکته‌ی جالبی را که می‌توان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
به تمام قسمت‌های برنامه است. برای نمونه در همان مثال قسمت قبل، قصد داریم تنظیمات برنامه را در لایه سرویس آن خوانده و مورد استفاده قرار دهیم. برای اینکار باید مراحل ذیل طی شوند:
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده می‌کنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آن‌را چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container();
container.Configure(config =>
{
    config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
و یا اگر از همان IoC Container توکار ASP.NET Core استفاده می‌کنید، روش انجام این‌کار در متد ConfigureServices به صورت زیر است:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
طول عمر آن هم singleton مشخص شده‌است تا تنها یکبار وهله سازی و سپس کش شود (مناسب برای کار با تنظیمات سراسری برنامه).

ب) فایل project.json کتابخانه‌ی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ 
    "dependencies": {
        //same as before 
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
    }
این وابستگی امکان دسترسی به اینترفیس IConfigurationRoot را در اسمبلی‌های دیگر میسر می‌کند.

ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازنده‌ی سرویس MessagesService تزریق می‌کنیم:
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
 
    public MessagesService(IConfigurationRoot configurationRoot)
    {
        _configurationRoot = configurationRoot;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        return $"DNT {key1}";
    }
}
در ادامه، نحوه‌ی استفاده‌ی از آن، همانند نکاتی است که در قسمت «دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot» عنوان شد.
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده می‌کند:
public void Configure(
    IApplicationBuilder app,
    IHostingEnvironment env,
    IMessagesService messagesService)
{ 
    app.Run(async context =>
    {
        var siteName = messagesService.GetSiteName();
        await context.Response.WriteAsync($"Hello {siteName}");
    });
}
چنین خروجی را خواهیم داشت:



خواندن تنظیمات از حافظه

الزاما نیازی به استفاده از فایل‌های JSON و یا XML در اینجا وجود ندارد. ابتدایی‌ترین حالت کار با بسته‌ی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا می‌توان لیستی از key/value‌ها را ذکر کرد:
var builder = new ConfigurationBuilder()
                    .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                });
 و نحوه‌ی کار با آن نیز همانند قبل است:
 var theValue = Configuration["the-key"];


امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی

در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder می‌توان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایل‌های کانفیگ دیگر را اضافه کرد:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                })
                            .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                            .AddJsonFile($"appsettings.{env}.json", optional: true);
        Configuration = builder.Build();
    }
و نکته‌ی مهم اینجا است که تنظیمات فایل دوم، تنظیمات مشابه فایل اول را بازنویسی می‌کند.
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی می‌کند.


امکان دسترسی به متغیرهای محیطی سیستم عامل

در انتهای زنجیره‌ی ConfigurationBuilder می‌توان متد AddEnvironmentVariables را نیز ذکر کرد:
 var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
این متد سبب می‌شود تا تمام اطلاعات قسمت Environment سیستم عامل، به مجموعه‌ی تنظیمات جاری اضافه شوند (در صورت نیاز) که نمونه‌ای از آن‌را در تصویر ذیل مشاهده می‌کنید:



امکان نگاشت تنظیمات برنامه به کلاس‌‌های متناظر

کار کردن با key/valueهای رشته‌ای، هرچند روش پایه‌ای استفاده‌ی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شده‌است. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "Smtp": {
        "Server": "0.0.0.1",
        "User": "user@company.com",
        "Pass": "123456789",
        "Port": "25"
    }
}
مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شده‌است، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشه‌ی src کلیک راست کرده و گزینه‌ی Add new project را انتخاب کنید و سپس یک پروژه‌ی جدید از نوع NET Core -> Class library. را به آن با نام Core1RtmEmptyTest.ViewModels اضافه کنید (تصویر ذیل).


در این کتابخانه‌ی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels
{
    public class SmtpConfig
    {
        public string Server { get; set; }
        public string User { get; set; }
        public string Pass { get; set; }
        public int Port { get; set; }
    }
}
از این جهت این کلاس را در یک library جداگانه قرار داده‌ایم تا بتوان از آن در لایه‌ی سرویس و همچنین خود برنامه استفاده کرد. اگر این کلاس را در برنامه‌ی اصلی قرار می‌دادیم، امکان دسترسی به آن در لایه‌ی سرویس میسر نمی‌شد.
سپس به پروژه‌ی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آن‌را بتوان در پروژه‌ی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{
    "dependencies": {
        // same as before        
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*"
    },
اکنون با فرض وجود تنظیمات خواندن فایل appsettings.json در سازنده‌ی کلاس آغازین برنامه، نیاز است بسته‌ی نیوگت Microsoft.Extensions.Configuration.Binder را نصب کنید:


و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوه‌ی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
   services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
در اینجا مشخص شده‌است که کار وهله سازی کلاس SmtpConfig بر اساس اطلاعات قسمت smtp فایل کانفیگ تامین می‌شود. متغیر Configuration ایی که در اینجا استفاده شده‌است همان خاصیت عمومی public IConfigurationRoot Configuration کلاس آغازین برنامه است.

سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژه‌ی Core1RtmEmptyTest.ViewModels به لایه‌ی سرویس برنامه اضافه می‌کنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بسته‌ی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایه‌ی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگی‌های فایل project.json این لایه چنین شکلی را پیدا می‌کند:
    "dependencies": {
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
        "Microsoft.Extensions.Options": "1.0.0",
        "NETStandard.Library": "1.6.0"
    }
پس از ذخیره سازی این کلاس و بازیابی خودکار وابستگی‌های آن، اکنون برای دسترسی به این تنظیم باید از اینترفیس ویژه‌ی IOptions استفاده کرد (به همین جهت بسته‌ی جدید نیوگت Microsoft.Extensions.Options را نصب کردیم):
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
    private readonly IOptions<SmtpConfig> _settings;
 
    public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings)
    {
        _configurationRoot = configurationRoot;
        _settings = settings;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        var server = _settings.Value.Server;
        return $"DNT {key1} - {server}";
    }
}
همانطور که ملاحظه می‌کنید <IOptions<SmtpConfig به سازنده‌ی کلاس تزریق شده‌است و سپس از طریق خاصیت Value آن می‌توان به تمام اطلاعات کلاس SmtpConfig به شکل strongly typed دسترسی یافت.

اکنون اگر برنامه را جرا کنید، این خروجی را می‌توان مشاهده کرد (که در آن آدرس Server دریافت شده‌ی از فایل کانفیگ نیز مشخص است):


البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگی‌ها در تمام قسمت‌های برنامه کار می‌کند. برای مثال در کنترلرها هم می‌توان <IOptions<SmtpConfig را به همین نحو تزریق کرد.


نحوه‌ی واکنش به تغییرات فایل‌های کانفیگ

در نگارش‌های قبلی ASP.NET، هر تغییری در فایل web.config، سبب ری‌استارت شدن کل برنامه می‌شد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران می‌شد.
در ASP.NET Core، برنامه‌ی وب ما دیگر متکی به فایل web.config نبوده و همچنین می‌توان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایل‌های کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
 .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
این پارامتر در صورت true بودن، به صورت خودکار سبب بارگذاری مجدد اطلاعات فایل کانفیگ می‌شود (بدون ری‌استارت کل برنامه).
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 9 - بررسی تغییرات مسیریابی
فعال سازی تنظیمات مسیریابی

یکی دیگر از تغییرات عمده‌ی ASP.NET Core با نگارش‌های قبلی آن، نحوه‌ی مدیریت مسیریابی‌های سیستم است. در نگارش‌های قبلی مبتنی بر HTTP Moduleها، مسیریابی‌ها توسط یک HTTP Module مخصوص، با pipeline اصلی ASP.NET یکپارچه شده‌اند و زمانیکه مسیر درخواستی با تنظیمات سیستم تطابق داشته باشد، پردازش کار به HTTP Handler مخصوص ASP.NET MVC منتقل می‌شود:


اما در ASP.NET Core مبتنی بر میان افزارها، زیر ساخت مسیریابی به صورت زیر تغییر کرده‌است:


میان افزار ASP.NET MVC را که در قسمت قبل فعال کردیم، باید بتواند کنترلر و اکشن متد متناظر با URL درخواستی را مشخص کند. این تصمیم گیری نیز بر اساس تنظیماتی به نام Routing انجام می‌شود. در قسمت قبل، حالت ساده و پیش فرض این تنظیمات را مورد استفاده قرار دادیم
 app.UseMvcWithDefaultRoute();
که مطابق سورس ASP.NET Core، معادل است با فراخوانی متد app.UseMvc، با قالب پیش فرضی به صورت زیر:
    public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app)
    {
      if (app == null)
        throw new ArgumentNullException("app");
      return app.UseMvc((Action<IRouteBuilder>) (routes => routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}")));
    }
قالب مشخص شده‌ی در اینجا به ASP.NET MVC می‌گوید که از کدام قسمت‌های URL باید نام کلاس کنترلر (کلاس ویژه‌ای که به کلمه‌ی Controller ختم می‌شود) و نام اکشن متد متناظر با آن‌را انتخاب کند (اکشن متد، متدی است عمومی در آن کلاس).
روش دیگر معرفی این تنظیمات، استفاده از Attribute routing است:
 [Route("[controller]/[action]")]


مسیریابی‌های قراردادی

در قسمت قبل، یک POCO Controller را به صورت ذیل تعریف کردیم و این کنترلر، بدون تعریف هیچ نوع مسیریابی خاصی در دسترس بود:
namespace Core1RtmEmptyTest.Controllers
{
  public class HomeController
  {
   public string Index()
   {
    return "Running a POCO controller!";
   }
  }
}
علت کار کردن مسیریابی آن نیز به ذکر متد app.UseMvcWithDefaultRoute در کلاس آغازین برنامه بر می‌گردد و همانطور که عنوان شد، این فراخوانی را می‌توان با فراخوانی واضح‌تر ذیل جایگزین کرد:
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
});
پارامتر این متد که جایگزین متد ConfigureRoutes، در نگارش‌های قبلی ASP.NET MVC شده‌است، از نوع IRouteBuilder می‌باشد.
در این تعاریف، هر کدام از قسمت‌های قرارگرفته‌ی داخل {}، مشخص کننده‌ی قسمتی از URL دریافتی بوده و نام‌های controller و action در اینجا جزو نام‌های از پیش مشخص شده هستند و برای نگاشت اطلاعات مورد استفاده قرار می‌گیرند. برای مثال اگر آدرس home/index/ درخواست شد، برنامه به کلاس HomeController و متد عمومی Index آن هدایت می‌شود. همچنین قسمت آخر این پردازش به ?id ختم شده‌است. وجود  ?، به معنای اختیاری بودن این پارامتر است و اگر در URL ذکر شود، به پارامتر id این اکشن متد، نگاشت خواهد شد. مواردی که پس از = ذکر شده‌اند، مقادیر پیش فرض مسیریابی هستند. برای مثال اگر صرفا آدرس home/ درخواست شود، مقدار اکشن متد آن با مقدار پیش فرض index جایگزین خواهد شد و اگر تنها مسیر / درخواست شود، کنترل Home و اکشن متد Index آن پردازش می‌شوند.
در اینجا به هر تعدادی که نیاز است می‌توان متدهای routes.MapRoute را فراخوانی و استفاده کرد؛ اما ترتیب تعریف آن‌ها حائز اهمیت است. هر مسیریابی که در ابتدای لیست اضافه شود، حق تقدم بالاتری خواهد داشت و هر تطابقی با یکی از مسیریابی‌های تعریف شده، در همان سطح سبب خاتمه‌ی پردازش سایر مسیریابی‌ها می‌شود.


استفاده از Attributes برای تعریف مسیریابی‌ها

بجای تعریف قرار دادهای پیش فرض مسیریابی در کلاس آغازین برنامه، می‌توان از ویژگی Route نیز استفاده کرد. هرچند روش تعریف مسیریابی‌های قراردادی، از نگارش‌های آغازین ASP.NET MVC به همراه آن بوده‌اند، اما با زیاد شدن تعداد کنترلرها و مسیریابی‌های سفارشی هر کدام، اینبار با نگاه کردن به یک کنترلر، سریع نمی‌توان تشخیص داد که چه مسیریابی‌های خاصی به آن مرتبط هستند. برای ساده سازی مدیریت برنامه‌های بزرگ و ساده سازی تعاریف مسیریابی‌های خاص آن‌ها، استفاده از ویژگی Route نیز به ASP.NET MVC اضافه شده‌است.
یک مثال: کنترلر About را درنظر بگیرید:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  public class AboutController : Controller
  {
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
این کلاس و کنترلر، به صورت پیش فرض نیاز به تعریف هیچ نوع مسیریابی جدیدی ندارد. همان مسیریابی پیش فرض ثبت شده‌ی در کلاس آغازین برنامه، تمام متدهای عمومی آن یا همان اکشن متدهای آن‌را پوشش می‌دهد. برای مثال جهت رسیدن به اکشن متد SiteName آن، می‌توان آدرس /About/SiteName/ را درخواست داد.
اما اگر آدرس /About/ را درخواست دهیم چطور؟ چون در مسیریابی پیش فرض، تعریف {action=Index} را داریم، یعنی هر زمانیکه در URL درخواستی، قسمت action آن ذکر نشد، آن‌را با index جایگزین کن و این کنترلر دارای متد Index نیست. در ادامه اگر بخواهیم متد Hello را تبدیل به متد پیش فرض این کنترلر کنیم، می‌توان با استفاده از ویژگی Route به صورت ذیل عمل کرد:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  [Route("About")]
  public class AboutController : Controller
  {
   [Route("")]
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   [Route("SiteName")]
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
در اینجا با اولین Route تعریف شده، مشخص کرده‌ایم که اگر قسمت اول URL درخواستی معادل about بود، پردازش برنامه باید به این کنترلر هدایت شود. بدیهی است الزامی به یکی بودن نام Route، با نام کنترلر، وجود ندارد. همچنین Route تعریف شده‌ی با رشته‌ی خالی، به معنای مسیریابی پیش فرض است. یعنی اگر آدرس /about/ درخواست داده شد، اکشن متد پیش فرض آن، متد Hello خواهد بود. در این حالت، ذکر Route بعدی برای اکشن متد SiteName الزامی است و اگر این‌کار صورت نگیرد، به استثنای ذیل خواهیم رسید:
 AmbiguousActionException: Multiple actions matched. The following actions matched route data and had all constraints satisfied:

Core1RtmEmptyTest.Controllers.AboutController.Hello (Core1RtmEmptyTest)
Core1RtmEmptyTest.Controllers.AboutController.SiteName (Core1RtmEmptyTest)
که عنوان کرده‌است در این حالت مشخص نیست که اکشن متد پیش فرض، کدام است.

روش بهتر و refactoring friendly آن نیز به صورت ذیل است:
using Microsoft.AspNetCore.Mvc;
 
namespace Core1RtmEmptyTest.Controllers
{
  [Route("[controller]")]
  public class AboutController : Controller
  {
   [Route("")]
   public ActionResult Hello()
   {
    return Content("Hello from DNT!");
   }
 
   [Route("[action]")]
   public ActionResult SiteName()
   {
    return Content("DNT");
   }
  }
}
عموما مرسوم است که نام مسیریابی کنترلر همان نام کنترلر باشد و نام مسیریابی اکشن متد، همان نام اکشن متد مربوطه. به همین جهت می‌توان از توکن‌های ویژه‌ی [controller] و [action] نیز در اینجا استفاده کرد که دقیقا به همان نام کنترلر و اکشن متد متناظر با آن‌ها تفسیر خواهند شد. مزیت این‌کار این است که در صورت تغییر نام متدها یا کنترلرها، دیگر نیازی نیست تا نام‌های تعریف شده‌ی در ویژگی‌های Route را نیز تغییر داد.

یک نکته: در حین تعریف مسیریابی یک کنترلر می‌توان پیشوندهایی را نیز ذکر کرد؛ برای مثال:
 [Route("api/[controller]")]
وجود api در اینجا به این معنا است که از این پس تنها آدرس /api/about/ پردازش خواهد شد و اگر صرفا آدرس /about/ درخواست شود، با خطای 404 و یا یافت نشد، کار خاتمه می‌یابد.


تعریف قیود، برای مسیریابی‌های تعریف شده

فرض کنید به کنترلر About فوق، اکشن متد ذیل را که یک خروجی JSON را بازگشت می‌دهد، اضافه کرده‌ایم:
//[Route("/Users/{userid}")]
[Route("Users/{userid}")]
public IActionResult GetUsers(int userId)
{
    return Json(new { userId = userId });
}
در اینجا تعریف مسیریابی آن با users/ و user معانی کاملا متفاوتی را دارند. اگر مسیریابی Users/{userid}/ را تعریف کنیم، یعنی مسیر ذیل از ریشه‌ی سایت باید درخواست شود: http://localhost:7742/users/1
و اگر مسیریابی Users/{userid} را تعریف کنیم، یعنی این مسیریابی پس از ذکر کنترلر about، به عنوان یک اکشن متد آن مفهوم پیدا می‌کند:
http://localhost:7742/about/users/1
در هر دو حالت، ذکر پارامتر userid الزامی است (چون با ? مشخص نشده‌است)؛ مانند:
[Route("/Users/{userid:int?}")]
در اینجا اگر بخواهیم نوع پارامتر درخواستی را نیز دقیقا مشخص و مقید کنیم، می‌توان به صورت ذیل عمل کرد:
 [Route("Users/{userid:int}")]
اگر این کار را انجام ندهیم، با درخواست مسیر http://localhost:7742/dnt/about/users/test مقدار صفر به userId ارسال می‌شود (چون پارامتر test عددی نیست). اگر تنظیم فوق را انجام دهیم، کاربر خطای 404 را دریافت می‌کند.

قیودی را که در اینجا می‌توان ذکر کرد به شرح زیر هستند:
• alpha - معادل است با  (a-z, A-Z).
• bool - برای تطابق با مقادیر بولی.
• datetime - برای تطابق با تاریخ میلادی.
• decimal - برای تطابق با ورودی‌های اعشاری.
• double - برای تطابق با اعداد اعشاری 64 بیتی.
• float - برای تطابق با اعداد اعشاری 32 بیتی.
• guid - برای تطابق با GUID ها
• int - برای تطابق با اعداد صحیح 32 بیتی.
• length - برای تعیین طول رشته.
• long - برای تطابق با اعداد صحیح 64 بیتی.
• max - برای ذکر حداکثر مقدار یک عدد صحیح.
• maxlength - جهت ذکر حداکثر طول رشته‌ی مجاز ورودی.
• min - برای ذکر حداقل مقدار یک عدد صحیح.
• minlength - جهت ذکر حداقل طول رشته‌ی مجاز ورودی.
• range - ذکر بازه‌ی اعداد صحیح مجاز.
• regex - ذکر یک عبارت با قاعده جهت مشخص سازی الگوی قابل پذیرش.

برای ترکیب چندین قید مختلف نیز می‌توان از : استفاده کرد:
 [Route("/Users/{userid:int:max(1000):min(10)}")]


ذکر نام Route برای ساده سازی تعریف آدرسی به آن

در حین تعریف یک Route می‌توان نام دلخواهی را نیز به آن انتساب داد (همانند نام default مسیریابی ثبت شده‌ی در کلاس آغازین برنامه):
 [Route("/Users/{userid:int}", Name="GetUserById")]
مزیت آن این است که اکنون برای اشاره‌ی به این مسیریابی خاص می‌توان از این نام تعریف شده استفاده کرد:
 string uri = Url.Link("GetUserById", new { userid = 1 });
پارامتر اول ذکر شده، نام مسیریابی و پارامتر دوم، پارامترهای مرتبط با این مسیریابی هستند.


مشخص سازی ترتیب پردازش مسیریابی‌ها

ترتیب مسیریابی‌های ثبت شده‌ی در کلاس آغازین برنامه، همان ترتیب افزوده شدن و ذکر آن‌ها است.
در اینجا می‌توان از خاصیت order نیز استفاده کرد و اعداد کوچکتر، ابتدا پردازش می‌شوند (مقدار پیش فرض آن نیز صفر است):
 [Route("/Users/{userid:int}", Name = "GetUserById", Order = 1)]


امکان تعریف قیود سفارشی

اگر قیودی که تا اینجا ذکر شدند، برای کار شما مناسب نبودند و نیاز بود تا الگوریتم خاصی را جهت محدود سازی دسترسی به یک مسیریابی خاص پیاده سازی کنید، می‌توان به صورت ذیل عمل کرد:
using System;
using System.Globalization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
 
namespace Core1RtmEmptyTest
{
  public class CustomRouteConstraint : IRouteConstraint
  {
   public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values,
    RouteDirection routeDirection)
   {
    object value;
    if (!values.TryGetValue(routeKey, out value) || value == null)
    {
      return false;
    }
 
    long longValue;
    if (value is long)
    {
      longValue = (long)value;
      return longValue != 10;
    }
 
    var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
    if (long.TryParse(valueString, NumberStyles.Integer,
      CultureInfo.InvariantCulture, out longValue))
    {
      return longValue != 10;
    }
    return false;
   }
  }
}
در اینجا یک کلاس جدید را که اینترفیس IRouteConstraint را پیاده سازی می‌کند تعریف کرده‌ایم:
public class CustomRouteConstraint : IRouteConstraint
سپس در متد match آن بررسی کرده‌ایم که اگر userid=10 بود، خطای 404 صادر شود.
در آخر برای ثبت و معرفی آن باید به متد ConfigureServices کلاس آغازین برنامه مراجعه کرد:
public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting(options =>options.ConstraintMap.Add("Custom", typeof(CustomRouteConstraint)));
پس از آن، این نام جدید ثبت شده‌ی در اینجا، به نحو ذیل قابل استفاده است:
 [Route("/Users/{userid:int:custom}")]
به این ترتیب userid باید از نوع int بوده و همچنین قید custom را نیز پوشش دهد (یعنی userid=10 نباشد).

یک نکته:  اگر به سورس ASP.NET Core مراجعه کنید ، تمام قیودی را که پیشتر نام بردیم (مانند int، guid و امثال آن) نیز به همین روش تعریف و پیشتر ثبت شده‌اند.


معرفی بسته‌ی نیوگت Microsoft.AspNetCore.SpaServices

مسیریابی‌های پیش فرض ASP.NET Core با مسیریابی‌های برنامه‌های SPA مانند AngularJS (و امثال آن) تداخل دارند؛ از این جهت که درخواست‌های رسیده‌ی به سرور، ابتدا به موتور پردازشی ASP.NET وارد می‌شوند و اگر یافت نشدند، کاربر با پیام 404 مواجه خواهد شد و دیگر در اینجا برنامه به مسیریابی خاص مثلا AngularJS 2.0 هدایت نمی‌شود.
برای این موارد مرسوم است که یک fallback route را در انتهای مسیریابی‌های موجود اضافه کنند (به آن catch all هم می‌گویند)
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
 
  routes.MapRoute(
   name: "spa-fallback",
   template: "{*url}",
   defaults: new { controller = "Home", action = "Index" });
});
در اینجا هر درخواستی که با مسیریابی default تطابق نداشت، توسط الگوی عمومی {url*} پردازش می‌شود و این پردازش در نهایت سبب راه اندازی برنامه‌ی SPA می‌گردد. اما مشکل اینجا است که برای فایل‌های استاتیک غیرموجود مانند تصاویر، فایل‌های js و css نیز خروجی HTML ایی خواهیم داشت؛ بجای خروجی 404 و یافت نشد.
برای حل این مشکل مایکروسافت بسته‌ای را به نام Microsoft.AspNetCore.SpaServices ارائه داده است.
برای افزودن آن بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.AspNetCore.SpaServices را جستجو کرده و نصب نمائید:


انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
    "dependencies": {
       //same as before  
       "Microsoft.AspNetCore.SpaServices": "1.0.0-beta-000007"
 },
پس از بازیابی و نصب آن، اکنون catch all را حذف کرده و با یک سطر routes.MapSpaFallbackRoute ذیل جایگزین کنید:
app.UseMvc(routes =>
{
  routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
 
  routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" });
});
و برای یادآوری مطلب «ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک» در AngularJS 2.0، علاوه بر عمومی کردن پوشه‌ی wwwroot توسط UseFileServer نیاز است پوشه‌ی node_modules را هم با تنظیمات ذیل عمومی کرد و در معرض دید عموم قرار داد (جایی که بسته‌های node.js نصب می‌شوند):
// Serve wwwroot as root
app.UseFileServer();
 
// Serve /node_modules as a separate root (for packages that use other npm modules client side)
app.UseFileServer(new FileServerOptions
{
  // Set root of file server
  FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "node_modules")),
  // Only react to requests that match this path
  RequestPath = "/node_modules",
  // Don't expose file system
  EnableDirectoryBrowsing = false
});
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 4 - فعال سازی پردازش فایل‌های استاتیک
همانطور که در قسمت قبل، با معرفی مقدماتی Middlewareها عنوان شد، تمام قابلیت‌های یک برنامه‌ی ASP.NET Core، به صورت پیش فرض غیرفعال هستند؛ مگر آنکه Middlewareهای مخصوص آن‌ها را به صورت دستی و با آگاهی کامل، به کلاس آغازین برنامه اضافه کنید. در این قسمت قصد داریم تعداد دیگری از این Middlewareهای توکار را مورد بررسی قرار دهیم.


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

در مورد پوشه‌ی جدید wwwroot در «قسمت 2 - بررسی ساختار جدید Solution» مطالبی عنوان شدند. جهت یادآوری:
اگر فایل Program.cs را بررسی کنید، یک چنین تعاریفی را مشاهده خواهید کرد:
public class Program
{
    public static void Main(string[] args)
    {
        var host = new WebHostBuilder()
            .UseKestrel()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartup<Startup>()
            .Build();
 
        host.Run();
    }
}
در کدهای فوق، سطر UseContentRoot، پوشه‌ی خاصی را به نام content root معرفی می‌کند که در اینجا به همان پوشه‌ی اصلی برنامه اشاره می‌کند و پوشه‌ی wwwroot از مسیر content root/wwwroot خوانده می‌شود که جهت ارائه‌ی تمام فایل‌های عمومی برنامه مورد استفاده قرار می‌گیرد (مانند تصاویر، فایل‌های JS ،CSS و امثال آن). هدف این است که کدهای سمت سرور برنامه (قرار گرفته در content root) از کدهای عمومی آن (قرار گرفته در پوشه‌ی ویژه‌ی content root/wwwroot) جدا شده و به این ترتیب احتمال نشتی اطلاعات سمت سرور به حداقل برسد.

یک مثال: زمانیکه فایل استاتیک images/banner3.svg در پوشه‌ی wwwroot قرار می‌گیرد، با آدرس http://localhost:9189/images/banner3.svg توسط عموم قابل دسترسی خواهد بود.

یک نکته‌ی امنیتی مهم
در برنامه‌های ASP.NET Core، هنوز فایل web.config را نیز مشاهده می‌کنید. این فایل تنها کاربردی که در اینجا دارد، تنظیم ماژول AspNetCoreModule برای IIS است تا IIS static file handler آن، راسا اقدام به توزیع فایل‌های یک برنامه‌ی ASP.NET Core نکند. بنابراین توزیع این فایل را بر روی سرورهای IIS فراموش نکنید. همچنین بهتر است در ویندوزهای سرور، به قسمت Modules feature مراجعه کرده و StaticFileModule را از لیست ویژگی‌های موجود حذف کرد.


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

در قسمت قبل با نحوه‌ی نصب و فعال سازی middleware مخصوص WelcomePage آشنا شدیم. روال کار در اینجا نیز دقیقا به همان صورت است:
الف) نصب بسته‌ی نیوگت Microsoft.AspNetCore.StaticFiles
برای اینکار می‌توان بر روی گره‌ی references کلیک راست کرده و سپس از منوی ظاهر شده،‌گزینه‌ی manage nuget packages را انتخاب کرد. سپس ابتدا برگه‌ی browse را انتخاب کنید و در اینجا نام Microsoft.AspNetCore.StaticFiles را جستجو کرده و سپس نصب کنید.


انجام این کارها معادل افزودن یک سطر ذیل به فایل project.json است و سپس ذخیره‌ی آن که کار بازیابی بسته‌ها را به صورت خودکار آغاز می‌کند:
 "dependencies": {
   // same as before
    "Microsoft.AspNetCore.StaticFiles": "1.0.0"
},
ب) معرفی Middleware پردازش فایل‌های استاتیک
برای اینکار به فایل Startup.cs مراجعه کرده و سطر UseStaticFiles را به متد Configure اضافه کنید (به UseWelcomePage هم دیگر نیازی نداریم):
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }
 
    public void Configure(IApplicationBuilder app)
    {
        app.UseStaticFiles();
        //app.UseWelcomePage();
 
        app.Run(async context =>
        {
            await context.Response.WriteAsync("Hello DNT!");
        });
    }
}

یک مثال: بر روی پوشه‌ی wwwroot کلیک راست کرده و گزینه‌ی add->new item را انتخاب کنید. سپس یک HTML page جدید را به نام index.html به این پوشه اضافه کنید.
با این محتوا:
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Hello World</title>
</head>
<body>
    Hello World!
</body>
</html>
در این حالت برنامه را اجرا کنید. خروجی ذیل را مشاهده خواهید کرد:


که این خروجی دقیقا خروجی app.Run برنامه است و نه محتوای فایل index.html ایی که اضافه کردیم.
در ادامه اگر مسیر کامل این فایل را (http://localhost:7742/index.html) درخواست دهیم، آنگاه می‌توان خروجی این فایل استاتیک را مشاهده کرد:


این رفتار اندکی متفاوت است نسبت به نگارش‌های قبلی ASP.NET که فایل index.html را به عنوان فایل پیش فرض، درنظر می‌گرفت و محتوای آن‌را نمایش می‌داد. منظور از فایل پیش فرض، فایلی است که با درخواست ریشه‌ی یک مسیر، به کاربر ارائه داده می‌شود و index.html یکی از آن‌ها است.
برای رفع این مشکل، نیاز است Middleware مخصوص آن‌را به نام Default Files نیز به برنامه معرفی کرد:
public void Configure(IApplicationBuilder app)
{
   app.UseDefaultFiles();
   app.UseStaticFiles();
در این حالت است که با درخواست ریشه‌ی سایت، فایل پیش فرض آن نمایش داده خواهد شد:


فعال سازی Default Files، سبب جستجوی یکی از 4 فایل ذیل به صورت پیش فرض می‌شود (اگر تنها ریشه‌ی پوشه‌ای درخواست شود):
default.htm
default.html
index.htm
index.html

اگر خواستید فایل سفارشی خاص دیگری را معرفی کنید، نیاز است پارامتر DefaultFilesOptions آن‌را مقدار دهی نمائید:
 // Serve my app-specific default file, if present.
DefaultFilesOptions options = new DefaultFilesOptions();
options.DefaultFileNames.Clear();
options.DefaultFileNames.Add("mydefault.html");
app.UseDefaultFiles(options);


ترتیب معرفی Middlewares مهم است

در قسمت قبل، در حین معرفی تفاوت‌های Middlewareها با HTTP Modules، عنوان شد که اینبار برنامه نویس می‌تواند بر روی ترتیب اجرای Middlewareها کنترل کاملی داشته باشد و این ترتیب معادل است با ترتیب معرفی آن‌ها در متد Configure، به نحوی که مشاهده می‌کنید. برای آزمایش این مطلب، متد معرفی middleware فایل‌های پیش فرض را پس از متد معرفی فایل‌های استاتیک قرار دهید:
public void Configure(IApplicationBuilder app)
{
  app.UseStaticFiles();
  app.UseDefaultFiles();
در این حالت اگر برنامه را اجرا کنید، به این خروجی خواهید رسید:


بله. اینبار تعریف فایل‌های پیش فرض، هیچ تاثیری نداشته و درخواست ریشه‌ی سایت، بدون ذکر صریح نام فایلی، مجددا به app.Run ختم شده‌است.


توزیع فایل‌های استاتیک خارج از wwwroot

همانطور که در ابتدای بحث عنوان شد، با فعال سازی UseStaticFiles به صورت پیش فرض مسیر content root/wwwroot در معرض دید دنیای خارج قرار می‌گیرد و توسط وب سرور قابل توزیع خواهد شد:
○ wwwroot
   § css
   § images
   § ...
○ MyStaticFiles
   § test.png
اما اگر قصد داشته باشیم تا تصویر test.png موجود در پوشه‌ی MyStaticFiles خارج از wwwroot را نیز عمومی کنیم چه باید کرد؟
برای این منظور می‌توان از پارامتر StaticFileOptions متد UseStaticFiles به نحو ذیل جهت معرفی پوشه‌ی MyStaticFiles استفاده کرد:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
    RequestPath = new PathString("/StaticFiles")
});
در این حالت، مسیر دسترسی عمومی به این فایل، به صورت  http://<app>/StaticFiles/test.png خواهد بود (بر مبنای RequestPath تنظیم شده).


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


فرض کنید پوشه‌ی تصاویر را به پوشه‌ی عمومی wwwroot اضافه کرده‌ایم. برای فعال سازی مرور محتوای این پوشه می‌توان از Middleware دیگری به نام DirectoryBrowser استفاده کرد:
app.UseDirectoryBrowser(new DirectoryBrowserOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});
بعد از انجام اینکار به خطای ذیل خواهید رسید:
 Unable to resolve service for type 'System.Text.Encodings.Web.HtmlEncoder' while attempting to activate 'Microsoft.AspNetCore.StaticFiles.DirectoryBrowserMiddleware'.
برای رفع آن، سرویس آن نیز باید به متد ConfigureServices اضافه شود:
public void ConfigureServices(IServiceCollection services)
{
   services.AddDirectoryBrowser();
}
در این حالت پس از اجرای برنامه، اگر مسیر http://localhost:7742/myimages را درخواست دهید (MyImages از RequestPath تنظیم شده، گرفته می‌شود)، به تصویر ذیل خواهید رسید:


مشکل! در این حالت که DirectoryBrowser را فعال کرده‌ایم، اگر بر روی لینک فایل تصویر نمایش داده شده کلیک کنیم، باز پیام Hello DNT یا اجرای app.Run را شاهد خواهیم بود.
به این دلیل که UseStaticFiles پیش فرض، مسیر درخواستی MyImages را که بر روی file system وجود ندارد، نمی‌شناسد. برای رفع این مشکل تنها کافی است مسیریابی این Request Path خاص را نیز فعال کنیم:
app.UseStaticFiles(new StaticFileOptions
{
    FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
    RequestPath = new PathString("/MyImages")
});


بررسی خلاصه‌ی تنظیماتی که به فایل آغازین برنامه اضافه شدند

تا اینجا اگر توضیحات را قدم به قدم دنبال و اجرا کرده باشید، یک چنین تنظیماتی را خواهید داشت:
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
 
namespace Core1RtmEmptyTest
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDirectoryBrowser();
        }
 
        public void Configure(IApplicationBuilder app)
        {
            app.UseDefaultFiles();
 
            app.UseStaticFiles(); // For the wwwroot folder
 
            // For the files outside of the wwwroot
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
                RequestPath = new PathString("/StaticFiles")
            });
 
            // For DirectoryBrowser
            app.UseStaticFiles(new StaticFileOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            app.UseDirectoryBrowser(new DirectoryBrowserOptions
            {
                FileProvider = new PhysicalFileProvider(root: Path.Combine(Directory.GetCurrentDirectory(), @"wwwroot\images")),
                RequestPath = new PathString("/MyImages")
            });
 
            //app.UseWelcomePage();
 
            app.Run(async context =>
            {
                await context.Response.WriteAsync("Hello DNT!");
            });
        }
    }
}
services.AddDirectoryBrowser برای فعال سازی مرور پوشه‌ها اضافه شده‌است.
UseDefaultFiles کار فعال سازی شناسایی فایل‌های پیش فرضی مانند index.html را در صورت ذکر نام ریشه‌ی یک پوشه، انجام می‌دهد.
اولین UseStaticFiles تعریف شده، تمام مسیرهای فیزیکی ذیل wwwroot را عمومی می‌کند.
دومین UseStaticFiles تعریف شده، پوشه‌ی MyStaticFiles واقع در خارج از wwwroot را عمومی می‌کند.
سومین UseStaticFiles تعریف شده، پوشه‌ی فیزیکی wwwroot\images را به مسیر درخواست‌های MyImages نگاشت می‌کند (http://localhost:7742/myimages) تا توسط DirectoryBrowser تعریف شده، قابل استفاده شود.
در آخر هم DirectoryBrowser تعریف شده‌است.


یک نکته‌ی امنیتی مهم
یک چنین قابلیتی (مرور فایل‌های درون یک پوشه) به صورت پیش فرض بر روی تمام IIS‌ها به دلایل امنیتی غیرفعال است. به همین جهت بهتر است Middleware فوق را هیچگاه استفاده نکنید و به این قسمت صرفا از دیدگاه اطلاعات عمومی نگاه کنید.


ساده سازی تعاریف توزیع فایل‌های استاتیک

Middleware دیگری به نام FileServer کار تعریف توزیع فایل‌های استاتیک را ساده می‌کند. اگر آن‌را تعریف کنید:
 app.UseFileServer();
اینکار به معنای تعریف یکباره‌ی UseStaticFiles و UseDefaultFiles، با ترتیب صحیح آن‌ها است.
اگر خواستید DirectoryBrowsing آن‌را نیز فعال کنید، پارامتر ورودی آن‌را به true مقدار دهی کنید (که به صورت پیش فرض غیرفعال است):
 app.UseFileServer(enableDirectoryBrowsing: true);
همچنین در اینجا می‌توانید مسیر پوشه‌ی MyStaticFiles خارج از wwwroot را نیز با مقدار دهی پارامتر FileServerOptions آن، مشخص کنید:
app.UseFileServer(new FileServerOptions
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), @"MyStaticFiles")),
        RequestPath = new PathString("/StaticFiles"),
        EnableDirectoryBrowsing = false
    });


توزیع فایل‌های ناشناخته

اگر به سورس ASP.NET Core 1.0 دقت کنید، کلاسی را به نام FileExtensionContentTypeProvider خواهید یافت. این‌ها پسوندها و mime typeهای متناظری هستند که توسط ASP.NET Core شناخته شده و توزیع می‌شوند. برای مثال اگر فایلی را به نام test.xyz به پوشه‌ی wwwroot اضافه کنید، درخواست آن توسط کاربر، به Hello DNT ختم می‌شود؛ چون در این کلاس پایه، پسوند xyz تعریف نشده‌است.
برای رفع این مشکل و تکمیل این لیست می‌توان به نحو ذیل عمل کرد:
 // Set up custom content types -associating file extension to MIME type
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".xyz"] = "text/html";
 
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = provider
}) ; // For the wwwroot folder
در اینجا ابتدا همان کلاس پایه FileExtensionContentTypeProvider را نمونه سازی می‌کنیم و سپس به دیکشنری آن، پسوند و mime type ویژه‌ی خود را اضافه می‌کنیم. سپس این provider را می‌توان به خاصیت ContentTypeProvider پارامتر StaticFileOptions آن نسبت داد. اکنون این فایل با پسوند xyz، قابل شناسایی می‌شود:


و یا اگر خواستید کمی تمیزتر کار کنید، بهتر است از کلاس پایه FileExtensionContentTypeProvider ارث بری کرده و سپس در سازنده‌ی این کلاس، خاصیت Mappings را ویرایش نمود:
public class XyzContentTypeProvider : FileExtensionContentTypeProvider
{
    public XyzContentTypeProvider()
    {
        this.Mappings.Add(".xyz", "text/html");
    }
}
و برای استفاده‌ی از آن خواهیم داشت:
app.UseStaticFiles(new StaticFileOptions
{
    ContentTypeProvider = new XyzContentTypeProvider()
}) ; // For the wwwroot folder

روش دیگر مدیریت این مساله، تنظیم مقدار خاصیت ServeUnknownFileTypes به true است:
app.UseStaticFiles(new StaticFileOptions
{
    ServeUnknownFileTypes = true,
    DefaultContentType = "image/png"
});
در اینجا هر پسوند شناخته نشده‌ای با mime type تصویر png، توزیع خواهد شد. البته از لحاظ امنیتی توصیه شده‌است که چنین کاری را انجام ندهید و از این تنظیم عمومی نیز صرفنظر کنید.
مطالب
استفاده از EF در اپلیکیشن های N-Tier : قسمت اول
تمام اپلیکیشن‌ها را نمی‌توان در یک پروسس بسته بندی کرد، بدین معنا که تمام اپلیکیشن روی یک سرور فیزیکی قرار گیرد. در عصر حاظر معماری بسیاری از اپلیکیشن‌ها چند لایه است و هر لایه روی سرور مجزایی توزیع می‌شود. بعنوان مثال یک معماری کلاسیک شامل سه لایه نمایش (presentation)، اپلیکیشن (application) و داده (data) است. لایه بندی منطقی (logical layering) یک اپلیکیشن می‌تواند در یک App Domain واحد پیاده سازی شده و روی یک کامپیوتر میزبانی شود. در این صورت لازم نیست نگران مباحثی مانند پراکسی ها، مرتب سازی (serialization)، پروتوکل‌های شبکه و غیره باشیم. اما اپلیکیشن‌های بزرگی که چندین کلاینت دارند و در مراکز داده میزبانی می‌شوند باید تمام این مسائل را در نظر بگیرند. خوشبختانه پیاده سازی چنین اپلیکیشن هایی با استفاده از Entity Framework و دیگر تکنولوژی‌های مایکروسافت مانند WCF, Web API ساده‌تر شده است. منظور از n-Tier معماری اپلیکیشن هایی است که لایه‌های نمایش، منطق تجاری و دسترسی داده هر کدام روی سرور مجزایی میزبانی می‌شوند. این تفکیک فیزیکی لایه‌ها به بسط پذیری، مدیریت و نگهداری اپلیکیشن‌ها در دراز مدت کمک می‌کند، اما معمولا تاثیری منفی روی کارایی کلی سیستم دارد. چرا که برای انجام عملیات مختلف باید از محدوده ماشین‌های فیریکی عبور کنیم.

معماری N-Tier چالش‌های بخصوصی را برای قابلیت‌های change-tracking در EF اضافه می‌کند. در ابتدا داده‌ها توسط یک آبجکت EF Context بارگذاری می‌شوند اما این آبجکت پس از ارسال داده‌ها به کلاینت از بین می‌رود. تغییراتی که در سمت کلاینت روی داده‌ها اعمال می‌شوند ردیابی (track) نخواهند شد. هنگام بروز رسانی، آبجکت Context جدیدی برای پردازش اطلاعات ارسالی باید ایجاد شود. مسلما آبجکت جدید هیچ چیز درباره Context پیشین یا مقادیر اصلی موجودیت‌ها نمی‌داند.

در نسخه‌های قبلی Entity Framework توسعه دهندگان با استفاده از قالب ویژه ای بنام Self-Tracking Entities می‌توانستند تغییرات موجودیت‌‌ها را ردیابی کنند. این قابلیت در نسخه EF 6 از رده خارج شده است و گرچه هنوز توسط ObjectContext پشتیبانی می‌شود، آبجکت DbContext از آن پشتیبانی نمی‌کند.

در این سری از مقالات روی عملیات پایه CRUD تمرکز می‌کنیم که در اکثر اپلیکیشن‌های n-Tier استفاده می‌شوند. همچنین خواهیم دید چگونه می‌توان تغییرات موجودیت‌ها را ردیابی کرد. مباحثی مانند همزمانی (concurrency) و مرتب سازی (serialization) نیز بررسی خواهند شد. در قسمت یک این سری مقالات، به بروز رسانی موجودیت‌های منفصل (disconnected) توسط سرویس‌های Web API نگاهی خواهیم داشت.


بروز رسانی موجودیت‌های منفصل با Web API

سناریویی را فرض کنید که در آن برای انجام عملیات CRUD از یک سرویس Web API استفاده می‌شود. همچنین مدیریت داده‌ها با مدل Code-First پیاده سازی شده است. در مثال جاری یک کلاینت Console Application خواهیم داشت که یک سرویس Web API را فراخوانی می‌کند. توجه داشته باشید که هر اپلیکیشن در Solution مجزایی قرار دارد. تفکیک پروژه‌ها برای شبیه سازی یک محیط n-Tier انجام شده است.

فرض کنید مدلی مانند تصویر زیر داریم.

همانطور که می‌بینید مدل جاری، سفارشات یک اپلیکیشن فرضی را معرفی می‌کند. می‌خواهیم مدل و کد دسترسی به داده‌ها را در یک سرویس Web API پیاده سازی کنیم، تا هر کلاینتی که از HTTP استفاده می‌کند بتواند عملیات CRUD را انجام دهد. برای ساختن سرویس مورد نظر مراحل زیر را دنبال کنید.

  • در ویژوال استودیو پروژه جدیدی از نوع ASP.NET Web Application بسازید و قالب پروژه را Web API انتخاب کنید. نام پروژه را به Recipe1.Service تغییر دهید.
  • کنترلر جدیدی از نوع WebApi Controller با نام OrderController به پروژه اضافه کنید.
  • کلاس جدیدی با نام Order در پوشه مدل‌ها ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Order
{
    public int OrderId { get; set; }
    public string Product { get; set; }
    public int Quantity { get; set; }
    public string Status { get; set; }
    public byte[] TimeStamp { get; set; }
}
  • با استفاده از NuGet Package Manager کتابخانه Entity Framework 6 را به پروژه اضافه کنید.
  • حال کلاسی با نام Recipe1Context ایجاد کنید و کد زیر را به آن اضافه نمایید.
public class Recipe1Context : DbContext
{
    public Recipe1Context() : base("Recipe1ConnectionString") { }
    
    public DbSet<Order> Orders { get; set; }
    
    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Order>().ToTable("Orders");
        // Following configuration enables timestamp to be concurrency token
        modelBuilder.Entity<Order>().Property(x => x.TimeStamp)
            .IsConcurrencyToken()
            .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed);
    }
}

  • فایل Web.config پروژه را باز کنید و رشته اتصال زیر را به قسمت ConnectionStrings اضافه نمایید.
<connectionStrings>
  <add name="Recipe1ConnectionString"
    connectionString="Data Source=.;
    Initial Catalog=EFRecipes;
    Integrated Security=True;
    MultipleActiveResultSets=True"
    providerName="System.Data.SqlClient" />
</connectionStrings>
  • فایل Global.asax را باز کنید و کد زیر را به آن اضافه نمایید. این کد بررسی Entity Framework Compatibility را غیرفعال می‌کند.
protected void Application_Start()
{
    // Disable Entity Framework Model Compatibilty
    Database.SetInitializer<Recipe1Context>(null);
    ...
}
  • در آخر کد کنترلر Order را با لیست زیر جایگزین کنید.
public class OrderController : ApiController
{
    // GET api/order
    public IEnumerable<Order> Get()
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.ToList();
        }
    }

    // GET api/order/5
    public Order Get(int id)
    {
        using (var context = new Recipe1Context())
        {
            return context.Orders.FirstOrDefault(x => x.OrderId == id);
        }
    }

    // POST api/order
    public HttpResponseMessage Post(Order order)
    {
        // Cleanup data from previous requests
        Cleanup();
        
        using (var context = new Recipe1Context())
        {
            context.Orders.Add(order);
            context.SaveChanges();
            // create HttpResponseMessage to wrap result, assigning Http Status code of 201,
            // which informs client that resource created successfully
            var response = Request.CreateResponse(HttpStatusCode.Created, order);
            // add location of newly-created resource to response header
            response.Headers.Location = new Uri(Url.Link("DefaultApi",
                new { id = order.OrderId }));
            return response;
        }
    }

    // PUT api/order/5
    public HttpResponseMessage Put(Order order)
    {
        using (var context = new Recipe1Context())
        {
            context.Entry(order).State = EntityState.Modified;
            context.SaveChanges();
            // return Http Status code of 200, informing client that resouce updated successfully
            return Request.CreateResponse(HttpStatusCode.OK, order);
        }
    }

    // DELETE api/order/5
    public HttpResponseMessage Delete(int id)
    {
        using (var context = new Recipe1Context())
        {
            var order = context.Orders.FirstOrDefault(x => x.OrderId == id);
            context.Orders.Remove(order);
            context.SaveChanges();
            // Return Http Status code of 200, informing client that resouce removed successfully
            return Request.CreateResponse(HttpStatusCode.OK);
        }
    }

    private void Cleanup()
    {
        using (var context = new Recipe1Context())
        {
            context.Database.ExecuteSqlCommand("delete from [orders]");
        }
    }
}

قابل ذکر است که هنگام استفاده از Entity Framework در MVC یا Web API، بکارگیری قابلیت Scaffolding بسیار مفید است. این فریم ورک‌های ASP.NET می‌توانند کنترلرهایی کاملا اجرایی برایتان تولید کنند که صرفه جویی چشمگیری در زمان و کار شما خواهد بود.

در قدم بعدی اپلیکیشن کلاینت را می‌سازیم که از سرویس Web API استفاده می‌کند.

  • در ویژوال استودیو پروژه جدیدی از نوع Console Application بسازید و نام آن را به Recipe1.Client تغییر دهید.
  • کلاس موجودیت Order را به پروژه اضافه کنید. همان کلاسی که در سرویس Web API ساختیم.

نکته: قسمت هایی از اپلیکیشن که باید در لایه‌های مختلف مورد استفاده قرار گیرند - مانند کلاس‌های موجودیت‌ها - بهتر است در لایه مجزایی قرار داده شده و به اشتراک گذاشته شوند. مثلا می‌توانید پروژه ای از نوع Class Library بسازید و تمام موجودیت‌ها را در آن تعریف کنید. سپس لایه‌های مختلف این پروژه را ارجاع خواهند کرد.

فایل program.cs را باز کنید و کد زیر را به آن اضافه نمایید.

private HttpClient _client;
private Order _order;

private static void Main()
{
    Task t = Run();
    t.Wait();
    
    Console.WriteLine("\nPress <enter> to continue...");
    Console.ReadLine();
}

private static async Task Run()
{
    // create instance of the program class
    var program = new Program();
    program.ServiceSetup();
    program.CreateOrder();
    // do not proceed until order is added
    await program.PostOrderAsync();
    program.ChangeOrder();
    // do not proceed until order is changed
    await program.PutOrderAsync();
    // do not proceed until order is removed
    await program.RemoveOrderAsync();
}

private void ServiceSetup()
{
    // map URL for Web API cal
    _client = new HttpClient { BaseAddress = new Uri("http://localhost:3237/") };
    // add Accept Header to request Web API content
    // negotiation to return resource in JSON format
    _client.DefaultRequestHeaders.Accept.
        Add(new MediaTypeWithQualityHeaderValue("application/json"));
}

private void CreateOrder()
{
    // Create new order
    _order = new Order { Product = "Camping Tent", Quantity = 3, Status = "Received" };
}

private async Task PostOrderAsync()
{
    // leverage Web API client side API to call service
    var response = await _client.PostAsJsonAsync("api/order", _order);
    Uri newOrderUri;
    
    if (response.IsSuccessStatusCode)
    {
        // Capture Uri of new resource
        newOrderUri = response.Headers.Location;
        // capture newly-created order returned from service,
        // which will now include the database-generated Id value
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully created order. Here is URL to new resource: {0}",  newOrderUri);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private void ChangeOrder()
{
    // update order
    _order.Quantity = 10;
}

private async Task PutOrderAsync()
{
    // construct call to generate HttpPut verb and dispatch
    // to corresponding Put method in the Web API Service
    var response = await _client.PutAsJsonAsync("api/order", _order);
    
    if (response.IsSuccessStatusCode)
    {
        // capture updated order returned from service, which will include new quanity
        _order = await response.Content.ReadAsAsync<Order>();
        Console.WriteLine("Successfully updated order: {0}", response.StatusCode);
    }
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

private async Task RemoveOrderAsync()
{
    // remove order
    var uri = "api/order/" + _order.OrderId;
    var response = await _client.DeleteAsync(uri);

    if (response.IsSuccessStatusCode)
        Console.WriteLine("Sucessfully deleted order: {0}", response.StatusCode);
    else
        Console.WriteLine("{0} ({1})", (int)response.StatusCode, response.ReasonPhrase);
}

اگر اپلیکیشن کلاینت را اجرا کنید باید با خروجی زیر مواجه شوید:

Successfully created order: http://localhost:3237/api/order/1054
Successfully updated order: OK
Sucessfully deleted order: OK

شرح مثال جاری

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

حال اپلیکیشن کنسول را باز کنید. روی خط اول کد program.cs یک breakpoint تعریف کرده و اپلیکیشن را اجرا کنید. ابتدا آدرس سرویس Web API را پیکربندی کرده و خاصیت Accept Header را مقدار دهی می‌کنیم. با این کار از سرویس مورد نظر درخواست می‌کنیم که داده‌ها را با فرمت JSON بازگرداند. سپس یک آبجکت Order می‌سازیم و با فراخوانی متد PostAsJsonAsync آن را به سرویس ارسال می‌کنیم. این متد روی آبجکت HttpClient تعریف شده است. اگر به اکشن متد Post در کنترلر Order یک breakpoint اضافه کنید، خواهید دید که این متد سفارش جدید را بعنوان یک پارامتر دریافت می‌کند و آن را به لیست موجودیت‌ها در Context جاری اضافه می‌نماید. این عمل باعث می‌شود که آبجکت جدید بعنوان Added علامت گذاری شود، در این مرحله Context جاری شروع به ردیابی تغییرات می‌کند. در آخر با فراخوانی متد SaveChanges داده‌ها را ذخیره می‌کنیم. در قدم بعدی کد وضعیت 201 (Created) و آدرس منبع جدید را در یک آبجکت HttpResponseMessage قرار می‌دهیم و به کلاینت ارسال می‌کنیم. هنگام استفاده از Web API باید اطمینان حاصل کنیم که کلاینت‌ها درخواست‌های ایجاد رکورد جدید را بصورت POST ارسال می‌کنند. درخواست‌های HTTP Post بصورت خودکار به اکشن متد متناظر نگاشت می‌شوند.

در مرحله بعد عملیات بعدی را اجرا می‌کنیم، تعداد سفارش را تغییر می‌دهیم و موجودیت جاری را با فراخوانی متد PutAsJsonAsync به سرویس Web API ارسال می‌کنیم. اگر به اکشن متد Put در کنترلر سرویس یک breakpoint اضافه کنید، خواهید دید که آبجکت سفارش بصورت یک پارامتر دریافت می‌شود. سپس با فراخوانی متد Entry و پاس دادن موجودیت جاری بعنوان رفرنس، خاصیت State را به Modified تغییر می‌دهیم، که این کار موجودیت را به Context جاری می‌چسباند. حال فراخوانی متد SaveChanges یک اسکریپت بروز رسانی تولید خواهد کرد. در مثال جاری تمام فیلدهای آبجکت Order را بروز رسانی می‌کنیم. در شماره‌های بعدی این سری از مقالات، خواهیم دید چگونه می‌توان تنها فیلدهایی را بروز رسانی کرد که تغییر کرده اند. در آخر عملیات را با بازگرداندن کد وضعیت 200 (OK) به اتمام می‌رسانیم.

در مرحله بعد، عملیات نهایی را اجرا می‌کنیم که موجودیت Order را از منبع داده حذف می‌کند. برای اینکار شناسه (Id) رکورد مورد نظر را به آدرس سرویس اضافه می‌کنیم و متد DeleteAsync را فراخوانی می‌کنیم. در سرویس Web API رکورد مورد نظر را از دیتابیس دریافت کرده و متد Remove را روی Context جاری فراخوانی می‌کنیم. این کار موجودیت مورد نظر را بعنوان Deleted علامت گذاری می‌کند. فراخوانی متد SaveChanges یک اسکریپت Delete تولید خواهد کرد که نهایتا منجر به حذف شدن رکورد می‌شود.

در یک اپلیکیشن واقعی بهتر است کد دسترسی داده‌ها از سرویس Web API تفکیک شود و در لایه مجزایی قرار گیرد.

مطالب
مهاجرت داده عضویت و پروفایل از Universal Providers به ASP.NET Identity
در این مقاله مهاجرت داده‌های سیستم عضویت، نقش‌ها و پروفایل‌های کاربران که توسط Universal Providers ساخته شده اند به مدل ASP.NET Identity را بررسی می‌کنیم. رویکردی که در این مقاله استفاده شده و قدم‌های لازمی که توضیح داده شده اند، برای اپلیکیشنی که با SQL Membership کار می‌کند هم می‌توانند کارساز باشند.

با انتشار Visual Studio 2013، تیم ASP.NET سیستم جدیدی با نام ASP.NET Identity معرفی کردند. می‌توانید  در این لینک  بیشتر درباره این انتشار بخوانید. 

در ادامه مقاله قبلی تحت عنوان  مهاجرت از SQL Membership به ASP.NET Identity ، در این پست به مهاجرت داده‌های یک اپلیکیشن که از مدل Providers برای مدیریت اطلاعات کاربران، نقش‌ها و پروفایل‌ها استفاده می‌کند به مدل جدید ASP.NET Identity می‌پردازیم. تمرکز این مقاله اساسا روی مهاجرت داده‌های پروفایل کاربران خواهد بود، تا بتوان به سرعت از آنها در اپلیکیشن استفاده کرد. مهاجرت داده‌های عضویت و نقش ها، شبیه پروسه مهاجرت SQL Membership است. رویکردی که در ادامه برای مهاجرت داده پروفایل‌ها دنبال شده است، می‌تواند برای اپلیکیشنی با SQL Membership نیز استفاده شود.

بعنوان یک مثال، با اپلیکیشن وبی شروع می‌کنیم که توسط Visual Studio 2012 ساخته شده و از مدل Providers استفاده می‌کند. پس از آن یک سری کد برای مدیریت پروفایل ها، ثبت نام کاربران، افزودن اطلاعات پروفایل به کاربران و مهاجرت الگوی دیتابیس می‌نویسیم و نهایتا اپلیکیشن را بروز رسانی می‌کنیم تا برای استفاده از سیستم Identity برای مدیریت کاربران و نقش‌ها آماده باشد. و بعنوان یک تست، کاربرانی که قبلا توسط Universal Providers ساخته شده اند باید بتوانند به سایت وارد شوند، و کاربران جدید هم باید قادر به ثبت نام در سایت باشند.

سورس کد کامل این مثال را می‌توانید از  این لینک  دریافت کنید.



خلاصه مهاجرت داده پروفایل ها

قبل از آنکه با مهاجرت‌ها شروع کنیم، بگذارید تا نگاهی به تجربه مان از ذخیره اطلاعات پروفایل‌ها در مدل Providers بیاندازیم. اطلاعات پروفایل کاربران یک اپلیکیشن به طرق مختلفی می‌تواند ذخیره شود. یکی از رایج‌ترین این راه ها، استفاده از تامین کننده‌های پیش فرضی است که بهمراه Universal Providers منتشر شدند. بدین منظور انجام مراحل زیر لازم است
  1. کلاس جدیدی بسازید که دارای خواصی برای ذخیره اطلاعات پروفایل است.
  2. کلاس جدیدی بسازید که از 'ProfileBase' ارث بری می‌کند و متدهای لازم برای دریافت پروفایل کاربران را پیاده سازی می‌کند.
  3. استفاده از تامین کننده‌های پیش فرض را، در فایل web.config فعال کنید. و کلاسی که در مرحله 2 ساختید را بعنوان کلاس پیش فرض برای خواندن اطلاعات پروفایل معرفی کنید.


اطلاعات پروفایل‌ها بصورت serialized xml و binary در جدول 'Profiles' ذخیره می‌شوند.


پس از آنکه به سیستم ASP.NET Identity مهاجرت کردیم، اطلاعات پروفایل  deserialized شده و در قالب خواص کلاس User ذخیره می‌شوند. هر خاصیت، بعدا می‌تواند به  یک ستون در دیتابیس متصل شود. مزیت بدست آمده این است که مستقیما از کلاس User به اطلاعات پروفایل دسترسی داریم. ناگفته نماند که دیگر داده‌ها serialize/deserialize هم نمی‌شوند.


شروع به کار

در Visual Studio 2012 پروژه جدیدی از نوع ASP.NET 4.5 Web Forms application بسازید. مثال جاری از یک قالب Web Forms استفاده می‌کند، اما می‌توانید از یک قالب MVC هم استفاده کنید.

پوشه جدیدی با نام 'Models' بسازید تا اطلاعات پروفایل را در آن قرار دهیم.

بعنوان یک مثال، بگذارید تا تاریخ تولد کاربر، شهر سکونت، قد و وزن او را در پروفایلش ذخیره کنیم. قد و وزن بصورت یک کلاس سفارشی (custom class) بنام 'PersonalStats' ذخیره می‌شوند. برای ذخیره و بازیابی پروفایل ها، به کلاسی احتیاج داریم که 'ProfileBase' را ارث بری می‌کند. پس کلاس جدیدی با نام 'AppProfile' بسازید.

public class ProfileInfo
{
    public ProfileInfo()
    {
        UserStats = new PersonalStats();
    }
    public DateTime? DateOfBirth { get; set; }
    public PersonalStats UserStats { get; set; }
    public string City { get; set; }
}

public class PersonalStats
{
    public int? Weight { get; set; }
    public int? Height { get; set; }
}

public class AppProfile : ProfileBase
{
    public ProfileInfo ProfileInfo
    {
        get { return (ProfileInfo)GetPropertyValue("ProfileInfo"); }
    }
    public static AppProfile GetProfile()
    {
        return (AppProfile)HttpContext.Current.Profile;
    }
    public static AppProfile GetProfile(string userName)
    {
        return (AppProfile)Create(userName);
    }
}

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

<profile defaultProvider="DefaultProfileProvider" enabled="true"
    inherits="UniversalProviders_ProfileMigrations.Models.AppProfile">
  <providers>
    .....
  </providers>
</profile>

برای دریافت اطلاعات پروفایل از کاربر، فرم وب جدیدی در پوشه Account بسازید و آنرا 'AddProfileData.aspx' نامگذاری کنید.

<h2> Add Profile Data for <%# User.Identity.Name %></h2>
<asp:Label Text="" ID="Result" runat="server" />
<div>
    Date of Birth:
    <asp:TextBox runat="server" ID="DateOfBirth"/>
</div>
<div>
    Weight:
    <asp:TextBox runat="server" ID="Weight"/>
</div>
<div>
    Height:
    <asp:TextBox runat="server" ID="Height"/>
</div>
<div>
    City:
    <asp:TextBox runat="server" ID="City"/>
</div>
<div>
    <asp:Button Text="Add Profile" ID="Add" OnClick="Add_Click" runat="server" />
</div>

کد زیر را هم به فایل code-behind اضافه کنید.

protected void Add_Click(object sender, EventArgs e)
{
    AppProfile profile = AppProfile.GetProfile(User.Identity.Name);
    profile.ProfileInfo.DateOfBirth = DateTime.Parse(DateOfBirth.Text);
    profile.ProfileInfo.UserStats.Weight = Int32.Parse(Weight.Text);
    profile.ProfileInfo.UserStats.Height = Int32.Parse(Height.Text);
    profile.ProfileInfo.City = City.Text;
    profile.Save();
}

دقت کنید که فضای نامی که کلاس AppProfile در آن قرار دارد را وارد کرده باشید.

اپلیکیشن را اجرا کنید و کاربر جدیدی با نام 'olduser' بسازید. به صفحه جدید 'AddProfileData' بروید و اطلاعات پروفایل کاربر را وارد کنید.

با استفاده از پنجره Server Explorer می‌توانید تایید کنید که اطلاعات پروفایل با فرمت xml در جدول 'Profiles' ذخیره می‌شوند.

مهاجرت الگوی دیتابیس

برای اینکه دیتابیس فعلی بتواند با سیستم ASP.NET Identity کار کند، باید الگوی ASP.NET Identity را بروز رسانی کنیم تا فیلدهای جدیدی که اضافه کردیم را هم در نظر بگیرد. این کار می‌تواند توسط اسکریپت‌های SQL انجام شود، باید جداول جدیدی بسازیم و اطلاعات موجود را به آنها انتقال دهیم. در پنجره 'Server Explorer' گره 'DefaultConnection' را باز کنید تا جداول لیست شوند. روی Tables کلیک راست کنید و 'New Query' را انتخاب کنید.

اسکریپت مورد نیاز را از آدرس https://raw.github.com/suhasj/UniversalProviders-Identity-Migrations/master/Migration.txt دریافت کرده و آن را اجرا کنید. اگر اتصال خود به دیتابیس را تازه کنید خواهید دید که جداول جدیدی اضافه شده اند. می‌توانید داده‌های این جداول را بررسی کنید تا ببینید چگونه اطلاعات منتقل شده اند.

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

پکیج‌های مورد نیاز برای ASP.NET Identity را نصب کنید:
  • Microsoft.AspNet.Identity.EntityFramework
  • Microsoft.AspNet.Identity.Owin
  • Microsoft.Owin.Host.SystemWeb
  • Microsoft.Owin.Security.Facebook
  • Microsoft.Owin.Security.Google
  • Microsoft.Owin.Security.MicrosoftAccount
  • Microsoft.Owin.Security.Twitter
اطلاعات بیشتری درباره مدیریت پکیج‌های NuGet از اینجا قابل دسترسی هستند.

برای اینکه بتوانیم از الگوی جاری دیتابیس استفاده کنیم، ابتدا باید مدل‌های لازم ASP.NET Identity را تعریف کنیم تا موجودیت‌های دیتابیس را Map کنیم. طبق قرارداد سیستم Identity کلاس‌های مدل یا باید اینترفیس‌های تعریف شده در Identity.Core dll را پیاده سازی کنند، یا می‌توانند پیاده سازی‌های پیش فرضی را که در Microsoft.AspNet.Identity.EntityFramework وجود دارند گسترش دهند. ما برای نقش ها، اطلاعات ورود کاربران و claim‌ها از پیاده سازی‌های پیش فرض استفاده خواهیم کرد. نیاز به استفاده از یک کلاس سفارشی User داریم. پوشه جدیدی در پروژه با نام 'IdentityModels' بسازید. کلاسی با نام 'User' در این پوشه بسازید و کد آن را با لیست زیر تطابق دهید.
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using UniversalProviders_ProfileMigrations.Models;

namespace UniversalProviders_Identity_Migrations
{
    public class User : IdentityUser
    {
        public User()
        {
            CreateDate = DateTime.UtcNow;
            IsApproved = false;
            LastLoginDate = DateTime.UtcNow;
            LastActivityDate = DateTime.UtcNow;
            LastPasswordChangedDate = DateTime.UtcNow;
            Profile = new ProfileInfo();
        }

        public System.Guid ApplicationId { get; set; }
        public bool IsAnonymous { get; set; }
        public System.DateTime? LastActivityDate { get; set; }
        public string Email { get; set; }
        public string PasswordQuestion { get; set; }
        public string PasswordAnswer { get; set; }
        public bool IsApproved { get; set; }
        public bool IsLockedOut { get; set; }
        public System.DateTime? CreateDate { get; set; }
        public System.DateTime? LastLoginDate { get; set; }
        public System.DateTime? LastPasswordChangedDate { get; set; }
        public System.DateTime? LastLockoutDate { get; set; }
        public int FailedPasswordAttemptCount { get; set; }
        public System.DateTime? FailedPasswordAttemptWindowStart { get; set; }
        public int FailedPasswordAnswerAttemptCount { get; set; }
        public System.DateTime? FailedPasswordAnswerAttemptWindowStart { get; set; }
        public string Comment { get; set; }
        public ProfileInfo Profile { get; set; }
    }
}
دقت کنید که 'ProfileInfo' حالا بعنوان یک خاصیت روی کلاس User تعریف شده است. بنابراین می‌توانیم مستقیما از کلاس کاربر با اطلاعات پروفایل کار کنیم.

محتویات پوشه‌های IdentityModels  و IdentityAccount  را از آدرس  https://github.com/suhasj/UniversalProviders-Identity-Migrations/tree/master/UniversalProviders-Identity-Migrations  دریافت و کپی کنید. این فایل‌ها مابقی مدل ها، و صفحاتی برای مدیریت کاربران و نقش‌ها در سیستم جدید ASP.NET Identity هستند.


انتقال داده پروفایل‌ها به جداول جدید

همانطور که گفته شد ابتدا باید داده‌های پروفایل را deserialize کرده و از فرمت xml خارج کنیم، سپس آنها را در ستون‌های جدول AspNetUsers ذخیره کنیم. ستون‌های جدید در مرحله قبل به دیتابیس اضافه شدند، پس تنها کاری که باقی مانده پر کردن این ستون‌ها با داده‌های ضروری است. بدین منظور ما از یک اپلیکیشن کنسول استفاده می‌کنیم که تنها یک بار اجرا خواهد شد، و ستون‌های جدید را با داده‌های لازم پر می‌کند.

در solution جاری یک پروژه اپلیکیشن کنسول بسازید.

آخرین نسخه پکیج Entity Framework را نصب کنید. همچنین یک رفرنس به اپلیکیشن وب پروژه بدهید (کلیک راست روی پروژه و گزینه 'Add Reference').

کد زیر را در کلاس Program.cs وارد کنید. این قطعه کد پروفایل تک تک کاربران را می‌خواند و در قالب 'ProfileInfo' آنها را serialize می‌کند و در دیتابیس ذخیره می‌کند.

public class Program
{
    var dbContext = new ApplicationDbContext();
    foreach (var profile in dbContext.Profiles)
    {
        var stringId = profile.UserId.ToString();
        var user = dbContext.Users.Where(x => x.Id == stringId).FirstOrDefault();
        Console.WriteLine("Adding Profile for user:" + user.UserName);
        var serializer = new XmlSerializer(typeof(ProfileInfo));
        var stringReader = new StringReader(profile.PropertyValueStrings);
        var profileData = serializer.Deserialize(stringReader) as ProfileInfo;
        if (profileData == null)
        {
            Console.WriteLine("Profile data deserialization error for user:" + user.UserName);
        }
        else
        {
            user.Profile = profileData;
        }
    }
    dbContext.SaveChanges();
}

برخی از مدل‌های استفاده شده در پوشه 'IdentityModels' تعریف شده اند که در پروژه اپلیکیشن وبمان قرار دارند، بنابراین افزودن فضاهای نام مورد نیاز فراموش نشود.


کد بالا روی دیتابیسی که در پوشه App_Data وجود دارد کار می‌کند، این دیتابیس در مراحل قبلی در اپلیکیشن وب پروژه ایجاد شد. برای اینکه این دیتابیس را رفرنس کنیم باید رشته اتصال فایل app.config اپلیکیشن کنسول را بروز رسانی کنید. از همان رشته اتصال web.config در اپلیکیشن وب پروژه استفاده کنید. همچنین آدرس فیزیکی کامل را در خاصیت 'AttachDbFilename' وارد کنید.


یک Command Prompt باز کنید و به پوشه bin اپلیکیشن کنسول بالا بروید. فایل اجرایی را اجرا کنید و نتیجه را مانند تصویر زیر بررسی کنید.

در پنجره Server Explorer جدول 'AspNetUsers' را باز کنید. حال ستون‌های این جدول باید خواص کلاس مدل را منعکس کنند.


کارایی سیستم را تایید کنید

با استفاده از صفحات جدیدی که برای کار با ASP.NET Identity پیاده سازی شده اند سیستم را تست کنید. با کاربران قدیمی که در دیتابیس قبلی وجود دارند وارد شوید. کاربران باید با همان اطلاعات پیشین بتوانند وارد سیستم شوند. مابقی قابلیت‌ها را هم بررسی کنید. مثلا افزودن OAuth، ثبت کاربر جدید، تغییر کلمه عبور، افزودن نقش ها، تخصیص کاربران به نقش‌ها و غیره.

داده‌های پروفایل کاربران قدیمی و جدید همگی باید در جدول کاربران ذخیره شده و بازیابی شوند. جدول قبلی دیگر نباید رفرنس شود.
مطالب
پیاده سازی یک تامین کننده 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 کار نسبتا ساده ای است.