- The solution name in title bar is truncated.
- Missing interpolation in warning when packaging Cloud Service projects.
- Visual Studio will no longer show an error message on startup after you reset or import settings in certain cases.
- Accessibility issues in Cloud Service projects where wrong inputs in the diagnostics configuration window are not read by screen readers have been fixed.
Vue.js interpolations IntelliSense: The Vetur extension now provides IntelliSense (smart completions) for Vue.js interpolations.
نظرات مطالب
AngularJS #2
اینجا
visual studio 2013 به صورت پیش فرض از angular پشتیبانی میکند.
در ضمن به آن صورت هم فکر نکنم احتیاجی به intellisense باشد. من به شخصه بدون intellisense به راحتی ازش استفاده میکنم.
اشتراکها
ASP.NET Core 6 Preview 5 منتشر شد
Here’s what’s new in this preview release:
- .NET Hot Reload updates for
dotnet watch
- ASP.NET Core SPA templates updated to Angular 11 and React 17
- Use Razor syntax in SVG
foreignObject
elements - Specify null for
Action
andRenderFragment
component parameters - Reduced Blazor WebAssembly download size with runtime relinking
- Configurable buffer threshold before writing to disk in Json.NET output formatter
- Subcategories for better filtering of Kestrel logs
- Faster get and set for HTTP headers
- Configurable unconsumed incoming buffer size for IIS
رفتار Blazorهای پیش از داتنت 8 در مورد مدیریت حالت
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
پیش از دات نت 8، دو حالت عمده برای توسعهی برنامههای Blazor وجود داشت: Blazor Server و Blazor WASM. در هر دو حالت، طول عمر سیستم تزریق وابستگیهای ایجاد و مدیریت شدهی توسط Blazor، معادل طول عمر برنامهاست.
در برنامههای Blazor Server، طول عمر سیستم تزریق وابستگیها، توسط ASP.NET Core قرار گرفتهی بر روی سرور مدیریت شده و نمونههای ایجاد شدهی سرویسهای توسط آن، به ازای هر کاربر متفاوت است. بنابراین اگر طول عمر سرویسی در اینجا به صورت Scoped تعریف شود، این سرویس فقط یکبار در طول عمر برنامه، به ازای یک کاربر جاری برنامه، تولید و نمونه سازی میشود. در این مدل برنامهها، سرویسهایی با طول عمر Singleton، بین تمام کاربران به اشتراک گذاشته میشوند. به همین جهت است که در این نوع برنامهها، مدیریت سرویس Context مخصوص EF-Core نکات خاصی را به همراه دارد. چون اگر بر اساس سیستم پیشفرض تزریق وابستگیها و طول عمر Scoped این سرویس عمل شود، یک Context فقط یکبار بهازای یک کاربر، یکبار نمونه سازی شده و تا پایان طول عمر برنامه، بدون تغییر زنده نگه داشته میشود؛ در حالیکه عموم توسعه دهندگان EF-Core تصور میکنند سرویسهای Scoped، پس از پایان یک درخواست، پایان یافته و Dispose میشوند، اما در اینجا پایان درخواستی نداریم. یک اتصال دائم SignalR را داریم و تا زمانیکه برقرار است، یعنی برنامه زندهاست. بنابراین در برنامههای Blazor Server، سرویسهای Scoped، به ازای هر کاربر، همانند Singleton رفتار میکنند (در سراسر برنامه به ازای یک کاربر در دسترس هستند) و سرویسهایی از اساس Singleton، بین تمام کاربران به اشتراک گذاشته میشوند.
در برنامههای Blazor WASM، طول عمر سیستم تزریق وابستگیها، توسط برنامهی وباسمبلی در حال اجرای بر روی مرورگر مدیریت میشود. یعنی مختص به یک کاربر بوده و طول عمر آن وابستهاست به طول عمر برگهی جاری مرورگر. بنابراین دراینجا بین سرویسهای Scoped و Singleton، تفاوتی وجود ندارد و همانند هم رفتار میکنند (هر دو مختص به یک کاربر و وابسته به طول عمر برگهی جاری هستند).
در هیچکدام از این حالتها، امکان دسترسی به HttpContext وجود ندارد (نه داخل اتصال دائم SignalR برنامههای Blazor Server و نه داخل برنامهی وباسمبلی در حال اجرای در مرورگر). اطلاعات بیشتر
بنابراین در این برنامهها برای نگهداری اطلاعات کاربر لاگین شدهی به سیستم و یا سایر اطلاعات سراسری برنامه، عموما از سرویسهایی با طول عمر Scoped استفاده میشود که در تمام قسمتهای برنامه به ازای هر کاربر، قابل دسترسی هستند.
رفتار Blazor 8x در مورد مدیریت حالت
هرچند دات نت 8 به همراه حالتهای رندر جدیدی است، اما هنوز هم میتوان برنامههایی کاملا توسعه یافته بر اساس مدلهای قبلی Blazor Server و یا Blazor WASM را همانند داتنتهای پیش از 8 داشت. بنابراین اگر تصمیم گرفتید که بجای استفاده از جزیرههای تعاملی، کل برنامه را به صورت سراسری تعاملی کنید، همان نکات قبلی، در اینجا هم صادق هستند و از لحاظ مدیریت حالت، تفاوتی نمیکنند.
اما ... اگر تصمیم گرفتید که از حالتهای رندر جدید استفاده کنید، مدیریت حالت آن متفاوت است؛ برای مثال دیگر با یک سیستم مدیریت تزریق وابستگیها که طول عمر آن با طول عمر برنامهی Blazor یکی است، مواجه نیستیم و حالتهای زیر برای آنها متصور است:
حالت رندر: صفحات رندر شدهی در سمت سرور یا Server-rendered pages
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و HTML نهایی آن به سمت مرورگر کاربر ارسال میشود. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
صفحات SSR، بدون حالت (stateless) هستند؛ به این معنا که حالت کاربر در بین هدایت به صفحات مختلف برنامه ذخیره نمیشود. به آنها میتوان از این لحاظ بهمانند برنامههای MVC/Razor pages نگاه کرد. در این حالت اگر میخواهید حالت کاربران را ذخیره کنید، استفاده از کوکیها و یا سشنها، راهحل متداول اینکار هستند.
حالت رندر: صفحات استریمی (Streamed pages)
مفهوم: یک صفحهی Blazor که در سمت سرور رندر شده و قطعات آماده شدهی HTML آن به صورت استریمی از دادهها، به سمت مرورگر کاربر ارسال میشوند. در این حالت هیچ اتصال SignalR و یا برنامهی وباسمبلی اجرا نخواهد شد.
عواقب: طول عمر سرویسهای Scoped، بهمحض پایان رندر صفحه در سمت سرور، پایان خواهند یافت.
بنابراین در این حالت طول عمر یک سرویس Scoped، بسیار کوتاه است (در حد ابتدا و انتهای رندر صفحه). همچنین چون برنامه در سمت سرور اجرا میشود، دسترسی کامل و بدون مشکلی را به HttpContext دارد.
حالت رندر: Blazor server page
مفهوم: یک صفحهی Blazor Server که یک اتصال دائم SignalR را با سرور دارد.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت. این نوع برنامهها اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: Blazor wasm page
مفهوم: صفحهای که به کمک فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر برگه و صفحهی جاری است و با بسته شدن آن، پایان میپذیرد. این نوع برنامهها نیز اصطلاحا stateful هستند و از لحاظ دسترسی به حالت کاربر، تجربهی کاربری همانند یک برنامهی دسکتاپ را ارائه میدهند (البته فقط درون مروگر کاربر).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor Server و یا Blazor server island
مفهوم: یک کامپوننت Blazor Server که درون یک صفحهی دیگر (که عموما از نوع SSR است) قرار گرفته و یک اتصال SignalR را با سرور برقرار میکند.
عواقب: طول عمر سرویسهای Scoped، معادل طول عمر اتصال SignalR است و با قطع این اتصال، پایان خواهند یافت؛ برای مثال کاربر به صفحهای دیگر در این برنامه مراجعه کند. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند.
در این نوع برنامهها و درون اتصال SignalR، دسترسی به HttpContext وجود ندارد.
حالت رندر: جزیرهی تعاملی Blazor WASM و یا Blazor wasm island
مفهوم: یک کامپوننت Blazor WASM که درون یک صفحهی دیگر (که عموما از نوع SSR است) توسط فناوری وباسمبلی، درون مرورگر کاربر اجرا میشود.
عواقب: طول عمر سرویسهای Scoped، معادل مدت زمان فعال بودن صفحهی جاری است. به محض اینکه کاربر به صفحهای دیگر مراجعه و این کامپوننت دیگر فعال نباشد، طول عمر آن خاتمه خواهد یافت. بنابراین این نوع کامپوننتها هم تا زمانیکه کاربر در صفحهی جاری قرار دارد، stateful هستند (البته این حالت درون مرورگر کاربر مدیریت میشود و نه در سمت سرور).
در این نوع برنامهها، دسترسی به HttpContext وجود ندارد.
نتیجهگیری
همانطور که مشاهده میکنید، در صفحات SSR، دسترسی کاملی به HttpContext سمت سرور وجود دارد (که البته کوتاه مدت بوده و با پایان رندر صفحه، خاتمه خواهد یافت؛ حالتی مانند صفحات MVC و Razor pages)، اما در جزایر تعاملی واقع در آنها، خیر.
مسالهی مهم در اینجا، مدیریت اختلاط حالت صفحات SSR و جزایر تعاملی واقع در آنها است. مایکروسافت جهت پیاده سازی اعتبارسنجی و احراز هویت کاربران در Blazor 8x و برای انتقال حالت به این جزایر، از دو روش Root-level cascading values و سرویس PersistentComponentState استفاده کردهاست که آنها را در دو قسمت بعدی، با توضیحات بیشتری بررسی میکنیم.
This command allows cleaning up project and assembly references that have no actual usages in source code. You can apply this command on a project, solution folder, or the entire solution. Before deletion is complete, you will be able to see all references that are going to be removed and. if necessary, preserve the ones that you want to keep.
بهبودهای Blazor 6x جهت تنظیم محتوای head صفحات
در اینجا پیشتر روشی را مبتنی بر جاوا اسکریپت جهت تنظیم عنوان صفحات مشاهده کردید. در Blazor 6x دیگر به این راه حل نیازی نیست.
کامپوننت جدید PageTitle
@page "/counter" <PageTitle>Counter</PageTitle>
کامپوننت جدید HeadContent
@page "/counter" <PageTitle>Counter</PageTitle> <HeadContent> <meta name="description" content="Use this page to count things!" /> <meta name="author" content="VahidN"> <link rel="icon" href="favicon.ico" type="image/x-icon"> <link rel="sitemap" type="application/xml" title="Sitemap" href="@(NavigationManager.BaseUri)sitemap.xml" /> <link rel="alternate" type="application/rss+xml" href="@(NavigationManager.BaseUri)atom.xml"> <link rel="canonical" href="@(NavigationManager.BaseUri)good-content" /> <meta name="robots" content="index, follow" /> </HeadContent>
یک نکته: در اینجا هم میتوان تگ استاندارد title را اضافه کرد. اما باید دقت داشت که در این حالت، صرفا کار افزودن این تگ صورت میگیرد؛ بدون توجه به وجود کامپوننت PageTitle تعریف شده. یعنی بیش از یک title در تگ head درج میشود که یک HTML غیرمعتبر به حساب خواهد آمد.
کامپوننت جدید HeadOutlet
این کامپوننت، کار پردازش دو کامپوننت یاد شده را انجام میدهد و محل تعریف آن، در فایل Program.cs است که در قالب پروژههای جدید Blazor 6x، به صورت خودکار، درج و تنظیم شدهاست:
var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); builder.RootComponents.Add<HeadOutlet>("head::after");
مطالب
Blazor 5x - قسمت 31 - احراز هویت و اعتبارسنجی کاربران Blazor WASM - بخش 1 - انجام تنظیمات اولیه
در قسمت قبل، امکان سفارش یک اتاق را به همراه پرداخت آنلاین آن، به برنامهی Blazor WASM این سری اضافه کردیم؛ اما ... هویت کاربری که مشغول انجام اینکار است، هنوز مشخص نیست. بنابراین در این قسمت میخواهیم مباحثی مانند ثبت نام و ورود به سیستم را تکمیل کنیم. البته مقدمات سمت سرور این بحث را در مطلب «Blazor 5x - قسمت 25 - تهیه API مخصوص Blazor WASM - بخش 2 - تامین پایهی اعتبارسنجی و احراز هویت»، بررسی کردیم.
ارائهی AuthenticationState به تمام کامپوننتهای یک برنامهی Blazor WASM
در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامههای Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب میشود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننتهای یک برنامهیBlazor ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائهی یکسری اطلاعات، به تمام زیر کامپوننتهای یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آنها را در بالاترین سطح سلسله مراتب کامپوننتهای تعریف شده تعریف کردیم، در تمام زیر کامپوننتهای آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامهی Blazor را بر اساس وضعیت اعتبارسنجی و نقشهای کاربر جاری، میسر میکند.
روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
کامپوننت CascadingAuthenticationState، اطلاعات AuthenticationState را در اختیار تمام کامپوننتهای برنامه قرار میدهد و کامپوننت AuthorizeRouteView، امکان نمایش یا عدم نمایش قسمتی از صفحه را بر اساس وضعیت لاگین شخص و یا محدود کردن دسترسی بر اساس نقشها، میسر میکند.
مشکل! برخلاف برنامههای Blazor Server، برنامههای Blazor WASM به صورت پیشفرض به همراه تامین کنندهی توکار AuthenticationState نیستند.
اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامههای Blazor Server، در قسمتهای 21 تا 23، پیشتر مشاهده کردهاید. همان مفاهیم، در برنامههای Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامهی سمت کلاینت WASM Blazor، از برنامهی Web API سمت سرور، نیاز است یک تامین کنندهی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامههای Blazor Server، این مورد به صورت خودکار مدیریت نمیشود و با ASP.NET Core Identity سمت سروری که JWT تولید میکند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار میکند. به همین جهت در ابتدا یک JWT Parser را طراحی میکنیم که رشتهی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim میکند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار میدهیم تا اطلاعات مورد نیاز کامپوننتهای CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.
نیاز به یک JWT Parser
در قسمت 25، پس از لاگین موفق، یک JWT تولید میشود که به همراه قسمتی از مشخصات کاربر است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی میرسیم و حاوی claims تعریف شدهاست:
بنابراین برای استخراج این claims در سمت کلاینت، نیاز به یک JWT Parser داریم که نمونهای از آن میتواند به صورت زیر باشد:
که آنرا در فایل BlazorWasm.Client\Utils\JwtParser.cs برنامهی کلاینت ذخیره خواهیم کرد. متد ParseClaimsFromJwt فوق، رشتهی JWT تولیدی حاصل از لاگین موفق در سمت Web API را دریافت کرده و تبدیل به لیستی از Claimها میکند.
تامین AuthenticationState مبتنی بر JWT مخصوص برنامههای Blazor WASM
پس از داشتن لیست Claims دریافتی از یک رشتهی JWT، اکنون میتوان آنرا تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بستهی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامهی کلاینت اضافه کرد:
سپس سرویس سفارشی AuthStateProvider خود را به پوشهی Services برنامه اضافه میکنیم و متد GetAuthenticationStateAsync کلاس پایهی AuthenticationStateProvider استاندارد را به نحو زیر بازنویسی و سفارشی سازی میکنیم:
- اگر با برنامههای سمت کلاینت React و یا Angular پیشتر کار کرده باشید، منطق این کلاس بسیار آشنا به نظر میرسد. در این برنامهها، مفهومی به نام Interceptor وجود دارد که توسط آن به صورت خودکار، هدر JWT را به تمام درخواستهای ارسالی به سمت سرور، اضافه میکنند تا از تکرار این قطعه کد خاص، جلوگیری شود. علت اینجا است که برای دسترسی به منابع محافظت شدهی سمت سرور، نیاز است هدر ویژهای را به نام "Authorization" که با مقدار "bearer jwt" تشکیل میشود، به ازای هر درخواست ارسالی به سمت سرور نیز ارسال کرد؛ تا تنظیمات ویژهی AddJwtBearer که در قسمت 25 در کلاس آغازین برنامهی Web API انجام دادیم، این هدر مورد انتظار را دریافت کرده و پردازش کند و در نتیجهی آن، شیء this.User، در اکشن متدهای کنترلرها تشکیل شده و قابل استفاده شود.
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده میکنید که مقدار token خودش را از Local Storage دریافت میکند که کلید متناظر با آنرا در پروژهی BlazorServer.Common به صورت زیر تعریف کردهایم:
به این ترتیب دیگر نیازی نخواهد بود در تمام سرویسهای برنامهی WASM که با HttpClient کار میکنند، مدام سطر مقدار دهی httpClient.DefaultRequestHeaders.Authorization را تکرار کنیم.
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفادهی در برنامهی Blazor WASM کردهایم.
پس از تعریف یک AuthenticationStateProvider سفارشی، باید آنرا به همراه Authorization، به سیستم تزریق وابستگیهای برنامه در فایل Program.cs اضافه کرد:
و برای سهولت استفادهی از امکانات اعتبارسنجی فوق در کامپوننتهای برنامه، فضای نام زیر را به فایل BlazorWasm.Client\_Imports.razor اضافه میکنیم:
تهیهی سرویسی برای کار با AccountController
اکنون میخواهیم در برنامهی سمت کلاینت، از AccountController سمت سرور که آنرا در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر میکند:
و به صورت زیر پیاده سازی میشود:
که به نحو زیر به سیستم تزریق وابستگیهای برنامه معرفی میشود:
توضیحات:
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی میکند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویسهای دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالتهایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد میشود و به این ترتیب میتوان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمتهای مختلف برنامه را به او نشان داد. نمونهی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامهی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ میدهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامهی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.
- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامههای سمت کلاینت Blazor، طول عمر تمام سرویسها، صرفنظر از نوع طول عمری که برای آنها مشخص میکنیم، همواره Singleton هستند (ماخذ).
بنابراین میتوان یک سرویس سراسری توکن را تهیه و به سادگی آنرا در تمام قسمتهای برنامه تزریق کرد. این روش هرچند کار میکند، اما همانطور که عنوان شد، به Refresh کامل صفحه حساس است. اگر برنامه در مرورگر کاربر Refresh نشود، تا زمانیکه باز است، سرویسهای در اصل Singleton تعریف شدهی در آن نیز در تمام قسمتهای برنامه در دسترس هستند؛ اما با Refresh کامل صفحه، به علت بارگذاری مجدد کل برنامه، سرویسهای آن نیز از نو، وهله سازی خواهند شد که سبب از دست رفتن حالت قبلی آنها میشود. بنابراین نیاز به روشی داریم که بتوانیم حالت قبلی برنامه را در زمان راه اندازی اولیهی آن بازیابی کنیم و یکی از روشهای استاندارد اینکار، استفاده از Local Storage خود مرورگر است که مستقل از برنامه و توسط مرورگر مدیریت میشود.
- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه میکنید. همانطور که عنوان شد، سرویسهای Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویسهای HttpClient تزریق شدهی به سایر سرویسهای برنامه نیز بلافاصله تاثیرگذار خواهد بود.
- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال میکند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواستهای ارسالی به منابع محافظت شدهی سمت سرور کفایت میکند.
- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال میکند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-31.zip
ارائهی AuthenticationState به تمام کامپوننتهای یک برنامهی Blazor WASM
در قسمت 22، با مفاهیم CascadingAuthenticationState و AuthorizeRouteView در برنامههای Blazor Server آشنا شدیم؛ این مفاهیم در اینجا نیز یکی هستند:
- کامپوننت CascadingAuthenticationState سبب میشود AuthenticationState (لیستی از Claims کاربر)، به تمام کامپوننتهای یک برنامهیBlazor ارسال شود. در مورد پارامترهای آبشاری، در قسمت نهم این سری بیشتر بحث شد و هدف از آن، ارائهی یکسری اطلاعات، به تمام زیر کامپوننتهای یک کامپوننت والد است؛ بدون اینکه نیاز باشد مدام این پارامترها را در هر زیر کامپوننتی، تعریف و تنظیم کنیم. همینقدر که آنها را در بالاترین سطح سلسله مراتب کامپوننتهای تعریف شده تعریف کردیم، در تمام زیر کامپوننتهای آن نیز در دسترس خواهند بود.
- کامپوننت AuthorizeRouteView امکان محدود کردن دسترسی به صفحات مختلف برنامهی Blazor را بر اساس وضعیت اعتبارسنجی و نقشهای کاربر جاری، میسر میکند.
روش اعمال این دو کامپوننت نیز یکی است و نیاز به ویرایش فایل BlazorWasm.Client\App.razor در اینجا وجود دارد:
<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true"> <Found Context="routeData"> <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"> <Authorizing> <p>Please wait, we are authorizing the user.</p> </Authorizing> <NotAuthorized> <p>Not Authorized</p> </NotAuthorized> </AuthorizeRouteView> </Found> <NotFound> <LayoutView Layout="@typeof(MainLayout)"> <p>Sorry, there's nothing at this address.</p> </LayoutView> </NotFound> </Router> </CascadingAuthenticationState>
مشکل! برخلاف برنامههای Blazor Server، برنامههای Blazor WASM به صورت پیشفرض به همراه تامین کنندهی توکار AuthenticationState نیستند.
اگر سری Blazor جاری را از ابتدا دنبال کرده باشید، کاربرد AuthenticationState را در برنامههای Blazor Server، در قسمتهای 21 تا 23، پیشتر مشاهده کردهاید. همان مفاهیم، در برنامههای Blazor WASM هم قابل استفاده هستند؛ البته در اینجا به علت جدا بودن برنامهی سمت کلاینت WASM Blazor، از برنامهی Web API سمت سرور، نیاز است یک تامین کنندهی سمت کلاینت AuthenticationState را بر اساس JSON Web Token دریافتی از سرور، تشکیل دهیم و برخلاف برنامههای Blazor Server، این مورد به صورت خودکار مدیریت نمیشود و با ASP.NET Core Identity سمت سروری که JWT تولید میکند، یکپارچه نیست.
بنابراین در اینجا نیاز است یک AuthenticationStateProvider سفارشی سمت کلاینت را تهیه کنیم که بر اساس JWT دریافتی از Web API کار میکند. به همین جهت در ابتدا یک JWT Parser را طراحی میکنیم که رشتهی JWT دریافتی از سرور را تبدیل به <IEnumerable<Claim میکند. سپس این لیست را در اختیار یک AuthenticationStateProvider سفارشی قرار میدهیم تا اطلاعات مورد نیاز کامپوننتهای CascadingAuthenticationState و AuthorizeRouteView تامین شده و قابل استفاده شوند.
نیاز به یک JWT Parser
در قسمت 25، پس از لاگین موفق، یک JWT تولید میشود که به همراه قسمتی از مشخصات کاربر است. میتوان محتوای این توکن را در سایت jwt.io مورد بررسی قرار داد که برای نمونه به این خروجی میرسیم و حاوی claims تعریف شدهاست:
{ "iss": "https://localhost:5001/", "iat": 1616396383, "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "vahid@dntips.ir", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress": "vahid@dntips.ir", "Id": "582855fb-e95b-45ab-b349-5e9f7de40c0c", "DisplayName": "vahid@dntips.ir", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "Admin", "nbf": 1616396383, "exp": 1616397583, "aud": "Any" }
using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Text.Json; namespace BlazorWasm.Client.Utils { /// <summary> /// From the Steve Sanderson’s Mission Control project: /// https://github.com/SteveSandersonMS/presentation-2019-06-NDCOslo/blob/master/demos/MissionControl/MissionControl.Client/Util/ServiceExtensions.cs /// </summary> public static class JwtParser { public static IEnumerable<Claim> ParseClaimsFromJwt(string jwt) { var claims = new List<Claim>(); var payload = jwt.Split('.')[1]; var jsonBytes = ParseBase64WithoutPadding(payload); var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes); claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString()))); return claims; } private static byte[] ParseBase64WithoutPadding(string base64) { switch (base64.Length % 4) { case 2: base64 += "=="; break; case 3: base64 += "="; break; } return Convert.FromBase64String(base64); } } }
تامین AuthenticationState مبتنی بر JWT مخصوص برنامههای Blazor WASM
پس از داشتن لیست Claims دریافتی از یک رشتهی JWT، اکنون میتوان آنرا تبدیل به یک AuthenticationStateProvider کرد. برای اینکار در ابتدا نیاز است بستهی نیوگت Microsoft.AspNetCore.Components.Authorization را به برنامهی کلاینت اضافه کرد:
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly"> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="5.0.4" /> </ItemGroup> </Project>
namespace BlazorWasm.Client.Services { public class AuthStateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ILocalStorageService _localStorage; public AuthStateProvider(HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _localStorage = localStorage ?? throw new ArgumentNullException(nameof(localStorage)); } public override async Task<AuthenticationState> GetAuthenticationStateAsync() { var token = await _localStorage.GetItemAsync<string>(ConstantKeys.LocalToken); if (token == null) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token); return new AuthenticationState( new ClaimsPrincipal( new ClaimsIdentity(JwtParser.ParseClaimsFromJwt(token), "jwtAuthType") ) ); } } }
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده میکنید که مقدار token خودش را از Local Storage دریافت میکند که کلید متناظر با آنرا در پروژهی BlazorServer.Common به صورت زیر تعریف کردهایم:
namespace BlazorServer.Common { public static class ConstantKeys { // ... public const string LocalToken = "JWT Token"; } }
- همچنین در اینجا به کمک متد JwtParser.ParseClaimsFromJwt که در ابتدای بحث تهیه کردیم، لیست Claims دریافتی از JWT ارسالی از سمت سرور را تبدیل به یک AuthenticationState قابل استفادهی در برنامهی Blazor WASM کردهایم.
پس از تعریف یک AuthenticationStateProvider سفارشی، باید آنرا به همراه Authorization، به سیستم تزریق وابستگیهای برنامه در فایل Program.cs اضافه کرد:
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddAuthorizationCore(); builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>(); // ... } } }
@using Microsoft.AspNetCore.Components.Authorization
تهیهی سرویسی برای کار با AccountController
اکنون میخواهیم در برنامهی سمت کلاینت، از AccountController سمت سرور که آنرا در قسمت 25 این سری تهیه کردیم، استفاده کنیم. بنابراین نیاز است سرویس زیر را تدارک دید که امکان لاگین، ثبت نام و خروج از سیستم را در سمت کلاینت میسر میکند:
namespace BlazorWasm.Client.Services { public interface IClientAuthenticationService { Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication); Task LogoutAsync(); Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration); } }
namespace BlazorWasm.Client.Services { public class ClientAuthenticationService : IClientAuthenticationService { private readonly HttpClient _client; private readonly ILocalStorageService _localStorage; public ClientAuthenticationService(HttpClient client, ILocalStorageService localStorage) { _client = client; _localStorage = localStorage; } public async Task<AuthenticationResponseDTO> LoginAsync(AuthenticationDTO userFromAuthentication) { var response = await _client.PostAsJsonAsync("api/account/signin", userFromAuthentication); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<AuthenticationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { await _localStorage.SetItemAsync(ConstantKeys.LocalToken, result.Token); await _localStorage.SetItemAsync(ConstantKeys.LocalUserDetails, result.UserDTO); _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", result.Token); return new AuthenticationResponseDTO { IsAuthSuccessful = true }; } else { return result; } } public async Task LogoutAsync() { await _localStorage.RemoveItemAsync(ConstantKeys.LocalToken); await _localStorage.RemoveItemAsync(ConstantKeys.LocalUserDetails); _client.DefaultRequestHeaders.Authorization = null; } public async Task<RegisterationResponseDTO> RegisterUserAsync(UserRequestDTO userForRegisteration) { var response = await _client.PostAsJsonAsync("api/account/signup", userForRegisteration); var responseContent = await response.Content.ReadAsStringAsync(); var result = JsonSerializer.Deserialize<RegisterationResponseDTO>(responseContent); if (response.IsSuccessStatusCode) { return new RegisterationResponseDTO { IsRegisterationSuccessful = true }; } else { return result; } } } }
namespace BlazorWasm.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); // ... builder.Services.AddScoped<IClientAuthenticationService, ClientAuthenticationService>(); // ... } } }
- متد LoginAsync، مشخصات لاگین کاربر را به سمت اکشن متد api/account/signin ارسال کرده و در صورت موفقیت این عملیات، اصل توکن دریافتی را به همراه مشخصاتی از کاربر، در Local Storage ذخیره سازی میکند. این مورد سبب خواهد شد تا بتوان به مشخصات کاربر در صفحات دیگر و سرویسهای دیگری مانند AuthStateProvider ای که تهیه کردیم، دسترسی پیدا کنیم. به علاوه مزیت دیگر کار با Local Storage، مواجه شدن با حالتهایی مانند Refresh کامل صفحه و برنامه، توسط کاربر است. در یک چنین حالتی، برنامه از نو بارگذاری مجدد میشود و به این ترتیب میتوان به مشخصات کاربر لاگین کرده، به سادگی دسترسی یافت و مجددا قسمتهای مختلف برنامه را به او نشان داد. نمونهی دیگر این سناریو، بازگشت از درگاه پرداخت بانکی است. در این حالت نیز از یک سرویس سمت سرور دیگر، کاربر به سمت برنامهی کلاینت، Redirect کامل خواهد شد که در اصل اتفاقی که رخ میدهد، با Refresh کامل صفحه یکی است. در این حالت نیز باید بتوان کاربری را که از درگاه بانکی ثالث، به سمت برنامهی کلاینت از نو بارگذاری شده، هدایت شده، بلافاصله تشخیص داد.
- اگر برنامه، Refresh کامل نشود، نیازی به Local Storage نخواهد بود؛ از این لحاظ که در برنامههای سمت کلاینت Blazor، طول عمر تمام سرویسها، صرفنظر از نوع طول عمری که برای آنها مشخص میکنیم، همواره Singleton هستند (ماخذ).
Blazor WebAssembly apps don't currently have a concept of DI scopes. Scoped-registered services behave like Singleton services.
- در متد LoginAsync، علاوه بر ثبت اطلاعات کاربر در Local Storage، مقدار دهی client.DefaultRequestHeaders.Authorization را نیز ملاحظه میکنید. همانطور که عنوان شد، سرویسهای Blazor WASM در اصل دارای طول عمر Singleton هستند. بنابراین تنظیم این هدر در اینجا، بر روی تمام سرویسهای HttpClient تزریق شدهی به سایر سرویسهای برنامه نیز بلافاصله تاثیرگذار خواهد بود.
- متد LogoutAsync، اطلاعاتی را که در حین لاگین موفق در Local Storage ذخیره کردیم، حذف کرده و همچنین client.DefaultRequestHeaders.Authorization را نیز نال میکند تا دیگر اطلاعات لاگین شخص قابل بازیابی نبوده و مورد استفاده قرار نگیرد. همین مقدار برای شکست پردازش درخواستهای ارسالی به منابع محافظت شدهی سمت سرور کفایت میکند.
- متد RegisterUserAsync، مشخصات کاربر در حال ثبت نام را به سمت اکشن متد api/account/signup ارسال میکند که سبب افزوده شدن کاربر جدیدی به بانک اطلاعاتی برنامه و سیستم ASP.NET Core Identity خواهد شد.
کدهای کامل این مطلب را از اینجا میتوانید دریافت کنید: Blazor-5x-Part-31.zip