مطالب
پیاده سازی سیاست‌های دسترسی پویای سمت سرور و کلاینت در برنامه‌های 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 ساخته شده و مقدار دهی اولیه می‌شود. لیست کاربران پیش‌فرض آن‌را در اینجا می‌توانید مشاهده کنید. ابتدا با کاربر ادمین وارد شده و سطوح دسترسی سایر کاربران را تغییر دهید. سپس بجای آن‌ها وارد سیستم شده و تغییرات منوها و سطوح دسترسی پویا را بررسی کنید.
مطالب
روش‌هایی برای بهبود تجربه‌ی کاربری صفحات لاگین و ثبت نام
عموما زمانیکه به طراحی صفحه‌ی لاگین و یا ثبت نام می‌رسیم، ورودی کلمه‌ی عبور را با "type="password علامتگذاری می‌کنیم و ... همین! فارغ از اینکه در سال‌های اخیر، مرورگرها چه امکانات قابل توجهی را در جهت غنی سازی همین یک ورودی ویژه، تدارک دیده‌اند تا کار ثبت نام و یا ورود به یک سایت و برنامه را ساده‌تر و امن‌تر کنند.


کمک به مرورگر، در جهت تمایز بین صفحات ورود و ثبت نام

مرورگرهای جدید قادرند برای صفحه‌ی لاگین، پر کردن خودکار فیلدهای نام کاربری و کلمه‌ی عبور و برای صفحه‌ی ثبت نام، تولید خودکار کلمات عبور قوی، به همراه به‌خاطر سپاری آن‌را ارائه دهند. برای فعال سازی یک چنین قابلیت‌های ویژه‌ای، نیاز است تا یک سری ویژگی را به فیلدهای ورود کلمات عبور اضافه کرد:
الف) نیاز است autocomplete را به نحو صحیحی مقدار دهی کرد:
- اگر مقدار آن مساوی new-password درنظر گرفته شود، یعنی صفحه‌ی جاری، صفحه‌ی ثبت نام است و نیاز است تا تولید کننده‌ی خودکار کلمات عبور مرورگر و بخاطر سپاری آن فعال شوند.


- اگر مقدار آن مساوی current-password درنظر گرفته شود، یعنی صفحه‌ی جاری، صفحه‌ی لاگین است و کاربر نیاز دارد تا کلمه‌ی عبوری را که پیشتر در حین ثبت نام و یا حتی لاگین موفق قبلی وارد کرده‌است، بتواند به سادگی از طریق password manager مرورگر دریافت کند.


ب) بنابراین تا اینجا روش تمایز بین فیلدهای پسورد صفحات لاگین و ثبت نام مشخص شد. اما در مورد نام کاربری چطور؟
برای این حالت فقط کافی است مقدار autocomplete را به username تنظیم کرد تا مرورگر بداند که چگونه باید اطلاعات password manager خودش را به صورت خودکار تامین کند. البته "autocomplete="username در اصل برای معرفی ایمیل‌ها طراحی شده‌است، اما در استفاده‌های برنامه‌های کاربردی ما، جهت معرفی مقدار نام کاربری که کاربر قرار است توسط آن به برنامه وارد شود، کفایت می‌کند.


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

عنوان شد که اگر مقدار autocomplete در صفحات ثبت نام، به مقدار new-password تنظیم شود، یک چنین فیلد ورودی به صورت خودکار به همراه قابلیت ویژه‌ی «تولید کننده‌ی خودکار کلمات عبور مرورگر» نیز خواهد بود. می‌توان این ابزار توکار مرورگرها را نیز سفارشی سازی کرد و بر روی نحوه‌ی تولید کلمات عبور آن تاثیر گذاشت:
<input type="password" autocomplete="new-password" 
  passwordrules="required: upper; required: lower; required: digit; 
                 minlength: 25; allowed: [-().&@?'#,/&quot;+]; max-consecutive: 2">
برای اینکار از ویژگی passwordrules استفاده می‌شود. برای مثال تنظیمات فوق به این معنا هستند:
- کلمه‌ی عبور تولیدی توسط مرورگر باید حداقل به همراه یک عدد، یک حرف بزرگ و یک حرف کوچک باشد.
- حداقل 25 حرف باشد.
- استفاده‌ی از حروف ویژه‌ی -().&@?'#,/"+ در آن مجاز است.
- حداکثر تعداد حروف مشابه متوالی آن باید 2 حرف باشد.


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

مرورگرها می‌توانند برای مثال حروف ابتدای عبارت وارد شده را به صورت خودکار تبدیل به کلمات بزرگ کنند و یا حتی با استفاده از واژه نامه‌ها، اطلاعات ورودی را تصحیح کنند. یک چنین قابلیتی نباید برای فلیدهای کلمات عبور فعال باشد. به همین جهت ضروری است این فلیدها با ویژگی‌های "autocapitalize="off و "autocorrect="off مزین شوند.


کمک به مرورگرها در جهت تعویض کلمات عبور ضعیف و فاش شده‌!

برای نمونه مرورگر کروم به همراه قسمتی است که بر اساس password manager توکار خودش و اطلاعات فاش شده‌ی بر روی اینترنت، عنوان می‌کند که کدامیک از کلمات عبور شما دیگر امن نیستند و بهتر است آن‌ها را عوض کنید. این صفحه را می‌توانید در آدرس ویژه‌ی chrome://settings/passwords مرورگر کروم مشاهده کنید.


نکته‌ی مهم اینجا است که این مرورگر، دکمه‌ی Change password و لینکی را نیز به سایت و برنامه‌ی مدنظر، برای تعویض کلمه‌ی عبور ارائه می‌دهد و این لینک، قالب ثابت و ویژه‌ای دارد: https://example.com/.well-known/change-password
یعنی هموار کاربر را به آدرس ثابت well-known/change-password. در سایت شما هدایت می‌کند و در این حالت بهتر است یک route ویژه‌ی خاص آن‌را تعریف کرده و کاربر را به صورت خودکار به صفحه‌ی اصلی تعویض کلمه‌ی عبور، برای مثال به آدرس فرضی https://example.com/settings/change-password به صورت خودکار هدایت کنید.

یک نمونه مثال این هدایت خودکار در یک برنامه‌ی ASP.NET Core به صورت زیر است:
app.UseEndpoints(endpoints =>
{
  // TODO: Add it to your app!
  // Improves chrome://settings/passwords page by managing `Change password` button next to a password.
  endpoints.MapGet("/.well-known/change-password",
                   context =>
                   {
                     // `/.well-known/change-password` address will be called by the `Change password` button of the Chrome.
                     // Now our Web-API app redirects the user to the `/change-password` address of the App.
                      context.Response.Redirect("/change-password", true);
                      return Task.CompletedTask;
                    });
  endpoints.MapRazorPages();
  // ...
});
مطالب
آشنایی با Virtual Directory
نمیدانم آیا تا به حال برای شما پیش آمده است که بخواهید اطلاعاتتان را در جایی غیر از زیرشاخه‌های wwwroot ذخیره نمایید یا خیر؟
یادم هست برای یکی از مشتریانم یک سرور خریده بودیم که دو پارتیشن داشت و آن موقع به ذهنم خطور کرد که اگه بخواهم مثلا فایل‌های سیستم مدیریتی را داخل یک پارتیشن دیگر قرار بدهم چگونه انجام می‌شود؛ چطوری میتوانم به مکانی غیر از شاخه‌ی wwwroot، عمل mappath را انجام بدم؟ چگونه میتوانم یک لینک مستقیم، به مکانی دیگر داشته باشم؟
جواب تمام این سوالات در امکانی از IIS به اسم virtual Directory بود. در این روش ما یک مکان فیزیکی را به iis معرفی می‌کنیم و به آن یک نام مجازی میدهیم که از آن در برنامه استفاده خواهیم کرد.
در iis، در بخش sites، همه سایت‌های ایجاد شده بر روی IIS لیست می‌شوند. یک context menu بر روی سایت مورد نظر خود باز کنید و گزینه‌ی add virtual directory انتخاب کنید. یک نام مجازی به آن بدهید و مکان مورد نظر را انتخاب کنید و کادر را تایید کنید.

برای ویرایش آن یعنی تغییر مسیر فیزیکی هم میتوان از طریق مسیر زیر عمل کرد

Right Click On virtual Directory>Manage Virtual Directory >Advanced Settings

از این پس در IIS دسترسی به پوشه، از طریق ~/media میسر هست؛ ولی بسیاری از ما برای تست، برنامه را از طریق IIS Express اجرا و تست می‌کنیم. پس بهتر هست این گزینه در آنجا هم مورد بررسی قرار بگیرد.

برای دسترسی به کانفیگ iis express عموما مسیرهای زیر معتبر هستند:

%userprofile%\documents\iisexpress\config\applicationhost.config
%userprofile%\my documents\iisexpress\config\applicationhost.config

فایل applicationhost.config  فایلی است که قرار است تغییر بدهیم.

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

یک context menu از طریق این آیکن باز کرده و گزینه‌ی show all applications را انتخاب کنید. لیست تمامی سایت‌های در حال اجرا نمایش داده می‌شود. یکی را انتخاب کنید تا در زیر اطلاعات نمایش یابد. در قسمت کانفیگ، آدرس فایل کانفیگ داده شده است و مسیر نیز مشخص است. با کلیک بر روی آن فایل applicationhost.config باز می‌شود.

فایل مورد نظر را باز کنید. این فایل را می‌توان با نوت پد یا یک xml editor گشود. در آن یک تگ sites وجود دارد که تمامی پروژه‌های تحت وبی را که تا الان دارید، درون خودش ذخیره کرده‌است. به ازای هر سایت یک تگ site هست و خصوصیات هر کدام، داخل این تگ قرار گرفته‌است. اگر دقت کنید هر پروژه شما یا همان تگ site، شامل تگ زیر می‌باشد:

<application path="/" applicationPool="Clr4IntegratedAppPool">
                    <virtualDirectory path="/" physicalPath="E:\website\oscar\panel\Oscar_Manager\Oscar_Manager" />
                </application>

در اینجا خود IIS Express اقدام به ساخت یک دایرکتوری مجازی که همان مسیر ذخیره پروژه هست کرده. برای معرفی دایرکتوری مجازی جدید، یک کپی از تگ application را ایجاد کنید.

برای مثال من قصد دارم یک دایرکتوری مجازی به اسم media بسازم تا تصاویر و دیگر فایل‌های چندرسانه ای را در آن ذخیره کنم و محل فیزیکی آن نیز D:\testvd می‌باشد. پس تگ کپی شده را به نحو زیر تغییر می‌دهم:

 <application path="/media" applicationPool="Clr4IntegratedAppPool">
                    <virtualDirectory path="/" physicalPath="d:\testvd" />
                </application>

بنابراین در کل، تگ site من به شکل زیر تغییر پیدا می‌کند:

<site name="Oscar_Manager" id="23">
                <application path="/" applicationPool="Clr4IntegratedAppPool">
                    <virtualDirectory path="/" physicalPath="E:\website\oscar\panel\Oscar_Manager\Oscar_Manager" />
                </application>
 <application path="/media" applicationPool="Clr4IntegratedAppPool">
                    <virtualDirectory path="/" physicalPath="d:\testvd" />
                </application>
                <bindings>
                    <binding protocol="http" bindingInformation="*:1844:localhost" />
                </bindings>
            </site>

الان مسیر مجازی ما ساخته شده است. برای تست صحت این کار، یک فایل تصویری را در آن قرار می‌دهم و در کنترلر مورد نظر، این کد را می‌نویسم تا یک تصویر به سمت کلاینت در یک virtual directory ارسال شود.

var dir = Server.MapPath("~/media");
            var path = System.IO.Path.Combine(dir, "1.jpg");
            return base.File(path, "image/jpeg");

کنترلر را صدا زده تا نتیجه کار را ببنید.

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

نکته:البته بنده این نکته را تایید نمی‌کنم ولی شنیده‌ام که در نسخه‌های بالاتر ویژوال استادیو با راست کلیک بر روی نام پروژه، گزینه Use IIS Express وجود دارد که به یک محیط گرافیکی ختم می‌شود و از آنجا که بنده نسخه 2012 را دارم این مورد را تست نکردم.


ایجاد virtual Directory از طریق Appcmd

دسترسی به appcmd از طریق مسیر زیر امکان پذیر است:

%systemroot%\system32\inetsrv\AppCmd.exe

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

یکی از این دستورات، برای ساخت Virtual Directory استفاده می‌شود:

APPCMD add vdir /app.name:"default we site/" /path:/vdir1 /physicalPath:d:\testvd

سوییچ /app برای نام وب سایت یا پروژه است و حتما در انتهای نام، علامت / قرار گیرد. سوییچ بعدی هم که /path هست برای دادن مسیر مجازی و سوییچ آخری هم آدرس محل فزیکی است. بعد از اجرای دستور، پیام زیر نمایش داده می‌شود:

VDIR object "Default Web Site/vdir1" added

پنجره commandprompt باید به صورت Run a Administrator باز شده باشد.

برای تغییر محل فیزیکی یک virtual directory میتوان از دستور زیر استفاده کرد:

appcmd.exe set vdir "Default Web Site/vdir1/" -physicalPath:"D:\Files"

از این پس مسیر فیزیکی این آدرس مجازی مسیر D:\Files خواهد بود.

البته مباحث پیشرفته‌تری چون application pool‌ها نیز وجود دارد که از حوصله این مقاله خارج هست و در وقت و مقاله دیگر بررسی خواهیم کرد.

برای ارسال دستور به IIS Express هم از طریق مسیر زیر appcmd را باز کنید:

%ProgramFiles%\IIS Express\appcmd.exe

امکان ایجاد virtual directory عموما در سرورهای مجازی و اختصاصی در پنل مربوطه نیز وجود دارد.

ساخت virtual Directory در وب سایت پنل 

ساحت virtual Directory در پلسک 

مطالب
ضمیمه کردن فایل در RavenDb
یکی از مواردی که در بانک‌های اطلاعاتی امروزه بیشتر مورد استفاده قرار میگیرد، ذخیره فایل‌ها در خود دیتابیس، بجای ذخیره نام یا آدرس آن‌ها بر روی دیسک سخت است. از همان ابتدا که Raven به بازار عرضه شد، امکان ذخیره فایل‌های باینری را با استفاده از افزونه هایی که به همراه داشت برای برنامه نویسان مهیا ساخت.  این امکان از طریق کد زیر برای ذخیره یک فایل کفایت میکرد:
using (var store = new DocumentStore
            {
                Url = "http://localhost:8080"
            }.Initialize())
            {
                using (var session = store.OpenSession())
                {
                    store.DatabaseCommands.PutAttachment(key: "file/1",
                                                         etag: null,
                                                         data: System.IO.File.OpenRead(@"D:\Prog\packages.config"),
                                                         metadata: new RavenJObject
                                                         {
                                                            { "Description", "توضیحات فایل" }
                                                         });
                    var question = new Question
                    {
                        By = "users/Vahid",
                        Title = "Raven Intro",
                        Content = "Test....",
                        FileId = "file/1"
                    };
                    session.Store(question);

                    session.SaveChanges();
                }
            }
ولی اگر از نسخه سه بعد RavenDb را استفاده کنید، می‌بینید که متد PutAttachment و دیگر خانواده این متد با ویژگی Obsolete (منسوخ شده) مزین شده‌اند و توصیه شده‌است از این پس از قابلیت جدیدی به نام RavenFs استفاده شود.
RavenFS یک فایل سیستم مجازی توزیع شده‌است و برای فایل‌های بزرگ چند گیگابایتی به طور بهینه‌ای طراحی گردیده‌است تا کارآیی بانک اطلاعاتی را بالا ببرد و وجود فایل‌های تکراری، از بین برود. این سیستم جدید شامل سیستم پیش فرض ایندکس گذاری میباشد و به شما این اجازه را میدهد تا بر روی متادیتاهای یک فایل از قبیل حجم، تاریخ آخرین نگارش و حتی متادیتاهای اختصاصی که شما در حین ذخیره سازی به آن اضافه می‌کنید، به جست‌وجو بپردازد. این سیستم جدید همچنین این امکان را به شما میدهد تا این اطلاعات را بین Node‌ها، با کمترین میزان انتقالات جابجا کنید و دسترسی سریعتری را بین نودهای مختلف داشته باشید.

برای ذخیره سازی یک فایل ابتدا باید یک FileStore را همانند آنچه که برای DocumentStore داشتید تعریف کنید. Url که شامل همان رشته اتصالی بوده و DefaultFileSystem هم  همانند DefaultDatabase که نام دیتابیس در آن ذکر میشد، در اینجا نام فایل سیستم ذکر می‌گردد:
 var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

بعد از آن باید از طریق Store جدید یک سشن ایجاد شود و فایل مورد نظر را در قالب یک استریم بخوانیم:
 var session = fileStore.OpenAsyncSession();
 var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv");  

توجه داشته باشید که برای کار با فایل سیستم، همه متدهای session به صورت غیرهمزمان بوده و متد همزمانی وجود ندارد. سپس در مرحله بعد میخواهم متادیتاهای شخصی نیز به آن اضافه کنیم:
  var metadata = new RavenJObject
            {
                {"User", "users/1345"},
              {"Director","Francis Ford Coppola" }, 
                {"Year","1979" }
            };

با استفاده از شیء RavenObject میتوانیم در قالب کلید و مقدار، مقادیر خود را ذخیره کنیم و بعد از آن همه موارد بالا که شامل فایل هدر، استریم و متادیتای اختصاصی است را رجیستر کنیم. اگر هم چندین فایل داریم میتوانید آن‌ها را هم در همینجا رجیستر کنید:
session.RegisterUpload("mkv/sample.mkv", stream, metadata);

در مرحله بعدی تغییرات را تایید و عملیات آپلود آغاز میگردد:
await session.SaveChangesAsync();

همانطور که می‌بینید تمامی متدهای کاربردی این سشن به طور غیرهمزمان طراحی شده‌اند.
کلیه عملیاتی که در بالا انجام شد:
    var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
             var stream = File.OpenRead("D:\\Apocalypse.Now.Redux.1979.BDRip.YIFY.mkv"); 
            var metadata = new RavenJObject
            {
                {"User", "users/1345"},
               {"Director","Francis Ford Coppola" }, 
                {"Year","1979" }
            };

            session.RegisterUpload("Mkv/sample.mkv", stream, metadata);
            await session.SaveChangesAsync();

حالا اگر به نسخه سرور ravenDb مراجعه کنید می‌بینید که فایل طبق فایل هدر داده شده قرار گرفته است و اطلاعات مربوط به آن ذخیره شده است:



{
    "User": "users/1345",
    "Country": "Iran",
    "City": "Kashan",
    "Raven-Synchronization-History": [
        {
            "Version": 4,
            "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
        },
        {
            "Version": 5,
            "ServerId": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
        }
    ],
    "Raven-Synchronization-Version": "6",
    "Raven-Synchronization-Source": "42d0cccb-103d-4bf0-9f3d-6f635b1c8ba4"
}

برای خواندن هم به شیوه زیر عمل میکنیم:
 از طریق Store ایجاد شده، یک سشن جدید را باز می‌کنیم و فایل مورد نظر را از طریق یکی از متادیتاهای تعریف شده بازیابی میکنیم:
var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
            var file = await session.Query()
                   .WhereEquals("Year", "1979")
                   .FirstOrDefaultAsync();
سپس با آدرس دهی فایل هدر، فایل باینری را داخل استریم قرار می‌دهیم:
var stream = await session.DownloadAsync("mkv/"+file.Name);

سپس استریم را روی دیسک سخت دخیره یا به هر مکانی که مد نظر است ارسال می‌کنیم:
   var fs = File.Create("D:\\file2.mkv");
            stream.CopyTo(fs);
            fs.Flush();
            fs.Close();
البته از آنجائیکه عملیات بازیابی توسط بانک اطلاعاتی به صورت غیرهمزمان انجام می‌گیرد، بهتر هست که باقی عملیات هم به صورت غیرهمزمان انجام شود:
await stream.CopyToAsync(fs);

سپس کل کد بازیابی را به شکل زیر می‌نویسیم:
            var fileStore = new FilesStore()
            {
                Url = "http://localhost:8080",
                DefaultFileSystem = "SampleFs"
            };
            fileStore.Initialize();

            var session = fileStore.OpenAsyncSession();
            var file = await session.Query()
                   .WhereEquals("Year", "1979")
                   .FirstOrDefaultAsync();

            var stream = await session.DownloadAsync("mkv/"+file.Name);

            var fs = File.Create("D:\\file2.mkv");

            await stream.CopyToAsync(fs);


مطالب
آزمایش Web APIs توسط Postman - قسمت چهارم - نوشتن آزمون‌ها و اسکریپت‌ها
اسکریپت و آزمون نویسی مقدماتی با Postman را در قسمت دوم بررسی کردیم. در این قسمت نکات و امکانات پیشرفته‌تری را در این مورد مرور خواهیم کرد.


بررسی اجزای اولین آزمایش با Postman

آزمون‌های Postman، عموما یک سری Assertion هستند؛ به این معنا که در آن‌ها، مقداری از Response دریافتی از سرور را با مقداری مشخص و مورد انتظار، مقایسه می‌کنیم:
pm.test("Status code is 200", function () {
    pm.response.to.have.status(200);
});
برای مثال اگر اجرای درخواستی موفقیت آمیز باشد، در بسیاری از موارد status code مساوی 200 از سمت سرور بازگشت داده می‌شود. بنابراین در آزمونی، مقدار status code دریافتی را با مقدار مورد انتظار 200، مقایسه می‌کنیم. در کدهای فوق، سطر pm.response‌، یک Assertion نامیده می‌شود.

چند نکته:
- آزمون‌های Postman، با متد pm.test شروع می‌شوند. هدف از آن‌، نوشتن بدنه‌ی یک آزمون متشکل از چندین Assertion است. اولین پارامتر آن، نام رشته‌ای آزمون است. پارامتر دوم آن، یک callback function است که پس از پایان درخواست جاری، اجرا می‌شود.
- روش اجرای آزمون‌ها در اینجا non-blocking است. یعنی آزمون‌های نوشته شده، به موازات هم اجرا شده و نتایج و حتی خطاهای آن‌ها بر روی یکدیگر تاثیرگذار نیستند.
- برای یک Response و اجزای مختلف آن، می‌توان چندین آزمون را نوشت و هر آزمون می‌تواند چندین Assertion را داشته باشد.
- در Postman، آزمون‌ها تنها پس از پایان اجرای درخواست‌ها، اجرا می‌شوند.
- به شیء pm.response در اینجا، response assertion API می‌گویند. توسط آن می‌توان به اجزای مختلف response مانند status code، هدرها و یا بدنه‌ی بازگشتی از سمت سرور، دسترسی یافت و برای آن‌ها یک Assertion را نوشت.


لیستی از بررسی‌های متداول، در حین نوشتن آزمون‌های Postman

تا اینجا روش بررسی status code را در حین نوشتن آزمون‌های Postman بررسی کردیم. در جدول زیر، مهم‌ترین حالاتی را که جهت بررسی خروجی یک API می‌توان مدنظر داشت، برشمرده شده‌اند:
  نوع بررسی    Response assertions 
  بررسی مقدار status code دریافتی از سرور با مقدار مورد انتظار
pm.response.to.have.status(200);
  آیا status code دریافتی، معادل یکی از مقادیر مشخص شده‌است؟ 
pm.expect(pm.response.code).to.be.oneOf([201,202]);
  آیا status name دریافتی از سرور، معادل عبارت مشخصی است؟ 
response.to.have.status("Created");
  مقایسه‌ی مقدار responseTime با مقدار مورد انتظار 
pm.expect(pm.response.responseTime).to.be.below(200);
  آیا هدر تنظیم شده‌ی توسط response، دارای کلید مورد انتظار است؟ 
pm.response.to.have.header("X-Cache");
  آیا هدر تنظیم شده‌ی توسط response، دارای کلید و مقدار مشخصی است؟ 
pm.response.to.have.header("X-Cache", "HIT");
  آیا نام یکی از کوکی‌های تنظیم شده‌ی توسط response، معادل مقدار مورد انتظار است؟ 
pm.expect(pm.cookies.has("sessionId")).to.be.true;
  آیا نام و مقدار یکی از کوکی‌های تنظیم شده‌ی توسط response، معادل مقادیر مشخصی هستند؟ 
pm.expect(pm.cookies.get("sessionId")).to.equal("abcb9s");
  آیا بدنه‌ی response، دقیقا معادل مقدار مشخص است؟ 
pm.response.to.have.body("OK");
آیا بدنه‌ی response، حاوی مقدار مشخصی است؟ 
pm.expect(pm.response.text()).to.include("Order placed.");

یک نکته: در رابط کاربری Postman، زمانیکه برگه‌ی Tests را انتخاب می‌کنیم، کنار آن لیستی از code snippets نیز قرار دارند که با کلیک بر روی آن‌ها، می‌توان حالت عمومی اکثر موارد فوق را به صورت خودکار تولید کرد:



روش بررسی اجزای خروجی با فرمت JSON از سرور

فرض کنید API شما یک چنین خروجی JSON ای را بازگشت می‌دهد:
{
  "id": 12,
  "name": "DNT",
  "isDeleted": false,
  "prefs": {
     "comments": "members",
     "voting": "disabled"
  }
}
برای نوشتن آزمون‌های Postman مخصوص به آن، می‌توان به صورت زیر عمل کرد:
pm.test("Your test name", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.name).to.eql("DNT");
    pm.expect(jsonData.prefs.voting).to.eql("disabled");
    pm.expect(jsonData.isDeleted).to.eql(false);
});
متد ()pm.response.json، بدنه‌ی response را تبدیل به یک شیء جاوا اسکریپتی می‌کند که پس از آن از طریق دسترسی به خواص و یا کلیدهای آن، می‌توان مقادیر هر یک را بررسی کرد. برای مثال jsonData.name به مقدار خاصیت name اشاره می‌کند و یا jsonData.prefs.voting، روش دسترسی به خاصیت یک شیء تو در تو را بیان کرده‌است.


ساماندهی بهتر آزمون‌های نوشته شده

البته می‌توان تمام Response assertions مدنظر را داخل یک callback function نیز قرار داد، اما بهتر است هر کدام را و یا گروهی از آن‌ها را که به هم مرتبط هستند، توسط یک pm.test جدید تعریف کرد تا بتوان به ساماندهی بهتر رسید و همچنین زمانیکه این آزمون‌ها بررسی می‌شوند، گزارش بهتری را نیز مشاهده نمود. به همین جهت برای نمونه می‌توان آزمایش فوق را به دو آزمایش مجزا تبدیل کرد که در یکی ایجاد مطلب جدید و در دیگری، ویژگی‌های آن مطلب بررسی شده‌اند:
pm.test("Post should be created", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.name).to.eql("DNT");
    pm.expect(jsonData.isDeleted).to.eql(false);
});

pm.test("Post's voting feature should be disabled", function () {
    var jsonData = pm.response.json();
    pm.expect(jsonData.prefs.voting).to.eql("disabled");
});

ساده سازی و همچنین بهبود کارآیی آزمون‌های نوشته شده

چون در اینجا چندین‌بار از ()var jsonData = pm.response.json داخل هر آزمایش استفاده شده‌است و در عمل یک شیء response نیز بیشتر وجود ندارد، می‌توان جهت کاهش این تکرار و بهبود کارآیی آزمون‌های نوشته شده، آن‌را به صورت یک ثابت، به پیش از تمام آزمایش‌ها منتقل کرد:
const jsonData = pm.response.json();

pm.test("Post should be created", function () {
    pm.expect(jsonData.name).to.eql("DNT");
    pm.expect(jsonData.isDeleted).to.eql(false);
});

pm.test("Post's voting feature should be disabled", function () {
    pm.expect(jsonData.prefs.voting).to.eql("disabled");
});
مطالب
ارتقاء به Angular 6: بررسی تغییرات Angular CLI
اولین مرحله‌ی ارتقاء به Angular 6، به روز رسانی Angular CLI 1.x به نگارش 6 آن است. این شماره نگارش نیز با شماره نگارش Angular یکی شده‌است و دیگر 1x نیست. CLI 6.0 فقط پروژه‌های Angular 5.x و 6x را پشتیبانی می‌کند و برای نصب آن نیاز به حداقل NodeJS 8.9 و NPM 5.5 را خواهید داشت. بنابراین ابتدا دستورات زیر را صادر کرده و اگر هنوز از نگارش‌های قدیمی این ابزارها استفاده می‌کنید، قبل از هر کاری باید آن‌ها را به روز رسانی کنید:
node -v
npm -v
این نگارش از CLI در پشت صحنه از Webpack 4 استفاده می‌کند که نسبت به نگارش‌های پیشین آن بسیار سریعتر است، tree-shaking بهتری را انجام می‌دهد و در نهایت سبب تولید برنامه‌هایی با حجم کمتر و با سرعت build بیشتری خواهد شد.


فایل پیشین angular-cli.json حذف و فایل جدید angular.json بجای آن معرفی شده‌است

یکی از مهم‌ترین تغییرات CLI 6.0 نسبت به نگارش‌های قبلی آن، پشتیبانی از چندین پروژه است و به همین منظور ساختار فایل تنظیمات آن‌را به طور کامل تغییر داده‌اند و اگر دستور ng new project1 را صادر کنید، دیگر از فایل پیشین angular-cli.json خبری نبوده و بجای آن فایل جدید angular.json قابل مشاهده‌است:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "project1": {
در اینجا ساختار ابتدای فایل angular.json را مشاهده می‌کنید که در آن مفهوم projects (و یا workspace در اینجا) به همراه project1 جدیدی که ایجاد کردیم، به عنوان زیر مجموعه‌ی آن قابل مشاهده‌است.
مزیت مهم این قابلیت، امکان ایجاد libraries است که به صورت توکار از این نگارش پشتیبانی می‌شود و می‌توان اجزایی مانند components ،directives ،pipes و services اشتراکی را در یک یا چندین کتابخانه قرار داد و سپس از آن‌ها در پروژه‌ی اصلی و یا چندین پروژه‌ی متصل استفاده کرد.
نگارش 6 در پشت صحنه، پروژه‌ی موفق ng-packagr را به مجموعه‌ی CLI اضافه کرده‌است و از این پس توسط خود CLI می‌توان کتابخانه‌های استاندارد Angular را تولید کرد. این مورد، مزیت استاندارد سازی کتابخانه‌های npm حاصل را نیز به همراه دارد؛ مشکلی که گاهی از اوقات به علت عدم رعایت این ساختار، با بسته‌های فعلی npm مخصوص Angular وجود دارند. برای مثال بدون استفاده‌ی از این ابزار، نیاز است مستندات 13 صفحه‌ای ساخت کتابخانه‌های Angular را سطر به سطر پیاده سازی کنید که توسط CLI 6.0، به صورت خودکار ایجاد و مدیریت می‌شود.

بنابراین اکنون سؤال مهمی که مطرح می‌شود این است: آیا باید فایل angular-cli.json پیشین را به صورت دستی به این فایل جدید به روز رسانی کرد و چگونه؟


به روز رسانی تمام بسته‌های سراسری سیستم

در ادامه پیش از هر کاری نیاز است تمام بسته‌های سراسری npm ایی را که هر از چندگاهی به سیستم خود اضافه کرده‌اید، به روز رسانی کنید. برای مشاهده‌ی لیست موارد تاریخ مصرف گذشته‌ی آن‌ها، دستور زیر را صادر کنید:
 npm outdated -g --depth=0
و برای به روز رسانی یکجای آن‌ها نیاز است دستور زیر اجرا شود:
 npm update -g


به روز رسانی خودکار ساختار فایل angular-cli.json

این به روز رسانی توسط CLI 6.0 به صورت خودکار پشتیبانی می‌شود و شامل این مراحل است:
ابتدا نیاز است بسته‌ی سراسری آن‌را به روز رسانی کرد:
 npm i -g @angular/cli
سپس وارد پوشه‌ی اصلی پروژه‌ی خود شده و این دستور را به صورت محلی نیز وارد کنید:
 npm install --save-dev @angular/cli@latest
با اینکار بسته‌ی CLI محلی پروژه به روز شده و اکنون می‌توانیم از قابلیت جدید آن که ng update نام دارد، استفاده کنیم. برای این منظور دستورات ذیل را به ترتیب اجرا کنید:
ng update @angular/cli
ng update @angular/core
ng update rxjs
دستور اول کار تبدیل خودکار فایل angular-cli.json قدیمی را به ساختار جدید آن انجام میدهد؛ با این لاگ:
DELETE .angular-cli.json
CREATE angular.json (4273 bytes)
UPDATE karma.conf.js (1008 bytes)
UPDATE src/tsconfig.spec.json (322 bytes)
UPDATE package.json (2076 bytes)
UPDATE tslint.json (3217 bytes)
دستور دوم بسته‌های هسته‌ی angular را به روز رسانی می‌کند و دستور سوم کار به روز رسانی کتابخانه‌ی rxjs را انجام می‌دهد.
لیست سایر بسته‌هایی را که می‌توان توسط این دستور به روز رسانی کرد، با اجرا دستور ng update می‌توانید مشاهده کنید. برای مثال اگر از Angular Material نیز استفاده می‌کنید، دستور به روز رسانی آن به صورت زیر است:
 ng update @angular/material


مشکل! دستور ng update کار نمی‌کند!

اگر پروژه‌ی شما صرفا مبتنی بر بسته‌های اصلی Angular باشد، مراحل یاد شده‌ی فوق را با موفقیت به پایان خواهید رساند. اما اگر از کتابخانه‌های ثالثی استفاده کرده باشید، منهای دستور «ng update @angular/cli» که کار تولید فایل جدید angular.json را انجام می‌دهد، مابقی با خطاهایی مانند «incompatible peer dependency» و یا «Invalid range:>=2.3.1 <3.0.0||>=4.0.0» متوقف می‌شوند.
در یک چنین حالتی نیاز است ابتدا وابستگی‌های محلی پروژه را به روز کرد و سپس دستورات ng update را تکرار نمود. برای این منظور ابتدا بسته‌ی npm-check-updates را نصب کنید:
 npm install npm-check-updates -g
کار آن به روز رسانی خودکار بسته‌های npm یک پروژه است. پس از آن دستورات زیر را صادر نمائید:
ncu -u
npm install
دستور اول تمام شماره نگارش‌های بسته‌های موجود در فایل package.json را به صورت خودکار به آخرین نگارش آن‌ها روز رسانی می‌کند و دستور دوم این بسته‌های جدید را دریافت و نصب خواهد کرد.
اکنون تمام وابستگی‌های محلی پروژه‌ی شما به صورت خودکار به آخرین نگارش آن‌ها به روز رسانی شده‌اند و اینبار اگر دستورات ذیل را اجرا کنید، با خطاهای یاد شده مواجه نخواهید شد:
ng update @angular/core
ng update rxjs

البته اگر در این حالت برنامه را کامپایل کنید، کار نخواهد کرد. علت اصلی آن به به‌روز رسانی rxjs به نگارش 6 آن مرتبط می‌شود که در مطلب بعدی پیگیری خواهد شد و این نگارش شامل حذفیات بسیاری است در جهت کاهش حجم آن، یکپارچکی و یک دست شدن syntax آن و همچنین بهبود قابل ملاحظه‌ی کارآیی آن. البته پیشنیاز الزامی آن آشنایی با pipe-able operators است. علت دیگر کامپایل نشدن برنامه هم می‌تواند عدم استفاده از HttpClient نگارش 4.3 به بعد باشد.


خاموش کردن اخطار «TypeScript version mismatch»

اگر نگارش TypeScript نصب شده‌ی در سیستم به صورت سراسری، با نگارش محلی پروژه‌ی شما یکی نباشد، اخطار «TypeScript version mismatch» را دریافت می‌کنید. روش خاموش کردن آن در CLI جدید با اجرای دستور زیر است:
 ng config cli.warnings.typescriptMismatch false
البته نگارش 6 نیاز به TypeScript 2.7.2 ذکر شده‌ی در package.json پروژه‌ی محلی را دارد؛ وگرنه اصلا شروع به کامپایل برنامه نمی‌کند:
{
  "devDependencies": {
    "typescript": "~2.7.2"
  }
}
بهترین راه برای یافتن این شماره، اجرای دستور ng new projet1 در یک پوشه‌ی خالی است و سپس مقایسه‌ی محتوای فایل package.json آن با فایل پروژه‌ی موجود.


 تغییرات ng build در نگارش 6

در نگارش 6، مفهوم پیشین environments به configurations تغییر یافته‌است و اینبار در فایل جدید angular.json تنظیم می‌شوند:
{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "angular-template-driven-forms-lab": {
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ]
            }
          }
        },
و پیشتر اگر برای تنظیم محیط build از سوئیچ env استفاده می‌شد:
 ng build --env staging
اکنون این سوئیچ به configuration تغییر یافته‌است و نام آن از مداخل configurations فایل angular.json، مانند مثال فوق که «production» نام دارد، باید دریافت و تنظیم شود:
 ng build --configuration staging
و یا برای مثال دستور «ng build --env=prod» دیگر اجرا نمی‌شود و env=prod حذف شده‌است و اکنون اجرای  ng build --prod مانند اجرای دستور ng build --configuration=prod است.


تغییرات نام‌های نهایی تولیدی

در CLI 6.0، نام‌های نهایی تولیدی دیگر به همراه bundle  و یا chunk نیستند. برای مثال دستور ng build یک چنین خروجی را تولید می‌کند:
 >ng build --watch

Date: 2018-05-05T09:10:50.158Z
Hash: a43eab94ff01539b8592
Time: 31733ms
chunk {main} main.js, main.js.map (main) 9.38 kB [initial] [rendered]
chunk {polyfills} polyfills.js, polyfills.js.map (polyfills) 226 kB [initial] [rendered]
chunk {runtime} runtime.js, runtime.js.map (runtime) 5.4 kB [entry] [rendered]
chunk {styles} styles.js, styles.js.map (styles) 15.6 kB [initial] [rendered]
chunk {vendor} vendor.js, vendor.js.map (vendor) 3.05 MB [initial] [rendered]

و هش‌های حالت prod به صورت زیر تولید و به نام فایل اضافه می‌شوند:
>ng build --prod --watch

Date: 2018-05-05T09:17:01.803Z
Hash: f25fd6788a4969c52b70
Time: 73279ms
chunk {0} runtime.6afe30102d8fe7337431.js (runtime) 1.05 kB [entry] [rendered]
chunk {1} styles.34c57ab7888ec1573f9c.css (styles) 0 bytes [initial] [rendered]
chunk {2} polyfills.6c08419970f9e4781b69.js (polyfills) 59.4 kB [initial] [rendered]


ساده شدن افزودن وابستگی‌های ثالث به پروژه‌های CLI

برای نصب یک کتابخانه‌ی ثالث، پیشتر می‌بایستی ابتدا بسته‌ی npm آن جداگانه نصب و سپس فایل config برنامه، جهت معرفی مداخل آن، ویرایش می‌شد. اکنون دستور جدید ng add تمام این مراحل را به صورت خودکار انجام می‌دهد:
 ng add @angular/material
برای نمونه دستور فوق نه تنها فایل‌های Angular Material را دریافت می‌کند، بلکه فایل‌های CSS و ماژول‌های مرتبط با آن‌را نیز import خواهد کرد. البته این مورد از کلید ng-add فایل package.json بسته‌ی در حال نصب دریافت و تنظیم می‌شود و کتابخانه‌های جدید باید خود را بر این اساس وفق دهند.
نظرات مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 10 - بررسی تغییرات Viewها
یکی دیگر از امکانات ViewImports_ :
که شما می‌توانید در هر پوشه View مربوط به هر کنترل یک ViewImports_ اختصاصی داشته باشید
برای مثال :

مسیر‌های زیر را در در پوشه Views در نظر بگیرید:


Views/_ViewImports با محتوای زیر : (موجود در ریشه پوشه Views )

@using Core1RtmTestResources   .Web
@using Core1RtmTestResources   .DomainLayer.Models
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Mvc.Localization

@using Core1RtmTestResources .Web.ViewModels.AccountViewModels //1 Move to Views/Account/_viewImports 

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

و  Views/Account که در این مسیر ViewImports_ اختصاصی در نظر گرفته شده است که می‌توان قطعه کد شماره 1 مشخص شده در کد بالا را به این فایل انتقال داد

@using Core1RtmTestResources .Web.ViewModels.AccountViewModels
مزایا:
وظیفه هر ViewImports_ مشخص می‌شود و بار از ViewImports_  موجود در ریشه Views کم شده و به اصطلاح کد تمیز‌تری خواهید داشت.


و اما طی بررسی هایی که انجام دادم هنوز نتونستم متغیر(های) سراسری در ViewImports_ موجود در مسیر Views/Account تعریف کنم که در View‌های موجود در همین مسیر قابل دسترسی باشد. به قطعه کد زیر توجه فرمایید :

@using Core1RtmTestResources.Web.ViewModels.AccountViewModels
@inject IHtmlLocalizerFactory HtmlLocalizerFactory
@{
    var localizer = HtmlLocalizerFactory.Create(
                baseName: "Controllers.TestLocalController" /*مشخصات کنترلر جاری*/,
                location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/);

    var sharedLocalizer = HtmlLocalizerFactory.Create(
                baseName: "SharedResource" /*مشخصات*/,
                location: "Core1RtmTestResources.ExternalResources" /*نام اسمبلی ثالث*/);
}
متغیر‌های localizer و sharedLocalizer در View‌های موجود در مسیر Views/Account غیر قابل دسترسی می‌باشند در حال حاضر فقط امکان تعریف این حالت در هر View به صورت مجزا راه حل پیشنهادی می‌باشد اما باعث ایجاد قطعه کد تکراری و حتی زمانبر بودن در توسعه و تغییرات می‌باشد.
نظرات مطالب
ارتقاء به Entity framework 6 و استفاده از بانک‌های اطلاعاتی غیر از SQL Server
- توضیحات فوق مربوط به EF Code first بود و حتی با VS 2010 نیز قابل پیاده سازی و استفاده است.
- برای حالت database first نیاز به VS 2013 دارید تا از کلیه امکانات EF 6 استفاده کنید (یا باید به روز رسانی خاصی را برای VS 2012 نصب کنید). حالت Code first مستقل است از IDE (یک مزیت دیگر).
Entity Framework Designer همراه با VS 2012 فقط از EF 5 پشتیبانی می‌کند (در حالت پیش فرض) و از تغییرات انجام شده در EF 6 آگاه نیست. به همین جهت اگر با VS 2012 بخواهید با EF6 کار کنید (در حالت database first) باز هم همان اسمبلی قدیمی System.Data.Entity.dll را مورد استفاده قرار می‌دهد که در EF 6 اصلا کاربردی ندارد و با آن یکی شده است.
البته در کل می‌تونید با VS 2012 هم در حالت database first با EF 6 کار کنید ولی نیاز به یک سری تغییرات دستی خواهید داشت (EF قدیمی و همچنین اسمبلی اضافی یاد شده را باید دستی حذف کنید) و به علاوه از بهبودهای جدید آن (در حالت پیش فرض و بدون به روز رسانی) محروم خواهید بود.
Entity Framework Designer سورس باز نیست  (برخلاف هسته EF) و جزئی از VS.NET است. قرار است در آینده این افزونه را هم سورس باز کنند تا بتوانند مستقل از چرخه طول عمر VS.NET، خود EF Database first را نیز به روز کنند.
ولی در کل اگر از Code first استفاده می‌کنید، EF6 حتی با VS 2010 هم سازگار است.
- زمانیکه با یک ORM کار می‌کنید (فرقی نمی‌کند به چه اسمی)، لایه DAL همان ORM هست. (دست به اختراع لایه‌های اضافی نزنید)
نظرات مطالب
خلاصه‌ای کوتاه در مورد WinRT
- بحث وب که سرجای خودش همانند سابق هست و هیچ فرقی نمی‌کند. برنامه‌های ASP.NET روی سرور اجرا می‌شوند و عموما روی سرور بجز یک سری سرویس‌های ویندوز NT‌ ، هیچ نرم افزار دیگری نصب نخواهد شد. مثلا IIS یا مثلا SQL Server و در همین حد. حتی عموما سرورها حتی مونیتور هم ندارند و با ریموت دسکتاپ یک سری کارهای مدیریتی آن‌ها را انجام می‌دهند و این کارها هم طوری نیست که هر روز تغییر کند. یکبار سرور تنظیم می‌شود که حداقل یکسال یا بیشتر کار کند. این مورد اصلا تغییری نخواهد داشت. بحث سمت سرور است. بنابراین سرمایه گذاری روی ASP.NET خوب است و شامل این بحث ویندوز 8 یا ویندوزهای بعدی نمی‌شود؛ چون این‌ها (WinRT) سمت کاربر محسوب می‌شوند.
- از این جهت که رابط‌های کاربری مبتنی بر WinRT ، یا بر پایه XAML است یا HTML/CSS ، یادگیری WPF و یا سیلورلایت (که قسمتی از WPF را به ارث برده) مفید خواهند بود؛ از این لحاظ که پایه رابط کاربری هر دوی این‌ها هم XAML است و اساسا طراحی XAML از اینجا به WinRT منتقل شده.

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

- یادگیری سی++ همیشه مفید است. حتی در کره مریخ هم تاجایی که اطلاع دارم (!) یک کامپایلر سی++ وجود دارد و می‌شود با آن برنامه‌ی Hello world را کامپایل کرد. اگر باور ندارید از این لینوکسی‌ها بپرسید!
مطالب
معرفی افزونه CAT.NET

اخیرا مایکروسافت افزونه‌ رایگانی را برای آنالیز امنیتی کدهای برنامه‌های نوشته شده با VS.Net ارائه داده است به نام CAT.Net.
دریافت افزونه 32 بیتی، 64 بیتی

این افزونه قابلیت بررسی کدهای شما را جهت یافتن خطرات جدی SQL Injection ، XSS و XPath Injection دارد.
نحوه استفاده:
VS.Net خود را ببندید. در ادامه، پس از نصب، به منوی Tools و گزینه‌ی جدید CAT.NET Code Analysis مراجعه نمائید.
صفحه‌ای ظاهر خواهد شد که پس از کلیک بر روی دکمه آغاز آنالیز آن، کار بررسی امنیتی پروژه را آغاز می‌کند (شکل زیر)




این افزونه همانند FxCop ، اسمبلی برنامه را آنالیز می‌کند. پس از پایان آنالیز،‌ با کلیک بر روی هر سطری که گزارش داده، آن سطر را می‌توان در پروژه یافت و تغییرات لازم را اعمال نمود.



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

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

البته، برنامه هنوز در مراحل آزمایشی به سر می‌برد. برای مثال پس از نصب در مسیر دیگری غیر از مسیر پیش فرض نصب، پیغام می‌داد که فایل‌ها را نمی‌تواند پیدا کند. اگر این مورد برای شما هم رخ داد، مسیری را که گزارش می‌دهد به صورت دستی درست کنید و فایل‌های کانفیگ آن‌را از جایی که نصب کرده‌اید به آنجایی که برنامه به شما گزارش می‌دهد کپی کنید تا کار کند!

یا می‌توان این افزونه را از طریق خط فرمان هم اجرا کرد (مسیر پروژه، اسمبلی آن و مسیر نصب cat.net‌ را لازم دارد). به صورت زیر:

CATNETCmd /file:"I:\prog\bin\prog.dll" /search:"I:\prog" /report:"I:\prog\report.xsl" /rule:"J:\microsoft\cat.net\Rules"

که نتیجه حاصل را در فایل xsl نهایی ثبت خواهد نمود.

اگر علاقمند به مطالعه تاریخچه‌ی این برنامه هستید به وبلاگ زیر مراجعه نمائید:
مشاهده وبلاگ