مطالب
خروجی گرفتن از برنامه‌های NET Core 3. بدون وابستگی به فریم‌ورک و در یک فایل Exe
دیشب نسخه 6 پیش‌نمایش دات نت کور 3، منتشر شد و ویژگی‌های بسیار خوبی را ارائه کرد و بهانه‌ای شد تا با برخی از ویژگی‌ها مخصوص خروجی گرفتن از برنامه در دات نت کور 3 آشنا بشویم.

PublishSingleFile
توسط این دستور میتوانید برنامه خودتان را همراه با تمام اسمبلی‌ها و فایل‌ها، در یک فایل Exe قرار دهید و اجرا کنید. برای بار اول اجرا ممکن است چند ثانیه طول بکشد ولی از دفعات بعد، تاخیری در اجرا نخواهیم داشت. در واقع این روش تمام فایل‌ها را فشرده کرده و با اجرا، در مسیر مشخصی آنپک می‌کند.
روش استفاده از آن به این صورت هست که پنجره CMD را در کنار فایل پروژه باز کنید و دستور زیر را اجرا کنید: 
 dotnet publish -r win10-x64 /p:PublishSingleFile=true

ReadyToRun 
 توسط این دستور میتوانید سرعت اجرای برنامه‌های دات نت کور را شدیدا بهبود بخشید. شما میتوانید با کامپایل کردن اسمبلی‌های برنامه به فرمت R2R، علاوه بر کاهش زمان اجرای برنامه، میزان استفاده از حافظه را هم کاهش دهید. البته حجم برنامه‌ی خروجی در این روش بیشتر است؛ چون هم شامل کدهای IL برنامه هست و هم شامل همان کدها بصورت Native تا بتواند اجرای برنامه را سرعت ببخشد.
نتایج زیر، بر روی پروژه‌ی ساده‌ی WPF اجرا شده 
 With ReadyToRun images  IL-only Application
 Startup time: 1.3 seconds   Startup time: 1.9 seconds 
 Memory usage: 55.7 MB   Memory usage: 69.1 MB 
 Application size: 156 MB   Application size: 150 MB 

برای فعالسازی آن، تگ زیر را به فایل پروژه اضافه کنید:
  <PublishReadyToRun>true</PublishReadyToRun>
و سپس با دستور زیر در cmd اقدام به گرفتن خروجی کنید:
 dotnet publish -r win-x64 -c Release
  در حال حاضر این روش فقط برای حالت Self-Contained کار می‌کند و در نسخه‌ی بعدی برای FramworkDependent هم فعال می‌شود.

Assembly linking
توسط این ابزار میتوانید حجم و سایز برنامه را کاهش دهید. روش کار به این صورت است که کدهای IL را اسکن می‌کند و فقط اسمبلی‌های استفاده شده در برنامه را در کنار برنامه قرار میدهد که باعث کاهش حجم برنامه می‌شود.
برای فعالسازی آن، تگ زیر را به فایل پروژه اضافه کنید
  <PublishTrimmed>true</PublishTrimmed>
و سپس با دستور زیر در cmd اقدام به گرفتن خروجی کنید:
 dotnet publish -r win-x64 -c Release
در یک پروژه‌ی ساده‌ی WPF، حجم برنامه از 68 مگابایت، به 28 مگابایت کاهش پیدا کرد.

نکته: شما میتوانید از ایمیج‌های ReadyToRun و Linker همزمان استفاده کنید. اولی حجم برنامه را افزایش میدهد و دومی کاهش! و این در حال حاضر باعث ایجاد مشکلاتی می‌شود که قرار هست در نسخه‌ی بعدی، یعنی پیش‌نمایش 7، رفع بشود. در حال حاضر بهتر است از هر دو دستور باهم استفاده نکنید.
مطالب
اشتباهات متداول برنامه‌نویس‌های دات نت

اشتباه 1:
استفاده از

throw ex;
بجای استفاده از
throw;
در حالت اول، تمام stack trace موجود تا نقطه‌ی فراخوانی دستور ذکر شده پاک خواهند شد، اما در حالت دوم stack trace‌ حفظ شده و دیباگ کردن کد را ساده می‌کند.

اشتباه 2:
درک اشتباه عملکرد متد replace :
string s = "Take this out";
s.Replace("this", "that"); //wrong
بجای استفاده از :
s = s.Replace("this", "that");  //correct

اگر از fxCop استفاده کنید، اینگونه خطاها را (عدم استفاده از مقدار بازگشتی) گوشزد می‌کند.

اشتباه 3:
استفاده‌ی بی دقت از متغیرهای استاتیک در یک برنامه وب. دو مثال زیر را در نظر بگیرید:
public static string GetCookieName(Cookie c)
{
return c.Name;
}

static List<string> cookieList = new List<string>();
public static void AddToCookieList(Cookie c)
{
cookieList.Add(c.Name);

}

برنامه‌های وب ذاتا چند ریسمانی هستند و زمانیکه یک متغیر را از نوع استاتیک تعریف می‌کنید، این متغیر، هنگام مراجعه‌ی کاربران بین آن‌ها به اشتراک گذاشته می‌شود و امکان تخریب یا استفاده‌ی ناصحیح از مقادیر آن‌ها وجود خواهد داشت. در حالت اول نیازی به مباحث همزمانی و قفل کردن منابع نیست زیرا متغیری که در متد استفاده می‌شود، thread safe است اما cookieList در مثال دوم خیر و حتما هنگام استفاده از آن باید مباحث قفل کردن منابع را درنظر داشت. (حتی اگر برنامه‌ی شما از نوع وبی هم نیست اما چند ریسمانی است این مطلب باز هم صادق می‌باشد)


اشتباه 4:
مقابله با خطاها به شکلی نادرست: (اصطلاحا خفه کردن خطاها!)
 try
{
//something
File.Delete(blah);
}
catch{}
در این حالت اگر خطایی رخ دهد کاربر متوجه نخواهد شد دقیقا مشکلی وجود داشته یا خیر و علت آن چیست.
هرچند گاهی از اوقات اگر خطای حاصل برای ما اهمیتی نداشت می‌توان از آن استفاده نمود، در غیراینصورت باید حتما از این روش پرهیز کرد.

اشتباه 5:
ارائه‌ی برنامه‌های ASP.Net با گزینه‌ی پیش فرض Debug=true در web.config که پیشتر در مورد آن در این سایت بحث شده است.


اشتباه 6:
عدم استفاده از امکانات ویژه‌ی دات نت فریم ورک هنگام کار با رشته‌ها:
string s = "This ";
s += "is ";
s += "not ";
s += "the ";
s += "best ";
s += "way.";

StringBuilder sb = new StringBuilder();
sb.Append("This ");
sb.Append("is ");
sb.Append("much ");
sb.Append("better. ");

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

اشتباه 7:
عدم استفاده از عبارت using هنگام استفاده از اشیایی از نوع Idisposable . مثالی در این مورد.

using (StreamReader reader=new StreamReader(file))
{
//your code here

}

اشتباه 8:
فراموش کردن بررسی نال بودن یک شیء هنگام استفاده از آن.
string val = Session["xyz"].ToString();

این نوع کد نویسی یکی از اشتباهات متداول تمامی تازه واردان به ASP.Net است. حتما باید پیش از استفاده از متد ToString بررسی شود که آیا این سشن نال است یا نه. در غیراینصورت حاصل کار فقط یک exception خواهد بود. (استفاده از افزونه‌ی ری شارپر در این موارد کمک بزرگی است، زیرا به محض قرار گرفتن مکان نما روی شیءایی که احتمال نال بودن آن میسر است، یک راهنما را به شما ارائه خواهد کرد)

اشتباه 9:
بازگشت دادن یک property عمومی از نوع لیست‌های جنریک.
با توجه به اینکه این نوع لیست‌ها فقط خواندنی نیستند و امکان دستکاری اطلاعات آن توسط فراخوان وجود دارد، توصیه می‌شود از نوع جنریک IEnumerable استفاده شود. همچنین توصیه شده است هنگام انتخاب نوع پارامترهای ورودی یک متد نیز به این مورد دقت شود.

مطالب
ذخیره تنظیمات متغیر مربوط به یک وب اپلیکیشن ASP.NET MVC با استفاده از EF
طی این  مقاله، نحوه‌ی ذخیره سازی تنظیمات متغیر و پویای یک برنامه را به صورت Strongly Typed ارائه خواهم داد. برای این منظور، یک API را که از Lazy Loading ، Cache ، Reflection و Entity Framework بهره میگیرد، خواهیم ساخت.
برنامه‌ی هدف ما که از این API استفاده می‌کند، یک اپلیکیشن Asp.net MVC است. قبل از شروع به ساخت API مورد نظر، یک دید کلی در مورد آنچه که قرار است در نهایت توسعه یابد، در زیر مشاهده میکنید:
public SettingsController(ISettings settings)
{
  // example of saving 
  _settings.General.SiteName = "دات نت تیپس";
  _settings.Seo.HomeMetaTitle = ".Net Tips";
  _settings.Seo.HomeMetaKeywords = "َAsp.net MVC,Entity Framework,Reflection";
  _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";
  _settings.Save();
}

همانطور که در کدهای بالا مشاهده میکنید، شی setting_ ما دارای دو پراپرتی فقط خواندنی بنام‌های General و Seo است که شامل  تنظیمات مورد نظر ما هستند و این دو کلاس از کلاس پایه‌ی SettingBase ارث بری کرده‌اند. دو دلیل برای انجام این کار وجود دارد:
  1. تنظیمات به صورت گروه بندی شده در کنار  هم قرار گرفته‌اند و یافتن تنظیمات برای زمانی که نیاز به دسترسی  به آنها داریم، راحت‌تر و ساده‌تر خواهد بود. 
  2. به این شکل تنظیمات قابل دسترس در یک گروه، از دیتابیس بازیابی خواهند شد.

اصلا چرا باید این تنظیمات را در دیتابیس ذخیره کنیم؟ 

شاید فکر کنید چرا باید تنظیمات را در دیتابیس ذخیره کنیم در حالی که فایل web.config در درسترس است و می‌توان توسط کلاس ConfigurationManager به اطلاعات آن دسترسی داشت.
جواب: دلیل این است که با تغییر فایل web.config، برنامه‌ی وب شما ری استارت خواهد شد (چه زمان‌هایی یک برنامه Asp.net ری استارت میشود).
برای جلوگیری از این مساله، راه حل مناسب برای ذخیره سازی اطلاعاتی که نیاز به تغییر در زمان اجرا دارند، استفاده از از دیتابیس می‌باشد. در این مقاله از Entity Framework و پایگاه داده Sql Sever استفاده می‌کنم.

مراحل ساخت Setting API مورد نظر به شرح زیر است:
  1. ساخت یک Asp.net Web Application 
  2. ساخت مدل Setting و افزودن آن به کانتکست Entity Framework 
  3. ساخت کلاس SettingBase برای بازیابی و ذخیره سازی تنظیمات با رفلکشن
  4. ساخت کلاس GenralSettins و SeoSettings که از کلاس SettingBase ارث بری کرده‌اند.
  5. ساخت کلاس Settings به منظور مدیریت تمام انواع تنظیمات 

یک برنامه‌ی Asp.Net Web Application را از نوع MVC ایجاد کنید. تا اینجا مرحله‌ی اول ما به پایان رسید؛ چرا که ویژوال استودیو کار‌های مورد نیاز ما را انجام خواهد داد.
 لازم است مدل خود را به ApplicationDbContext موجود در فایل IdentityModels.cs معرفی کنیم. به شکل زیر:
namespace DynamicSettingAPI.Models
{
    public interface IUnitOfWork
    {
        DbSet<Setting> Settings { get; set; }
        int SaveChanges();
    }
} 

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>,IUnitOfWork
    {
        public DbSet<Setting> Settings { get; set; }
        public ApplicationDbContext()
            : base("DefaultConnection", throwIfV1Schema: false)
        {
        }

        public static ApplicationDbContext Create()
        {
            return new ApplicationDbContext();
        }
    }


namespace DynamicSettingAPI.Models
{
    public class Setting
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Value { get; set; }
    }
}
مدل تنظیمات ما خیلی ساده است و دارای سه پراپرتی به نام‌های Name ، Type ، Value هست که به ترتیب برای دریافت مقدار تنظیمات، نام کلاسی که از کلاس SettingBase ارث برده و نام تنظیمی که لازم داریم ذخیره کنیم، در نظر گرفته شده‌اند. 
لازم است تا متد OnModelCreating مربوط به ApplicationDbContext را نیز تحریف کنیم تا کانفیگ مربوط به مدل خود را نیز اعمال نمائیم.
 protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Setting>()
                    .HasKey(x => new { x.Name, x.Type });

            modelBuilder.Entity<Setting>()
                        .Property(x => x.Value)
                        .IsOptional();

            base.OnModelCreating(modelBuilder);
        }
ساختاری به شکل زیر مد نظر ماست:

  کلاس SettingBase ما همچین ساختاری را خواهد داشت:
namespace DynamicSettingAPI.Service
{
    public abstract class SettingsBase
    {
        //1
        private readonly string _name;
        private readonly PropertyInfo[] _properties;

        protected SettingsBase()
        {
            //2
            var type = GetType();
            _name = type.Name;
            _properties = type.GetProperties();
        }

        public virtual void Load(IUnitOfWork unitOfWork)
        {
            //3 get setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                //get the setting from setting list
                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    //4 set 
                    propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
                }
            }
        }
        public virtual void Save(IUnitOfWork unitOfWork)
        {
            //5 get all setting for this type name
            var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();

            foreach (var propertyInfo in _properties)
            {
                var propertyValue = propertyInfo.GetValue(this, null);
                var value = (propertyValue == null) ? null : propertyValue.ToString();

                var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
                if (setting != null)
                {
                    // 6 update existing value
                    setting.Value = value;
                }
                else
                {
                    // 7 create new setting
                    var newSetting = new Setting()
                    {
                        Name = propertyInfo.Name,
                        Type = _name,
                        Value = value,
                    };
                    unitOfWork.Settings.Add(newSetting);
                }
            }
        }
    }
}
این کلاس قرار است توسط کلاس‌های تنظیمات ما به ارث برده شود و در واقع کارهای مربوط به رفلکشن را در این کلاس کپسوله کرده‌ایم. همانطور که مشخص است ما دو فیلد را به نام‌های name_ و properties_ به صورت فقط خواندنی در نظر گرفته ایم که نام کلاس مورد نظر ما که از این کلاس به ارث خواهد برد، به همراه پراپرتی‌های آن، در این ظرف‌ها قرار خواهند گرفت.
متد Load وظیفه‌ی واکشی تمام تنظیمات مربوط به Type و ست کردن مقادیر به دست آمده را به خصوصیات کلاس ما، برعهده دارد. کد زیر مقدار دریافتی از دیتابیس را به نوع داده پراپرتی مورد نظر تبدیل کرده و نتیجه را به عنوان Value پراپرتی ست میکند. 
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
متد Save نیز وظیفه‌ی ذخیره سازی مقادیر موجود در خصوصیات کلاس تنظیماتی را که از کلاس SettingBase ما به ارث برده است، به عهده دارد. 
این متد دیتا‌های موجود دردیتابیس را که متعلق به کلاس ارث برده مورد نظر ما هستند، واکشی میکند و در یک حلقه، اگر خصوصیتی در دیتابیس موجود بود، آن را ویرایش کرده وگرنه یک رکورد جدید را ثبت میکند.

  کلاس‌های تنظیمات شخصی سازی شده خود را به شکل زیر تعریف میکنیم :
  public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
        public bool RegisterUsersEnabled { get; set; }
    }

 public class GeneralSettings : SettingsBase
    {
        public string SiteName { get; set; }
        public string AdminEmail { get; set; }
    }
نیازی به توضیح ندارد.
برای اینکه تنظیمات را به صورت یکجا داشته باشیم و Abstraction ای را برای استفاده از این API ارائه دهیم، یک اینترفیس و یک کلاس که اینترفیس مذکور را پیاده کرده است در نظر میگیریم: 
public interface ISettings
{
    GeneralSettings General { get; }
    SeoSettings Seo { get; }
    void Save();
}

public class Settings : ISettings
{
    // 1
    private readonly Lazy<GeneralSettings> _generalSettings;
    // 2
    public GeneralSettings General { get { return _generalSettings.Value; } }

    private readonly Lazy<SeoSettings> _seoSettings;
    public SeoSettings Seo { get { return _seoSettings.Value; } }

    private readonly IUnitOfWork _unitOfWork;
    public Settings(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        // 3
        _generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
        _seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
    }

    public void Save()
    {
        // only save changes to settings that have been loaded
        if (_generalSettings.IsValueCreated)
            _generalSettings.Value.Save(_unitOfWork);

        if (_seoSettings.IsValueCreated)
            _seoSettings.Value.Save(_unitOfWork);

        _unitOfWork.SaveChanges();
    }
    // 4
    private T CreateSettings<T>() where T : SettingsBase, new()
    {
        var settings = new T();
        settings.Load(_unitOfWork);
        return settings;
    }
}
این اینترفیس مشخص می‌کند که ما به چه نوع تنظیماتی، دسترسی داریم و متد Save آن برای آپدیت کردن تنظیمات، در نظر گرفته شده است. هر کلاسی که از کلاس SettingBase ارث بری کرده را به صورت فیلد فقط خواندنی و با استفاده از کلاس Lazy درون آن ذکر میکنیم و به این صورت کلاس تنظیمات ما زمانی ساخته خواهد شد که برای اولین بار به آن دسترسی داشته باشیم.
متد CreateSetting وظیفه‌ی لود دیتا را از دیتابیس، بر عهده دارد که برای این منظور، متد لود Type مورد نظر را فراخوانی میکند. این متد وقتی به کلاس تنظیمات مورد نظر برای اولین بار دسترسی پیدا کنیم، فراخوانی خواهد شد.

 حتما امکان این وجود دارد که شما از امکان Caching هم بهره ببرید برای مثال همچین متد و سازنده‌ای را در کلاس Settings در نظر بگیرید:
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
    // ARGUMENT CHECKING SKIPPED FOR BREVITY
    _unitOfWork = unitOfWork;
    _cache = cache;
    _generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
    _seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}

private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
    // this is where you would implement loading from ICache
    throw new NotImplementedException();
}
در آخر هم به شکل زیر میتوان (به عنوان دمو فقط ) از این API استفاده کرد.
   public ActionResult Index()
        {
            using (var uow = new ApplicationDbContext())
            {
                var _settings = new Settings(uow);
                _settings.General.SiteName = "دات نت تیپس";
                _settings.General.AdminEmail = "admin@gmail.com";
                _settings.General.RegisterUsersEnabled = true;
                _settings.Seo.HomeMetaTitle = ".Net Tips";
                _settings.Seo.MetaKeywords = "Asp.net MVC,Entity Framework,Reflection";
                _settings.Seo.HomeMetaDescription = "ذخیره تنظیمات برنامه";

                var settings2 = new Settings(uow);
                var output = string.Format("SiteName: {0} HomeMetaDescription: {1}  MetaKeywords:  {2}  MetaTitle:  {3}  RegisterEnable:  {4}",
                    settings2.General.SiteName,
                    settings2.Seo.HomeMetaDescription,
                    settings2.Seo.MetaKeywords,
                    settings2.Seo.HomeMetaTitle,
                    settings2.General.RegisterUsersEnabled.ToString()
                    );
                return Content(output);
            }

        }

خروجی :

نکته: در پروژه ای که جدیدا در سایت ارائه داده‌ام و در حال تکمیل آن هستم، از بهبود یافته‌ی این مقاله استفاده می‌شود. حتی برای اسلاید شو‌های سایت هم میشود از این روش استفاده کرد و از فرمت json بهره برد برای این منظور. حتما در پروژه‌ی مذکور همچین امکانی را هم در نظر خواهم گرفتم.
پیشنها میکنم سورس SmartStore را بررسی کنید. آن هم به شکل مشابهی ولی پیشرفته‌تر از این مقاله، همچین امکانی را دارد.
مطالب
بررسی تغییرات Blazor 8x - قسمت دوم - بررسی حالت رندر سمت سرور
در قسمت قبل، حالت‌های مختلف رندر کامپوننت‌ها را در Blazor 8x معرفی کردیم. در این قسمت می‌خواهیم نحوه‌ی کارکرد دو حالت InteractiveServer و StreamRendering را به همراه چند مثال بررسی کنیم.


معرفی قالب‌های جدید شروع پروژه‌های Blazor در دات نت 8

پس از نصب SDK دات نت 8، دیگر خبری از قالب‌های قدیمی پروژه‌های blazor server و blazor wasm نیست! در اینجا در ابتدا باید مشخص کرد که سطح تعاملی برنامه در چه حدی است. در ادامه 4 روش شروع پروژه‌های Blazor 8x را مشاهده می‌کنید که توسط پرچم interactivity--، نوع رندر برنامه در آن‌ها مشخص شده‌است:

اجرای قسمت‌های تعاملی برنامه بر روی سرور:
dotnet new blazor --interactivity Server

اجرای قسمت‌های تعاملی برنامه در مرورگر، توسط فناوری وب‌اسمبلی:
dotnet new blazor --interactivity WebAssembly

برای اجرای قسمت‌های تعاملی برنامه، ابتدا حالت Server فعالسازی می‌شود تا فایل‌های WebAssembly دریافت شوند، سپس فقط از WebAssembly استفاده می‌کند:
dotnet new blazor --interactivity Auto

فقط از حالت SSR یا همان static server rendering استفاده می‌شود (این نوع برنامه‌ها تعاملی نیستند):
dotnet new blazor --interactivity None

سایر گزینه‌ها را با اجرای دستور dotnet new blazor --help می‌توانید مشاهده کنید.


نکته‌ی مهم! در قالب‌های آماده‌ی Blazor 8x، حالت SSR، پیش‌فرض است.

هرچند در تمام پروژه‌های فوق، انتخاب حالت‌های مختلف رندر را مشاهده می‌کنید، اما این انتخاب‌ها صرفا دو مقصود مهم را دنبال می‌کنند:
الف) تنظیم فایل Program.cs برنامه جهت افزودن وابستگی‌های مورد نیاز، به صورت خودکار.
ب) ایجاد پروژه‌ی کلاینت (علاوه بر پروژه‌ی سرور)، در صورت نیاز. برای مثال حالت‌های وب‌اسمبلی و Auto، هر دو به همراه یک پروژه‌ی کلاینت وب‌اسمبلی هم هستند؛ اما حالت‌های Server و None، خیر.

در تمام این پروژه‌ها هر صفحه و یا کامپوننتی که ایجاد می‌شود، به صورت پیش‌فرض بر اساس SSR رندر و نمایش داده خواهد شد؛ مگر اینکه به صورت صریحی این نحوه‌ی رندر را بازنویسی کنیم. برای مثال مشخص کنیم که قرار است بر اساس Blazor Server اجرا شود و یا وب‌اسمبلی و یا حالت Auto.

اما ... اگر علاقمند بودیم تا به حالت‌های پیش‌از دات نت 8 رجوع کنیم و تمام برنامه را به صورت یک‌دست تعاملی کنیم و حالت SSR پیش‌فرض نباشد، می‌توان از پرچم all-interactive-- که به انتهای دستورات فوق قابل افزوده شدن است، کمک گرفت. این پرچم، فایل App.Razor را جهت تنظیم سراسری حالت‌های رندر، ویرایش می‌کند. این مورد را در ادامه‌ی این مطلب، در قسمت «روشی ساده برای تعاملی کردن کل برنامه» بیشتر بررسی می‌کنیم.


بررسی حالت Server side rendering

برای بررسی این حالت یک پوشه‌ی جدید را ایجاد کرده و توسط خط فرمان، دستور dotnet new blazor --interactivity Server را در ریشه‌ی آن اجرا می‌کنیم. پس از ایجاد ساختار ابتدایی پروژه بر اساس این قالب انتخابی، فایل Program.cs جدید آن، چنین شکلی را دارد:
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorComponents().AddInteractiveServerComponents();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error", createScopeForErrors: true);
    app.UseHsts();
}

app.UseHttpsRedirection();

app.UseStaticFiles();
app.UseAntiforgery();

app.MapRazorComponents<App>().AddInteractiveServerRenderMode();

app.Run();
مهم‌ترین قسمت‌های آن، متدهای AddInteractiveServerComponents و AddInteractiveServerRenderMode هستند که server-side rendering را به همراه امکان داشتن کامپوننت‌های تعاملی، میسر می‌کنند.
server-side rendering به این معنا است که برنامه‌ی سمت سرور، کل DOM و HTML نهایی را تولید کرده و به مرورگر کاربر ارائه می‌کند. مرورگر هم این DOM را نمایش می‌دهد. فقط همین! در اینجا هیچ خبری از اتصال دائم SignalR نیست و محتوای ارائه شده، یک محتوای استاتیک است. این حالت رندر، برای ارائه‌ی محتواهای فقط خواندنی غیرتعاملی، فوق العاده‌است؛ امکان از لحظه‌ای که نیاز به کلیک بر روی دکمه‌ای باشد، دیگر پاسخگو نیست. به همین جهت در اینجا امکان تعاملی کردن تعدادی از کامپوننت‌های ویژه و مدنظر نیز پیش‌بینی شده‌اند تا بتوان به ترکیبی از server-side rendering و client-side rendering رسید.
حالت پیش‌فرض در اینجا، ارائه‌ی محتوای استاتیک است. بنابراین هر کامپوننتی در اینجا ابتدا بر روی سرور رندر شده (HTML ابتدایی آن آماده شده) و به سمت مرورگر کاربر ارسال می‌شود. اگر کامپوننتی نیاز به امکانات تعاملی داشت باید آن‌را دقیقا توسط ویژگی InteractiveXYZ مشخص کند؛ مانند مثال زیر:
@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
همانطور که مشاهده می‌کنید، این کامپوننت از روش رندر InteractiveServer استفاده می‌کند. برای درک نحوه‌ی کارکرد آن، همین سطر را حذف، یا کامنت کنید و سپس برنامه را اجرا کنید. در حین مشاهده‌ی این صفحه، همه چیز به خوبی نمایش داده می‌شود و حتی دکمه‌ی Click me هم مشخص است. اما ... با کلیک بر روی آن اتفاقی رخ نمی‌دهد! علت اینجا است که اکنون این صفحه، یک صفحه‌ی کاملا استاتیک است و دیگر تعاملی نیست.
در ادامه، مجددا سطر کامنت شده را به حالت عادی برگردانید و سپس برنامه را اجرا کنید. پیش از باز کردن صفحه‌ی Counter، ابتدا developer tools مرورگر خود را گشوده و برگه‌ی network آن‌را انتخاب و سپس صفحه‌ی Counter را باز کنید. در این لحظه‌است که مشاهده می‌کنید یک اتصال وب‌سوکت برقرار شد. این اتصال است که قابلیت‌های تعاملی صفحه را برقرار کرده و مدیریت می‌کند (این اتصال دائم SignalR است که این صفحه را همانند برنامه‌های Blazor Web Server پیشین مدیریت می‌کند).

یک نکته: در برنامه‌های Blazor Server سنتی، امکان فعالسازی قابلیتی به نام prerender نیز وجود دارد. یعنی سرور، ابتدا صفحه را رندر کرده و محتوای استاتیک آن‌را به سمت مرورگر کاربر ارسال می‌کند و سپس اتصال SignalR برقرار می‌شود. در دات نت 8، این حالت، حالت پیش‌فرض است. اگر آن‌را نمی‌خواهید باید به نحو زیر غیرفعالش کنید:
@rendermode InteractiveServerRenderModeWithoutPrerendering

@code{
  static readonly IComponentRenderMode InteractiveServerRenderModeWithoutPrerendering = 
        new InteractiveServerRenderMode(false);
}


روشی ساده برای تعاملی کردن کل برنامه

اگر می‌خواهید رفتار برنامه را همانند Blazor Server سابق کنید و نمی‌خواهید به ازای هر کامپوننت، نحوه‌ی رندر آن‌را به صورت سفارشی انتخاب کنید، فقط کافی است فایل App.razor را باز کرده:
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="MyApp.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>
و قسمت‌های HeadOutlet و مسیریابی آن‌را به صورت زیر تغییر دهید تا به کل برنامه اعمال شود:
<HeadOutlet @rendermode="@InteractiveServer" />
...
<Routes @rendermode="@InteractiveServer" />
این مورد دقیقا همان کاری است که پرچم all-interactive-- در هنگام ایجاد پروژه‌های جدید Blazor 8x به کمک NET CLI.، انجام می‌دهد. یک چنین گزینه‌ای در ویژوال استودیو نیز هنگام ایجاد پروژه‌ها‌ی جدید Blazor وجود دارد و به شما این امکان را می‌دهد که بین حالت‌های تعاملی Per page/component و Global، یکی را انتخاب کنید. در حین استفاده‌ی از CLI، نیازی به ذکر حالت تعاملی Per page/component نیست؛ چون حالت پیش‌فرض، یا همان SSR است. حالت Global هم فقط فایل App.Razor را به صورت فوق، ویرایش و تنظیم می‌کند.


در این حالت دیگر نیازی نیست تا به ازای هر کامپوننت و صفحه، نحوه‌ی رندر را مشخص کنیم؛ چون این نحوه، از بالاترین سطح، به تمام زیرکامپوننت‌ها به ارث می‌رسد (درباره‌ی این نکته در قسمت قبل، توضیحاتی ارائه شد).


بررسی حالت Streaming Rendering

در اینجا مثال پیش‌فرض Weather.razor قالب پیش‌فرض مورد استفاده‌ی جاری را کمی تغییر داده‌ایم که کدهای نهایی آن به صورت زیر است (2 قسمت forecasts.AddRange_ را اضافه‌تر دارد):

@page "/weather"
@attribute [StreamRendering(prerender: true)]

<PageTitle>Weather</PageTitle>

<h1>Weather</h1>

<p>This component demonstrates showing data.</p>

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

@code {
    private List<WeatherForecast>? _forecasts;

    protected override async Task OnInitializedAsync()
    {
    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(500);

        var startDate = DateOnly.FromDateTime(DateTime.Now);
        var summaries = new[]
                        {
                            "Freezing",
                            "Bracing",
                            "Chilly",
                            "Cool",
                            "Mild",
                            "Warm",
                            "Balmy",
                            "Hot",
                            "Sweltering",
                            "Scorching",
                        };
        _forecasts = GetWeatherForecasts(startDate, summaries).ToList();
        StateHasChanged();

    // Simulate asynchronous loading to demonstrate streaming rendering
        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
        StateHasChanged();

        await Task.Delay(1000);
        _forecasts.AddRange(GetWeatherForecasts(startDate, summaries));
    }

    private static IEnumerable<WeatherForecast> GetWeatherForecasts(DateOnly startDate, string[] summaries)
    {
        return Enumerable.Range(1, 5)
                         .Select(index => new WeatherForecast
                                          {
                                              Date = startDate.AddDays(index),
                                              TemperatureC = Random.Shared.Next(-20, 55),
                                              Summary = summaries[Random.Shared.Next(summaries.Length)],
                                          });
    }

    private class WeatherForecast
    {
        public DateOnly Date { get; set; }
        public int TemperatureC { get; set; }
        public string? Summary { get; set; }
        public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    }

}
برای بررسی این مثال، ابتدا سطر ویژگی StreamRendering را حذف و یا کامنت کرده و سپس برنامه را اجرا کنید. پس از اجرای برنامه، با انتخاب مشاهده‌ی صفحه‌ی Weather، هیچگاه قسمت loading که در حالت forecasts == null_ قرار است ظاهر شود، نمایش داده نمی‌شود؛ چون در این حالت (حذف نوع رندر)، صفحه‌ی نهایی که به کاربر ارائه خواهد شد، یک صفحه‌ی استاتیک کاملا رندر شده‌ی در سمت سرور است و کاربر باید تا زمان پایان این رندر در سمت سرور، منتظر بماند و سپس صفحه‌ی نهایی را دریافت و مشاهده کند. در این حالت امکانات تعاملی Blazor server وجود خارجی ندارند.
در ادامه مجددا سطر ویژگی StreamRendering را به حالت قبلی برگردانید و برنامه را اجرا کنید. در این حالت ابتدا قسمت loading ظاهر می‌شود و سپس در طی چند مرحله با توجه به Task.Delay‌های قرار داده شده، صفحه رندر شده و تکمیل می‌شود.
اتفاقی که در اینجا رخ می‌دهد، استفاده از فناوری  HTML Streaming است که مختص به مایکروسافت هم نیست. در حالت Streaming، هربار قطعه‌ای از HTML ای که قرار است به کاربر ارائه شود، به صورت جریانی به سمت مرورگر ارسال می‌شود و مرورگر این قطعه‌ی جدید را بلافاصله نمایش می‌دهد. نکته‌ی جالب این روش، عدم نیاز به اتصال SignalR و یا اجرای WASM درون مرورگر است.

Streaming rendering حالت بینابین رندر کامل در سمت سرور و رندر کامل در سمت کلاینت است. در حالت رندر سمت سرور، کل HTML صفحه ابتدا توسط سرور تهیه و بازگشت داده می‌شود و کاربر باید تا پایان عملیات تهیه‌ی این HTML نهایی، منتظر باقی بماند و در این بین چیزی را مشاهده نخواهد کرد. در حالت Streaming rendering، هنوز هم همان حالت تهیه‌ی HTML استاتیک سمت سرور برقرار است؛ به همراه تعدادی محل جایگذاری اطلاعات جدید. به محض پایان یک عمل async سمت سرور که سه نمونه‌ی آن را در مثال فوق مشاهده می‌کنید، برنامه، جریان قطعه‌ای از اطلاعات استاتیک را به سمت مرورگر کاربر ارسال می‌کند تا در مکان‌هایی از پیش تعیین شده، درج شوند.
در حالت SSR، فقط یکبار شانس ارسال کل اطلاعات به سمت مرورگر کاربر وجود دارد؛ اما در حالت Streaming rendering، ابتدا می‌توان یک قالب HTML ای را بازگشت داد و سپس مابقی محتوای آن‌را به محض آماده شدن در طی چند مرحله بازگشت داد. در این حالت نمایش گزارشی از اطلاعاتی که ممکن است با تاخیر در سمت سرور تهیه شوند، ساده‌تر می‌شود. یعنی می‌توان هربار قسمتی را که تهیه شده، برای نمایش بازگشت داد و کاربر تا مدت زیادی منتظر نمایش کل صفحه باقی نخواهد ماند.


روش نهایی معرفی نحوه‌ی رندر صفحات

بجای استفاده از ویژگی‌های RenderModeXyz جهت معرفی نحوه‌ی رندر کامپوننت‌ها و صفحات (که تا پیش از نگارش RTM معرفی شده بودند و چندبار هم تغییر کردند)، می‌توان از دایرکتیو جدیدی به نام rendermode@ با سه مقدار InteractiveServer، InteractiveWebAssembly و InteractiveAuto استفاده کرد. برای سهولت تعریف این موارد باید سطر ذیل را به فایل Imports.razor_ اضافه نمود:
@using static Microsoft.AspNetCore.Components.Web.RenderMode
در این حالت تعریف قدیمی سطر ذیل:
@attribute [RenderModeInteractiveServer]
به صورت زیر:
@rendermode InteractiveServer
تبدیل می‌شود.

اگر هم قصد سفارشی سازی آن‌ها را دارید، برای مثال می‌خواهید prerender را در آن‌ها false کنید، روش کار به صورت زیر است:
@rendermode renderMode

@code {
    static IComponentRenderMode renderMode = new InteractiveWebAssemblyRenderMode(prerender: false);
}
مطالب
Blazor 5x - قسمت 27 - برنامه‌ی Blazor WASM - کار با سرویس‌های Web API
در قسمت‌های Blazor Server مثال این سری، با روش کار با سرویس‌های سمت سرور برنامه، آشنا شدیم. در این نوع برنامه‌ها، فقط کافی است اصل سرویس مدنظر را مستقیما در کامپوننت‌های Razor تزریق کرد و سپس می‌توان به نحو متداولی با آن‌ها کار کرد؛ اما در برنامه‌های Blazor WASM خیر! به این نوع برنامه‌های سمت کلاینت باید همانند برنامه‌های React ، Angular ، Vue و یا حتی برنامه‌های مبتنی بر jQuery نگاه کرد. در تمام فناوری‌های سمت کلاینت، این درخواست‌های Ajax ای هستند که با سرویس‌های یک Web API سمت سرور، ارتباط برقرار کرده، اطلاعاتی را به آن‌ها ارسال و یا دریافت می‌کنند. در برنامه‌های Blazor WASM نیز باید به همین ترتیب عمل کرد و در اینجا HttpClient دات نت، جایگزین برای مثال jQuery Ajax ، Fetch API و یا XMLHttpRequest استاندارد می‌شود (البته jQuery Ajax در اصل یک محصور کننده‌ی استاندارد XMLHttpRequest است که برای اولین بار توسط مایکروسافت در برنامه‌ی Outlook web access معرفی شد).


ایجاد سرویس سمت کلاینت دریافت اطلاعات اتاق‌ها از Web API

در قسمت 24، HotelRoomController را تکمیل کردیم که کار آن، بازگشت اطلاعات تمام اتاق‌ها و یا یک اتاق مشخص به کلاینت است. اکنون می‌خواهیم در ادامه‌ی قسمت قبل، اگر کاربری بر روی دکمه‌ی Go صفحه‌ی اول رزرو اتاقی کلیک کرد، لیست تمام اتاق‌های تعریف شده را به او نمایش دهیم. به همین جهت نیاز به سرویس سمت کلاینتی داریم که بتواند با این Web API endpoint کار کند:
namespace BlazorWasm.Client.Services
{
    public interface IClientHotelRoomService
    {
        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate);
        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate);
    }
}
این سرویس را در پوشه‌ی Services پروژه‌ی BlazorWasm.Client ایجاد کرده‌ایم که HotelRoomDTO خود را از پروژه‌ی BlazorServer.Models دریافت می‌کند. به این ترتیب می‌توان مدلی را بین یک Web API سمت سرور و یک سرویس سمت کلاینت، به اشتراک گذاشت. بنابراین پروژه‌ی کلاینت، باید ارجاعی را به پروژه‌ی BlazorServer.Models.csproj نیز داشته باشد.

در ادامه اینترفیس فوق را به صورت زیر پیاده سازی می‌کنیم:
namespace BlazorWasm.Client.Services
{
    public class ClientHotelRoomService : IClientHotelRoomService
    {
        private readonly HttpClient _httpClient;

        public ClientHotelRoomService(HttpClient httpClient)
        {
            _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
        }

        public Task<HotelRoomDTO> GetHotelRoomDetailsAsync(int roomId, DateTime checkInDate, DateTime checkOutDate)
        {
            throw new NotImplementedException();
        }

        public Task<IEnumerable<HotelRoomDTO>> GetHotelRoomsAsync(DateTime checkInDate, DateTime checkOutDate)
        {
            // How to url-encode query-string parameters properly
            var uri = new UriBuilderExt(new Uri(_httpClient.BaseAddress, "/api/hotelroom"))
                            .AddParameter("checkInDate", $"{checkInDate:yyyy'-'MM'-'dd}")
                            .AddParameter("checkOutDate", $"{checkOutDate:yyyy'-'MM'-'dd}")
                            .Uri;
            return _httpClient.GetFromJsonAsync<IEnumerable<HotelRoomDTO>>(uri);
        }
    }
}
توضیحات:
- HttpClient یکی از سرویس‌های تنظیم شده‌ی در فایل Program.cs پروژه‌های سمت کلاینت است. بنابراین می‌توان آن‌را از طریق تزریق به سازنده‌ی این سرویس، به دست آورد.
- در اینجا برای دریافت اطلاعات JSON دریافتی از سمت سرور و سپس Deserialize خودکار آن به لیستی از DTO تعریف شده، از متد جدید GetFromJsonAsync استفاده شده‌است. این مورد جزو تازه‌های NET 5x. است.
- در اینجا استفاده از کلاس UriBuilderExt را نیز جهت تشکیل یک URL دارای کوئری استرینگ، مشاهده می‌کنید. هیچگاه نباید URL نهایی را از طریق جمع زدن اجزای آن به سمت سرور ارسال کرد؛ از این جهت که اجزای آن باید URL-encoded شوند؛ وگرنه در سمت سرور قابلیت پردازش نخواهند داشت. در ادامه تعریف کلاس جدید UriBuilderExt را مشاهده می‌کنید:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Json;
using System.Threading.Tasks;
using BlazorServer.Models;
using BlazorWasm.Client.Utils;

using System;
using System.Collections.Specialized;
using System.Web;

namespace BlazorWasm.Client.Utils
{
    public class UriBuilderExt
    {
        private readonly NameValueCollection _collection;
        private readonly UriBuilder _builder;

        public UriBuilderExt(Uri uri)
        {
            _builder = new UriBuilder(uri);
            _collection = HttpUtility.ParseQueryString(string.Empty);
        }

        public UriBuilderExt AddParameter(string key, string value)
        {
            _collection.Add(key, value);
            return this;
        }

        public Uri Uri
        {
            get
            {
                _builder.Query = _collection.ToString();
                return _builder.Uri;
            }
        }
    }
}
- در اینجا توسط متد AddParameter، کار افزودن کوئری استرینگ‌ها به یک Url از پیش مشخص، انجام می‌شود. کار encoding نهایی به صورت خودکار توسط HttpUtility استاندارد دات نت، انجام خواهد شد.
- تاریخ‌های ارسالی به سمت سرور را با فرمت yyyy'-'MM'-'dd تبدیل رشته کردیم. این قالب، یکی از قالب‌های پذیرفته شده‌است.
- جهت سهولت استفاده‌ی از سرویس فوق و همچنین مدل‌های برنامه، فضای نام آن‌ها را به فایل BlazorWasm.Client\_Imports.razor اضافه می‌کنیم تا در تمام کامپوننت‌های برنامه‌ی سمت کلاینت، قابل دسترسی شوند:
@using BlazorWasm.Client.Services
@using BlazorServer.Models
- در آخر این سرویس جدید را باید به لیست سرویس‌های برنامه معرفی کرد تا قابلیت تزریق در کامپوننت‌ها را پیدا کند:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ...

            builder.Services.AddScoped<IClientHotelRoomService, ClientHotelRoomService>();

            // ...
        }
    }
}

چند اصلاح جزئی در کنترلرها و سرویس‌های سمت سرور

در Url نهایی فوق، دو پارامتر جدید checkInDate و checkOutDate هم وجود دارند. به همین جهت این دو را به اکشن متدهای کنترلر HotelRoom:
namespace BlazorWasm.WebApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class HotelRoomController : ControllerBase
    {
        // ...

        [HttpGet]
        public async Task<IActionResult> GetHotelRooms(DateTime? checkInDate, DateTime? checkOutDate)
        {
          // ...
        }

        [HttpGet("{roomId}")]
        public async Task<IActionResult> GetHotelRoom(int? roomId, DateTime? checkInDate, DateTime? checkOutDate)
        {
           // ...
        }
    }
}
و همچنین سرویس سمت سرور IHotelRoomService نیز اضافه می‌کنیم:
namespace BlazorServer.Services
{
    public interface IHotelRoomService : IDisposable
    {
        Task<List<HotelRoomDTO>> GetAllHotelRoomsAsync(DateTime? checkInDate, DateTime? checkOutDate);
        Task<HotelRoomDTO> GetHotelRoomAsync(int roomId, DateTime? checkInDate, DateTime? checkOutDate);
        // ...
    }
}
البته فعلا پیاده سازی خاصی ندارند و آن‌ها را در قسمت‌های بعد مورد استفاده قرار خواهیم داد.


تنظیمات ویژه‌ی HttpClient برنامه‌ی سمت کلاینت

سرویس ClientHotelRoomService فوق، از HttpClient تزریق شده‌ی در سازنده‌ی خود استفاده می‌کند که BaseAddress خود را مطابق تنظیمات ابتدایی برنامه، از HostEnvironment دریافت می‌کند. در اینجا علاقمندیم تا بجای این تنظیم پیش‌فرض، فایل جدید appsettings.json را به پوشه‌ی BlazorWasm.Client\wwwroot\appsettings.json کلاینت اضافه کرده (محل قرارگیری آن در برنامه‌های سمت کلاینت، داخل پوشه‌ی wwwroot است و نه در داخل پوشه‌ی ریشه‌ی اصلی پروژه):
{
    "BaseAPIUrl": "https://localhost:5001/"
}
و از این تنظیم جدید به عنوان BaseAddress برنامه‌ی Web API استفاده کنیم که روش آن‌را در کدهای ذیل مشاهده می‌کنید:
namespace BlazorWasm.Client
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            // ... 

            // builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
            builder.Services.AddScoped(sp => new HttpClient
            {
                BaseAddress = new Uri(builder.Configuration.GetValue<string>("BaseAPIUrl"))
            });

            // ... 
        }
    }
}

تکمیل کامپوننت دریافت لیست تمام اتاق‌ها

در قسمت قبل، کامپوننت خالی HotelRooms.razor را تعریف کردیم. کاربران پس از کلیک بر روی دکمه‌ی Go صفحه‌ی اول، به این کامپوننت هدایت می‌شوند. اکنون می‌خواهیم، لیست تمام اتاق‌ها را در این کامپوننت، از Web API برنامه دریافت کنیم:
@page "/hotel/rooms"

@inject ILocalStorageService LocalStorage
@inject IJSRuntime JsRuntime
@inject IClientHotelRoomService HotelRoomService

<h3>HotelRooms</h3>

@code {
    HomeVM HomeModel = new HomeVM();
    IEnumerable<HotelRoomDTO> Rooms = new List<HotelRoomDTO>();

    protected override async Task OnInitializedAsync()
    {
        try
        {
            var model = await LocalStorage.GetItemAsync<HomeVM>(ConstantKeys.LocalInitialBooking);
            if (model is not null)
            {
                HomeModel = model;
            }
            else
            {
                HomeModel.NoOfNights = 1;
            }
            await LoadRooms();
        }
        catch (Exception e)
        {
            await JsRuntime.ToastrError(e.Message);
        }
    }

    private async Task LoadRooms()
    {
        Rooms = await HotelRoomService.GetHotelRoomsAsync(HomeModel.StartDate, HomeModel.EndDate);
    }
}
در اینجا در ابتدا سعی می‌شود تا HomeModel، از Local Storage که در قسمت قبل آن‌را تنظیم کردیم، خوانده شود. سپس با استفاده از متد GetHotelRoomsAsync، لیست اتاق‌ها را از Web API دریافت می‌کنیم. تمام این عملیات آغازین نیز باید در روال رویدادگران OnInitializedAsync صورت گیرند.


روش اجرای پروژه‌های Blazor WASM

تا اینجا اگر برنامه‌ی سمت کلاینت را توسط دستور dotnet watch run اجرا کنیم، هرچند صفحه‌ی خالی نمایش لیست اتاق‌ها ظاهر می‌شود، اما یک خطای fetch error را هم دریافت خواهیم کرد؛ چون نیاز است ابتدا پروژه‌ی Web API را اجرا کرد و سپس پروژه‌ی WASM را.
برای ساده سازی اجرای همزمان این دو پروژه، اگر از ویژوال استودیوی کامل استفاده می‌کنید، بر روی نام Solution کلیک راست کرده و از منوی ظاهر شده، گزینه‌ی «Set Startup projects» را انتخاب کنید. در صفحه دیالوگ ظاهر شده، گزینه‌ی «multiple startup projects» را انتخاب کرده و از لیست پروژه‌های موجود، دو پروژه‌ی Web API و WASM را انتخاب کنید و Action مقابل آن‌ها را به Start تنظیم کنید. در اینجا حتی می‌توان ترتیب اجرای این پروژه‌ها را هم تغییر داد. در این حالت زمانیکه بر روی دکمه‌ی Run، در ویژوال استودیو کلیک می‌کنید، هر دو پروژه را با هم برای شما اجرا خواهد کرد.

نکته‌ی مهم! در این حالت هم برنامه‌ی کلاینت نمی‌تواند با برنامه‌ی Web API ارتباط برقرار کند! چون شماره پورت iisExpress درج شده‌ی در فایل appsettings.json آن، باید به شماره sslPort مندرج در فایل Properties\launchSettings.json پروژه‌ی Web API تغییر داده شود که برای نمونه در اینجا این عدد 44314 است:
{
  "iisSettings": {
    "iisExpress": {
      "applicationUrl": "http://localhost:62930",
      "sslPort": 44314
    }
  }
}
و یا اگر می‌خواهید پروژه را از طریق NET Core CLI. با اجرای دستور dotnet watch run اجرا کنید ... به صورت پیش‌فرض نمی‌شود! چون برای اینکار باید به پوشه‌ی ریشه‌ی پروژه‌های Web API و WASM وارد شد و دوبار دستور یاد شده را به صورت مجزا اجرا کرد. در این حالت، هر دو پروژه، بر روی پورت پیش‌فرض 5001 اجرا می‌شوند. روش تغییر این پورت، مراجعه به فایل Properties\launchSettings.json این پروژه‌ها است. برای مثال همان پورت پیش‌فرض 5001 را که در فایل appsettings.json انتخاب کردیم، ثابت نگه می‌داریم. یعنی فایل launchSettings.json پروژه‌ی Web API را ویرایش نمی‌کنیم. اما این پورت را در پروژه‌ی کلاینت برای مثال به عدد 5002 تغییر می‌دهیم تا برنامه‌ی کلاینت، بر روی پورت پیش‌فرض برنامه‌ی Web API اجرا نشود:
{
    "BlazorWasm.Client": {
      "applicationUrl": "https://localhost:5002;http://localhost:5003",
    }  
}


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-27.zip
مطالب
پَرباد - آموزش پیاده‌سازی پرداخت آنلاین در دات نت - مقدمه
پَرباد یک کتابخانه رایگان و اوپن سورس است که امکان افزودن قابلیت پرداخت آنلاین را به وب اپلیکیشن‌ها محیا میکند.

با توجه به نسخه قدیمی و درخواست کاربران جهت ارائه نسخه‌ای برای پشتیبانی از NET Core. ، نسخه‌ی کاملا جدیدی از این پروژه آماده گردید.

مزایا و ویژگی‌ها
  • نصب آسان با استفاده از Nuget
  • بدون نیاز به هیچگونه وب سرویس و یا دانش پیاده سازی سیستم‌های پرداخت آنلاین 
  • پشتیبانی از درگاه‌های: ملت، ملی (سداد)، پارسیان، پاسارگاد، ایران کیش و سامان
  • انجام پرداخت، فقط با نوشتن ۳ خط کد
  • طراحی کاملا یکپارچه برای انجام عملیات پرداخت با تمامی بانک‌ها
  • رعایت نکات امنیتی پرداخت آنلاین
  • درگاه مجازی، برای شبیه سازی عملیات پرداخت 
  • امکان استفاده از پروکسی برای سرور‌های خارج از ایران در صورت نیاز
  • استفاده از تکنولوژی‌های مدرن و استاندارد
  • قابل نصب بر روی پروژه‌های: ASP.NET Core, ASP.NET MVC, ASP.NET WebForms

آنچه شما در این سری مقالات یاد خواهید گرفت:

نحوه نصب
  • ASP.NET WebForms
  • ASP.NET MVC
  • ASP.NET CORE

آموزش پایه
  • درخواست پرداخت
  • تایید پرداخت
  • بازگرداندن مبلغ پرداخت شده

تنظیمات
  • درگاه‌ها
  • HttpContext
  • پایگاه داده
  • پیام‌ها

آموزش پیشرفته
  • ایجاد یک صورت‌حساب پرداخت با استفاده از InvoiceBuilder
  • درگاه مجازی پرداخت
  • استفاده از پروکسی
  • توکن پرداخت
  • تزریق وابستگی
  • Logging

نمونه پروژه‌ها
نحوه نصب
مطالب
استفاده از کلاس‌های *My وی بی در #C

یک سری قابلیت در فضای نام Microsoft.VisualBasic وجود دارد که به ظاهر سایر برنامه نویسان دات نت از آن محروم هستند. برای مثال My.Computer.Network.IsAvailable برای بررسی اینکه آیا اتصال به شبکه برقرار است یا My.Computer.Audio.Play جهت نواختن یک فایل صوتی، کلاس‌های My.Application، My.Computer، My.User My.Webservices، My.DataSources و امثال آن.
از این فضای نام در C# یا تمامی زبان‌های دیگر دات نت نیز می‌توان استفاده کرد. تنها کافی است ارجاعی را به Microsoft.VisualBasic.dll اضافه کنید، در ادامه using Microsoft.VisualBasic.MyServices و سپس معادل‌های آن‌ها به صورت زیر خواهند بود:

'VB code
Me.cbNetworked.Checked = My.Computer.Network.IsAvailable

// C# code
MyComputer mc = new MyComputer();
cbNetworked.Checked = mc.Network.IsAvailable;

'VB code
Me.cbAltKey.Checked = My.Computer.Keyboard.AltKeyDown
Me.cbCapsLock.Checked = My.Computer.Keyboard.CapsLock
Me.cbCtrlKey.Checked = My.Computer.Keyboard.CtrlKeyDown
' etc...

// C# code
MyComputer mc = new MyComputer();
this.cbAltKey.Checked = mc.Computer.Keyboard.AltKeyDown;
this.cbCapsLock.Checked = mc.Computer.Keyboard.CapsLock;
this.cbCtrlKey.Checked = mc.Computer.Keyboard.CtrlKeyDown;
' etc...

'VB code
My.Computer.Audio.Play(lbClips.SelectedItem)

// C# code
MyAudio ma = new MyAudio();
ma.Play(lbClips.SelectedItem);

'VB code
My.Computer.Info.TotalPhysicalMemory
'etc...

// C# code
MyComputer mc = new MyComputer();
mc.Info.TotalPhysicalMemory;
// etc...

هر چند در واقعیت این فضای نام تنها محصور کننده‌ی یک سری از کلاس‌های دیگر دات نت است. برای مثال اگر به سورس دات نت فریم ورک مراجعه کنید، My.Computer.Network.IsAvailable آن دقیقا محصور کننده‌ی متد NetworkInterface.GetIsNetworkAvailable واقع شده در فضای نام استاندارد System.Net.NetworkInformation است.

نظرات مطالب
پشتیبانی توکار از GDPR در ASP.NET Core 2.1
بسته‌ی AspNetCoreCompat آن برای MVC 5x هست (البته پشتیبانی آنچنانی هم ندارد) و در کل خیر. معماری این‌ها یکی نیست. وابستگی‌های این‌ها یکی نیست. از دیدگاه مایکروسافت، MVC 5x در فاز نگهداری هست و نه توسعه‌ی اصلی. حتی قصد بازنشسته کردن Full .NET framework را با NET Core 3. دارند. قسمت‌های باقیمانده‌ی Full .NET framework که در NET Core. نیست، همان قسمت‌های دسکتاپ هستند (WPF و WinForms). این‌ها را هم قصد دارند در نگارش 3 به NET Core. اضافه کنند (به صورت غیرچندسکویی و فقط مختص ویندوز) که معنای غیرمستقیم آن هدایت توسعه دهندگان دات نت به صرفا NET Core. هست.
نظرات مطالب
سایت‌های مهمی که از ASP.NET MVC استفاده می‌کنند
- خوب، اصلاحش کنید و به ASP.NET نگارش فعلی ویکی‌پدیا، MVC را هم اضافه کنید. ویکی پدیا به همین نحو مدیریت می‌شود.
- ماخذ این گفته، اسکات گاتری هست (دقیقه 52 این ویدیو). اسکات گاتری مدیر اجرایی Microsoft Azure و همچنین خیلی از تیم‌ها منجمله دات نت است. ایشان اخیرا به عنوان جانشین مدیر جدید مایکروسافت در قسمت‌های Cloud و Enterprise منصوب شده.
- این ماخذ اصلی را هم به ویکی‌پدیا اضافه کنید.

برای تکرار؛ روش تشخیص دستی
افزونه‌ی server spy مشخص می‌کند که برای ارائه سایت بینگ از IIS در حال استفاده است (یعنی ویندوز و ASP.NET در حالت کلی). افزونه‌ی ViewState Size مشخص می‌کند که در آن ViewState ایی وجود ندارد (یعنی وب فرم نیست).