مطالب
بومی سازی منابع در پروژه‌های ASP.NET Core Web API
اگر پروژه‌ی ما فقط از یک Web API تشکیل شده و نیاز است در قسمت‌های مختلف آن، مانند کنترلرها، سرویس‌ها، اعتبارسنج‌ها و غیره از منابع بومی شده استفاده شود، می‌توان از یک راه حل ساده‌ی «SharedResource» استفاده کرد؛ با این مزایا و شرایط:
 - تمام تعاریف بومی سازی مورد نیاز برنامه در یک تک فایل SharedResource.fa.resx قرار می‌گیرند. این فایل نیز در یک اسمبلی مستقل از برنامه‌ی اصلی اضافه می‌شود.
 - با استفاده از تزریق سرویس IStringLocalizer می‌توان به کلیدهای فایل SharedResource.fa.resx در هر قسمتی از برنامه‌ی Web API دسترسی یافت.
 - در این بین اگر کلیدی یافت نشد، خطایی با ذکر دقیق جزئیات منبع جستجو شده، لاگ می‌شود.
 - کلیدهای بومی سازی data annotations نیز قابل دریافت از فایل SharedResource.fa.resx می‌باشند.
 
در ادامه روش پیاده سازی یک چنین امکاناتی را بررسی می‌کنیم.
 
 
قرار دادن فایل منبع اشتراکی در اسمبلی ExternalResources

پس از ایجاد پروژه‌ی ابتدایی Web API به نام Core3xSharedResource.WebApi، یک اسمبلی جدید را برای مثال به نام Core3xSharedResource.ExternalResources تعریف کرده و در داخل آن پوشه‌ی جدید Resources را تعریف می‌کنیم. به این پوشه، فایل منبع جدیدی را به نام SharedResource.fa.resx اضافه می‌کنیم. در کنار آن باید یک کلاس خالی به نام SharedResource.cs نیز وجود داشته باشد.

کار با ین فایل (و یا فایل‌های دیگری مانند SharedResource.en.resx) همانند تمام فایل‌های منبع استاندارد است و نکته‌ی خاصی را به همراه ندارد.


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

پس از ایجاد و تکمیل فایل منبع اشتراکی، برای معرفی آن به برنامه، ابتدا کلاس جدید LocalizationConfig را تعریف کرده و در آن متد جدید AddCustomLocalization را به صورت زیر معرفی می‌کنیم:
    public static class LocalizationConfig
    {
        public static IMvcBuilder AddCustomLocalization(this IMvcBuilder mvcBuilder, IServiceCollection services)
        {
            mvcBuilder.AddDataAnnotationsLocalization(options =>
                    {
                        const string resourcesPath = "Resources";
                        string baseName = $"{resourcesPath}.{nameof(SharedResource)}";
                        var location = new AssemblyName(typeof(SharedResource).GetTypeInfo().Assembly.FullName).Name;

                        options.DataAnnotationLocalizerProvider = (type, factory) =>
                        {
                            // to use `SharedResource.fa.resx` file
                            return factory.Create(baseName, location);
                        };
                    });

            services.AddLocalization();
            services.AddScoped<IStringLocalizer>(provider =>
                            provider.GetRequiredService<IStringLocalizer<SharedResource>>());

            services.AddScoped<ISharedResourceService, SharedResourceService>();
            return mvcBuilder;
        }
    }
- در اینجا در ابتدا توسط متد AddDataAnnotationsLocalization، کار معرفی اسمبلی ثالثی که باید تعاریف بومی سازی را از آن دریافت کرد، صورت گرفته‌است.
- سپس با استفاده از متد AddLocalization، سرویس‌های پایه‌ی بومی سازی ASP.NET Core به برنامه اضافه می‌شوند. برای مثال پس از این تعریف اگر در هر جائی از برنامه سرویس <IStringLocalizer<SharedResource را تزریق کنید، می‌توان به مداخل فایل منبع اشتراکی، دسترسی یافت.
- در ادامه امکان تزریق سرویس غیرجنریک IStringLocalizer را نیز میسر کرده‌ایم که تعاریف خودش را از همان سرویس توکار <IStringLocalizer<SharedResource دریافت می‌کند. مزیت اینکار، فراهم شدن امکانات بومی سازی، برای مثال در کتابخانه‌هایی مانند Fluent Validation است که دقیقا از سرویس غیرجنریک IStringLocalizer برای دریافت منابع استفاده می‌کنند.
- در آخر تعریف یک سرویس سفارشی را نیز مشاهده می‌کنید که در ادامه‌ی بحث تکمیل خواهد شد.

هدف از متد AddCustomLocalization فوق، خلوت کردن فایل startup برنامه است. این متد به صورت زیر مورد استفاده قرار می‌گیرد:
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddHttpContextAccessor();
            services.AddControllers().AddCustomLocalization(services);
        }

پس از آن نیاز است میان‌افزار بومی سازی را نیز فعال کرد. متد UseCustomRequestLocalization زیر، اینکار را انجام می‌دهد:
    public static class LocalizationConfig
    {
        public static IApplicationBuilder UseCustomRequestLocalization(this IApplicationBuilder app)
        {
            var requestLocalizationOptions = new RequestLocalizationOptions
            {
                DefaultRequestCulture = new RequestCulture(new CultureInfo("fa-IR")),
                SupportedCultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                },
                SupportedUICultures = new[]
                {
                    new CultureInfo("en-US"),
                    new CultureInfo("fa-IR")
                }
            };
            app.UseRequestLocalization(requestLocalizationOptions);
            return app;
        }
    }
محل قرارگیری متد UseCustomRequestLocalization فوق در فایل آغازین برنامه، باید به صورت زیر باید باشد:
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseHttpsRedirection();

            app.UseCustomRequestLocalization();

            app.UseRouting();

            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }


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

در اینجا تعریف RegisterModel را مشاهده می‌کنید که ErrorMessage‌های آن هرچند به ظاهر یک رشته‌ی معمولی هستند، اما در عمل از فایل منبع اشتراکی خوانده می‌شوند:
using System.ComponentModel.DataAnnotations;

namespace Core3xSharedResource.Models.Account
{
    public class RegisterModel
    {
        [Required(ErrorMessage = "Please enter an email address")] // -->> from the shared resources
        [EmailAddress(ErrorMessage = "Please enter a valid email address")] // -->> from the shared resources
        public string Email { get; set; }
    }
}

فایل resx ما دارای یک چنین کلیدهایی است:
<?xml version="1.0" encoding="utf-8"?>
<root>
  <data name="&lt;b&gt;Hello&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;" xml:space="preserve">
    <value>&lt;b&gt;سلام&lt;/b&gt;&lt;i&gt; {0}&lt;/i&gt;</value>
  </data>
  <data name="About Title" xml:space="preserve">
    <value>درباره</value>
  </data>
  <data name="DNT" xml:space="preserve">
    <value>.NET Tips</value>
  </data>
  <data name="SiteName" xml:space="preserve">
    <value>DNT</value>
  </data>
  <data name="Please enter an email address" xml:space="preserve">
    <value>لطفا ایمیلی را وارد کنید</value>
  </data>
  <data name="Please enter a valid email address" xml:space="preserve">
    <value>لطفا ایمیل معتبری را وارد کنید</value>
  </data>
</root>
یک نکته: در اینجا بهتر است کلیدها را به صورت جملات کامل انگلیسی وارد کرد، تا اگر منبع فارسی معادل آن‌ها یافت نشدند، دقیقا از همان کلید، به عنوان مقدار خروجی سیستم بومی سازی استفاده کند.


آزمایش برنامه

اکنون برنامه‌ی Web API، ‌برای آزمایش آماده‌است. برای مثال در کنترلر زیر، سرویس عمومی IStringLocalizer به سازنده‌ی کلاس تزریق شده‌است و سپس قصد بازگشت مقدار کلید «About Title» را دارد. همچنین خطاهای بومی شده‌ی مدل برنامه را نیز بررسی می‌کنیم:
using Core3xSharedResource.Models.Account;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Core3xSharedResource.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class NormalIStringLocalizerController : ControllerBase
    {
        private readonly IStringLocalizer _localizer;

        public NormalIStringLocalizerController(IStringLocalizer localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public ActionResult<string> Get()
        {
            var localizedString = _localizer["About Title"];
            if (localizedString.ResourceNotFound)
            {
                return NotFound($"The localization resource with ID:`{localizedString.Name}` not found. SearchedLocation: `{localizedString.SearchedLocation}`.");
            }
            return localizedString.Value;
        }

        [HttpPost]
        public ActionResult<RegisterModel> Post(RegisterModel model)
        {
            return model;
        }
    }
}


حالت get را در تصویر فوق مشاهده می‌کنید. در Web API برای تنظیم زبان مورد استفاده می‌توان از هدری به نام Accept-Language استفاده کرد که برای مثال در اینجا به fa تنظیم شده‌است و نتیجه‌ی آن مراجعه به فایل SharedResource.fa.resx خواهد بود. اگر en-us وارد شود، نیاز خواهد بود تا فایل منبع اشتراکی دیگری را تعریف کنید. البته اگر این هدر تنظیم نشود، با توجه به تنظیمات متد UseCustomRequestLocalization، مقدار پیش‌فرض آن همان fa-IR خواهد بود.

حالت post را نیز در تصویر زیر می‌توان مشاهده کرد:


در اینجا چون ایمیل وارد نشده، هر دو خطای تنظیم شده‌ی در مدل برنامه را دریافت کرده‌ایم و این خطاها نیز فارسی هستند. به این معنا که بومی سازی data annotations نیز به درستی کار می‌کند.


تعریف یک سرویس عمومی برای محصور سازی قابلیت‌های بومی سازی، در برنامه‌های Web API

در ادامه تعریف سرویس SharedResourceService را مشاهده می‌کنید که ثبت آن‌را پیشتر انجام دادیم:
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;

namespace Core3xSharedResource.Services
{
    public interface ISharedResourceService
    {
        string this[string index] { get; }

        IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
        string GetString(string name, params object[] arguments);
        string GetString(string name);
    }

    public class SharedResourceService : ISharedResourceService
    {
        private readonly IStringLocalizer _sharedLocalizer;
        private readonly ILogger<SharedResourceService> _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public SharedResourceService(
            IStringLocalizer sharedHtmlLocalizer,
            IHttpContextAccessor httpContextAccessor,
            ILogger<SharedResourceService> logger
            )
        {
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
            _sharedLocalizer = sharedHtmlLocalizer ?? throw new ArgumentNullException(nameof(sharedHtmlLocalizer));
            _httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
        }

        public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
        {
            return _sharedLocalizer.GetAllStrings(includeParentCultures);
        }

        public string this[string index] => GetString(index);

        public string GetString(string name, params object[] arguments)
        {
            var result = _sharedLocalizer.GetString(name, arguments);
            logError(name, result);
            return result;
        }

        private void logError(string name, LocalizedString result)
        {
            if (result.ResourceNotFound)
            {
                var acceptLanguage = _httpContextAccessor?.HttpContext?.Request?.Headers["Accept-Language"];
                _logger.LogError($"The localization resource with Accept-Language:`{acceptLanguage}` & ID:`{name}` not found. SearchedLocation: `{result.SearchedLocation}`.");
            }
        }

        public string GetString(string name)
        {
            var result = _sharedLocalizer.GetString(name);
            logError(name, result);
            return result;
        }
    }
}
این سرویس نه فقط دسترسی به IStringLocalizer را محصور می‌کند، بلکه در متد logError آن اینبار خطای بسیار مفیدی جهت دیباگ کردن سیستم بومی سازی لاگ خواهد شد. اگر کلیدی یافت نشود، فایلی یافت نشود و یا زبان ارسالی تنظیمی یافت نشود، خطای آن‌را در لاگ‌های برنامه می‌توانید مشاهده کنید که در حالت عادی کار با IStringLocalizer، لاگ نمی‌شوند و همچنین هیچ خطا و یا استثنائی را نیز سبب نمی‌شوند. به همین جهت دیباگ کردن سیستم بومی سازی بدون این لاگ‌ها، تقریبا غیرممکن است. برای مثال مقدار baseNameهایی را که در کدهای این مطلب مشاهده می‌کنید، بر اساس همین لاگ‌ها تشخیص داده شدند و بدون آن‌ها تشکیل این مقادیر غیرممکن بودند.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Core3xSharedResource.zip
نظرات مطالب
حذف فضاهای خالی در خروجی صفحات ASP.NET MVC
سلام من بعداز اجرای کد زیر توی متغیر html کارکتر‌های عجیب میبینم و چیزی به اسم کد html  نمی‌بینم مشکل از کجا میتونه باشه
  public override void Write(byte[] buffer, int offset, int count)
        {
            string html = Encoding.UTF8.GetString(buffer);
}

مطالب
آشنایی با الگوی IOC یا Inversion of Control (واگذاری مسئولیت)

کلاس Kid را با تعریف زیر در نظر بگیرید. هدف از آن نگهداری اطلاعات فرزندان یک شخص خاص می‌باشد:

namespace IOCBeginnerGuide
{
class Kid
{
private int _age;
private string _name;

public Kid(int age, string name)
{
_age = age;
_name = name;
}

public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}

اکنون کلاس والد را با توجه به اینکه در حین ایجاد این شیء، فرزندان او نیز باید ایجاد شوند؛ در نظر بگیرید:
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private Kid _obj;

public Parent(int personAge, string personName, int kidsAge, string kidsName)
{
_obj = new Kid(kidsAge, kidsName);
_age = personAge;
_name = personName;
}

public override string ToString()
{
Console.WriteLine(_obj);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

و نهایتا مثالی از استفاده از آن توسط یک کلاینت:

using System;

namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
Parent p = new Parent(35, "Dev", 6, "Len");
Console.WriteLine(p);

Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}

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

KID's Age: 6, Kid's Name: Len
ParentAge: 35, ParentName: Dev

مثال فوق نمونه‌ای از الگوی طراحی ترکیب یا composition می‌باشد که به آن Object Dependency یا Object Coupling نیز گفته می‌شود. در این حالت ایجاد شیء والد وابسته است به ایجاد شیء فرزند.

مشکلات این روش:
1- با توجه به وابستگی شدید والد به فرزند، اگر نمونه سازی از شیء فرزند در سازنده‌ی کلاس والد با موفقیت روبرو نشود، ایجاد نمونه‌ی والد با شکست مواجه خواهد شد.
2- با از بین رفتن شیء والد، فرزندان او نیز از بین خواهند رفت.
3- هر تغییری در کلاس فرزند، نیاز به تغییر در کلاس والد نیز دارد (اصطلاحا به آن Dangling Reference هم گفته می‌شود. این کلاس آویزان آن کلاس است!).

چگونه این مشکلات را برطرف کنیم؟
بهتر است کار وهله سازی از کلاس Kid به یک شیء، متد یا حتی فریم ورک دیگری واگذار شود. به این واگذاری مسئولیت، delegation و یا inversion of control - IOC نیز گفته می‌شود.

بنابراین IOC می‌گوید که:
1- کلاس اصلی (یا همان Parent) نباید به صورت مستقیم وابسته به کلاس‌های دیگر باشد.
2- رابطه‌ی بین کلاس‌ها باید بر مبنای تعریف کلاس‌های abstract باشد (و یا استفاده از interface ها).

تزریق وابستگی یا Dependency injection
برای پیاده سازی IOC از روش تزریق وابستگی یا dependency injection استفاده می‌شود که می‌تواند بر اساس constructor injection ، setter injection و یا interface-based injection باشد و به صورت خلاصه پیاده سازی یک شیء را از مرحله‌ی ساخت وهله‌ای از آن مجزا و ایزوله می‌سازد.

مزایای تزریق وابستگی‌ها:
1- گره خوردگی اشیاء را حذف می‌کند.
2- اشیاء و برنامه را انعطاف پذیرتر کرده و اعمال تغییرات به آن‌ها ساده‌تر می‌شود.

روش‌های متفاوت تزریق وابستگی به شرح زیر هستند:

تزریق سازنده یا constructor injection :
در این روش ارجاعی از شیء مورد استفاده، توسط سازنده‌ی کلاس استفاده کننده از آن دریافت می‌شود. برای نمونه در مثال فوق از آنجائیکه کلاس والد به کلاس فرزندان وابسته است، یک ارجاع از شیء Kid به سازنده‌ی کلاس Parent باید ارسال شود.
اکنون بر این اساس تعاریف، کلاس‌های ما به شکل زیر تغییر خواهند کرد:

//IBuisnessLogic.cs
namespace IOCBeginnerGuide
{
public interface IBuisnessLogic
{
}
}

//Kid.cs
namespace IOCBeginnerGuide
{
class Kid : IBuisnessLogic
{
private int _age;
private string _name;

public Kid(int age, string name)
{
_age = age;
_name = name;
}

public override string ToString()
{
return "KID's Age: " + _age + ", Kid's Name: " + _name;
}
}
}

//Parent.cs
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;
private IBuisnessLogic _refKids;

public Parent(int personAge, string personName, IBuisnessLogic obj)
{
_age = personAge;
_name = personName;
_refKids = obj;
}

public override string ToString()
{
Console.WriteLine(_refKids);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

//CIOC.cs
using System;

namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;

public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David", objKid);
}

public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Constructor Injection";
}
}
}

//Program.cs
using System;

namespace IOCBeginnerGuide
{
class Program
{
static void Main(string[] args)
{
CIOC obj = new CIOC();
obj.FactoryMethod();
Console.WriteLine(obj);

Console.ReadKey();
Console.WriteLine("Press a key...");
}
}
}

توضیحات:
ابتدا اینترفیس IBuisnessLogic ایجاد خواهد شد. تنها متدهای این اینترفیس در اختیار کلاس Parent قرار خواهند گرفت.
از آنجائیکه کلاس Kid توسط کلاس Parent استفاده خواهد شد، نیاز است تا این کلاس نیز اینترفیس IBuisnessLogic را پیاده سازی کند.
اکنون سازنده‌ی کلاس Parent بجای ارجاع مستقیم به شیء Kid ، از طریق اینترفیس IBuisnessLogic با آن ارتباط برقرار خواهد کرد.
در کلاس CIOC کار پیاده سازی واگذاری مسئولیت وهله سازی از اشیاء مورد نظر صورت گرفته است. این وهله سازی در متدی به نام Factory انجام خواهد شد.
و در نهایت کلاینت ما تنها با کلاس IOC سرکار دارد.

معایب این روش:
- در این حالت کلاس business logic، نمی‌تواند دارای سازنده‌ی پیش فرض باشد.
- هنگامیکه وهله‌ای از کلاس ایجاد شد دیگر نمی‌توان وابستگی‌ها را تغییر داد (چون از سازنده‌ی کلاس جهت ارسال مقادیر مورد نظر استفاده شده است).

تزریق تنظیم کننده یا Setter injection
این روش از خاصیت‌ها جهت تزریق وابستگی‌ها بجای تزریق آن‌ها به سازنده‌ی کلاس استفاده می‌کند. در این حالت کلاس Parent می‌تواند دارای سازنده‌ی پیش فرض نیز باشد.

مزایای این روش:
- از روش تزریق سازنده بسیار انعطاف پذیرتر است.
- در این حالت بدون ایجاد وهله‌ای می‌توان وابستگی اشیاء را تغییر داد (چون سر و کار آن با سازنده‌ی کلاس نیست).
- بدون نیاز به تغییری در سازنده‌ی یک کلاس می‌توان وابستگی اشیاء را تغییر داد.
- تنظیم کننده‌ها دارای نامی با معناتر و با مفهوم‌تر از سازنده‌ی یک کلاس می‌باشند.

نحوه‌ی پیاده سازی آن:
در اینجا مراحل ساخت Interface و همچنین کلاس Kid با روش قبل تفاوتی ندارند. همچنین کلاینت نهایی استفاده کننده از IOC نیز مانند روش قبل است. تنها کلاس‌های IOC و Parent باید اندکی تغییر کنند:

//Parent.cs
using System;

namespace IOCBeginnerGuide
{
class Parent
{
private int _age;
private string _name;

public Parent(int personAge, string personName)
{
_age = personAge;
_name = personName;
}

public IBuisnessLogic RefKID {set; get;}

public override string ToString()
{
Console.WriteLine(RefKID);
return "ParentAge: " + _age + ", ParentName: " + _name;
}
}
}

//CIOC.cs
using System;

namespace IOCBeginnerGuide
{
class CIOC
{
Parent _p;

public void FactoryMethod()
{
IBuisnessLogic objKid = new Kid(12, "Ren");
_p = new Parent(42, "David");
_p.RefKID = objKid;
}

public override string ToString()
{
Console.WriteLine(_p);
return "Displaying using Setter Injection";
}
}
}

همانطور که ملاحظه می‌کنید در این روش یک خاصیت جدید به نام RefKID به کلاس Parent اضافه شده است که از هر لحاظ نسبت به روش تزریق سازنده با مفهوم‌تر و خود توضیح دهنده‌تر است. سپس کلاس IOC جهت استفاده از این خاصیت اندکی تغییر کرده است.

ماخذ

نظرات مطالب
توسعه برنامه های Cross Platform با Xamarin Forms & Bit Framework - قسمت چهارم

من از طریق کابل گوشیم رو وصل کردم اما در موارد زیر مشکل دارم میشه توضیح بدید چه باید کرد:

1- آنجا که نوشته اید " اولین بار دو برنامه بر روی گوشی شما نصب می‌شوند که برای کار دیباگ در Xamarin لازم هستند" باید دو برنامه در گوشیم نصب بشه ولی فقط یه برنامه نصب شده .

2- طبق نوشته " خوشبختانه به واسطه وجود Xaml edit and continue احتیاجی به Stop - Start کردن پروژه و بیلد کردن برای اعمال تغییرات UI نیست و به محض تغییر Xaml ، می‌توانید تاثیر آن را در گوشی خود ببینید. ولی برای هر تغییر CSharp باید Stop - Start و Build کنید." اگر بصورت Emulator برنامه را اجرا کنم با تغییر کد در xml موارد در شبیه ساز هم تغییر میکند اما در کدهای سی شارپ اصلا اجازه نمیده کدها رو تغییر بدم (از طریق stop – continue ) و در حالتی که از طریق   گوشیم بخوام استفاده کنم (بجای شبیه ساز)، با تغییر کدهای xml هم نمیشه تغییرات را روی گوشیم دید و برای کدهای سی شارپ هم که مثل Emulator عمل میکنه یعنی اجازه تغییر نمیده. ولی اگر  روی کدی break point بذارم برنامه را در همانجا break point درست عمل میکنه اما باز نمیتوان کد سی شارپ را تغییر داد.

  ویندوز 10 ورژن 1809
vs2017 ورژن 15.9.4
نظرات مطالب
بررسی خطاهای ممکن در حین راه اندازی اولیه برنامه‌های ASP.NET Core در IIS
یکی از شرایطی که در زمان publish برنامه و share کردن برنامه در IIS باعث بروز خطای 502.5 می‌شود  مطلب فعالسازی Development time IIS support می‌باشد.
برای انجام این فعالسازی، تغییر زیر در تگ aspNetCore در فایل web.config بوجود می‌آید که در زمان publish برنامه هم باقی می‌ماند:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    ...
    <aspNetCore processPath="bin\IISSupport\VSIISExeLauncher.exe" arguments="-argFile IISExeLauncherArgs.txt" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" stdoutLogEnabled="false" />
    ...
  </system.webServer>
</configuration>
که باعث بروز 
خطای 502.5  می‌باشد. برای حل این مشکل و همچنین استفاده از قابلیت ذکر شده در زمان توسعه برنامه، پس از publish برنامه در پوشه خروجی به فایل web.config مراجعه کرده تگ  aspNetCore  را به حالت اولیه آن باز می‌گردانیم:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <system.webServer>
    ...
     <aspNetCore processPath=".\ApplicationName.exe" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
    ...
  </system.webServer>
</configuration>
که در آن ApplicationName.exe نام فایل خروجی است که باید با نام مناسب جایگزین گردد.
مطالب
بررسی Source Generators در #C - قسمت دوم - یک مثال
یک مثال: پیاده سازی INotifyPropertyChanged توسط Source Generators

هدف از اینترفیس INotifyPropertyChanged که به همراه یک رخ‌داد است:
public interface INotifyPropertyChanged  
{ 
   event PropertyChangedEventHandler PropertyChanged;  
}
مطلع سازی استفاده کننده‌ی از یک شیء، از تغییرات رخ‌داده‌ی در مقادیر خواص آن است که نمونه‌ی آن، در برنامه‌های WPF، جهت به روز رسانی UI، زیاد مورد استفاده قرار می‌گیرد. البته این رخ‌داد به خودی خود کار خاصی را انجام نمی‌دهد و برای استفاده‌ی از آن، باید مقدار زیادی کد نوشت و این مقدار کد نیز باید به ازای تک تک خواص یک کلاس مدل، تکرار شوند:
  partial class CarModel : INotifyPropertyChanged
  {

    private double _speedKmPerHour;
    
    public double SpeedKmPerHour
    {
      get => _speedKmPerHour;
      set
      {
        _speedKmPerHour = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SpeedKmPerHour)));
      }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
  }
همچنین باید درنظر داشت که با تغییر نام خاصیتی، میزان قابل ملاحظه‌ای از این کدهای تکراری نیز باید به روز رسانی شوند که این عملیات می‌تواند ایده‌ی خوبی برای استفاده‌ی از Source Generators باشد.
اگر بخواهیم تولید این کدهای تکراری را به Source Generators محول کنیم، می‌توان برای مثال فیلد خصوصی مرتبط را نگه داشت و تولید مابقی کدها را خودکار کرد:
  partial class CarModel : INotifyPropertyChanged
  {
    private double _speedKmPerHour;    
  }
در این حالت کلاس مدل، به صورت partial تعریف می‌شود و فقط فیلد خصوصی، در کدهای ما حضور خواهد داشت. مابقی کدهای این کلاس partial به صورت خودکار توسط یک Source Generator سفارشی تولید خواهد شد. همانطور که ملاحظه می‌کنید، کاهش حجم قابل ملاحظه‌ای حاصل شده و همچنین اگر فیلد خصوصی دیگری نیز در اینجا اضافه شود، واکنش Source Generator ما آنی خواهد بود و بلافاصله کدهای مرتبط را تولید می‌کند و برنامه، بدون مشکلی کامپایل خواهد شد؛ هرچند به ظاهر INotifyPropertyChanged ذکر شده، در این کلاس اصلا پیاده سازی نشده‌است.


ایجاد پروژه‌ی Source Generator

در ابتدا برای ایجاد تولید کننده‌ی خودکار کدهای INotifyPropertyChanged، یک class library را به solution جاری اضافه می‌کنیم. سپس نیاز است ارجاعاتی را به دو بسته‌ی نیوگت زیر نیز افزود:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3">
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.2.0" PrivateAssets="all" />
  </ItemGroup>
</Project>
سپس کلاس جدید NotifyPropertyChangedGenerator را به نحو زیر به آن اضافه می‌کنیم:
  [Generator]
  public class NotifyPropertyChangedGenerator : ISourceGenerator
  {
    public void Initialize(GeneratorInitializationContext context)
    {
    }

    public void Execute(GeneratorExecutionContext context)
    {
- این کلاس باید اینترفیس ISourceGenerator را پیاده سازی کرده و همچنین مزین به ویژگی Generator باشد.
- اینترفیس ISourceGenerator به همراه دو متد Initialize و Execute است که در صورت نیاز باید پیاده سازی شوند.

در متد Execute، به خاصیت context.Compilation دسترسی داریم. این خاصیت تمام اطلاعاتی را که کامپایلر از Solution جاری در اختیار دارد، به توسعه دهنده ارائه می‌دهد. برای نمونه پیاده سازی متد Execute تولید کننده‌ی کد مثال جاری، چنین شکلی را دارد:
    public void Execute(GeneratorExecutionContext context)
    {
      // uncomment to debug the actual build of the target project
      // Debugger.Launch();
      var compilation = context.Compilation;
      var notifyInterface = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged");

      foreach (var syntaxTree in compilation.SyntaxTrees)
      {
        var semanticModel = compilation.GetSemanticModel(syntaxTree);
        var immutableHashSet = syntaxTree.GetRoot()
          .DescendantNodesAndSelf()
          .OfType<ClassDeclarationSyntax>()
          .Select(x => semanticModel.GetDeclaredSymbol(x))
          .OfType<ITypeSymbol>()
          .Where(x => x.Interfaces.Contains(notifyInterface))
          .ToImmutableHashSet();

        foreach (var typeSymbol in immutableHashSet)
        {
          var source = GeneratePropertyChanged(typeSymbol);
          context.AddSource($"{typeSymbol.Name}.Notify.cs", source);
        }
      }
    }
در اینجا با استفاده از context.Compilation به اطلاعات کامپایلر دسترسی پیدا کرده و سپس SyntaxTrees آن‌را یکی یکی، جهت یافتن کلاس‌ها و یا همان ClassDeclarationSyntax ها، پیمایش و بررسی می‌کنیم. سپس از بین این کلاس‌ها، کلاس‌هایی که INotifyPropertyChanged را پیاده سازی کرده باشند، انتخاب می‌کنیم که اطلاعات آن در پایان کار، به متد GeneratePropertyChanged جهت تولید مابقی کدهای partial class ارسال شده و کد تولیدی، به context اضافه می‌شود تا به نحو متداولی همانند سایر کدهای برنامه، به مجموعه کدهای مورد بررسی کامپایلر اضافه شود.

نکته‌ی مهم و جالب در اینجا این است که نیازی نیست تا قطعه کد جدید را به صورت SyntaxTrees در آورد و به کامپایلر اضافه کرد. می‌توان این قطعه کد را به نحو متداولی، به صورت یک قطعه رشته‌ی استاندارد #C، تولید و به کامپایلر با متد context.AddSource ارائه کرد که نمونه‌ای از آن‌را در ذیل مشاهده می‌کنید:
    private string GeneratePropertyChanged(ITypeSymbol typeSymbol)
    {
      return $@"
using System.ComponentModel;

namespace {typeSymbol.ContainingNamespace}
{{
  partial class {typeSymbol.Name}
  {{
    {GenerateProperties(typeSymbol)}
    public event PropertyChangedEventHandler? PropertyChanged;
  }}
}}";
    }

    private static string GenerateProperties(ITypeSymbol typeSymbol)
    {
      var sb = new StringBuilder();
      var suffix = "BackingField";

      foreach (var fieldSymbol in typeSymbol.GetMembers().OfType<IFieldSymbol>()
        .Where(x=>x.Name.EndsWith(suffix)))
      {
        var propertyName = fieldSymbol.Name[..^suffix.Length];
        sb.AppendLine($@"
    public {fieldSymbol.Type} {propertyName}
    {{
      get => {fieldSymbol.Name};
      set
      {{
        {fieldSymbol.Name} = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof({propertyName})));
      }}
    }}");
      }

      return sb.ToString();
    }
در اینجا در ابتدا بدنه‌ی کلاس partial تکمیل می‌شود. سپس خواص عمومی آن بر اساس فیلدهای خصوصی تعریف شده، تکمیل می‌شوند. در این مثال اگر یک فیلد خصوصی به عبارت BackingField ختم شود، به عنوان فیلدی که قرار است معادل کدهای INotifyPropertyChanged را داشته باشد، شناسایی می‌شود و به همراه کدهای تولید شده‌ی خودکار خواهد بود.

کدهای source generator ما همین مقدار بیش‌تر نیست. اکنون می‌خواهیم از آن در یک برنامه‌ی کنسول جدید (برای مثال به نام NotifyPropertyChangedGenerator.Demo) استفاده کنیم. برای اینکار نیاز است ارجاعی را به آن اضافه کنیم؛ اما این ارجاع، یک ارجاع متداول نیست و نیاز به ذکر چنین ویژگی خاصی وجود دارد:
<Project Sdk="Microsoft.NET.Sdk">

  <ItemGroup>
    <ProjectReference Include="..\NotifyPropertyChangedGenerator\NotifyPropertyChangedGenerator.csproj"
                      OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
  </ItemGroup>
</Project>
در اینجا میسر دهی پروژه‌ی تولید کننده‌ی کد، همانند سایر پروژه‌ها است؛ اما نوع آن باید آنالایزر معرفی شود. به همین جهت از خاصیت OutputItemType با مقدار Analyzer استفاده شده‌است. همچنین تنظیم ReferenceOutputAssembly به false به این معنا است که این اسمبلی ویژه، یک وابستگی و dependency واقعی پروژه‌ی جاری نیست و ما قرار نیست به صورت مستقیمی از کدهای آن استفاده کنیم.

برای آزمایش این تولید کننده‌ی کد، کلاس CarModel را به صورت زیر به پروژه‌ی کنسول آزمایشی اضافه می‌کنیم:
using System.ComponentModel;

namespace NotifyPropertyChangedGenerator.Demo
{
  public partial class CarModel : INotifyPropertyChanged
  {
    private double SpeedKmPerHourBackingField;
    private int NumberOfDoorsBackingField;
    private string ModelBackingField = "";

    public void SpeedUp() => SpeedKmPerHour *= 1.1;
  }
}
این کلاس پیاده سازی کننده‌ی INotifyPropertyChanged است؛ اما به همراه هیچ خاصیت عمومی نیست. فقط به همراه یکسری فیلد خصوصی ختم شده‌ی به «BackingField» است که توسط تولید کننده‌ی کد شناسایی شده و اطلاعات آن‌ها تکمیل می‌شود. فقط باید دقت داشت که این کلاس حتما باید به صورت partial تعریف شود تا امکان تکمیل خودکار کدهای آن وجود داشته باشد.

یک نکته:   در این حالت هرچند برنامه بدون مشکل کامپایل و اجرا می‌شود، ممکن است خطوط قرمزی را در IDE خود مشاهده کنید که عنوان می‌کند این قطعه از کد قابل کامپایل نیست. اگر با چنین صحنه‌ای مواجه شدید، یکبار solution را بسته و مجددا باز کنید تا تولید کننده‌ی کد، به خوبی شناسایی شود. البته نگارش‌های جدیدتر Visual Studio و Rider به همراه قابلیت auto reload پروژه برای کار با تولید کننده‌‌های کد هستند و دیگر شاهد چنین صحنه‌هایی نیستیم و حتی اگر برای مثال فیلد جدیدی را به CarModel اضافه کنیم، نه فقط بلافاصله کدهای متناظر آن تولید می‌شوند، بلکه خواص عمومی تولید شده در Intellisense نیز قابل دسترسی هستند.


نحوه‌ی مشاهده‌ی کدهای خودکار تولید شده

اگر علاقمند باشید تا کدهای خودکار تولید شده را مشاهده کنید، در Visual Studio، در قسمت و درخت نمایشی dependencies پروژه، گره‌ای به نام Analyzers وجود دارد که در آن برای مثال نام NotifyPropertyChangedGenerator و ذیل آن، کلاس‌های تولید شده‌ی توسط آن، قابل مشاهده و دسترسی هستند و حتی قابل دیباگ نیز می‌باشند؛ یعنی می‌توان بر روی سطور مختلف آن، break-point قرار داد.


کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: SourceGeneratorTests.zip

معرفی تعدادی منبع تکمیلی
- برنامه Source generator playground
در اینجا تعدادی مثال را که توسط مایکروسافت توسعه یافته‌است، مشاهده می‌کنید که اتفاقا یکی از آن‌ها پیاده سازی تولید کننده‌ی کد اینترفیس INotifyPropertyChanged است. در این برنامه، خروجی کدهای تولیدی نیز به سادگی قابل مشاهده‌است.

- برنامه SharpLab
برای توسعه‌ی تولید کننده‌های کد، عموما نیاز است تا با Roslyn API آشنا بود. در این برنامه اگر از منوی بالای صفحه قسمت results، گزینه‌ی «syntax tree» را انتخاب کنید و سپس قسمتی از کد خود را انتخاب کنید، بلافاصله معادل Roslyn API آن، در سمت راست صفحه نمایش داده می‌شود.

- معرفی مجموعه‌ای از Source Generators
در اینجا می‌توان مجموعه‌ای از پروژه‌های سورس باز Source Generators را مشاهده و کدهای آن‌ها را مطالعه کنید و یا از آن‌ها در پروژه‌های خود استفاده نمائید.

- معرفی یک cookbook در مورد Source Generators
این cookbook توسط خود مایکروسافت تهیه شده‌است و جهت شروع به کار با این فناوری، بسیار مفید است.

- مجموعه مثال‌های Source generators از مایکروسافت
در اینجا می‌توانید مجموعه مثال‌هایی از Source generators را که توسط مایکروسافت تهیه شده‌است، مشاهده کنید. شرح و توضیحات تعدادی از آن‌ها را هم در اینجا مطالعه کنید.
مطالب
توابع ارسال پیام و ارسال فایل به کانال‌های تلگرام
امروزه استفاده از کانال‌های تلگرام بسیار پر کاربرد شده و کاربران صفحات مجازی را مشغول کرده است. برنامه نویسان وب برای استفاده از API‌های تلگرام میتوانند به https://core.telegram.org/ مراجعه و توابع بیشتری را در پروژه‌های خود استفاده کنند.

  ایجاد یک کانال و بات
بعد از ایجاد یک کانال public نیاز است یک بات ایجاد کنید. سپس این بات را به عنوان Administrator به کانال خود اضافه کنید.


  ارسال پیام به کانال

var token="175287941:AAFcpXIIj1HuFlC5aB0QDBdWQBsTHqflkna"; // توکن خود را جایگزین کنید

var channelId ="@ChannelName";
var message ="تست ارسال";
 var uri = String.Format("https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}", Token, channelId, message);
using (WebClient client = new WebClient())
{
       dynamic s = client.DownloadString(uri);
}
بعد از ایجاد بات، سیستم به شما یک توکن می‌دهد که باید همان توکن را در ارسال‌ها استفاده کنید. دستورات بالا به این صورت عمل میکنند که از طرف توکن، به عنوان مدیر کانال، یک پیام در کانال درج می‌شود.


ارسال فایل به کانال

public bool SendFileToChannel(byte[] file, string fileName)
{
   var token="175287941:AAFcpXIIj1HuFlC5aB0QDBdWQBsTHqflkna"; // توکن خود را جایگزین کنید 
   var channelId ="@ChannelName"; 
           
   using (var client = new HttpClient())
   {
      var uri = String.Format("https://api.telegram.org/bot{0}/sendDocument?chat_id={1}", Token, channelId   );
       using (var multipartFormDataContent = new MultipartFormDataContent())
       {
            var streamContent = new StreamContent(new MemoryStream(file));
                        streamContent.Headers.Add("Content-Type", "application/octet-stream");
            streamContent.Headers.Add("Content-Disposition", "form-data; name=\"document\"; filename=\"" + fileName + "\"");
            multipartFormDataContent.Add(streamContent, "file", fileName);

            using (var message = client.PostAsync(uri, multipartFormDataContent))
            {
                   var contentString = message.Result.Content.ReadAsStringAsync();
            }
        }
     }
  
   return true;
}


طریقه فراخوانی تابع ارسال فایل
string path = @"c:\myFile.txt"; // All File Allowed : pdf, mp3, jpg ,...
string fileName = Path.GetFileName(path);
byte[] file = System.IO.File.ReadAllBytes(path);
SendFileToChannel(file, fileName);


ارسال پیام به یک کانال private
 برای ارسال پیام به یک کانال خصوصی ابتدا باید کانال عمومی داشته باشید. در تابع ارسال پیام به یک کانال تلگرام، متغیر uri هنگام اجرا دارای مقدار شده و شما میتوانید این آدرس را در مرورگر وارد کنید. اتفاقی که می‌افتد این است که ارسال به کانال انجام شده و نتیجه‌ی آن روی صفحه ظاهر میگردد.
 var uri = String.Format("https://api.telegram.org/bot{0}/sendMessage?chat_id={1}&text={2}", Token, channelId, message);
در متن بازگشتی یک پارامتر بنام ChannelId نمایش داده می‌شود که معمولا بصورت یک عدد منفی مثل 1001001734568- است که این کد، کد یکتای کانال ایجاد شده می‌باشد. شما از این به بعد میتوانید کانال را Private یا حتی Rename کنید. همچنین میتوانید در ارسال به کانال، بجای استفاده از ChannelName@، از کد یکتای بدست آمده استفاده نمائید.
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت پنجم - پیاده سازی ورود و خروج از سیستم
پس از راه اندازی IdentityServer، نوبت به امن سازی برنامه‌ی Mvc Client توسط آن می‌رسد و اولین قسمت آن، ورود به سیستم و خروج از آن می‌باشد.


بررسی اجزای Hybrid Flow

در قسمت سوم در حین «انتخاب OpenID Connect Flow مناسب برای یک برنامه‌ی کلاینت از نوع ASP.NET Core» به این نتیجه رسیدیم که Flow مناسب یک برنامه‌ی Mvc Client از نوع Hybrid است. در اینجا هر Flow، شروع به ارسال درخواستی به سمت Authorization Endpoint می‌کند؛ با یک چنین قالبی:
https://idpHostAddress/connect/authorize? 
client_id=imagegalleryclient 
&redirect_uri=https://clientapphostaddress/signin-oidcoidc 
&scope=openid profile 
&response_type=code id_token 
&response_mode=form_post
&nonce=63626...n2eNMxA0
- در سطر اول، Authorization Endpoint مشخص شده‌است. این آدرس از discovery endpoint که یک نمونه تصویر محتوای آن‌را در قسمت قبل مشاهده کردید، استخراج می‌شود.
- سپس client_id جهت تعیین برنامه‌ای که درخواست را ارسال می‌کند، ذکر شده‌است؛ از این جهت که یک IDP جهت کار با چندین نوع کلاینت مختلف طراحی شده‌است.
- redirect_uri همان Redirect Endpoint است که در سطح برنامه‌ی کلاینت تنظیم می‌شود.
- در مورد scope در قسمت قبل در حین راه اندازی IdentityServer توضیح دادیم. در اینجا برنامه‌ی کلاینت، درخواست scopeهای openid و profile را داده‌است. به این معنا که نیاز دارد تا Id کاربر وارد شده‌ی به سیستم و همچنین Claims منتسب به او را در اختیار داشته باشد.
- response_type نیز به code id_token تنظیم شده‌است. توسط response_type، نوع Flow مورد استفاده مشخص می‌شود. ذکر code به معنای بکارگیری Authorization code flow است. ذکر id_token و یا id_token token هر دو به معنای استفاده‌ی از implicit flow است. اما برای مشخص سازی Hybrid flow یکی از سه مقدار code id_token و یا code token و یا code id_token token با هم ذکر می‌شوند:


- در اینجا response_mode مشخص می‌کند که اطلاعات بازگشتی از سمت IDP که توسط response_type مشخص شده‌اند، با چه قالبی به سمت کلاینت بازگشت داده شوند که می‌تواند از طریق Form POST و یا URI باشد.


در Hybrid flow با response_type از نوع code id_token، ابتدا کلاینت یک درخواست Authentication را به Authorization Endpoint ارسال می‌کند (با همان قالب URL فوق). سپس در سطح IDP، کاربر برای مثال با ارائه‌ی کلمه‌ی عبور و نام کاربری، تعیین اعتبار می‌شود. همچنین در اینجا IDP ممکن است رضایت کاربر را از دسترسی به اطلاعات پروفایل او نیز سؤال بپرسد (تحت عنوان مفهوم Consent). سپس IDP توسط یک Redirection و یا Form POST، اطلاعات authorization code و identity token را به سمت برنامه‌ی کلاینت ارسال می‌کند. این همان اطلاعات مرتبط با response_type ای است که درخواست کرد‌ه‌ایم. سپس برنامه‌ی کلاینت این اطلاعات را تعیین اعتبار کرده و در صورت موفقیت آمیز بودن این عملیات، اکنون درخواست تولید توکن هویت را به token endpoint ارسال می‌کند. برای این منظور کلاینت سه مشخصه‌ی authorization code ،client-id و client-secret را به سمت token endpoint ارسال می‌کند. در پاسخ یک identity token را دریافت می‌کنیم. در اینجا مجددا این توکن تعیین اعتبار شده و سپس Id کاربر را از آن استخراج می‌کند که در برنامه‌ی کلاینت قابل استفاده خواهد بود. این مراحل را در تصویر زیر می‌توانید ملاحظه کنید.
البته اگر دقت کرده باشید، یک identity token در همان ابتدای کار از Authorization Endpoint دریافت می‌شود. اما چرا از آن استفاده نمی‌کنیم؟ علت اینجا است که token endpoint نیاز به اعتبارسنجی client را نیز دارد. به این ترتیب یک لایه‌ی امنیتی دیگر نیز در اینجا بکار گرفته می‌شود. همچنین access token و refresh token نیز از همین token endpoint قابل دریافت هستند.




تنظیم IdentityServer جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به برنامه‌ی MVC قسمت دوم، نیاز است تغییراتی را در برنامه‌ی کلاینت و همچنین IDP اعمال کنیم. برای این منظور کلاس Config پروژه‌ی IDP را که در قسمت قبل ایجاد کردیم، به صورت زیر تکمیل می‌کنیم:
namespace DNT.IDP
{
    public static class Config
    {
        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientName = "Image Gallery",
                    ClientId = "imagegalleryclient",
                    AllowedGrantTypes = GrantTypes.Hybrid,
                    RedirectUris = new List<string>
                    {
                        "https://localhost:5001/signin-oidc"
                    },
                    PostLogoutRedirectUris = new List<string>
                    {
                        "https://localhost:5001/signout-callback-oidc"
                    },
                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    },
                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    }
                }
             };
        }
    }
}
در اینجا بجای بازگشت لیست خالی کلاینت‌ها، یک کلاینت جدید را تعریف و تکمیل کرده‌ایم.
- ابتدا نام کلاینت را مشخص می‌کنیم. این نام و عنوان، در صفحه‌ی لاگین و Consent (رضایت دسترسی به اطلاعات پروفایل کاربر)، ظاهر می‌شود.
- همچنین نیاز است یک Id دلخواه را نیز برای آن مشخص کنیم؛ مانند imagegalleryclient در اینجا.
- AllowedGrantTypes را نیز به Hybrid Flow تنظیم کرده‌ایم. علت آن‌را در قسمت سوم این سری بررسی کردیم.
- با توجه به اینکه Hybrid Flow از Redirectها استفاده می‌کند و اطلاعات نهایی را به کلاینت از طریق Redirection ارسال می‌کند، به همین جهت آدرس RedirectUris را به آدرس برنامه‌ی Mvc Client تنظیم کرده‌ایم (که در اینجا بر روی پورت 5001 کار می‌کند). قسمت signin-oidc آن‌را در ادامه تکمیل خواهیم کرد.
- در قسمت AllowedScopes، لیست scopeهای مجاز قابل دسترسی توسط این کلاینت مشخص شده‌اند که شامل دسترسی به ID کاربر و Claims آن است.
- به ClientSecrets نیز جهت client authenticating نیاز داریم.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات ورود به سیستم بر اساس جزئیات Hybrid Flow

برای افزودن قسمت لاگین به سیستم، کلاس آغازین پروژه‌ی MVC Client را به نحو زیر تکمیل می‌کنیم:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddAuthentication(options =>
            {
                options.DefaultScheme = "Cookies";
                options.DefaultChallengeScheme = "oidc";
            }).AddCookie("Cookies")
              .AddOpenIdConnect("oidc", options =>
              {
                  options.SignInScheme = "Cookies";
                  options.Authority = "https://localhost:6001";
                  options.ClientId = "imagegalleryclient";
                  options.ResponseType = "code id_token";
                  //options.CallbackPath = new PathString("...")
                  //options.SignedOutCallbackPath = new PathString("...")
                  options.Scope.Add("openid");
                  options.Scope.Add("profile");
                  options.SaveTokens = true;
                  options.ClientSecret = "secret";
                  options.GetClaimsFromUserInfoEndpoint = true;
              });
این قسمت تنظیمات، سمت کلاینت OpenID Connect Flow را مدیریت می‌کند.

- ابتدا با فراخوانی AddAuthentication، کار تنظیمات میان‌افزار استاندارد Authentication برنامه‌های ASP.NET Core انجام می‌شود. در اینجا DefaultScheme آن به Cookies تنظیم شده‌است تا عملیات Sign-in و Sign-out سمت کلاینت را میسر کند. سپس DefaultChallengeScheme به oidc تنظیم شده‌است. این مقدار با Scheme ای که در ادامه آن‌را تنظیم خواهیم کرد، تطابق دارد.

- سپس متد AddCookie فراخوانی شده‌است که authentication-Scheme را به عنوان پارامتر قبول می‌کند. به این ترتیب cookie based authentication در برنامه میسر می‌شود. پس از اعتبارسنجی توکن هویت دریافتی و تبدیل آن به Claims Identity، در یک کوکی رمزنگاری شده برای استفاده‌های بعدی ذخیره می‌شود.

- در آخر تنظیمات پروتکل OpenID Connect را ملاحظه می‌کنید. به این ترتیب مراحل اعتبارسنجی توسط این پروتکل در اینجا که Hybrid flow است، پشتیبانی خواهد شد.  اینجا است که کار درخواست Authorization، دریافت و اعتبارسنجی توکن هویت صورت می‌گیرد. اولین پارامتر آن authentication-Scheme است که به oidc تنظیم شده‌است. به این ترتیب اگر قسمتی از برنامه نیاز به Authentication داشته باشد، OpenID Connect به صورت پیش‌فرض مورد استفاده قرار می‌گیرد. به همین جهت DefaultChallengeScheme را نیز به oidc تنظیم کردیم. در اینجا SignInScheme به Cookies تنظیم شده‌است که با DefaultScheme اعتبارسنجی تطابق دارد. به این ترتیب نتیجه‌ی موفقیت آمیز عملیات اعتبارسنجی در یک کوکی رمزنگاری شده ذخیره خواهد شد. مقدار خاصیت Authority به آدرس IDP تنظیم می‌شود که بر روی پورت 6001 قرار دارد. تنظیم این مسیر سبب خواهد شد تا این میان‌افزار سمت کلاینت، به discovery endpoint دسترسی یافته و بتواند مقادیر سایر endpoints برنامه‌ی IDP را به صورت خودکار دریافت و استفاده کند. سپس ClientId تنظیم شده‌است که باید با مقدار تنظیم شده‌ی آن در سمت IDP یکی باشد و همچنین مقدار ClientSecret در اینجا نیز باید با ClientSecrets سمت IDP یکی باشد. ResponseType تنظیم شده‌ی در اینجا با AllowedGrantTypes سمت IDP تطابق دارد که از نوع Hybrid است. سپس دو scope درخواستی توسط این برنامه‌ی کلاینت که openid و profile هستند در اینجا اضافه شده‌اند. به این ترتیب می‌توان به مقادیر Id کاربر و claims او دسترسی داشت. مقدار CallbackPath در اینجا به RedirectUris سمت IDP اشاره می‌کند که مقدار پیش‌فرض آن همان signin-oidc است. با تنظیم SaveTokens به true امکان استفاده‌ی مجدد از آن‌ها را میسر می‌کند.

پس از تکمیل قسمت ConfigureServices و انجام تنظیمات میان‌افزار اعتبارسنجی، نیاز است این میان‌افزار را نیز به برنامه افزود که توسط متد UseAuthentication انجام می‌شود:
namespace ImageGallery.MvcClient.WebApp
{
    public class Startup
    {
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseAuthentication();

پس از این تنظیمات، با اعمال ویژگی Authorize، دسترسی به کنترلر گالری برنامه‌ی MVC Client را صرفا محدود به کاربران وارد شده‌ی به سیستم می‌کنیم:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
    // .... 
   
        public async Task WriteOutIdentityInformation()
        {
            var identityToken = await HttpContext.GetTokenAsync(OpenIdConnectParameterNames.IdToken);
            Debug.WriteLine($"Identity token: {identityToken}");

            foreach (var claim in User.Claims)
            {
                Debug.WriteLine($"Claim type: {claim.Type} - Claim value: {claim.Value}");
            }
        }
در اینجا علاوه بر اعمال فیلتر Authorize به کل اکشن متدهای این کنترلر، یک اکشن متد جدید دیگر را نیز به انتهای آن اضافه کرده‌ایم تا صرفا جهت دیباگ برنامه، اطلاعات دریافتی از IDP را در Debug Window، برای بررسی بیشتر درج کند. البته این روش با Debug Window مخصوص Visual Studio کار می‌کند. اگر می‌خواهید آن‌را در صفحه‌ی کنسول dotnet run مشاهده کنید، بجای Debug باید از ILogger استفاده کرد.

فراخوانی متد GetTokenAsync با پارامتر IdToken، همان Identity token دریافتی از IDP را بازگشت می‌دهد. این توکن با تنظیم SaveTokens به true در تنظیمات AddOpenIdConnect که پیشتر انجام دادیم، قابل استخراج از کوکی اعتبارسنجی برنامه شده‌است.
این متد را در ابتدای اکشن متد Index فراخوانی می‌کنیم:
        public async Task<IActionResult> Index()
        {
            await WriteOutIdentityInformation();
            // ....


اجرای برنامه جهت آزمایش تنظیمات انجام شده

برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.

اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید:


در این حالت چون فیلتر Authorize به کل اکشن متدهای کنترلر گالری اعمال شده، میان‌افزار Authentication که در فایل آغازین برنامه‌ی کلاینت MVC تنظیم شده‌است، وارد عمل شده و کاربر را به صفحه‌ی لاگین سمت IDP هدایت می‌کند (شماره پورت آن 6001 است). لاگ این اعمال را هم در برگه‌ی network مرورگر می‌تواند مشاهده کنید.

در اینجا نام کاربری و کلمه‌ی عبور اولین کاربر تعریف شده‌ی در فایل Config.cs برنامه‌ی IDP را که User 1 و password است، وارد می‌کنیم. پس از آن صفحه‌ی Consent ظاهر می‌شود:


در اینجا از کاربر سؤال می‌پرسد که آیا به برنامه‌ی کلاینت اجازه می‌دهید تا به Id و اطلاعات پروفایل و یا همان Claims شما دسترسی پیدا کند؟
فعلا گزینه‌ی remember my design را انتخاب نکنید تا همواره بتوان این صفحه را در دفعات بعدی نیز مشاهده کرد. سپس بر روی گزینه‌ی Yes, Allow کلیک کنید.
اکنون به صورت خودکار به سمت برنامه‌ی MVC Client هدایت شده و می‌توانیم اطلاعات صفحه‌ی اول سایت را کاملا مشاهده کنیم (چون کاربر اعتبارسنجی شده‌است، از فیلتر Authorize رد خواهد شد).


همچنین در اینجا اطلاعات زیادی نیز جهت دیباگ برنامه لاگ می‌شوند که در آینده جهت عیب یابی آن می‌توانند بسیار مفید باشند:


با دنبال کردن این لاگ می‌توانید مراحل Hybrid Flow را مرحله به مرحله با مشاهده‌ی ریز جزئیات آن بررسی کنید. این مراحل به صورت خودکار توسط میان‌افزار Authentication انجام می‌شوند و در نهایت اطلاعات توکن‌های دریافتی به صورت خودکار در اختیار برنامه برای استفاده قرار می‌گیرند. یعنی هم اکنون کوکی رمزنگاری شده‌ی اطلاعات اعتبارسنجی کاربر در دسترس است و به اطلاعات آن می‌توان توسط شیء this.User، در اکشن متدهای برنامه‌ی MVC، دسترسی داشت.


تنظیم برنامه‌ی MVC Client جهت انجام عملیات خروج از سیستم

ابتدا نیاز است یک لینک خروج از سیستم را به برنامه‌ی کلاینت اضافه کنیم. برای این منظور به فایل Views\Shared\_Layout.cshtml مراجعه کرده و لینک logout را در صورت IsAuthenticated بودن کاربر جاری وارد شده‌ی به سیستم، نمایش می‌دهیم:
<div class="navbar-collapse collapse">
    <ul class="nav navbar-nav">
        <li><a asp-area="" asp-controller="Gallery" asp-action="Index">Home</a></li>
        <li><a asp-area="" asp-controller="Gallery" asp-action="AddImage">Add an image</a></li>
        @if (User.Identity.IsAuthenticated)
        {
            <li><a asp-area="" asp-controller="Gallery" asp-action="Logout">Logout</a></li>
        }
    </ul>
</div>


شیء this.User، هم در اکشن متدها و هم در Viewهای برنامه، جهت دسترسی به اطلاعات کاربر اعتبارسنجی شده، در دسترس است.
این لینک به اکشن متد Logout، در کنترلر گالری اشاره می‌کند که آن‌را به صورت زیر تکمیل خواهیم کرد:
namespace ImageGallery.MvcClient.WebApp.Controllers
{
    [Authorize]
    public class GalleryController : Controller
    {
        public async Task Logout()
        {
            // Clears the  local cookie ("Cookies" must match the name of the scheme)
            await HttpContext.SignOutAsync("Cookies");
            await HttpContext.SignOutAsync("oidc");
        }
در اینجا ابتدا کوکی Authentication حذف می‌شود. نامی که در اینجا انتخاب می‌شود باید با نام scheme انتخابی مرتبط در فایل آغازین برنامه یکی باشد.
سپس نیاز است از برنامه‌ی IDP نیز logout شویم. به همین جهت سطر دوم SignOutAsync با پارامتر oidc را مشاهده می‌کنید. بدون وجود این سطر، کاربر فقط از برنامه‌ی کلاینت logout می‌شود؛ اما اگر به IDP مجددا هدایت شود، مشاهده خواهد کرد که در آن سمت، هنوز نام کاربری او توسط IDP شناسایی می‌شود.


بهبود تجربه‌ی کاربری Logout

پس از logout، بدون انجام یکسری از تنظیمات، کاربر مجددا به برنامه‌ی کلاینت به صورت خودکار هدایت نخواهد شد و در همان سمت IDP متوقف می‌شد. برای بهبود این وضعیت و بازگشت مجدد به برنامه‌ی کلاینت، اینکار را یا توسط مقدار دهی خاصیت SignedOutCallbackPath مربوط به متد AddOpenIdConnect می‌توان انجام داد و یا بهتر است مقدار پیش‌فرض آن‌را به تنظیمات IDP نسبت داد که پیشتر در تنظیمات متد GetClients آن‌را ذکر کرده بودیم:
PostLogoutRedirectUris = new List<string>
{
     "https://localhost:5001/signout-callback-oidc"
},
با وجود این تنظیم، اکنون IDP می‌داند که پس از logout، چه آدرسی را باید به کاربر جهت بازگشت به سیستم قبلی ارائه دهد:


البته هنوز یک مرحله‌ی انتخاب و کلیک بر روی لینک بازگشت وجود دارد. برای حذف آن و خودکار کردن Redirect نهایی آن، می‌توان کدهای IdentityServer4.Quickstart.UI را که در قسمت قبل به برنامه‌ی IDP اضافه کردیم، اندکی تغییر دهیم. برای این منظور فایل src\IDP\DNT.IDP\Quickstart\Account\AccountOptions.cs را گشوده و سپس فیلد AutomaticRedirectAfterSignOut را که false است، به true تغییر دهید.

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

به صورت پیش‌فرض، Identity Server اطلاعات Claims کاربر را ارسال نمی‌کند و Identity token صرفا به همراه اطلاعات Id کاربر است. برای تنظیم آن می‌توان در سمت تنظیمات IDP، در متد GetClients، زمانیکه new Client صورت می‌گیرد، خاصیت AlwaysIncludeUserClaimsInIdToken هر کلاینت را به true تنظیم کرد؛ اما ایده خوبی نیست. Identity token از طریق Authorization endpoint دریافت می‌شود. در اینجا اگر این اطلاعات از طریق URI دریافت شود و Claims به Identity token افزوده شوند، به مشکل بیش از حد طولانی شدن URL نهایی خواهیم رسید و ممکن است از طرف وب سرور یک چنین درخواستی برگشت بخورد. به همین جهت به صورت پیش‌فرض اطلاعات Claims به Identity token اضافه نمی‌شوند.
در اینجا برای دریافت Claims، یک endpoint دیگر در IDP به نام UserInfo endpoint درنظر گرفته شده‌است. در این حالت برنامه‌ی کلاینت، مقدار Access token دریافتی را که به همراه اطلاعات scopes متناظر با Claims است، به سمت UserInfo endpoint ارسال می‌کند. باید دقت داشت زمانیکه Identity token دوم از Token endpoint دریافت می‌شود (تصویر ابتدای بحث)، به همراه آن یک Access token نیز صادر و ارسال می‌گردد. اینجا است که میان‌افزار oidc، این توکن دسترسی را به سمت UserInfo endpoint ارسال می‌کند تا user claims را دریافت کند:


در تنظیمات سمت کلاینت AddOpenIdConnect، درخواست openid و profile، یعنی درخواست Id کاربر و Claims آن وجود دارند:
options.Scope.Add("openid");
options.Scope.Add("profile");
برای بازگشت آن‌ها به سمت کلاینت، درخواست دریافت claims از UserInfo Endpoint را در سمت کلاینت تنظیم می‌کنیم:
options.GetClaimsFromUserInfoEndpoint = true;
همین اندازه تنظیم میان‌افزار oidc، برای انجام خودکار کل گردش کاری یاد شده کافی است.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی src\WebApi\ImageGallery.WebApi.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا WebAPI برنامه راه اندازی شود.
- سپس به پوشه‌ی src\IDP\DNT.IDP مراجعه کرده و و dotnet_run.bat آن‌را اجرا کنید تا برنامه‌ی IDP راه اندازی شود.
- در آخر به پوشه‌ی src\MvcClient\ImageGallery.MvcClient.WebApp وارد شده و dotnet_run.bat آن‌را اجرا کنید تا MVC Client راه اندازی شود.
اکنون که هر سه برنامه با هم در حال اجرا هستند، مرورگر را گشوده و مسیر https://localhost:5001 را درخواست کنید. در صفحه‌ی login نام کاربری را User 1 و کلمه‌ی عبور آن‌را password وارد کنید.
مطالب
React 16x - قسمت 7 - ترکیب کامپوننت‌ها - بخش 1 - ارسال داده‌ها، مدیریت رخ‌دادها
تا اینجا، تنها با یک تک کامپوننت کار کردیم؛ اما یک برنامه‌ی واقعی ترکیبی است از چندین کامپوننت که در نهایت درخت کامپوننت‌ها را در React تشکیل می‌دهند. به همین جهت در طی چند قسمت، نکات ترکیب کامپوننت‌ها را بررسی می‌کنیم.


ترکیب کامپوننت‌ها

در ادامه، همان برنامه‌ی تا قسمت 5 را که کار نمایش یک counter را انجام می‌دهد، تکمیل می‌کنیم. در این برنامه اگر به فایل index.js دقت کنید، کار رندر تک کامپوننت Counter را انجام می‌دهیم:
ReactDOM.render(<Counter />, document.getElementById("root"));
اما یک برنامه‌ی واقعی React، متشکل از درختی از کامپوننت‌ها است. به این ترتیب با ترکیب و در کنار هم قرار دادن کامپوننت‌های مختلف، می‌توان به UI ای کارآمد و پیچیده رسید.
برای نمایش این مفهوم، کامپوننت جدید src\components\counters.jsx را ایجاد می‌کنیم. قصد داریم در این کامپوننت، لیستی از کامپوننت‌های Counter را رندر کنیم. سپس در index.js، بجای رندر کامپوننت Counter، کامپوننت جدید Counters را رندر می‌کنیم. به این ترتیب درخت کامپوننت‌های برنامه، در سطح بالایی خودش از کامپوننت Counters شروع می‌شود و سپس فرزندان آن‌را کامپوننت‌های Counter تشکیل می‌دهند. به همین جهت فایل index.js را به صورت زیر ویرایش می‌کنیم تا به کامپوننت Counters اشاره کند:
import Counters from "./components/counters";

ReactDOM.render(<Counters />, document.getElementById("root"));
سپس به فایل جدید src\components\counters.jsx مراجعه کرده و با استفاده از قطعه کدهای کمکی imrc و cc که در قسمت‌های قبل با آن‌ها آشنا شدیم، ساختار بدنه‌ی کامپوننت جدید Counters را ایجاد می‌کنیم. اکنون در متد render آن، یک div را ایجاد کرده و داخل آن، چندین کامپوننت Counter را رندر می‌کنیم:
import React, { Component } from "react";

import Counter from "./counter";

class Counters extends Component {
  state = {};

  render() {
    return (
      <div>
        <Counter />
        <Counter />
        <Counter />
        <Counter />
      </div>
    );
  }
}

export default Counters;
در این حالت اگر به مرورگر مراجعه کنیم، مشاهده خواهیم کرد که هر کامپوننت، state خاص خودش را دارد و از سایر کامپوننت‌ها ایزوله است:


در مرحله‌ی بعد، بجای رندر و درج دستی این کامپوننت‌ها، آرایه‌ای از اشیاء counter را ایجاد کرده و سپس آن‌ها را توسط متد Array.map رندر می‌کنیم:
import React, { Component } from "react";
import Counter from "./counter";

class Counters extends Component {
  state = {
    counters: [
      { id: 1, value: 0 },
      { id: 2, value: 0 },
      { id: 3, value: 0 },
      { id: 4, value: 0 }
    ]
  };

  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} />
        ))}
      </div>
    );
  }
}

export default Counters;
در اینجا یک خاصیت جدید را به شیء منتسب به خاصیت state به نام counters اضافه کرده‌ایم. این خاصیت حاوی آرایه‌ای از اشیاء counter است که هر کدام دارای یک id (که در قسمت key ذکر خواهد شد) و مقداری اولیه است. سپس آرایه‌ی this.state.counters را توسط متد map، رندر کرده‌ایم. تا اینجا پس از ذخیره‌ی فایل و بارگذاری مجدد برنامه، همان خروجی قبلی را مشاهده خواهیم کرد.


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

مشکل! مقدار value هر شیء شمارشگر تعریف شده، به کامپوننت‌های مرتبط رندر شده اعمال نشده‌است. برای مثال اگر value اولین شیء را به 4 تغییر دهیم، هنوز هم این کامپوننت با همان مقدار صفر شروع به کار می‌کند. برای رفع این مشکل، به همان روشی که ویژگی key کامپوننت Counter را مقدار دهی کردیم، می‌توان ویژگی‌های سفارشی دیگری را تعریف و مقدار دهی کرد:
  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} value={counter.value} selected={true} />
        ))}
      </div>
    );
پس از تعریف ویژگی‌های دلخواه value و selected که یکی از آن‌ها به مقدار value شیء counter مرتبط متصل است، به خود کامپوننت Counter مراجعه کرده و سپس در ابتدای متد render آن، خاصیت props به ارث رسیده شده‌ی از کلاس پایه‌ی Component را جهت بررسی بیشتر لاگ می‌کنیم:
class Counter extends Component {
  state = {
    count: 0
  };

  render() {
    console.log("props", this.props);
    //...
پس از ذخیره‌ی فایل counter.jsx و بارگذاری مجدد برنامه، یک چنین خروجی در کنسول توسعه دهندگان مرورگر قابل مشاهده است:


خاصیت this.props، یک شیء ساده‌ی جاوا اسکریپتی است و شامل تمام ویژگی‌هایی می‌باشد که ما در کامپوننت Counters برای هر کدام از کامپوننت‌های Counter رندر شده‌ی توسط آن، تعریف کردیم. برای نمونه دو ویژگی جدید value و selected را که به تعاریف المان‌های Counter در کامپوننت Counters اضافه کردیم، در اینجا به همراه مقادیر منتسب به آن‌ها، قابل مشاهده هستند. البته در این خروجی، key را ملاحظه نمی‌کنید؛ چون هدف اصلی آن، معرفی یکتای المان‌ها در DOM مجازی React است.
بنابراین اکنون می‌توان به value تنظیم شده‌ی در کامپوننت Counters به صورت this.props.value در کامپوننت Counter دسترسی یافت و سپس از آن جهت مقدار دهی اولیه‌ی counter استفاده کرد.
class Counter extends Component {
  state = {
    count: this.props.value
  };
اکنون اگر تغییرات کامپوننت Counter را ذخیره کرده و به مرورگر مراجعه کنیم، در اولین بار نمایش برنامه و بدون اعمال هیچگونه تغییری، یک چنین خروجی حاصل می‌شود:


یک نکته: در اینجا selected={true} را داریم. اگر مقدار آن‌را حذف کنیم، یعنی selected تنها درج شود، مقدار آن، همان true دریافت خواهد شد.


تعریف فرزند برای المان‌های کامپوننت‌ها

ویژگی‌های اضافه شده‌ی به تعاریف المان‌های کامپوننت‌ها، توسط خاصیت this.props، به هر کدام از آن کامپوننت‌ها منتقل می‌شوند. این خاصیت props، یک خاصیت ویژه را به نام children، نیز دارا است و از آن برای دسترسی به المان‌های تعریف شده‌ی بین تگ‌های یک المان اصلی استفاده می‌شود:
  render() {
    return (
      <div>
        {this.state.counters.map(counter => (
          <Counter key={counter.id} value={counter.value} selected={true}>
            <h4>‍Counter #{counter.id}</h4>
          </Counter>
        ))}
      </div>
    );
  }
در اینجا بین تگ‌های ابتدا و انتهای تعریف المان Counter، یک محتوا نیز تعریف شده‌است. اکنون اگر به خروجی کنسول توسعه دهندگان مرورگر دقت کنیم، خاصیت جدید اضافه شده‌ی children را نیز می‌توان مشاهده کرد:


یک نمونه مثال واقعی این قابلیت، امکان تعریف محتوای دیالوگ باکس‌ها، توسط استفاده کنند‌ه‌ی از آن است.


روش دیباگ برنامه‌های React

افزونه‌ی مفید React developer tools را می‌توانید برای مرورگرهای کروم و فایرفاکس، دریافت و نصب کنید. برای نمونه پس از نصب آن در مرورگر کروم، یک برگه‌ی جدید به لیست برگه‌های کنسول توسعه دهندگان آن اضافه می‌شود:


همانطور که مشاهده کنید، درخت کامپوننت‌های برنامه را در برگه‌ی جدید Components، می‌توان مشاهده کرد. در اینجا با انتخاب هر کدام از فرزندان این درخت، مشخصات آن نیز مانند props و state، در کنار صفحه ظاهر می‌شوند. همچنین در بالای همین قسمت، 4 آیکن مشاهده‌ی سورس، مشاهده‌ی DOM و یا لاگ کردن جزئیات شیء کامپوننت انتخابی در کنسول هم درج شده‌اند:


که برای نمونه چنین خروجی را لاگ می‌کند:



بررسی تفاوت‌های خواص props و state

در کامپوننت Counter، از props برای مقدار دهی اولیه‌ی state استفاده می‌کنیم:
class Counter extends Component {
  state = {
    count: this.props.value
  };
اکنون این سؤال مطرح می‌شود که چه تفاوتی بین props و state وجود دارد؟
- props حاوی اطلاعاتی است که به یک کامپوننت ارسال می‌کنیم؛ اما state حاوی اطلاعاتی است که مختص به آن کامپوننت بوده و private است. یعنی سایر کامپوننت‌ها نمی‌توانند به state کامپوننت دیگری دسترسی پیدا کنند. برای مثال در کامپوننت Counters، تمام attributes سفارشی تنظیم شده‌ی بر روی تعاریف المان‌های کامپوننت Counter، جزئی از اطلاعات props خواهند بود. در اینجا نمی‌توان به state کامپوننت مدنظری دسترسی یافت و آن‌را مقدار دهی کرد. به همین ترتیب state کامپوننت Counters نیز در سایر کامپوننت‌ها قابل دسترسی نیست.
- همچنین باید درنظر داشت که props، در مقایسه با state، فقط خواندنی است. به عبارتی مقدار ورودی به یک کامپوننت را داخل آن کامپوننت نمی‌توان تغییر داد. برای مثال سعی کنید در داخل متد رویدادگردان کلیک موجود در کامپوننت Counter، مقدار this.props.value را به صفر تنظیم کنید. در این حالت با کلیک بر روی دکمه‌ی Increment، بلافاصله خطای readonly بودن خواص شیء منتسب به props را دریافت می‌کنیم. در اینجا اگر نیاز است این مقدار را داخل کامپوننت تغییر دهیم، باید ابتدا این مقدار را دریافت کرده و سپس آن‌را داخل state قرار دهیم. پس از آن امکان ویرایش اطلاعات منتسب به state، داخل یک کامپوننت وجود خواهد داشت.


صدور و مدیریت رخ‌دادها

در ادامه می‌خواهیم در کنار هر دکمه‌ی Increment کامپوننت شمارشگر، یک دکمه‌ی Delete هم قرار دهیم:


مشکل! اگر کد مدیریتی handleDelete را در کامپوننت Counter قرار دهیم، چگونه باید به لیست آرایه‌ی اشیاء counters والد آن، یعنی کامپوننت Counters که سبب رندر شدن کامپوننت‌های شمارشگر شده (state = { counters: [ ] })، دسترسی یافت و شیء‌ای را از آن حذف کرد؟ در React، کامپوننتی که state ای را تعریف می‌کند، باید کامپوننتی باشد که قرار است آن‌را تغییر دهد و اطلاعات state هر کامپوننت، صرفا متعلق به آن کامپوننت بوده و جزو اطلاعات خصوصی آن است. بنابراین مدیریت حذف و یا افزودن کامپوننت‌ها در لیست نمایش داده شده، باید جزو وظایف کامپوننت Counters باشد و نه Counter.
برای حل این مشکل، کامپوننت Counter تعریف شده (کامپوننت فرزند) باید سبب بروز رخ‌داد onDelete شود تا کامپوننت Counters (کامپوننت والد)، آن‌را توسط متد handleDelete مدیریت کند. بنابراین ابتدا به کامپوننت Counters (کامپوننت والد) مراجعه کرده و متد رویدادگردان handleDelete را به آن اضافه می‌کنیم:
  handleDelete = () => {
    console.log("handleDelete called.");
  };
سپس ارجاعی از این متد را به صورت خاصیتی از props به کامپوننت Counter (کامپوننت فرزند) ارسال خواهیم کرد؛ برای این منظور در کامپوننت Counters (کامپوننت والد)، ویژگی onDelete را به تعریف المان Counter اضافه کرده و آن‌را با ارجاعی به متدhandleDelete  مقدار دهی می‌کنیم:
<Counter
     key={counter.id}
     value={counter.value}
     selected={true}
     onDelete={this.handleDelete}
/>
پس از آن به کامپوننت Counter مراجعه کرده و دکمه‌ی جدید Delete را به صورت زیر در کنار دکمه‌ی Increment تعریف می‌کنیم:
<button
  onClick={this.props.onDelete}
  className="btn btn-danger btn-sm m-2"
>
  Delete
</button>
در اینجا onClick، به خاصیت onDelete شیء props ارسالی به کامپوننت متصل شده‌است.
اکنون اگر برنامه را ذخیره کرده و پس از بارگذاری مجدد برنامه در مرورگر بر روی دکمه‌ی Delete کلیک کنیم، پیام «handleDelete called» در کنسول توسعه دهندگان مرورگر لاگ می‌شود. به این ترتیب کامپوننت فرزند سبب بروز رخ‌دادی شده و والد آن، این رخ‌داد را مدیریت می‌کند.


به روز رسانی state

تا اینجا دکمه‌ی Delete فرزند، به متد handleDelete والد متصل شده‌است. مرحله‌ی بعد، پیاده سازی واقعی حذف یک المان از DOM مجازی و به روز رسانی state است. برای اینکار ابتدا به رخ‌دادگردان onClick، در کامپوننت شمارشگر، مراجعه کرده و id دریافتی را به سمت والد ارسال می‌کنیم:
onClick={() => this.props.onDelete(this.props.id)}
البته در سمت والد نیز باید این id را به صورت یک خاصیت جدید به props اضافه کنیم (تا this.props.id فوق کار کند)؛ چون ویژگی key، مختص DOM مجازی بوده و به props اضافه نمی‌شود:
<Counter
  key={counter.id}
  value={counter.value}
  selected={true}
  onDelete={this.handleDelete}
  id={counter.id}
/>
اکنون این id را در کامپوننت والد دریافت و به آن واکنش نشان می‌دهیم:
  handleDelete = counterId => {
    console.log("handleDelete called.", counterId);
    const counters = this.state.counters.filter(
      counter => counter.id !== counterId
    );
    this.setState({ counters }); // = this.setState({ counters: counters });
  };
همانطور که پیشتر نیز در این سری عنوان شده، در React، مقدار state را به صورت مستقیم تغییر نمی‌دهیم و اینکار باید از طریق متد setState آن صورت گیرد. به عبارت دیگر مستقیما خاصیت counters شیء منتسب به خاصیت state را تغییر نمی‌دهیم. ابتدا یک آرایه‌ی جدید از المان‌ها را تولید کرده و به متد setState ارسال می‌کنیم. سپس React، هم خاصیت counters و هم UI را بر این اساس به روز رسانی خواهد کرد. در اینجا، لیست جدید counters، بر اساس id دریافتی از کامپوننت فرزند، تولید شده و به متد this.setState ارسال می‌شود. در این حالت اگر برنامه را ذخیره کرده و پس از بارگذاری مجدد آن در مرورگر، بر روی دکمه‌ی Delete هر ردیف کلیک کنیم، آن ردیف از UI حذف خواهد شد.

البته پیاده سازی ما تا به اینجا بدون مشکل کار می‌کند، اما به ازای هر خاصیت counter، یک ویژگی جدید را به تعریف المان مرتبط اضافه کرده‌ایم که در طول زمان بیش از اندازه طولانی خواهد شد. برای رفع این مشکل، خود شیء counter را به صورت یک ویژگی جدید به کامپوننت مرتبط با آن ارسال می‌کنیم. به این ترتیب اگر در آینده خاصیتی را به این شیء اضافه کردیم، دیگر نیازی نیست تا آن‌را به صورت دستی و مجزا تعریف کنیم. به همین جهت ابتدا تعریف المان Counter را به صورت زیر خلاصه می‌کنیم که در آن ویژگی جدید counter، حاوی کل شیء counter است:
<Counter
  key={counter.id}
  counter={counter}
  onDelete={this.handleDelete}
/>
سپس در سمت کامپوننت فرزند شمارشگر، دو تغییر this.props.counter.value و this.props.counter.id باید صورت گیرند تا مقادیر شیء counter به درستی خوانده شوند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-07.zip
مطالب
چگونه نرم افزارهای تحت وب سریعتری داشته باشیم؟ قسمت پنجم
15.استفاده از using
اگر از objectهایی استفاده می‌کنید که interface مربوط به IDisposable را پیاده سازی کرده اند، حتما از عبارت using استفاده کنید. استفاده از دستور using باعث می‌شود زمانی که دیگر نیازی به object شما نباشد، به صورت خودکار از حافظه حذف شود و در روال جمع آوری زباله (GC) قرار گیرد. این عمل باعث حداقل رسیدن احتمال نشت حافظه در نرم افزار شما می‌شود. برای مثال:
using System;
using System.Text;

class Program
{
    static void Main()
    {
// Use using statement with class that implements Dispose.
using (SystemResource resource = new SystemResource())
{
    Console.WriteLine(1);
}
Console.WriteLine(2);
    }
}

class SystemResource : IDisposable
{
    public void Dispose()
    {
// The implementation of this method not described here.
// ... For now, just report the call.
Console.WriteLine(0);
    }
}
برای اطلاعات بیشتر می‌توانید از این مقاله  استفاده کنید.

16.اطلاعات ارسالی توسط شبکه را به حداقل برسانید
حجم اطلاعات ارسالی به شبکه را به حداقل برسانید. ارسال اطلاعات در شبکه به معنی گذر اطلاعات شما از 7 لایه مختلف شبکه در رایانه شما، گذر از media شبکه، گذر مجدد از 7 لایه شبکه در رایانه مقصد می‌باشد. به این معنی که هرچه اطلاعات بیشتری در شبکه ارسال کنید، سربار بیشتری متوجه سیستم شما خواهد بود. برای رفع این مشکل از فشرده سازهای css و javascript استفاده کنید. این فشرده سازها فواصل خالی، دستورات اضافی و... را از کد شما حذف و حجم آن را به حداقل می‌رسانند.
کم کردن تعداد درخواست‌ها و در نتیجه کم کردن تعداد فایل‌های ارسالی از سرور به کاربر نیز حربه ای در این زمینه می‌باشد.
برای مقایسه فشرده سازها به صورت آنلاین و استفاده از بهترین آنها (متناسب کد شما) می‌توانید از این سایت  استفاده کنید.
امکانات توکاری هم وجود دارد که در زمان اجرای برنامه css و javascript شما را فشرده و تلفیق می‌کند ولی با توجه به اینکه برای سرور در هر مرتبه فراخوانی سربار دارد (حتی در صورت کش کردن) اکیدا توصیه می‌شود از فشرده سازها قبل از اجرای برنامه (Pre-Compressed) به جای فشرده سازهای زمان اجرا (Run-time Compression)استفاده کنید.