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

روش اول: دریافت اطلاعات سمت سرور به کمک درخواست‌های Ajax

استفاده از Ajax یکی از روش‌های کلاسیک دریافت اطلاعات سمت سرور در کدهای جاوا اسکریپتی است.
<script type="text/javascript">
var products = [];
$(function() {
    $.getJSON("/home/products", function(response) {
        products = response.products;
    });
});
</script>
برای نمونه در اینجا با استفاده از امکانات jQuery، درخواست Ajax ایی به سرور ارسال شده و سپس نتیجه‌ی دریافتی، به آرایه‌ی جاوا اسکریپتی products نسبت داده شده‌است.
- مزایا: استفاده از Ajax، روشی بسیار متداول و شناخته شده‌است و به کمک انواع و اقسام روش‌های بازگشت JSON از سرور، می‌توان با آن کار کرد.
- معایب: درخواست Ajax، صرفا پس از بارگذاری اولیه‌ی صفحه به سمت سرور ارسال خواهد شد و در این بین، کاربر وقفه‌ای را مشاهده خواهد کرد. همچنین در اینجا بجای یک درخواست از سرور، حداقل دو درخواست باید ارسال شوند؛ یکی برای بارگذاری صفحه‌ی اصلی و دیگری برای دریافت اطلاعات Ajax ایی از سرور به صورت غیرهمزمان.


روش دوم: دریافت اطلاعات از یک فایل جاوا اسکریپتی خارجی

اطلاعات سمت کاربر را از یک فایل جاوا اسکریپتی خارجی الحاق شده‌ی به صفحه‌ی جاری نیز می‌توان تهیه کرد:
 <script src="/file.js"></script>
یک چنین فایلی را می‌توان توسط کدهای سمت سرور نیز بازگشت داد و اطلاعات آن‌را تهیه و پر کرد. در این حالت فقط کافی است که ContentType شیء Response را از نوع application/javascript مشخص کنیم و سپس یک خروجی متنی جاوا اسکریپتی را از سمت سرور بازگشت دهیم.
این روش نیز تقریبا مانند حالت یک درخواست Ajax ایی کار می‌کند و اطلاعات مورد نیاز را در طی یک درخواست جداگانه، پس از بارگذاری صفحه‌ی اصلی، از سرور دریافت خواهد کرد. البته در حالت کار با Ajax، می‌توان در طی یک callback، نتیجه را دریافت کرد و سپس عکس العمل نشان داد؛ اما در اینجا callback ایی وجود ندارد.


روش سوم: استفاده از SignalR

در SignalR ابتدا سعی می‌شود تا با استفاده از Web Sockets ارتباطی ماندگار بین کلاینت و سرور برقرار شود و سپس در این حالت، سرور می‌تواند مدام اطلاعاتی، مانند تغییرات داده‌های خود را به سمت کاربر، جهت نمایش و یا محاسبات خاص خود ارسال کند. اگر حالت Web Socket میسر نباشد (توسط سرور یا کلاینت پشتیبانی نشود)، به حالت‌های دیگری مانند server events, forever frames, long polling سوئیچ خواهد کرد. اطلاعات بیشتر


روش چهارم: قرار دادن اطلاعات سمت سرور در کدهای HTML صفحه

روش متداول دیگری جهت تامین اطلاعات جاوا اسکریپتی سمت کاربر، قرار دادن آن‌ها در ویژگی‌های data-* ارائه شده در HTML5 است.
<ul>
@foreach (var product in products)
{
  <li id="product@product.Id" data-rank="@product.Rank">@product.Name</li>
}
</ul>
در اینجا لیستی از محصولات، به صورت متداولی از کنترلر دریافت گردیده و سپس ویژگی data-rank هر ردیف نمایش داده شده نیز توسط این اطلاعات سمت سرور مقدار دهی شده‌اند.
اکنون برای دسترسی به مقدار data-rank سطری مانند product1، در کدهای جاوا اسکریپتی صفحه می‌توان نوشت:
<script type="text/javascript">
var product1Rank = $("#product1").data("rank");
</script>
این روش برای قرار دادن اطلاعات ثابت اشیاء، روش مرسومی است.


روش پنجم: قرار دادن اطلاعات سمت سرور در کدهای جاوا اسکریپتی صفحه

این روش همانند روش چهارم است، با این تفاوت که اینبار اطلاعات مورد نیاز، مستقیما به یک متغیر جاوا اسکریپتی انتساب داده شده‌است:
 <script type="text/javascript">
var product1Name = "@product1.Name";
</script>
مزیت این روش، عدم ارسال درخواست اضافه‌تری به سرور برای دریافت اطلاعات مورد نیاز است. مقدار product1Name در همان بار اول رندر صفحه از سمت سرور، مشخص می‌گردد.


روش ششم: انتساب یک شیء دات نتی به یک متغیر جاوا اسکریپتی

این روش همانند روش پنجم است، با این تفاوت که اینبار قصد داریم بجای یک مقدار ثابت رشته‌ای یا عددی، برای مثال، آرایه‌ای از اشیاء را به یک متغیر جاوا اسکریپتی انتساب دهیم. در اینجا ابتدا اطلاعات مورد نظر را به فرمت JSON تبدیل می‌کنیم:
//سمت سرور
[HttpGet]
public ActionResult Index()
{
    var array = new[]
    {
        "Afghanistan",
        "Albania",
        "Algeria",
        "American Samoa",
        "Andorra",
        "Angola",
        "Anguilla",
        "Antarctica",
        "Antigua and/or Barbuda"
     };
     ViewBag.JsonString = new JavaScriptSerializer().Serialize(array);
     return View();
}
سپس توسط متد Html.Raw می‌توان این رشته‌ی JSON را که اکنون حاوی آرایه جاوا اسکریپتی سمت سرور است، به یک متغیر جاوا اسکریپتی نسبت داد:
 //سمت کلاینت
<script type="text/javascript">
var jsonArray = @Html.Raw(@ViewBag.JsonString);
</script>
استفاده از Html.Raw در این حالت از این جهت ضروری است که اطلاعاتی مانند [] به صورت encode شده در سمت کاربر نمایش داده نشوند؛ چون Razor به صورت پیش فرض اطلاعات را Encode می‌کند.
و یا اینکار را به صورت خلاصه به شکل زیر نیز می‌توان در سمت کاربر انجام داد:
 <script type="text/javascript">
  var model = @Html.Raw(Json.Encode(Model));
  // your js code here
</script>
در اینجا کار تبدیل اطلاعات مدل به JSON، در همان سمت RazorView انجام شده‌است.
مطالب
مهارت‌های تزریق وابستگی‌ها در برنامه‌های NET Core. - قسمت دوم - الگوی Service Locator
در قسمت قبل برای دریافت وهله‌ای از سرویس TestService، به صورت ()<serviceProvider.GetService<ITestService عمل کردیم. این روش در اصل الگوی Service Locator نام دارد که جزئیات بیشتری از آن‌را در این قسمت بررسی خواهیم کرد.


قلب سیستم تزریق وابستگی‌های NET Core. اینترفیس IServiceProvider است

IServiceProvider که اساس IoC Container برنامه‌های مبتنی بر NET Core. را تشکیل می‌دهد، در اسمبلی System.ComponentModel و در فضای نام System تعریف شده‌است:
namespace System
{
    public interface IServiceProvider
    {
        object GetService(Type serviceType);
    }
}
زمانیکه به کمک IServiceCollection، تمام اینترفیس‌ها و کلاس‌های خود را به IoC Container معرفی کردیم، مرحله‌ی بعدی، فراهم آوردن روشی برای دریافت وهله‌ای از این سرویس‌ها توسط متد GetService است.
استفاده‌ی مستقیم از اینترفیس IServiceProvider برای دسترسی به وهله‌های سرویس‌ها، اصطلاحا الگوی Service Locator نامیده می‌شود و باید تا حد ممکن از آن پرهیز کرد؛ چون وابستگی مستقیمی از IoC Container را درون کدهای ما قرار می‌دهد و به این ترتیب یک مرحله، نوشتن آزمون‌های واحد برای آن‌را مشکل‌تر می‌کند؛ چون زمان وهله سازی از یک سرویس، دقیقا مشخص نیست به چه وابستگی‌هایی نیاز دارد. به همین جهت همیشه باید با روش تزریق وابستگی‌ها در سازنده‌ی کلاس شروع کرد و اگر به هر دلیلی این روش مهیا نبود و توسط سیستم تزریق وابستگی‌های جاری شناسایی و یا پشتیبانی نمی‌شد (مانند تزریق وابستگی در سازنده‌های Attributes)، آنگاه می‌توان به الگوی Service Locator مراجعه کرد.
برای مثال در اکثر قسمت‌های برنامه‌های ASP.NET Core امکان تزریق وابستگی‌ها در سازنده‌ی کنترلرها، میان افزارها و سایر اجزای آن وجود دارد و در این حالات نیازی به مراجعه‌ی مستقیم به IServiceProvider برای دریافت وهله‌های سرویس‌های مورد نیاز نیست. به عبارتی نگرانی در مورد IServiceProvider بهتر است مشکل IoC Container باشد و نه ما.

در مثال زیر، روش استفاده‌ی از IServiceProvider را جهت انجام تزریق وابستگی‌ها (یا به عبارتی بهتر، روش دسترسی به وهله‌های وابستگی‌ها) را مشاهده می‌کنید:
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;

namespace CoreIocServices
{
    public interface IProductService
    {
        void Delete(int id);
    }

    public class ProductService : IProductService
    {
        private readonly ITestService _testService;
        private readonly ILogger<ProductService> _logger;

        public ProductService(IServiceProvider serviceProvider)
        {
            _testService = serviceProvider.GetRequiredService<ITestService>();
            _logger = serviceProvider.GetService<ILogger<ProductService>>() ?? NullLogger<ProductService>.Instance;
        }

        public void Delete(int id)
        {
            _testService.Run();
            _logger.LogInformation($"Deleted a product with id = {id}");
        }
    }
}
این روش یا کار مستقیم با Service locator، هر چند کار می‌کند، اما روشی است که باید تا حد ممکن از آن پرهیز کنید؛ زیرا:
- با نگاه کردن به امضای سازنده‌ی این سرویس مشخص نیست که دقیقا از چه وابستگی‌هایی استفاده می‌کند. اینکار نوشتن آزمون‌های واحد آن‌را مشکل می‌کند.
- این سرویس یک وابستگی اضافه‌تر را به نام IServiceProvider، نیز پیدا کرده‌است که اگر از روش متداول تزریق وابستگی‌ها در سازنده‌ی کلاس استفاده می‌شد، نیازی به ذکر آن نبود.
- پیاده سازی Dispose Pattern در این حالت مشکل‌تر است و در قسمتی دیگر بررسی خواهد شد.



تفاوت‌های بین متدهای ()<GetService<T  و  ()<GetRequiredService<T

از آنجائیکه دیگر از NET 1.0. استفاده نمی‌کنیم، استفاده‌ی از متد GetService با امضایی که در اینترفیس IServiceProvider تعریف شده و strongly typed نیست، بیشتر برای کارهای پویا مناسب است. به همین جهت دو نگارش جنریک از آن در اسمبلی Microsoft.Extensions.DependencyInjection.Abstractions با امضای زیر تعریف شده‌اند که نمونه‌ای از آن‌را در قسمت قبل نیز استفاده کردیم و برای استفاده‌ی از آن‌ها ذکر فضای نام Microsoft.Extensions.DependencyInjection ضروری است:
namespace Microsoft.Extensions.DependencyInjection
{
    public static class ServiceProviderServiceExtensions
    {
        public static T GetRequiredService<T>(this IServiceProvider provider);
        public static T GetService<T>(this IServiceProvider provider);
    }
}
اکنون این سؤال مطرح می‌شود که تفاوت‌های بین این دو متد چیست؟
- متد GetService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا نال؛ اگر سرویسی از نوع T، پیشتر به سیستم معرفی نشده باشد.
- متد GetRequiredService یک شیء سرویس از نوع T را بازگشت می‌دهد و یا اگر سرویسی از نوع T پیشتر به سیستم معرفی نشده باشد، استثنای InvalidOperationException را صادر می‌کند.

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


با توجه به این تفاوت‌ها کدامیک از متدهای GetService و یا GetRequiredService را باید استفاده کرد؟

همانطور که پیشتر نیز در توضیحات الگوی Service locator عنوان شد، هیچکدام! ابتدا با تزریق وابستگی‌های در سازنده‌ی کلاس شروع کنید و اگر تامین این وابستگی، توسط IoC Container جاری پشتیبانی نمی‌شد، آنگاه نیاز به استفاده‌ی از یکی از نگارش‌های متد GetService خواهد بود و متد توصیه شده نیز GetRequiredService است و نه GetService؛ به این دلایل:
- حذف کدهای تکراری: اگر از GetService استفاده کنید، نیاز خواهید داشت پس از تمام فراخوانی‌های آن، بررسی نال بودن آن‌را نیز انجام دهید. برای حذف این نوع کدهای تکراری، بهتر است از همان متد GetRequiredService استفاده کنید که به صورت توکار این بررسی را نیز انجام می‌دهد.
- پشتیبانی از روش Fail Fast و یا همان Defensive programming: اگر بررسی نال بودن GetService را فراموش کنید، در سطرهای بعدی، یافتن علت NullReferenceException صادر شده مشکل‌تر از رسیدگی به InvalidOperationException صادر شده‌ی توسط GetRequiredService خواهد بود که توضیحات دقیقی را در مورد سرویس ثبت نشده ارائه می‌دهد.
- اگر بر روی IoC Container پیش‌فرض NET Core. یک IoC Container دیگر را مانند AutoFac قرار داده‌اید، استفاده‌ی از GetRequiredService، سبب می‌شود تا اینگونه IoC Containerهای ثالث بتوانند اطلاعات مفیدتری را از سرویس‌های ثبت نشده ارائه دهند.

تنها حالتی که استفاده‌ی از روش GetService را نیاز دارد، شرطی کردن ثبت و معرفی کردن سرویس‌ها به IoC Container است؛ اگر سرویسی ثبت شده بود، آنگاه قطعه کدی اجرا شود.
نظرات اشتراک‌ها
اعتبارسنجی Domain Model‎
با توجه به مطلب «طراحی و پیاده سازی زیرساختی برای مدیریت خطاهای حاصل از Business Rule Validationها در ServiceLayer» می‌توان طراحی زیر را انجام داد:
public class Customer
{
    public string Email { get; private set; }
    public string Name { get; private set; }

    private Customer(email, name)
    {
        Email = email;
        Name = name;
    }

    public static Result<Customer> New(string email, string name, INewCustomerPolicy policy)
    {
        var isUnique = policy.IsUnique(email);
        if (!isUnique)
        {
            return Result.Fail<Customer>("Customer with this email already exists.");
        }

        var customer = new Customer(email, name);

        //customer.AddDomainEvent(new CustomerRegistered(customer));

        return Result.Ok(customer);
    }
}
کلاس Result در زیرساخت DNTFrameworkCore قابل دسترس می‌باشد.
مطالب
روشی سریع برای ایجاد RSS و Sitemap در ASP.NET MVC
برای مطالعه روش‌های بدست آوردن خروجی xml مربوط به Rss و Sitemap،  میتوانید از مقالات مشخص شده استفاده کنید .[اینجا] و [اینجا].

در صورتیکه طراحی شما بر اساس MVC صورت گرفته است، در کمتر از چند دقیقه و در سه مرحله میتوانید پرونده Rss و Sitemap را برای همیشه ببندید. 

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

مراحل کار : 

مرحله 1. ایجاد نوع(Type) مورد نیاز برای ایجاد Xml ‌های فوق

مرحله 2 . ایجاد کنترلر XML

مرحله 3. ایجاد مسیریابی(Routing)


مرحله 1 : ابتدا یک کلاس به منظور شکل دهی به اطلاعات، بر اساس خواسته‌های xml مرتبط با RSS و Sitemap تشکیل دهید:

public class PostToXml
    {
        
        public int PostId { get; set; }

        public string title { get; set; }

        public string link { get; set; }
        
        public string description { get; set; }

        public Nullable<DateTime> pubDate { get; set; }

      
    }

مرحله 2 : یک کنترلر به نام xml ایجاد کنید و اکشن متدهای زیر را درون آن قرار دهید :

       public ContentResult RSS()
        {
            
            var items = GetRssFeed();
            var rss = new XDocument(new XDeclaration("1.0", "utf-8", "yes"),
              new XElement("rss",
                new XAttribute("version", "2.0"),
                  new XElement("channel",
                    new XElement("title", "آخرین مطالب سایت"),
                    new XElement("link", "http://" + Request.Url.Host+"/rss"),
                    new XElement("description", "آخرین مطالب سایت من"),
                    new XElement("copyright","(c)" + DateTime.Now.Year + ", نام سایت من.تمامی حقوق محفوظ است"),
                  from item in items
                  select
                  new XElement("item",
                    new XElement("title", item.title),
                    new XElement("description", item.description),
                    new XElement("link", item.link),
                    new XElement("pubDate", item.pubDate)

                  )
                )
              )
            );
            return Content(rss.ToString(), "text/xml");
        }



        public ContentResult Sitemap()
        {
            XNamespace ns = "http://www.sitemaps.org/schemas/sitemap/0.9";
            var items = GetLinks();
            var sitemap = new XDocument(new XDeclaration("1.0", "utf-8", "yes"),
                new XElement(ns + "urlset",
                    from item in items
                    select
                    new XElement("url",
                      new XElement("loc", item.link),
                      new XElement("changefreq", "monthly"),
                      new XElement("priority", "0.5")
                      )
                    )
                  );
            return Content(sitemap.ToString(), "text/xml");
        }


        public IEnumerable<PostToXml> GetRssFeed()
        {
          // یک کوئری که لیستی از تایپ مشخص شده به ما بدهد
        }

         public IEnumerable<PostToXml> GetLinks()
        {
            // یک کوئری که لیستی از تایپ مشخص شده به ما بدهد
        }

این کنترلر دارای دو اکشن متد Rss و Sitemap است و این اکشن‌ها وظیفه‌ی ایجاد فایل‌های Xml را به عهده دارند. مواد اولیه این xml ‌ها از دو متد GetRssFeed و GetLinks تهیه می‌شوند. ما این مواد را در تمپلیت Rss و Sitemap جایگذاری خواهیم کرد. (به کمک دو اکشن متد Rss و Sitemap )

کافیست لیستی از مواردی را که می‌خواهیم در Rss یا Sitemap ثبت شوند، تهیه کنیم. این لیست بر اساس شکل تنظیم دیتابیس و مسیریابی سایت، می‌تواند پیچیده و یا ساده باشد. (به کمک کوئری گرفتن با linq و یا اضافه کردن مستقیم آدرس‌ها به لیست و یا ترکیبی از هر دو مورد) برای درک بهتر موضوع، لطفا تصویر موجود در ابتدای مقاله را مشاهده نمایید.

مرحله 3 : در مرحله آخر کافیست دو مورد زیر را به فایل RoutConfig.cs بیافزایید: 

routes.MapRoute(
              "Sitemap", "sitemap",
              new { controller = "XML", action = "Sitemap" });
 routes.MapRoute(
              "RSS", "rss",
               new { controller = "XML", action = "RSS" });

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

http://domain.com/rss
http://domain.com/sitemap  

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

MVC_RSS_Sitemap-43ad3c6681734b34b91deaaabcdba871.rar 

مطالب
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>
    <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>
کامپوننت 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 تعریف شده‌است:
{
  "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"
}
بنابراین برای استخراج این claims در سمت کلاینت، نیاز به یک JWT Parser داریم که نمونه‌ای از آن می‌تواند به صورت زیر باشد:
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);
        }
    }
}
که آن‌را در فایل BlazorWasm.Client\Utils\JwtParser.cs برنامه‌ی کلاینت ذخیره خواهیم کرد. متد ParseClaimsFromJwt فوق، رشته‌ی JWT تولیدی حاصل از لاگین موفق در سمت Web API را دریافت کرده و تبدیل به لیستی از Claimها می‌کند.


تامین 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>
سپس سرویس سفارشی AuthStateProvider خود را به پوشه‌ی Services برنامه اضافه می‌کنیم و متد GetAuthenticationStateAsync کلاس پایه‌ی AuthenticationStateProvider استاندارد را به نحو زیر بازنویسی و سفارشی سازی می‌کنیم:
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")
                        )
                    );
        }
    }
}
- اگر با برنامه‌های سمت کلاینت React و یا Angular پیشتر کار کرده باشید، منطق این کلاس بسیار آشنا به نظر می‌رسد. در این برنامه‌ها، مفهومی به نام Interceptor وجود دارد که توسط آن به صورت خودکار، هدر JWT را به تمام درخواست‌های ارسالی به سمت سرور، اضافه می‌کنند تا از تکرار این قطعه کد خاص، جلوگیری شود. علت اینجا است که برای دسترسی به منابع محافظت شده‌ی سمت سرور، نیاز است هدر ویژه‌ای را به نام "Authorization" که با مقدار "bearer jwt" تشکیل می‌شود، به ازای هر درخواست ارسالی به سمت سرور نیز ارسال کرد؛ تا تنظیمات ویژه‌ی AddJwtBearer که در قسمت 25 در کلاس آغازین برنامه‌ی Web API انجام دادیم، این هدر مورد انتظار را دریافت کرده و پردازش کند و در نتیجه‌ی آن، شیء this.User، در اکشن متدهای کنترلرها تشکیل شده و قابل استفاده شود.
در اینجا نیز مقدار دهی خودکار httpClient.DefaultRequestHeaders.Authorization را مشاهده می‌کنید که مقدار token خودش را از Local Storage دریافت می‌کند که کلید متناظر با آن‌را در پروژه‌ی BlazorServer.Common به صورت زیر تعریف کرده‌ایم:
namespace BlazorServer.Common
{
    public static class ConstantKeys
    {
        // ...
        public const string LocalToken = "JWT Token";
    }
}
به این ترتیب دیگر نیازی نخواهد بود در تمام سرویس‌های برنامه‌ی WASM که با HttpClient کار می‌کنند، مدام سطر مقدار دهی httpClient.DefaultRequestHeaders.Authorization را تکرار کنیم.
- همچنین در اینجا به کمک متد 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>();

            // ...
        }
    }
}
و برای سهولت استفاده‌ی از امکانات اعتبارسنجی فوق در کامپوننت‌های برنامه، فضای نام زیر را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم:
@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.
بنابراین می‌توان یک سرویس سراسری توکن را تهیه و به سادگی آن‌را در تمام قسمت‌های برنامه تزریق کرد. این روش هرچند کار می‌کند، اما همانطور که عنوان شد، به 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
نظرات مطالب
مسیریابی در AngularJs #بخش دوم
مشکلی رخ نداد.
1 - ماژول مسیریابی (ngRoute) رو باید تزریق کنید به ماژول اصلی :
var myFirstRoute = angular.module('myFirstRoute', ['ngRoute']);
2- route$ رو به کنترلر هم تزریق کنید :
myFirstRoute.controller('ShowPage1Controller', function ($route) {
    this.message = 'Content of page-one.html';
    //access the foodata property using $route.current
    var foo = $route.current.foodata;
    alert(foo);
});
3- این هم یک نمونه از تنظیمات مسیریابی :
myFirstRoute.config(['$routeProvider',
  function($routeProvider) {
    $routeProvider.
      when('/pageOne', {
        templateUrl: 'templates/page_one.html',
        controller: 'ShowPage1Controller',
        controllerAs: 'tCtrlOne',
        foodata: 'valueOne'
      }).
      when('/pageTwo', {
        templateUrl: 'templates/page_two.html',
        controller: 'ShowPage2Controller',
        controllerAs: 'tCtrlTwo',
        foodata: 'valueTwo'
      }).
      otherwise({
        redirectTo: '/pageOne'
      });
}]);
البته اینها فقط برای تصحیح اشتباه است. وگر نه به ControllerAs ارتباطی ندارد. به احتمال زیاد مشکل شما عدم تزریق route$ به کنترلر بوده.
مطالب
Blazor 5x - قسمت 19 - کار با فرم‌ها - بخش 7 - نکات ویژه‌ی کار با EF-Core در برنامه‌های Blazor Server
تا قسمت قبل، روشی را که برای کار با EF-Core درنظر گرفتیم، روش متداول کار با آن، در برنامه‌های ASP.NET Core Web API بود؛ یعنی این روش با برنامه‌های مبتنی بر Blazor WASM که از دو قسمت مجزای Web API سمت سرور و Web Assembly سمت کلاینت تشکیل شده‌اند، به خوبی جواب می‌دهد؛ اما ... با Blazor Server یکپارچه که تمام قسمت‌های مدیریتی آن سمت سرور رخ می‌دهند، خیر! در این مطلب، دلایل این موضوع را به همراه ارائه راه‌حلی، بررسی خواهیم کرد.


طول عمر سرویس‌ها، در برنامه‌های Blazor Server متفاوت هستند

هنگامیکه با یک ASP.NET Core Web API متداول کار می‌کنیم، درخواست‌های HTTP رسیده، از میان‌افزارهای موجود رد شده و پردازش می‌شوند. اما هنگامیکه با Blazor Server کار می‌کنیم، به علت وجود یک اتصال دائم SignalR که عموما از نوع Web socket است، دیگر درخواست HTTP وجود ندارد. تمام رفت و برگشت‌های برنامه به سرور و پاسخ‌های دریافتی، از طریق Web socket منتقل می‌شوند و نه درخواست‌ها و پاسخ‌های متداول HTTP.
این روش پردازشی، اولین تاثیری را که بر روی رفتار یک برنامه می‌گذارد، تغییر طول عمر سرویس‌های آن است. برای مثال در برنامه‌های Web API، طول عمر درخواست‌ها، از نوع Scoped هستند و با شروع پردازش یک درخواست، سرویس‌های مورد نیاز وهله سازی شده و در پایان درخواست، رها می‌شوند.
این مساله در حین کار با EF-Core نیز بسیار مهم است؛ از این جهت که در برنامه‌های Web API نیز EF-Core و DbContext آن، به صورت سرویس‌هایی با طول عمر Scoped تعریف می‌شوند. برای مثال زمانیکه یک چنین تعریفی را در برنامه داریم:
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
امضای واقعی متد AddDbContext مورد استفاده‌ی فوق، به صورت زیر است:
public static IServiceCollection AddDbContext<TContext>(
    [NotNullAttribute] this IServiceCollection serviceCollection, 
    [CanBeNullAttribute] Action<DbContextOptionsBuilder> optionsAction = null, 
    ServiceLifetime contextLifetime = ServiceLifetime.Scoped, 
    ServiceLifetime optionsLifetime = ServiceLifetime.Scoped) where TContext : DbContext;
همانطور که مشاهده می‌کنید، طول عمرهای پیش‌فرض تعریف شده‌ی در اینجا، از نوع Scoped هستند. یعنی زمانیکه سرویس‌های ApplicationDbContext را از طریق سیستم تزریق وابستگی‌های برنامه دریافت می‌کنیم، در ابتدای یک درخواست Web API، به صورت خودکار وهله سازی شده و در پایان درخواست رها می‌شوند. به این ترتیب به ازای هر درخواست رسیده، وهله‌ی متفاوتی از DbContex را دریافت می‌کنیم که با وهله‌ی استفاده شده‌ی در درخواست قبلی، یکی نیست.
اما زمانیکه مانند یک برنامه‌ی مبتنی بر Blazor Server، دیگر HTTP Requests متداولی را نداریم، چطور؟ در این حالت زمانیکه یک اتصال SignalR برقرار شد، وهله‌ای از DbContext که در اختیار برنامه‌ی Blazor Server قرار می‌گیرد، تا زمانیکه کاربر این اتصال را به نحوی قطع نکرده (مانند بستن کامل مرورگر و یا ریفرش صفحه)، ثابت باقی خواهد ماند. یعنی به ازای هر اتصال SignalR، طول عمر ServiceLifetime.Scoped پیش‌فرض تعریف شده، همانند یک وهله‌ی با طول عمر Singleton عمل می‌کند. در این حالت تمام صفحات و کامپوننت‌های یک برنامه‌ی Blazor Server، از یک تک وهله‌ی مشخص DbContext که در ابتدای کار دریافت کرده‌اند، کار می‌کنند و از آنجائیکه DbContext به صورت thread-safe کار نمی‌کند، این تک وهله مشکلات زیادی را ایجاد خواهد کرد که یک نمونه از آن‌را در عمل، در پایان قسمت قبل مشاهده کردید:
«اگر برنامه را اجرا کرده و سعی در حذف یک ردیف کنیم، به خطای زیر می‌رسیم و یا حتی اگر کاربر شروع کند به کلیک کردن سریع در قسمت‌های مختلف برنامه، باز هم این خطا مشاهده می‌شود:
 An exception occurred while iterating over the results of a query for context type 'BlazorServer.DataAccess.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context before a previous operation completed.
This is usually caused by different threads concurrently using the same instance of DbContext.
For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
عنوان می‌کند که متد OnConfirmDeleteRoomClicked، بر روی ترد دیگری نسبت به ترد اولیه‌ای که DbContext بر روی آن ایجاد شده، در حال اجرا است و چون DbContext برای یک چنین سناریوهایی، thread-safe نیست، اجازه‌ی استفاده‌ی از آن‌را نمی‌دهد.»
هر درخواست Web API نیز بر روی یک ترد جداگانه اجرا می‌شود؛ اما چون ابتدا و انتهای درخواست‌ها مشخص است، طول عمر Scoped، در ابتدای درخواست شروع شده و در پایان آن رها سازی می‌شود. به همین جهت استثنائی را که در اینجا مشاهده می‌کنید، در برنامه‌های Web API شاید هیچگاه مشاهده نشود.


معرفی DbContextFactory در EF Core 5x

همواره باید طول عمر DbContext را تا جای ممکن، کوتاه نگه داشت. مشکل فعلی ما، Singleton رفتار کردن DbContext‌ها (داشتن طول عمر طولانی) در برنامه‌های Blazor Server هستند. یک چنین رفتاری را شاید در برنامه‌های دسکتاپ هم پیشتر مشاهده کرده باشید. برای مثال در برنامه‌های دسکتاپ WPF، تا زمانیکه یک فرم باز است، Context ایجاد شده‌ی در آن هم برقرار است و Dispose نمی‌شود. در یک چنین حالت‌هایی، عموما Context را در زمان نیاز، ایجاد کرده و پس از پایان آن کار کوتاه، Context را رها می‌کنند. به همین جهت نیاز به DbContext Factory ای وجود دارد که بتواند یک چنین پیاده سازی‌هایی را میسر کند و خوشبختانه از زمان EF Core 5x، یک چنین امکانی خصوصا برای برنامه‌های Blazor Server تحت عنوان DbContextFactory ارائه شده‌است که به عنوان راه حل استاندارد دسترسی به DbContext در اینگونه برنامه‌ها مورد استفاده قرار می‌گیرد.
برای کار با DbContextFactory، اینبار در فایل BlazorServer.App\Startup.cs، بجای استفاده از services.AddDbContext، از متد AddDbContextFactory استفاده می‌شود:
public void ConfigureServices(IServiceCollection services)
{
    var connectionString = Configuration.GetConnectionString("DefaultConnection");
    //services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
    services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
سپس باید دقت داشت که روش استفاده‌ی از آن، نسبت به کار مستقیم با ApplicationDbContext، کاملا متفاوت است. هدف از DbContextFactory، ساخت دستی Context در زمان نیاز و سپس Dispose صریح آن است. بنابراین طول عمر Context دریافت شده‌ی توسط آن باید توسط برنامه نویس مدیریت شود و به صورت خودکار توسط IoC Container برنامه مدیریت نخواهد شد. در این حالت دو روش استفاده‌ی از آن در کامپوننت‌های برنامه‌های Blazor Server، پیشنهاد می‌شود.


روش اول کار با DbContextFactory در کامپوننت‌های Blazor Server : وهله سازی از نو، به ازای هر متد

در این روش پس از ثبت AddDbContextFactory در فایل Startup برنامه مانند مثال فوق، ابتدا سرویس IDbContextFactory که به ApplicationDbContext اشاره می‌کند به ابتدای کامپوننت تزریق می‌شود:
@inject IDbContextFactory<ApplicationDbContext> DbFactory
سپس در هر جائی که نیاز به وهله‌ای از ApplicationDbContext است، آن‌را به صورت دستی وهله سازی کرده و همانجا هم Dispose می‌کنیم:
private async Task DeleteImageAsync()
{
    using var context = DbFactory.CreateDbContext();

    var image = await context.HotelRoomImages.FindAsync(1);

   // ...
}
در اینجا یکی متدهای یک کامپوننت فرضی را مشاهده می‌کند که از DbFactory تزریق شده استفاده کرد و سپس با استفاده از متد ()CreateDbContext، وهله‌ی جدیدی از ApplicationDbContext را ایجاد می‌کند. همچنین در همان سطر، وجود عبارت using نیز مشاهده می‌شود. یعنی در پایان کار این متد، context ایجاد شده حتما Dispose شده و طول عمر کوتاهی خواهد داشت.


روش دوم کار با DbContextFactory در کامپوننت‌های Blazor Server : یکبار وهله سازی Context به ازای هر کامپوننت

در این روش می‌توان طول عمر Context را معادل طول عمر کامپوننت تعریف کرد که مزیت استفاده‌ی از Change tracking موجود در EF-Core را به همراه خواهد داشت. در این حالت کامپوننت‌های Blazor Server، شبیه به فرم‌های برنامه‌های دسکتاپ عمل می‌کنند:
@implements IDisposable
@inject IDbContextFactory<ApplicationDbContext> DbFactory


@code
{
   private ApplicationDbContext Context;

   protected override async Task OnInitializedAsync()
   {
       Context = DbFactory.CreateDbContext();
       await base.OnInitializedAsync();
   }

   private async Task DeleteImageAsync()
   {
       var image = await Context.HotelRoomImages.FindAsync(1);
       // ...
   }

   public void Dispose()
   {
     Context.Dispose();
   }
}
- در اینجا همانند روش اول، کار با تزریق IDbContextFactory شروع می‌شود
-  اما بجای اینکه به ازای هر متد، کار فراخوانی DbFactory.CreateDbContext صورت گیرد، یکبار در آغاز کار کامپوننت و در روال رویدادگردان OnInitializedAsync، کار وهله سازی Context کامپوننت انجام شده و از این تک Context در تمام متدهای کامپوننت استفاده خواهد شد.
- در این حالت کار Dispose خودکار این Context به متد Dispose نهایی کل کامپوننت واگذار شده‌است. برای اینکه این متد فراخوانی شود، نیاز است در ابتدای تعاریف کامپوننت، از دایرکتیو implements IDisposable@ استفاده کرد.


سؤال: اگر سرویسی از ApplicationDbContext تزریق شده‌ی در سازنده‌ی خود استفاده می‌کند، چکار باید کرد؟

برای نمونه سرویس‌های از پیش تعریف شده‌ی ASP.NET Core Identity، در سازنده‌ی خود از ApplicationDbContext استفاده می‌کنند و نه از IDbContextFactory. در این حالت برای تامین ApplicationDbContext‌های تزریق شده، فقط کافی است از روش زیر استفاده کنیم:
services.AddScoped<ApplicationDbContext>(serviceProvider =>
     serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
در این حالت به ازای هر Scope تعریف شده‌ی در برنامه، جهت دسترسی به ApplicationDbContext از طریق سیستم تزریق وابستگی‌ها، کار فراخوانی DbFactory.CreateDbContext به صورت خودکار انجام خواهد شد.


سؤال: روش پیاده سازی سرویس‌های یک برنامه Blazor Server به چه صورتی باید تغییر کند؟

تا اینجا روش‌هایی که برای استفاده از IDbContextFactory معرفی شدند (که روش‌های رسمی و توصیه شده‌ی اینکار نیز هستند)، فرض را بر این گذاشته‌اند که ما قرار است تمام منطق تجاری کار با بانک اطلاعاتی را داخل همان متدهای کامپوننت‌ها انجام دهیم (این روش برنامه نویسی، بسیار مورد علاقه‌ی مایکروسافت است و در تمام مثال‌های رسمی آن به صورت ضمنی توصیه می‌شود!). اما اگر همانند مثالی که تاکنون در این سری بررسی کردیم، نخواهیم اینکار را انجام دهیم و علاقمند باشیم تا این منطق تجاری را به سرویس‌های مجزایی، با مسئولیت‌های مشخصی انتقال دهیم، روش استفاده‌ی از IDbContextFactory چگونه خواهد بود؟
در این حالت از ترکیب روش دوم مطرح شده‌ی استفاده از IDbContextFactory که به همراه مزیت دسترسی کامل به Change Tracking توکار EF-Core و پیاده سازی الگوی واحد کار است و وهله سازی خودکار ApplicationDbContext که معرفی شد، استفاده خواهیم کرد؛ به این صورت:
الف) تمام سرویس‌های EF-Core یک برنامه‌ی Blazor Server باید اینترفیس IDisposable را پیاده سازی کنند.
این مورد برای سرویس‌های پروژه‌های Web API، ضروری نیست؛ چون طول عمر Context آن‌ها توسط خود IoC Container مدیریت می‌شود؛ اما در برنامه‌های Blazor Server، مطابق توضیحاتی که ارائه شد، خودمان باید این طول عمر را مدیریت کنیم.
بنابراین به پروژه‌ی سرویس‌های برنامه مراجعه کرده و هر سرویسی که ApplicationDbContext تزریق شده‌ای را در سازنده‌ی خود می‌پذیرد، یافته و تعریف اینترفیس آن‌را به صورت زیر تغییر می‌دهیم:
public interface IHotelRoomService : IDisposable
{
   // ...
}

public interface IHotelRoomImageService : IDisposable
{
   // ...
}
سپس باید اینترفیس‌های IDisposable را پیاده سازی کرد که روش مورد پذیرش code analyzer‌ها در این زمینه، رعایت الگوی زیر، دقیقا به همین شکل است و باید از دو متد تشکیل شود:
    public class HotelRoomService : IHotelRoomService
    {
        private bool _isDisposed;

        // ...

        public void Dispose()
        {
            Dispose(disposing: true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (!_isDisposed)
            {
                try
                {
                    if (disposing)
                    {
                        _dbContext.Dispose();
                    }
                }
                finally
                {
                    _isDisposed = true;
                }
            }
        }
    }
این الگو را به همین شکل برای سرویس HotelRoomImageService نیز پیاده سازی می‌کنیم.


ب) Dispose دستی تمام سرویس‌ها، در کامپوننت‌های مرتبط
در ادامه تمام کامپوننت‌هایی را که از سرویس‌های فوق استفاده می‌کنند یافته و ابتدا دایرکتیو implements IDisposable@ را به ابتدای آن‌ها اضافه می‌کنیم. سپس متد Dispose آن‌ها را جهت فراخوانی متد Dispose سرویس‌های فوق، تکمیل خواهیم کرد:
بنابراین ابتدا به فایل BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomUpsert.razor مراجعه کرده و تغییرات زیر را اعمال می‌کنیم:
@page "/hotel-room/create"
@page "/hotel-room/edit/{Id:int}"

@implements IDisposable
// ...


@code
{
    // ...

    public void Dispose()
    {
        HotelRoomImageService.Dispose();
        HotelRoomService.Dispose();
    }
}
و همچنین به کامپوننت BlazorServer\BlazorServer.App\Pages\HotelRoom\HotelRoomList.razor مراجعه کرده و آن‌را به صورت زیر جهت Dispose دستی سرویس‌ها، تکمیل می‌کنیم:
@page "/hotel-room"

@implements IDisposable
// ...


@code
{
    // ...

    public void Dispose()
    {
        HotelRoomService.Dispose();
    }
}


مشکل! اینبار خطای dispose شدن context را دریافت می‌کنیم!

System.ObjectDisposedException: Cannot access a disposed context instance.
A common cause of this error is disposing a context instance that was resolved from dependency injection and then
later trying to use the same context instance elsewhere in your application. This may occur if you are calling 'Dispose'
on the context instance, or wrapping it in a using statement. If you are using dependency injection, you should let the
dependency injection container take care of disposing context instances.
Object name: 'ApplicationDbContext'.
هم برنامه‌های Blazor WASM و هم برنامه‌های Blazor Server از مفهوم طول عمرهای تنظیم شده‌ی سرویس‌ها پشتیبانی نمی‌کنند! در هر دوی این‌ها اگر سرویسی را با طول عمر Scoped تنظیم کردیم، رفتار آن همانند سرویس‌های Singleton خواهد بود. تنها زمانی رفتارهای Scoped و یا Transient پشتیبانی می‌شوند که درخواست HTTP ای رخ داده باشد که این مورد خارج است از طول عمر یک برنامه‌ی Blazor WASM و همچنین اتصال SignalR برنامه‌های Blazor Server. فقط قسمت‌هایی از برنامه‌ی Blazor Server که با مدل قبلی Razor pages طراحی شده‌اند، چون سبب شروع یک درخواست HTTP معمولی می‌شوند، همانند برنامه‌های متداول ASP.NET Core رفتار می‌کنند و در این حالت طول عمرهای غیر Singleton مفهوم پیدا می‌کنند.

مشکلی که در اینجا رخ داده این است که سرویس‌هایی را داریم با طول عمر به ظاهر Scoped که یکی از وابستگی‌های آن‌ها را به صورت دستی Dispose کرده‌ایم. چون طول عمر Scoped در اینجا وجود ندارد و طول عمرها در اصل Singleton هستند، هربار که سرویس مدنظر مجددا درخواست شود، همان وهله‌ی ابتدایی که اکنون یکی از وابستگی‌های آن Dispose شده، در اختیار برنامه قرار می‌گیرد.
پس از این تغییرات، اولین باری که برنامه را اجرا می‌کنیم، لیست اتاق‌ها به خوبی نمایش داده می‌شوند و مشکلی نیست. بعد در همین حال و در همین صفحه، اگر بر روی دکمه‌ی افزودن یک اتاق جدید کلیک کنیم، اتفاقی که رخ می‌دهد، فراخوانی متد Dispose کامپوننت لیست اتاق‌ها است (بر روی آن یک break-point قرار دهید). بنابراین متد Dispose یک کامپوننت، با هدایت به یک مسیر دیگر، به صورت خودکار فراخوانی می‌شود. در این حالت Context برنامه Dispose شده و در کامپوننت ثبت یک اتاق جدید دیگر، در دسترس نخواهد بود؛ چون IHotelRoomService مورد استفاده مجددا وهله سازی نمی‌شود و از همان وهله‌ای که بار اول ایجاد شده، استفاده خواهد شد.
 
بنابراین سؤال اینجا است که چگونه می‌توان سیستم تزریق وابستگی‌ها را وادار کرد تا تمام سرویس‌های تزریق شده‌ی به سازنده‌ها‌ی سرویس‌های HotelRoomService و  HotelRoomImageService را مجددا وهله سازی کند و سعی نکند از همان وهله‌های قبلی استفاده کند؟

پاسخ: یک روش این است که IHotelRoomImageService را خودمان به ازای هر کامپوننت به صورت دستی در روال رویدادگردان OnInitializedAsync وهله سازی کرده و DbFactory.CreateDbContext جدیدی را مستقیما به سازنده‌ی آن ارسال کنیم. در این حالت مطمئن خواهیم شد که این وهله، جای دیگری به اشتراک گذاشته نمی‌شود:
@code
{
   private IHotelRoomImageService HotelRoomImageService;

   protected override async Task OnInitializedAsync()
   {
       HotelRoomImageService =  new HotelRoomImageService(DbFactory.CreateDbContext(), mapper);
       await base.OnInitializedAsync();
   }

   private async Task DeleteImageAsync()
   {
       await HotelRoomImageService.DeleteAsync(1);
       // ...
   }

   public void Dispose()
   {
     HotelRoomImageService.Dispose();
   }
}
هرچند این روش کار می‌کند، اما در زمان استفاده از IoC Container‌ها قرار نیست کار انجام new‌ها را خودمان به صورت دستی انجام دهیم و بهتر است مدیریت این مساله به آن‌ها واگذار شود.


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

بنابراین مشکل ما Singleton رفتار کردن سرویس‌ها، در برنامه‌های Blazor است. برای مثال در برنامه‌های Blazor Server، تا زمانیکه اتصال SignalR برنامه برقرار است (مرورگر بسته نشده، برگه‌ی جاری بسته نشده و یا کاربر صفحه را ریفرش نکرده)، هیچ سرویسی دوباره وهله سازی نمی‌شود.
برای رفع این مشکل، امکان Scoped رفتار کردن سرویس‌های یک کامپوننت نیز در نظر گرفته شده‌اند. برای نمونه کدهای کامپوننت HotelRoomList.razor را به صورت زیر تغییر می‌دهیم:
@page "/hotel-room"

@*@implements IDisposable*@
@*@inject IHotelRoomService HotelRoomService*@
@inherits OwningComponentBase<IHotelRoomService>
با استفاده از دایرکتیو جدید inherits OwningComponentBase@ می‌توان میدان دید یک سرویس را به طول عمر کامپوننت جاری محدود کرد. هربار که این کامپوننت نمایش داده می‌شود، وهله سازی شده و هربار که به کامپوننت دیگری هدایت می‌شویم، به صورت خودکار سرویس مورد استفاده را Dispose می‌کند. بنابراین در اینجا دیگر نیازی به ذکر دایرکتیو implements IDisposable@ نیست.

چند نکته:
- فقط یکبار به ازای هر کامپوننت می‌توان از دایرکتیو inherits استفاده کرد.
- زمانیکه طول عمر سرویسی را توسط OwningComponentBase مدیریت می‌کنیم، در حقیقت یک کلاس پایه را برای آن کامپوننت درنظر گرفته‌ایم که به همراه یک خاصیت عمومی ویژه، به نام Service و از نوع سرویس مدنظر ما است. در این حالت یا می‌توان از خاصیت Service به صورت مستقیم استفاده کرد و یا می‌توان به صورت زیر، همان کدهای قبلی را داشت و هربار که نیازی به HotelRoomService بود، آن‌را به خاصیت عمومی Service هدایت کرد:
@code
{
   private IHotelRoomService HotelRoomService => Service;
- اگر نیاز به بیش از یک سرویس وجود داشت، چون نمی‌توان بیش از یک inherits را تعریف کرد، می‌توان از نمونه‌ی غیرجنریک OwningComponentBase استفاده کرد:
@page "/preferences"
@using Microsoft.Extensions.DependencyInjection
@inherits OwningComponentBase


@code {
    private IHotelRoomService HotelRoomService { get; set; }
    private IHotelRoomImageService HotelRoomImageService { get; set; }

    protected override void OnInitialized()
    {
        HotelRoomService = ScopedServices.GetRequiredService<IHotelRoomService>();
        HotelRoomImageService = ScopedServices.GetRequiredService<IHotelRoomImageService>();
    }
}
در این حالت کلاس پایه‌ی OwningComponentBase، به همراه خاصیت جدید ScopedServices است که با فراخوانی متد GetRequiredService در روال رویدادگردان OnInitialized بر روی آن، سبب وهله سازی Scoped سرویس مدنظر خواهد شد. نمونه‌ی جنریک آن، تمام این موارد را در پشت صحنه انجام می‌دهد و کار کردن با آن ساده‌تر و خلاصه‌تر است.


خلاصه‌ی بحث جاری در مورد روش مدیریت DbContext برنامه‌های Blazor Server:

- بجای services.AddDbContext متداول، باید از AddDbContextFactory استفاده کرد:
services.AddDbContextFactory<ApplicationDbContext>(options => options.UseSqlServer(connectionString));
services.AddScoped<ApplicationDbContext>(serviceProvider =>
        serviceProvider.GetRequiredService<IDbContextFactory<ApplicationDbContext>>().CreateDbContext());
- تمام سرویس‌هایی که از ApplicationDbContext استفاده می‌کنند، باید به همراه پیاده سازی Dispose آن نیز باشند؛ چون Scope یک سرویس، معادل طول عمر اتصال SignalR برنامه است و مدام وهله سازی نمی‌شود. در این حالت باید وهله سازی و Dispose آن‌را دستی مدیریت کرد.
- کامپوننت‌های برنامه، سرویس‌هایی را که باید Scoped عمل کنند، دیگر نباید از طریق تزریق مستقیم آن‌ها دریافت کنند؛ چون در این حالت همواره به همان وهله‌ای که در ابتدای کار ایجاد شده، می‌رسیم:
@inject IHotelRoomService HotelRoomService
این دریافت باید با استفاده از کلاس پایه OwningComponentBase صورت گیرد:
@inherits OwningComponentBase<IHotelRoomService>
تا عملیات فراخوانی خودکار ScopedServices.GetRequiredService (دریافت وهله‌ی جدید Scoped) و همچنین Dispose خودکار آن‌ها را به ازای هر کامپوننت مجزا، مدیریت کند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-19.zip
مطالب
اعمال SEO بر روی AngularJS
در این بخش قصد داریم سئو را بر روی یک برنامه‌ی نوشته شده با آنگلولار و Asp.net Mvc اعمال نماییم. انگولار جی‌اس، صفحات را با  استفاده از جاوااسکریپت رندر میکند، ولی اکثر کرالر‌ها نمیتوانند جاوااسکریپت را اجرا کنند و موقع اجرای صفحات سایت ما  فقط یک div خالی را میبینند.
کاری که سرویس Prerender یا فیلتر سفارشی AjaxCrawlable برای ما انجام میدهد، درخواست‌هایی را که از طرف کرالرها آمده‌است را شناسایی میکند و مانند یک مرورگر، با استفاده از phantomjs آنرا اجرا میکند و نتیجه‌ی کامل صفحات ما را به صورت اچ تی ام ال استاتیک برمی‌گرداند.
فانتوم جی اس، موتور اختصاصی برای شبیه سازی مرورگر مبتنی بر Webkit می‌باشد. فانتوم جی اس را میتوانید بر روی ویندوز، لینوکس و مک نصب نمایید. فانتوم جی اس یک Console در اختیار برنامه نویس قرار می‌دهد که می‌توان توسط آن، برنامه‌های جاوااسکریپت را اجرا نمود. همچنین فانتوم جی اس میتواند اسکرین شاتی را نیز از محتوای وب سایت ما فراهم نماید.
 برای اینکه صفحات انگولار جی اس،ایندکس شوند سه مرحله وجود دارند:
1- به کرالر اطلاع دهیم که رندر کردن سایت، توسط جاوااسکریپت انجام میگردد؛ با اضافه کردن متاتگ زیر در اچ تی ام ال سایت (البته در حالت استفاده HTML5 push state ) :
<meta name="fragment" content="!">
<base href="/">
2- بعد از اضافه کردن متاتگ بالا، کرالر درخواست‌های خود را به صورت زیر به سایت ما ارسال میکند:
http://www.example.com/?_escaped_fragment_=
ما در این مثال از  HTML5 push state  استفاده میکنیم. بنابراین لینکی مانند http://www.example.com/user/123 توسط کرالر به صورت زیر دیده میشود: 
http://www.example.com/user/123?_escaped_fragment_=
3- اچ تی ام ال کاملا رندر شده توسط سایت ما به کرالر ارسال گردد.
برای رندر کردن  اچ تی ام ال صفحات، چندین روش وجود دارد:
روش اول: میتوانیم از سرویس‌های آماده‌ای همچون Prerender.io   استفاده کنیم که سرویسهایی را برای زبانهای مختلف ارائه کرده‌اند. باتوجه به توضیحات نمونه استفاده از آن در Asp.Net Mvc کافیست در سایت Prerender.io  ثبت نام کرده، Token را دریافت کنیم و در کانفیگ برنامه قرار دهیم و در کلاس PreStart قطعه کد زیر را قرار دهیم:
DynamicModuleUtility.RegisterModule(typeof(Prerender.io.PrerenderModule));
مثال استفاده از Prerender.io را میتوانید از این آدرس Simple_Demo_Prerender.zip دانلود نمایید.
 
یکی از ابزارهای مناسب تست کردن اینکه صفحات توسط کرالر ایندکس میشوند یا خیر، برنامه screamingfrog میباشد.
در پنل Ajax آن، صفحات ایندکس شده ما نمایش داده میشوند. لینکی مشابه زیر را در مرورگر اجرا کرده، با ViewPage Source کردن آن میتوانید نتیجه اچ تی ام ال کاملا رندر شده را مشاهده نمایید.
http://www.example.com/user/123?_escaped_fragment_=
نسخه رایگان سرویس Prerender.io تا 250 صفحه را پوشش میدهد.

روش دوم: فیلتر سفارشی AjaxCrawlable. در اولین قدم نیاز به نصب فانتوم جی اس داریم:
<package id="PhantomJS" version="1.9.2" targetFramework="net452" />
<package id="phantomjs.exe" version="1.9.2.1" targetFramework="net452" />
فایل phantomjs.exe را از پوشه packages\PhantomJS.1.9.2\tools\phantomjs\phantomjs.exe یافته و در پوشه bin برنامه قرار دهید. با Attribute زیر هر درخواستی که توسط کرالر ارسال گردد به اکشن returnHTML منتقل میگردد.
برای اینکه خطای معروف A potentially dangerous Request.Form value was detected from the client را دریافت نکنیم، کافیست قسمتهایی از آدرس را که شامل کاراکترهای خاصی مانند :// میباشند، از url حذف کنیم و در اکشن returnHtml قسمتهای حذف شده را  به url  اضافه نماییم.
کرالرها  با مشاهده تگ fragment، تمام لینکها را به همراه کوئری استرینگ _escaped_fragment_  میفرستند، که ما در سرور باید آنرا  با رشته خالی جایگزین نماییم.
 public class AjaxCrawlableAttribute : System.Web.Mvc.ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;
            var url = request.Url.ToString();
            if (request.QueryString[Fragment] != null && !url.Contains("HtmlSnapshot/returnHTML"))
            {
                url = url.Replace("?_escaped_fragment_=", string.Empty).Replace(request.Url.Scheme + "://", string.Empty);
                url = url.Split(':')[1];
                filterContext.Result = new RedirectToRouteResult(
                   new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
Route‌های پیشفرض را با کدهای زیر جایگزین میکنیم:
public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
             name: "HtmlSnapshot",
             url: "HtmlSnapshot/returnHTML/{*url}",
             defaults: new { controller = "HtmlSnapshot", action = "returnHTML", url = UrlParameter.Optional });

            routes.MapRoute(
            name: "SPA",
            url: "{*catchall}",
            defaults: new { controller = "Home", action = "Index" })
        }
 اضافه کردن این فیلتر به فیلترهای Asp.net Mvc 
 public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
ایجاد کنترلر HtmlSnapshot و متد returnHTML :
Url را به عنوان آرگومان به تابع page.open فایل جاوااسکریپتی فانتوم میدهیم و بعد از اجرای کامل، خروجی را درViewData قرار میدهیم 
public ActionResult returnHTML(string url)
        {
            var prefix = HttpContext.Request.Url.Scheme + "://" + HttpContext.Request.Url.Host+":";
            url = prefix+url;
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
            var startInfo = new ProcessStartInfo
            {
                Arguments = string.Format("{0} {1}", Path.Combine(appRoot, "Scripts\\seo.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output1 = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output1.Replace("<!-- ngView:  -->", "").Replace("ng-view=\"\"", "");
            return View();
        }
در فایل renderHtml.cshtml
@{ 
    Layout = null;
}
@Html.Raw(ViewBag.result)
ایجاد فایل seo.js  در پوشه Scripts سایت :
در این بخش webpage  را ایجاد میکنیم و آدرس صفحه را از[system.args[1  دریافت کرده و عملیات کپچر کردن را آغاز میکنیم و بعد از تکمیل اطلاعات در سرور، کد زیر اجرا میشود:
console.log(page.content)

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();;
page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () {

});

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        console.log(page.content);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
صفحه Layout.Cshtml
<!DOCTYPE html>
<html ng-app="appOne">
<head>
    <meta name="fragment" content="!">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta charset="utf-8" />
    <link href="~/favicon.ico" rel="shortcut icon" type="image/x-icon" />
    <meta name="viewport" content="width=device-width" />
    <base href="/">
    @Styles.Render("~/Content/css")
    @Scripts.Render("~/bundles/modernizr")
    <script src="~/Scripts/angular/angular.js"></script>
    <script src="~/Scripts/angular/angular-route.js"></script>
    <script src="~/Scripts/angular/angular-animate.js"></script>
    <script>
        angular.module('appOne', ['ngRoute'], function ($routeProvider, $locationProvider) {
            $routeProvider.when('/one', {
                template: "<div>one</div>", controller: function ($scope) {
                }
            })
            .when('/two', {
                template: "<div>two</div>", controller: function ($scope) {
                }
            }).when('/', {
                template: "<div>home</div>", controller: function ($scope) {
                }
            });
            $locationProvider.html5Mode({
                enabled: true
            });
        });
    </script>
</head>
<body>
    <div id="body">
        <section ng-view></section>
        @RenderBody()
    </div>
    <div id="footer">
        <ul class='xoxo blogroll'>
            <li><a href="one">one</a></li>
            <li><a href="two">two</a></li>
        </ul>
    </div>
</body>
</html>

چند نکته تکمیلی:
* فانتوم جی اس قادر به اجرای لینکهای فارسی (utf-8) نمیباشد.
 * اگر خطای syntax error را دریافت کردید ممکن است پروژه شما در مسیری طولانی در روی هارد دیسک قرار داشته باشد.
نظرات مطالب
الگوی استراتژی - Strategy Pattern
- اگر پروژه خودتون هست، از اینترفیس استفاده کنید. تغییرات آن و نگارش‌های بعدی آن تحت کنترل خودتان است و build دیگران را تحت تاثیر قرار نمی‌دهد.
- در پروژه‌های سورس باز دات نت، عموما از ترکیب این دو استفاده می‌شود. مواردی که قرار است در اختیار عموم باشند حتی دو لایه هم می‌شوند. مثلا در MVC یک اینترفیس IController هست و بعد یک کلاس Abstract به نام Controller، که این اینترفیس را پیاده سازی کرده برای ورژن پذیری بعدی و کنترلرهای پروژه‌های عمومی MVC از این کلاس Abstract مشتق می‌شوند یا در پروژه RavenDB از کلاس‌های Abstract زیاد استفاده شده، مانند AbstractIndexCreationTask و AbstractMultiMapIndexCreationTask و غیره.
مطالب
معماری لایه بندی نرم افزار #3

Service Layer

نقش لایه‌ی سرویس این است که به عنوان یک مدخل ورودی به برنامه کاربردی عمل کند. در برخی مواقع این لایه را به عنوان لایه‌ی Facade نیز می‌شناسند. این لایه، داده‌ها را در قالب یک نوع داده ای قوی (Strongly Typed) به نام View Model، برای لایه‌ی Presentation فراهم می‌کند. کلاس View Model یک Strongly Typed محسوب می‌شود که نماهای خاصی از داده‌ها را که متفاوت از دید یا نمای تجاری آن است، بصورت بهینه ارائه می‌نماید. در مورد الگوی View Model در مباحث بعدی بیشتر صحبت خواهم کرد.

الگوی Facade یک Interface ساده را به منظور کنترل دسترسی به مجموعه ای از Interface‌ها و زیر سیستم‌های پیچیده ارائه می‌کند. در مباحث بعدی در مورد آن بیشتر صحبت خواهم کرد.

کلاسی با نام ProductViewModel را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductViewModel
{
    Public int ProductId {get; set;}
    public string Name { get; set; }
    public string Rrp { get; set; }
    public string SellingPrice { get; set; }
    public string Discount { get; set; }
    public string Savings { get; set; }
}

برای اینکه کلاینت با لایه‌ی سرویس در تعامل باشد باید از الگوی Request/Response Message استفاده کنیم. بخش Request توسط کلاینت تغذیه می‌شود و پارامترهای مورد نیاز را فراهم می‌کند. کلاسی با نام ProductListRequest را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Service
{
    public class ProductListRequest
    {
        public CustomerType CustomerType { get; set; }
    }
}

در شی Response نیز بررسی می‌کنیم که درخواست به درستی انجام شده باشد، داده‌های مورد نیاز را برای کلاینت فراهم می‌کنیم و همچنین در صورت عدم اجرای صحیح درخواست، پیام مناسب را به کلاینت ارسال می‌نماییم. کلاسی با نام ProductListResponse را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductListResponse
{
    public bool Success { get; set; }
    public string Message { get; set; }
    public IList<ProductViewModel> Products { get; set; }
}

به منظور تبدیل موجودیت Product به ProductViewModel، به دو متد نیاز داریم، یکی برای تبدیل یک Product و دیگری برای تبدیل لیستی از Product. شما می‌توانید این دو متد را به کلاس Product موجود در Domain Model اضافه نمایید، اما این متدها نیاز واقعی منطق تجاری نمی‌باشند. بنابراین بهترین انتخاب، استفاده از Extension Method‌ها می‌باشد که باید برای کلاس Product و در لایه‌ی سرویس ایجاد نمایید. کلاسی با نام ProductMapperExtensionMethods را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public static class ProductMapperExtensionMethods
{
    public static ProductViewModel ConvertToProductViewModel(this Model.Product product)
    {
        ProductViewModel productViewModel = new ProductViewModel();
        productViewModel.ProductId = product.Id;
        productViewModel.Name = product.Name;
        productViewModel.RRP = String.Format(“{0:C}”, product.Price.RRP);
        productViewModel.SellingPrice = String.Format(“{0:C}”, product.Price.SellingPrice);
        if (product.Price.Discount > 0)
            productViewModel.Discount = String.Format(“{0:C}”, product.Price.Discount);
        if (product.Price.Savings < 1 && product.Price.Savings > 0)
            productViewModel.Savings = product.Price.Savings.ToString(“#%”);
        return productViewModel;
    }
    public static IList<ProductViewModel> ConvertToProductListViewModel(
        this IList<Model.Product> products)
    {
        IList<ProductViewModel> productViewModels = new List<ProductViewModel>();
        foreach(Model.Product p in products)
        {
            productViewModels.Add(p.ConvertToProductViewModel());
        }
        return productViewModels;
    }
}

حال کلاس ProductService را جهت تعامل با کلاس سرویس موجود در Domain Model و به منظور برگرداندن لیستی از محصولات و تبدیل آن به لیستی از ProductViewModel، ایجاد می‌نماییم. کلاسی با نام ProductService را با کد زیر به پروژه SoCPatterns.Layered.Service اضافه کنید:

public class ProductService
{
    private Model.ProductService _productService;
    public ProductService(Model.ProductService ProductService)
    {
        _productService = ProductService;
    }
    public ProductListResponse GetAllProductsFor(
        ProductListRequest productListRequest)
    {
        ProductListResponse productListResponse = new ProductListResponse();
        try
        {
            IList<Model.Product> productEntities =
                _productService.GetAllProductsFor(productListRequest.CustomerType);
            productListResponse.Products = productEntities.ConvertToProductListViewModel();
            productListResponse.Success = true;
        }
        catch (Exception ex)
        {
            // Log the exception…
            productListResponse.Success = false;
            // Return a friendly error message
            productListResponse.Message = ex.Message;
        }
        return productListResponse;
    }
}

کلاس Service تمامی خطاها را دریافت نموده و پس از مدیریت خطا، پیغامی مناسب را به کلاینت ارسال می‌کند. همچنین این لایه محل مناسبی برای Log کردن خطاها می‌باشد. در اینجا کد نویسی لایه سرویس به پایان رسید و در ادامه به کدنویسی Data Layer می‌پردازیم.

Data Layer

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

برای اینکه کدهای بانک اطلاعاتی را سریعتر تولید کنیم از روش Linq to SQL در Data Layer استفاده می‌کنم. برای این منظور یک Data Context برای Linq to SQL به این لایه اضافه می‌کنیم. بر روی پروژه SoCPatterns.Layered.Repository کلیک راست نمایید و گزینه Add > New Item را انتخاب کنید. در پنجره ظاهر شده و از سمت چپ گزینه Data و سپس از سمت راست گزینه Linq to SQL Classes را انتخاب نموده و نام آن را Shop.dbml تعیین نمایید.

از طریق پنجره Server Explorer به پایگاه داده مورد نظر متصل شوید و با عمل Drag & Drop جدول Product را به بخش Design کشیده و رها نمایید.

اگر به یاد داشته باشید، در لایه Model برای برقراری ارتباط با پایگاه داده از یک Interface به نام IProductRepository استفاده نمودیم. حال باید این Interface را پیاده سازی نماییم. کلاسی با نام ProductRepository را با کد زیر به پروژه SoCPatterns.Layered.Repository اضافه کنید:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SoCPatterns.Layered.Model;

namespace SoCPatterns.Layered.Repository
{
    public class ProductRepository : IProductRepository
    {
        public IList<Model.Product> FindAll()
        {
            var products = from p in new ShopDataContext().Products
                                select new Model.Product
                                {
                                    Id = p.ProductId,
                                    Name = p.ProductName,
                                    Price = new Model.Price(p.Rrp, p.SellingPrice)
                                };
            return products.ToList();
        }
    }
}

در متد FindAll، با استفاده از دستورات Linq to SQL، لیست تمامی محصولات را برگرداندیم. کدنویسی لایه‌ی Data هم به پایان رسید و در ادامه به کدنویسی لایه‌ی Presentation و UI می‌پردازیم.

Presentation Layer

به منظور جداسازی منطق نمایش (Presentation) از رابط کاربری (User Interface)، از الگوی Model View Presenter یا همان MVP استفاده می‌کنیم که در مباحث بعدی با جزئیات بیشتری در مورد آن صحبت خواهم کرد. یک Interface با نام IProductListView را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

public interface IProductListView
{
    void Display(IList<ProductViewModel> Products);
    Model.CustomerType CustomerType { get; }
    string ErrorMessage { set; }
}

این Interface توسط Web Form‌های ASP.NET و یا Win Form‌ها باید پیاده سازی شوند. کار با Interface‌ها موجب می‌شود تا تست View‌ها به راحتی انجام شوند. کلاسی با نام ProductListPresenter را با کد زیر به پروژه SoCPatterns.Layered.Presentation اضافه کنید:

using SoCPatterns.Layered.Service;

namespace SoCPatterns.Layered.Presentation
{
    public class ProductListPresenter
    {
        private IProductListView _productListView;
        private Service.ProductService _productService;
        public ProductListPresenter(IProductListView ProductListView,
            Service.ProductService ProductService)
        {
            _productService = ProductService;
            _productListView = ProductListView;
        }
        public void Display()
        {
            ProductListRequest productListRequest = new ProductListRequest();
            productListRequest.CustomerType = _productListView.CustomerType;
            ProductListResponse productResponse =
                _productService.GetAllProductsFor(productListRequest);
            if (productResponse.Success)
            {
                _productListView.Display(productResponse.Products);
            }
            else
            {
                _productListView.ErrorMessage = productResponse.Message;
            }
        }
    }
}

کلاس Presenter وظیفه‌ی واکشی داده ها، مدیریت رویدادها و بروزرسانی UI را دارد. در اینجا کدنویسی لایه‌ی Presentation به پایان رسیده است. از مزایای وجود لایه‌ی Presentation این است که تست نویسی مربوط به نمایش داده‌ها و تعامل بین کاربر و سیستم به سهولت انجام می‌شود بدون آنکه نگران دشواری Unit Test نویسی Web Form‌ها باشید. حال می‌توانید کد نویسی مربوط به UI را انجام دهید که در ادامه به کد نویسی در Win Forms و Web Forms خواهیم پرداخت.