مطالب
استفاده از چندین بانک اطلاعاتی به صورت همزمان در EF Code First
یکی از روش‌های تهیه‌ی برنامه‌های چند مستاجری، ایجاد بانک‌های اطلاعاتی مستقلی به ازای هر مشتری است؛ یا نمونه‌ی دیگر آن، برنامه‌هایی هستند که اطلاعات هر سال را در یک بانک اطلاعاتی جداگانه نگه‌داری می‌کنند. در ادامه قصد داریم، نحوه‌ی کار با این بانک‌های اطلاعاتی را به صورت همزمان، توسط EF Code first و در حالت استفاده از الگوی واحد کار و تزریق وابستگی‌ها، به همراه فعال سازی خودکار مباحث migrations و به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، بررسی کنیم.


مشخص سازی رشته‌های متفاوت اتصالی

فرض کنید برنامه‌ی جاری شما قرار است از دو بانک اطلاعاتی مشخص استفاده کند که تعاریف رشته‌های اتصالی آن‌ها در وب کانفیگ به صورت ذیل مشخص شده‌اند:
  <connectionStrings>
    <clear />
    <add name="Sample07Context" connectionString="Data Source=(local);Initial Catalog=TestDbIoC;Integrated Security = true" providerName="System.Data.SqlClient" />
    <add name="Database2012" connectionString="Data Source=(local);Initial Catalog=testdb2012;Integrated Security = true" providerName="System.Data.SqlClient" />
  </connectionStrings>
البته، ذکر این مورد کاملا اختیاری است و می‌توان رشته‌های اتصالی را به صورت پویا نیز در زمان اجرا مشخص و مقدار دهی کرد.


تغییر Context برنامه جهت پذیرش رشته‌های اتصالی پویای قابل تغییر در زمان اجرا

اکنون که قرار است کاربران در حین ورود به برنامه، بانک اطلاعاتی مدنظر خود را انتخاب کنند و یا سیستم قرار است به ازای کاربری خاص، رشته‌ی اتصالی خاص او را به Context ارسال کند، نیاز است Context برنامه را به صورت ذیل تغییر دهیم:
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using EF_Sample07.DomainClasses;
 
namespace EF_Sample07.DataLayer.Context
{
    public class Sample07Context : DbContext, IUnitOfWork
    {
        public DbSet<Category> Categories { set; get; }
        public DbSet<Product> Products { set; get; }
 
        /// <summary>
        /// It looks for a connection string named Sample07Context in the web.config file.
        /// </summary>
        public Sample07Context()
            : base("Sample07Context")
        {
        }
 
        /// <summary>
        /// To change the connection string at runtime. See the SmObjectFactory class for more info.
        /// </summary>
        public Sample07Context(string connectionString)
            : base(connectionString)
        {
            //Note: defaultConnectionFactory in the web.config file should be set.
        }
 
 
        public void SetConnectionString(string connectionString)
        {
            this.Database.Connection.ConnectionString = connectionString;
        }
    }
}
در اینجا دو متد سازنده را مشاهده می‌کنید. سازنده‌ی پیش فرض، از رشته‌ای اتصالی با نامی مساوی Sample07Context استفاده می‌کند و سازنده‌ی دوم، امکان پذیرش یک رشته‌ی اتصالی پویا را دارد. مقدار پارامتر ورودی آن می‌تواند نام رشته‌ی اتصالی و یا حتی مقدار کامل رشته‌ی اتصالی باشد. حالت پذیرش نام رشته‌ی اتصالی زمانی مفید است که همانند مثال ابتدای بحث، این نام‌ها را پیشتر در فایل کانفیگ برنامه ثبت کرده باشید و حالت پذیرش مقدار کامل رشته‌ی اتصالی، جهت مقدار دهی پویای آن بدون نیاز به ثبت اطلاعاتی در فایل کانفیگ برنامه مفید است.

یک متد دیگر هم در اینجا در انتهای کلاس به نام SetConnectionString تعریف شده‌است. از این متد در حین ورود کاربر به سایت می‌توان استفاده کرد. برای مثال حداقل دو نوع طراحی را می‌توان درنظر گرفت:
الف) کاربر با برنامه‌ای کار می‌کند که به ازای سال‌های مختلف، بانک‌های اطلاعاتی مختلفی دارد و در ابتدای ورود، یک drop down انتخاب سال کاری برای او درنظر گرفته شده‌است (علاوه بر سایر ورودی‌های استانداردی مانند نام کاربری و کلمه‌ی عبور). در این حالت بهتر است متد SetConnectionString نام رشته‌ی اتصالی را بر اساس سال انتخابی، در حین لاگین دریافت کند که اطلاعات آن در فایل کانفیگ سایت پیشتر مشخص شده‌است.
ب) کاربر یا مشتری پس از ورود به سایت، نیاز است صرفا از بانک اطلاعاتی خاص خودش استفاده کند. بنابراین اطلاعات تعریف کاربران و مشتری‌ها در یک بانک اطلاعاتی مجزا قرار دارند و پس از لاگین، نیاز است رشته‌ی اتصالی او به صورت پویا از بانک اطلاعاتی خوانده شده و سپس توسط متد SetConnectionString تنظیم گردد.


مدیریت سشن‌های رشته‌ی اتصالی جاری

پس از اینکه کاربر، در حین ورود مشخص کرد که از چه بانک اطلاعاتی قرار است استفاده کند و یا اینکه برنامه بر اساس اطلاعات ثبت شده‌ی او تصمیم‌گیری کرد که باید از کدام رشته‌ی اتصالی استفاده کند، نگهداری این رشته‌ی اتصالی نیاز به سشن دارد تا به ازای هر کاربر متصل به سایت منحصربفرد باشد. در مورد مدیریت سشن‌ها در برنامه‌های وب، از نکات مطرح شده‌ی در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» استفاده خواهیم کرد:
using System;
using System.Threading;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.ServiceLayer;
using StructureMap;
using StructureMap.Web;
using StructureMap.Web.Pipeline;
 
namespace EF_Sample07.IoCConfig
{
    public static class SmObjectFactory
    {
        private static readonly Lazy<Container> _containerBuilder =
            new Lazy<Container>(defaultContainer, LazyThreadSafetyMode.ExecutionAndPublication);
 
        public static IContainer Container
        {
            get { return _containerBuilder.Value; }
        }
 
        public static void HttpContextDisposeAndClearAll()
        {
            HttpContextLifecycle.DisposeAndClearAll();
        }
 
        private static Container defaultContainer()
        {
            return new Container(ioc =>
            {
                // session manager setup
                ioc.For<ISessionProvider>().Use<DefaultWebSessionProvider>();
                ioc.For<HttpSessionStateBase>()
                   .Use(ctx => new HttpSessionStateWrapper(HttpContext.Current.Session));
 
                ioc.For<IUnitOfWork>()
                   .HybridHttpOrThreadLocalScoped()
                   .Use<Sample07Context>()
                    // Remove these 2 lines if you want to use a connection string named Sample07Context, defined in the web.config file.
                   .Ctor<string>("connectionString")
                   .Is(ctx => getCurrentConnectionString(ctx));
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.For<ICategoryService>().Use<EfCategoryService>();
                ioc.For<IProductService>().Use<EfProductService>();
 
                ioc.Policies.SetAllProperties(properties =>
                {
                    properties.OfType<IUnitOfWork>();
                    properties.OfType<ICategoryService>();
                    properties.OfType<IProductService>();
                    properties.OfType<ISessionProvider>();
                });
            });
        }
 
        private static string getCurrentConnectionString(IContext ctx)
        {
            if (HttpContext.Current != null)
            {
                // this is a web application
                var sessionProvider = ctx.GetInstance<ISessionProvider>();
                var connectionString = sessionProvider.Get<string>("CurrentConnectionString");
                if (string.IsNullOrWhiteSpace(connectionString))
                {
                    // It's a default connectionString.
                    connectionString = "Database2012";
                    // this session value should be set during the login phase
                    sessionProvider.Store("CurrentConnectionStringName", connectionString);
                }
 
                return connectionString;
            }
            else
            {
                // this is a desktop application, so you can store this value in a global static variable.
                return "Database2012";
            }
        }
    }
}
در اینجا نحوه‌ی پویا سازی تامین رشته‌ی اتصالی را مشاهده می‌کنید. در مورد اینترفیس ISessionProvider و کلاس پایه HttpSessionStateBase پیشتر در مطلب «مدیریت سشن‌ها در برنامه‌های وب به کمک تزریق وابستگی‌ها» بحث شد.
نکته‌ی مهم این تنظیمات، قسمت مقدار دهی سازنده‌ی کلاس Context برنامه به صورت پویا توسط IoC Container جاری است. در اینجا هر زمانیکه قرار است وهله‌ای از Sample07Context ساخته شود، از سازنده‌ی دوم آن که دارای پارامتری به نام connectionString است، استفاده خواهد شد. همچنین مقدار آن به صورت پویا از متد getCurrentConnectionString که در انتهای کلاس تعریف شده‌است، دریافت می‌گردد.
در این متد ابتدا مقدار HttpContext.Current بررسی شده‌است. این مقدار اگر نال باشد، یعنی برنامه‌ی جاری یک برنامه‌ی دسکتاپ است و مدیریت رشته‌ی اتصالی جاری آن‌را توسط یک خاصیت Static یا Singleton تعریف شده‌ی در برنامه نیز می‌توان تامین کرد. از این جهت که در هر زمان، تنها یک کاربر در App Domain جاری برنامه‌ی دسکتاپ می‌تواند وجود داشته باشد و Singleton یا Static تعریف شدن اطلاعات رشته‌ی اتصالی، مشکلی را ایجاد نمی‌کند. اما در برنامه‌های وب، چندین کاربر در یک App Domain به سیستم وارد می‌شوند. به همین جهت است که مشاهده می‌کنید در اینجا از تامین کننده‌ی سشن، برای نگهداری اطلاعات رشته‌ی اتصالی جاری کمک گرفته شده‌است.

کلید این سشن نیز در این مثال مساوی CurrentConnectionStringName تعریف شده‌است. بنابراین در حین لاگین موفقیت آمیز کاربر، دو مرحله‌ی زیر باید طی شوند:
 sessionProvider.Store("CurrentConnectionString", "Sample07Context");
uow.SetConnectionString(WebConfigurationManager.ConnectionStrings[_sessionProvider.Get<string>("CurrentConnectionString")].ConnectionString);
ابتدا باید سشن CurrentConnectionStringName به بانک اطلاعاتی انتخابی کاربر تنظیم شود. برای نمونه در این مثال خاص، از نام رشته‌ی اتصالی مشخص شده‌ی در وب کانفیگ برنامه (مثال ابتدای بحث) به نام Sample07Context استفاده شده‌است.
سپس از متد SetConnectionString برای خواندن مقدار نام مشخص شده در سشن CurrentConnectionStringName کمک گرفته‌ایم. هرچند سازنده‌ی کلاس Context برنامه، هر دو حالت استفاده از نام رشته‌ی اتصالی و یا مقدار کامل رشته‌ی اتصالی را پشتیبانی می‌کند، اما خاصیت this.Database.Connection.ConnectionString تنها رشته‌ی کامل اتصالی را می‌پذیرد (بکار رفته در متد SetConnectionString).

تا اینجا کار پویا سازی انتخاب و استفاده از رشته‌ی اتصالی برنامه به پایان می‌رسد. هر زمانیکه قرار است Context برنامه توسط IoC Container نمونه سازی شود، به متد getCurrentConnectionString رجوع کرده و مقدار رشته‌ی اتصالی را از سشن تنظیم شده‌‌ای به نام CurrentConnectionStringName دریافت می‌کند. سپس از مقدار آن جهت مقدار دهی سازنده‌ی دوم کلاس Context استفاده خواهد کرد.


مدیریت migrations خودکار برنامه در حالت استفاده از چندین بانک اطلاعاتی

یکی از مشکلات کار با برنامه‌های چند دیتابیسی، به روز رسانی ساختار تمام بانک‌های اطلاعاتی مورد استفاده، پس از تغییری در ساختار مدل‌های برنامه است. از این جهت که اگر تمام بانک‌های اطلاعاتی به روز نشوند، کوئری‌های جدید برنامه که از خواص و فیلدهای جدید استفاده می‌کنند، دیگر کار نخواهند کرد. پویا سازی اعمال این تغییرات را می‌توان به صورت ذیل انجام داد:
using System;
using System.Data.Entity;
using System.Web;
using EF_Sample07.DataLayer.Context;
using EF_Sample07.IoCConfig;
 
namespace EF_Sample07.WebFormsAppSample
{
    public class Global : HttpApplication
    {
        void Application_Start(object sender, EventArgs e)
        {
            initDatabases();
        }
 
        private static void initDatabases()
        {
            // defined in web.config
            string[] connectionStringNames =
            {
                "Sample07Context",
                "Database2012"
            };
 
            foreach (var connectionStringName in connectionStringNames)
            {
                Database.SetInitializer(
                    new MigrateDatabaseToLatestVersion<Sample07Context, Configuration>(connectionStringName));
 
                using (var ctx = new Sample07Context(connectionStringName))
                {
                    ctx.Database.Initialize(force: true);
                }
            }
        }
 
        void Application_EndRequest(object sender, EventArgs e)
        {
            SmObjectFactory.HttpContextDisposeAndClearAll();
        } 
    }
}
نکته‌ی مهمی که در اینجا بکار گرفته شده‌است، مشخص سازی صریح سازنده‌ی شیء MigrateDatabaseToLatestVersion است. به صورت معمول در اکثر برنامه‌های تک دیتابیسی، نیازی به مشخص سازی پارامتر سازنده‌ی این کلاس نیست و در این حالت از سازنده‌ی بدون پارامتر کلاس Context برنامه استفاده خواهد شد. اما اگر سازنده‌ی آن‌را مشخص کنیم، به صورت خودکار از متد سازنده‌ای در کلاس Context استفاده می‌کند که پارامتر رشته‌ی اتصالی را به صورت پویا می‌پذیرد.
در این مثال خاص، متد initDatabases در حین آغاز برنامه فراخوانی شده‌است. منظور این است که اینکار در طول عمر برنامه تنها کافی است یکبار انجام شود و پس از آن است که EF Code first می‌تواند از رشته‌های اتصالی متفاوتی که به آن ارسال می‌شود، بدون مشکل استفاده کند. زیرا اطلاعات نگاشت کلاس‌های مدل برنامه به جداول بانک اطلاعاتی به این ترتیب است که کش می‌شوند و یا بر اساس کلاس Configuration به صورت خودکار به بانک اطلاعاتی اعمال می‌گردند.


کدهای کامل این مثال را که در حقیقت نمونه‌ی بهبود یافته‌ی مطلب «EF Code First #12» است، از اینجا می‌توانید دریافت کنید:
UoW-Sample
مطالب
امن سازی برنامه‌های ASP.NET Core توسط IdentityServer 4x - قسمت دهم- ذخیره سازی اطلاعات کاربران IDP در بانک اطلاعاتی
تا اینجا تمام قسمت‌های این سری، برای اساس اطلاعات یک کلاس Config استاتیک تشکیل شده‌ی در حافظه ارائه شدند. این روش برای دمو و توضیح مفاهیم پایه‌ی IdentityServer بسیار مفید است؛ اما برای دنیای واقعی خیر. بنابراین در ادامه می‌خواهیم این قسمت را با اطلاعات ذخیره شده‌ی در بانک اطلاعاتی تعویض کنیم. یک روش مدیریت آن، نصب ASP.NET Core Identity دقیقا داخل همان پروژه‌ی IDP است. در این حالت کدهای ASP.NET Core Identity مایکروسافت، کار مدیریت کاربران IDP را انجام می‌دهند. روش دیگر اینکار را که در اینجا بررسی خواهیم کرد، تغییر کدهای Quick Start UI اضافه شده‌ی در «قسمت چهارم - نصب و راه اندازی IdentityServer»، جهت پذیرفتن مدیریت کاربران مبتنی بر بانک اطلاعاتی تهیه شده‌ی توسط خودمان است. مزیت آن آشنا شدن بیشتر با کدهای Quick Start UI و درک زیرساخت آن است.


تکمیل ساختار پروژه‌ی IDP

تا اینجا برای IDP، یک پروژه‌ی خالی وب را ایجاد و به مرور، آن‌را تکمیل کردیم. اما اکنون نیاز است پشتیبانی از بانک اطلاعاتی را نیز به آن اضافه کنیم. برای این منظور چهار پروژه‌ی Class library کمکی را نیز به Solution آن اضافه می‌کنیم:


- DNT.IDP.DomainClasses
در این پروژه، کلاس‌های متناظر با موجودیت‌های جداول مرتبط با اطلاعات کاربران قرار می‌گیرند.
- DNT.IDP.DataLayer
این پروژه Context برنامه و Migrations آن‌را تشکیل می‌دهد. همچنین به همراه تنظیمات و Seed اولیه‌ی اطلاعات بانک اطلاعاتی نیز می‌باشد.
رشته‌ی اتصالی آن نیز در فایل DNT.IDP\appsettings.json ذخیره شده‌است.
- DNT.IDP.Common
الگوریتم هش کردن اطلاعات، در این پروژه‌ی مشترک بین چند پروژه‌ی دیگر قرار گرفته‌است. از آن جهت هش کردن کلمات عبور، در دو پروژه‌ی DataLayer و همچنین Services استفاده می‌کنیم.
- DNT.IDP.Services
کلاس سرویس کاربران که با استفاده از DataLayer با بانک اطلاعاتی ارتباط برقرار می‌کند، در این پروژه قرار گرفته‌است.


ساختار بانک اطلاعاتی کاربران IdentityServer

در اینجا ساختار بانک اطلاعاتی کاربران IdentityServer، بر اساس جداول کاربران و Claims آن‌ها تشکیل می‌شود:
namespace DNT.IDP.DomainClasses
{
    public class User
    {
        [Key]
        [MaxLength(50)]       
        public string SubjectId { get; set; }
    
        [MaxLength(100)]
        [Required]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }

        [Required]
        public bool IsActive { get; set; }

        public ICollection<UserClaim> UserClaims { get; set; }

        public ICollection<UserLogin> UserLogins { get; set; }
    }
}
در اینجا SubjectId همان Id کاربر، در سطح IDP است. این خاصیت به صورت یک کلید خارجی در جداول UserClaims و UserLogins نیز بکار می‌رود.
ساختار Claims او نیز به صورت زیر تعریف می‌شود که با تعریف یک Claim استاندارد، سازگاری دارد:
namespace DNT.IDP.DomainClasses
{
    public class UserClaim
    {         
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimType { get; set; }

        [Required]
        [MaxLength(250)]
        public string ClaimValue { get; set; }
    }
}
همچنین کاربر می‌توان تعدادی لاگین نیز داشته باشد:
namespace DNT.IDP.DomainClasses
{
    public class UserLogin
    {
        public int Id { get; set; }

        [MaxLength(50)]
        [Required]
        public string SubjectId { get; set; }
        
        public User User { get; set; }

        [Required]
        [MaxLength(250)]
        public string LoginProvider { get; set; }

        [Required]
        [MaxLength(250)]
        public string ProviderKey { get; set; }
    }
}
هدف از آن، یکپارچه سازی سیستم، با IDPهای ثالث مانند گوگل، توئیتر و امثال آن‌ها است.

در پروژه‌ی DNT.IDP.DataLayer در پوشه‌ی Configurations آن، کلاس‌های UserConfiguration و UserClaimConfiguration را مشاهده می‌کنید که حاوی اطلاعات اولیه‌ای برای تشکیل User 1 و User 2 به همراه Claims آن‌ها هستند. این اطلاعات را دقیقا از فایل استاتیک ‍Config که در قسمت‌های قبل تکمیل کردیم، به این دو کلاس جدید IEntityTypeConfiguration منتقل کرده‌ایم تا به این ترتیب متد GetUsers فایل استاتیک Config را با نمونه‌ی دیتابیسی آن جایگزین کنیم.
سرویسی که از طریق Context برنامه با بانک اطلاعاتی ارتباط برقرار می‌کند، چنین ساختاری را دارد:
    public interface IUsersService
    {
        Task<bool> AreUserCredentialsValidAsync(string username, string password);
        Task<User> GetUserByEmailAsync(string email);
        Task<User> GetUserByProviderAsync(string loginProvider, string providerKey);
        Task<User> GetUserBySubjectIdAsync(string subjectId);
        Task<User> GetUserByUsernameAsync(string username);
        Task<IEnumerable<UserClaim>> GetUserClaimsBySubjectIdAsync(string subjectId);
        Task<IEnumerable<UserLogin>> GetUserLoginsBySubjectIdAsync(string subjectId);
        Task<bool> IsUserActiveAsync(string subjectId);
        Task AddUserAsync(User user);
        Task AddUserLoginAsync(string subjectId, string loginProvider, string providerKey);
        Task AddUserClaimAsync(string subjectId, string claimType, string claimValue);
    }
که توسط آن امکان دسترسی به یک کاربر، اطلاعات Claims او و افزودن رکوردهایی جدید وجود دارد.
تنظیمات نهایی این سرویس‌ها و Context برنامه نیز در فایل DNT.IDP\Startup.cs جهت معرفی به سیستم تزریق وابستگی‌ها، صورت گرفته‌اند. همچنین در اینجا متد initializeDb را نیز مشاهده می‌کنید که با فراخوانی متد context.Database.Migrate، تمام کلاس‌های Migrations پروژه‌ی DataLayer را به صورت خودکار به بانک اطلاعاتی اعمال می‌کند.


غیرفعال کردن صفحه‌ی Consent در Quick Start UI

در «قسمت چهارم - نصب و راه اندازی IdentityServer» فایل‌های Quick Start UI را به پروژه‌ی IDP اضافه کردیم. در ادامه می‌خواهیم قدم به قدم این پروژه را تغییر دهیم.
در صفحه‌ی Consent در Quick Start UI، لیست scopes درخواستی برنامه‌ی کلاینت ذکر شده و سپس کاربر انتخاب می‌کند که کدامیک از آن‌ها، باید به برنامه‌ی کلاینت ارائه شوند. این صفحه، برای سناریوی ما که تمام برنامه‌های کلاینت توسط ما توسعه یافته‌اند، بی‌معنا است و صرفا برای کلاینت‌های ثالثی که قرار است از IDP ما استفاده کنند، معنا پیدا می‌کند. برای غیرفعال کردن آن کافی است به فایل استاتیک Config مراجعه کرده و خاصیت RequireConsent کلاینت مدنظر را به false تنظیم کرد.


تغییر نام پوشه‌ی Quickstart و سپس اصلاح فضای نام پیش‌فرض کنترلرهای آن

در حال حاضر کدهای کنترلرهای Quick Start UI داخل پوشه‌ی Quickstart برنامه‌ی IDP قرار گرفته‌اند. با توجه به اینکه قصد داریم این کدها را تغییر دهیم و همچنین این پوشه در اساس، همان پوشه‌ی استاندارد Controllers است، ابتدا نام این پوشه را به Controllers تغییر داده و سپس در تمام کنترلرهای ذیل آن، فضای نام پیش‌فرض IdentityServer4.Quickstart.UI را نیز به فضای نام متناسبی با پوشه بندی پروژه‌ی جاری تغییر می‌دهیم. برای مثال کنترلر Account واقع در پوشه‌ی Account، اینبار دارای فضای نام DNT.IDP.Controllers.Account خواهد شد و به همین ترتیب برای مابقی کنترل‌ها عمل می‌کنیم.
پس از این تغییرات، عبارات using موجود در Viewها را نیز باید تغییر دهید تا برنامه در زمان اجرا به مشکلی برنخورد. البته ASP.NET Core 2.1 در زمان کامپایل برنامه، تمام Viewهای آن‌را نیز کامپایل می‌کند و اگر خطایی در آن‌ها وجود داشته باشد، امکان بررسی و رفع آن‌ها پیش از اجرای برنامه، میسر است.
و یا می‌توان جهت سهولت کار، فایل DNT.IDP\Views\_ViewImports.cshtml را جهت معرفی این فضاهای نام جدید ویرایش کرد تا نیازی به تغییر Viewها نباشد:
@using DNT.IDP.Controllers.Account;
@using DNT.IDP.Controllers.Consent;
@using DNT.IDP.Controllers.Grants;
@using DNT.IDP.Controllers.Home;
@using DNT.IDP.Controllers.Diagnostics;
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers


تعامل با IdentityServer از طریق کدهای سفارشی

پس از تشکیل «ساختار بانک اطلاعاتی کاربران IdentityServer» و همچنین تهیه سرویس‌های متناظری جهت کار با آن، اکنون نیاز است مطمئن شویم IdentityServer از این بانک اطلاعاتی برای دریافت اطلاعات کاربران خود استفاده می‌کند.
در حال حاضر، با استفاده از متد الحاقی AddTestUsers معرفی شده‌ی در فایل DNT.IDP\Startup.cs، اطلاعات کاربران درون حافظه‌ای برنامه را از متد ()Config.GetUsers دریافت می‌کنیم.
بنابراین اولین قدم، بررسی ساختار متد AddTestUsers است. برای این منظور به مخزن کد IdentityServer4 مراجعه کرده و کدهای متد الحاقی AddTestUsers را بررسی می‌کنیم:
 public static class IdentityServerBuilderExtensions
 {
        public static IIdentityServerBuilder AddTestUsers(this IIdentityServerBuilder builder, List<TestUser> users)
        {
            builder.Services.AddSingleton(new TestUserStore(users));
            builder.AddProfileService<TestUserProfileService>();
            builder.AddResourceOwnerValidator<TestUserResourceOwnerPasswordValidator>();

            return builder;
        }
}
- ابتدا یک TestUserStore را به صورت Singleton ثبت کرده‌است.
- سپس سرویس پروفایل کاربران را اضافه کرده‌است. این سرویس با پیاده سازی اینترفیس IProfileService تهیه می‌شود. کار آن اتصال یک User Store سفارشی به سرویس کاربران و دریافت اطلاعات پروفایل آن‌ها مانند Claims است.
- در آخر TestUserResourceOwnerPasswordValidator، کار اعتبارسنجی کلمه‌ی عبور و نام کاربری را در صورت استفاده‌ی از Flow ویژه‌ای به نام ResourceOwner که استفاده‌ی از آن توصیه نمی‌شود (ROBC Flow)، انجام می‌دهد.

برای جایگزین کردن AddTestUsers، کلاس جدید IdentityServerBuilderExtensions را در ریشه‌ی پروژه‌ی IDP با محتوای ذیل اضافه می‌کنیم:
using DNT.IDP.Services;
using Microsoft.Extensions.DependencyInjection;

namespace DNT.IDP
{
    public static class IdentityServerBuilderExtensions
    {
        public static IIdentityServerBuilder AddCustomUserStore(this IIdentityServerBuilder builder)
        {
            // builder.Services.AddScoped<IUsersService, UsersService>();
            builder.AddProfileService<CustomUserProfileService>();
            return builder;
        }
    }
}
در اینجا ابتدا IUsersService سفارشی برنامه معرفی شده‌است که User Store سفارشی برنامه است. البته چون UsersService ما با بانک اطلاعاتی کار می‌کند، نباید به صورت Singleton ثبت شود و باید در پایان هر درخواست به صورت خودکار Dispose گردد. به همین جهت طول عمر آن Scoped تعریف شده‌است. در کل ضرورتی به ذکر این سطر نیست؛ چون پیشتر کار ثبت IUsersService در کلاس Startup برنامه انجام شده‌است.
سپس یک ProfileService سفارشی را ثبت کرده‌ایم. این سرویس، با پیاده سازی IProfileService به صورت زیر پیاده سازی می‌شود:
namespace DNT.IDP.Services
{
    public class CustomUserProfileService : IProfileService
    {
        private readonly IUsersService _usersService;

        public CustomUserProfileService(IUsersService usersService)
        {
            _usersService = usersService;
        }

        public async Task GetProfileDataAsync(ProfileDataRequestContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            var claimsForUser = await _usersService.GetUserClaimsBySubjectIdAsync(subjectId);
            context.IssuedClaims = claimsForUser.Select(c => new Claim(c.ClaimType, c.ClaimValue)).ToList();
        }

        public async Task IsActiveAsync(IsActiveContext context)
        {
            var subjectId = context.Subject.GetSubjectId();
            context.IsActive = await _usersService.IsUserActiveAsync(subjectId);
        }
    }
}
سرویس پروفایل، توسط سرویس کاربران برنامه که در ابتدای مطلب آن‌را تهیه کردیم، امکان دسترسی به اطلاعات پروفایل کاربران را مانند Claims او، پیدا می‌کند.
در متدهای آن، ابتدا subjectId و یا همان Id منحصربفرد کاربر جاری سیستم، دریافت شده و سپس بر اساس آن می‌توان از usersService، جهت دریافت اطلاعات مختلف کاربر، کوئری گرفت و نتیجه را در خواص context جاری، برای استفاده‌های بعدی، ذخیره کرد.

اکنون به کلاس src\IDP\DNT.IDP\Startup.cs مراجعه کرده و متد AddTestUsers را با AddCustomUserStore جایگزین می‌کنیم:
namespace DNT.IDP
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddIdentityServer()
             .AddDeveloperSigningCredential()
             .AddCustomUserStore()
             .AddInMemoryIdentityResources(Config.GetIdentityResources())
             .AddInMemoryApiResources(Config.GetApiResources())
             .AddInMemoryClients(Config.GetClients());
تا اینجا فقط این سرویس‌های جدید را ثبت کرده‌ایم، اما هنوز کار خاصی را انجام نمی‌دهند و باید از آن‌ها در برنامه استفاده کرد.


اتصال IdentityServer به User Store سفارشی

در ادامه، سازنده‌ی کنترلر DNT.IDP\Quickstart\Account\AccountController.cs را بررسی می‌کنیم:
        public AccountController(
            IIdentityServerInteractionService interaction,
            IClientStore clientStore,
            IAuthenticationSchemeProvider schemeProvider,
            IEventService events,
            TestUserStore users = null)
        {
            _users = users ?? new TestUserStore(TestUsers.Users);

            _interaction = interaction;
            _clientStore = clientStore;
            _schemeProvider = schemeProvider;
            _events = events;
        }
- سرویس توکار IIdentityServerInteractionService، کار تعامل برنامه با IdentityServer4‌  را انجام می‌دهد.
- IClientStore پیاده سازی محل ذخیره سازی اطلاعات کلاینت‌ها را ارائه می‌دهد که در حال حاضر توسط متد استاتیک Config در اختیار آن قرار می‌گیرد.
- IEventService رخ‌دادهایی مانند لاگین موفقیت آمیز یک کاربر را گزارش می‌دهد.
- در آخر، TestUserStore تزریق شده‌است که می‌خواهیم آن‌را با User Store سفارشی خودمان جایگزین کنیم.  بنابراین در ابتدا TestUserStore را با UserStore سفارشی خودمان جایگزین می‌کنیم:
        private readonly TestUserStore _users;
        private readonly IUsersService _usersService;
        public AccountController(
    // ...
            IUsersService usersService)
        {
            _usersService = usersService;
    // ...
        }
فعلا فیلد TestUserStore را نیز سطح کلاس جاری باقی نگه می‌داریم. از این جهت که قسمت‌های لاگین خارجی سیستم (استفاده از گوگل، توئیتر و ...) هنوز از آن استفاده می‌کنند و آن‌را در قسمتی دیگر تغییر خواهیم داد.
پس از معرفی فیلد usersService_، اکنون در قسمت زیر از آن استفاده می‌کنیم:
در اکشن متد لاگین، جهت بررسی صحت نام کاربری و کلمه‌ی عبور و همچنین یافتن کاربر متناظر با آن:
        public async Task<IActionResult> Login(LoginInputModel model, string button)
        {
    //...
            if (ModelState.IsValid)
            {
                if (await _usersService.AreUserCredentialsValidAsync(model.Username, model.Password))
                {
                    var user = await _usersService.GetUserByUsernameAsync(model.Username);
تا همینجا برنامه را کامپایل کرده و اجرا کنید. پس از لاگین در آدرس https://localhost:5001/Gallery/IdentityInformation، هنوز اطلاعات User Claims کاربر وارد شده‌ی به سیستم نمایش داده می‌شوند که بیانگر صحت عملکرد CustomUserProfileService است.


افزودن امکان ثبت کاربران جدید به برنامه‌ی IDP

پس از اتصال قسمت login برنامه‌ی IDP به بانک اطلاعاتی، اکنون می‌خواهیم امکان ثبت کاربران را نیز به آن اضافه کنیم.
این قسمت شامل تغییرات ذیل است:
الف) اضافه شدن RegisterUserViewModel
این ViewModel که فیلدهای فرم ثبت‌نام را تشکیل می‌دهد، ابتدا با نام کاربری و کلمه‌ی عبور شروع می‌شود:
    public class RegisterUserViewModel
    {
        // credentials       
        [MaxLength(100)]
        public string Username { get; set; }

        [MaxLength(100)]
        public string Password { get; set; }
سپس سایر خواصی که در اینجا اضافه می‌شوند:
    public class RegisterUserViewModel
    {
   // ...

        // claims 
        [Required]
        [MaxLength(100)]
        public string Firstname { get; set; }

        [Required]
        [MaxLength(100)]
        public string Lastname { get; set; }

        [Required]
        [MaxLength(150)]
        public string Email { get; set; }

        [Required]
        [MaxLength(200)]
        public string Address { get; set; }

        [Required]
        [MaxLength(2)]
        public string Country { get; set; }
در کنترلر UserRegistrationController، تبدیل به UserClaims شده و در جدول مخصوص آن ذخیره خواهند شد.
ب) افزودن UserRegistrationController
این کنترلر، RegisterUserViewModel را دریافت کرده و سپس بر اساس آن، شیء User ابتدای بحث را تشکیل می‌دهد. ابتدا نام کاربری و کلمه‌ی عبور را در جدول کاربران ثبت می‌کند و سپس سایر خواص این ViewModel را در جدول UserClaims:
varuserToCreate=newUser
{
  Password=model.Password.GetSha256Hash(),
  Username=model.Username,
  IsActive=true
};
userToCreate.UserClaims.Add(newUserClaim("country",model.Country));
userToCreate.UserClaims.Add(newUserClaim("address",model.Address));
userToCreate.UserClaims.Add(newUserClaim("given_name",model.Firstname));
userToCreate.UserClaims.Add(newUserClaim("family_name",model.Lastname));
userToCreate.UserClaims.Add(newUserClaim("email",model.Email));
userToCreate.UserClaims.Add(newUserClaim("subscriptionlevel","FreeUser"));
ج) افزودن RegisterUser.cshtml
این فایل، view متناظر با ViewModel فوق را ارائه می‌دهد که توسط آن، کاربری می‌تواند اطلاعات خود را ثبت کرده و وارد سیستم شود.
د) اصلاح فایل ViewImports.cshtml_ جهت تعریف فضای نام UserRegistration
در RegisterUser.cshtml از RegisterUserViewModel استفاده می‌شود. به همین جهت بهتر است فضای نام آن‌را به ViewImports اضافه کرد.
ه) افزودن لینک ثبت نام به صفحه‌ی لاگین در Login.cshtml
این لینک دقیقا در ذیل چک‌باکس Remember My Login اضافه شده‌است.


اکنون اگر برنامه را اجرا کنیم، ابتدا مشاهده می‌کنیم که صفحه‌ی لاگین به همراه لینک ثبت نام ظاهر می‌شود:


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


برای آزمایش، کاربری را ثبت کنید. پس از ثبت اطلاعات، بلافاصله وارد سیستم خواهید شد. البته چون در اینجا subscriptionlevel به FreeUser تنظیم شده‌است، این کاربر یکسری از لینک‌های برنامه‌ی MVC Client را به علت نداشتن دسترسی، مشاهده نخواهد کرد.



کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
برای اجرای برنامه:
- ابتدا به پوشه‌ی 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 وارد کنید.
مطالب
امکان استفاده‌ی از قیود مسیریابی سفارشی ASP.NET Core در Blazor SSR برای رمزگشایی خودکار پارامترهای دریافتی

در Blazor می‌توان مسیریابی‌های پارامتری را به صورت زیر نیز تعریف کرد:

@page "/post/edit/{EditId:int}"

که در اینجا EditId، یک پارامتر مسیریابی از نوع int تعریف شده و به صورت زیر در کدهای صفحه‌ی مرتبط، قابل دسترسی است:

[Parameter] public int? EditId { set; get; }

int تعریف شده‌ی در این مسیریابی، یک routing constraint و یا یک قید مسیریابی محسوب می‌شود و استفاده‌ی از آن، چنین مزایایی را به همراه دارد:

- در این حالت فقط EditId های عددی پردازش می‌شوند و اگر رشته‌ای دریافت شود، کاربر با خروجی از نوع 404 و یا «یافت نشد»، مواجه خواهد شد.

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

قیود پیش‌فرض تعریف شده‌ی در Blazor

اگر به مستندات مسیریابی Blazor مراجعه کنیم، به‌نظر فقط این موارد را می‌توان به‌عنوان قیود پارامترهای مسیریابی تعریف کرد:

bool, datetime, decimal, double, float, guid, int, long, nonfile 

و ... توضیحاتی در مورد اینکه آیا امکان بسط آن‌ها وجود دارند یا خیر، فعلا در مستندات رسمی آن، ذکر نشده‌اند.

در Blazor 8x می‌توان از قیود مسیریابی سفارشی ASP.NET Core نیز استفاده کرد!

ASP.NET Core سمت سرور، به همراه امکان سفارشی سازی قیودمسیریابی خود نیز هست که آن‌را می‌توان به کمک اینترفیسIRouteConstraint پیاده سازی کرد:

namespace Microsoft.AspNetCore.Routing  
{  
    public interface IRouteConstraint  
    {  
        bool Match(  
            HttpContext httpContext,  
            IRouter route,  
            string routeKey,  
            RouteValueDictionary values,  
            RouteDirection routeDirection);  
    }  
} 

جالب اینجا است که می‌توان این نمونه‌های سفارشی را حداقل در نگارش جدید Blazor 8x SSR نیز استفاده کرد؛ هرچند در مستندات رسمی Blazor هنوز به آن‌ اشاره‌ای نشده‌است.

در امضای متد Match فوق، دو پارامتر routeKey و values آن بیش از مابقی مهم هستند:

- routeKey مشخص می‌کند که الان کدام پارامتر مسیریابی (مانند EditId در این مطلب) در حال پردازش است.

- values، یک دیکشنری است که کلید هر عضو آن، پارامتر مسیریابی و مقدار آن، مقدار دریافتی از URL جاری است.

- اگر این متد مقدار true را برگرداند، یعنی مسیریابی وارد شده‌ی به آن، با موفقیت پردازش و اعتبارسنجی شده و می‌توان صفحه‌ی مرتبط را نمایش داد؛ در غیراینصورت، کاربر پیام یافت نشدن آن صفحه و مسیر درخواستی را مشاهده می‌کند.

پیاده سازی یک قید سفارشی رمزگشایی پارامترهای مسیریابی

فرض کنید قصد ندارید که پارامترهای مسیریابی ویرایش رکوردهای خاصی را دقیقا بر اساس Id متناظر عددی آن‌ها در بانک اطلاعاتی، نمایش دهید؛ برای مثال نمی‌خواهید دقیقا آدرس post/edit/1 را به کاربر نمایش دهید؛ چون نمایش این اعداد عموما ساده و ترتیبی، حدس زدن آن‌ها را ساده‌ کرده و ممکن است در آینده مشکلات امنیتی را به همراه داشته باشد.

می‌خواهیم از آدرس‌های متداول و ساده‌ی عددی زیر:

@page "/post/edit/{EditId:int}"

به آدرس رمزنگاری شده‌ی زیر برسیم:

@page "/post/edit/{EditId:encrypt}"

اگر به این آدرس جدید دقت کنید، در اینجا از نام قید جدیدی به نام encrypt استفاده شده‌است که جزو قیود پیش‌فرض سیستم مسیریابی Blazor نیست. روش تعریف آن به صورت زیر است:

using System.Globalization;
using DNTCommon.Web.Core;

namespace Blazor8xSsrComponents.Utils;

public class EncryptedRouteConstraint(IProtectionProviderService protectionProvider) : IRouteConstraint
{
    public const string Name = "encrypt";

    public bool Match(HttpContext? httpContext,
        IRouter? route,
        string routeKey,
        RouteValueDictionary values,
        RouteDirection routeDirection)
    {
        ArgumentNullException.ThrowIfNull(routeKey);
        ArgumentNullException.ThrowIfNull(values);

        if (!values.TryGetValue(routeKey, out var routeValue))
        {
            return false;
        }

        var valueString = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
        values[routeKey] = string.IsNullOrEmpty(valueString) ? null : protectionProvider.Decrypt(valueString);

        return true;
    }
}

توضیحات:

- در قیود سفارشی می‌توان سرویس‌ها را به سازنده‌ی کلاس تزریق کرد و برای مثال از سرویس IProtectionProviderService که در کتابخانه‌ی DNTCommon.Web.Core تعریف شده، برای رمزگشایی اطلاعات رسیده، استفاده کرده‌ایم.

- یک نام را هم برای آن درنظر گرفته‌ایم که از این نام در فایل Program.cs به صورت زیر استفاده می‌شود:

builder.Services.Configure<RouteOptions>(opt =>
{
    opt.ConstraintMap.Add(EncryptedRouteConstraint.Name, typeof(EncryptedRouteConstraint));
});

یعنی زمانیکه سیستم مسیریابی به قید جدیدی به نام encrypt می‌رسد:

@page "/post/edit/{EditId:encrypt}"

آن‌را در لیست ConstraintMap ای که به نحو فوق به سیستم معرفی شده، جستجو می‌کند. اگر این نام یافت شد، سپس کلاس EncryptedRouteConstraint متناظر را نمونه سازی کرده و در جهت پردازش مسیر رسیده، مورد استفاده قرار می‌دهد.

- در کلاس EncryptedRouteConstraint و متد Match آن، مقدار رشته‌ای EditId دریافت شده‌ی از طریق آدرس جاری درخواستی، رمزگشایی شده و بجای مقدار فعلی رمزنگاری شده‌ی آن درج می‌شود. همین اندازه برای مقدار دهی خودکار پارامتر EditId ذیل در صفحات مرتبط، کفایت می‌کند:

[Parameter] public string? EditId { set; get; }

یعنی دیگر نیازی نیست تا در صفحات مرتبط، کار رمزگشایی EditId، به صورت دستی انجام شود.

مطالب
CoffeeScript #12

بخش‌های بد

جاوااسکریپت یک زبان پیچیده است که شما برای کار با آن، نیاز است قسمت‌هایی را که باید از آن‌ها دوری کنید و قسمت‌های مهمی را که باید استفاده کنید، بشناسید. همانطور که Sun Tzu گفته "دشمن خود را بشناس"، ما نیز در این قسمت می‌خواهیم برای شناخت بیشتر قسمت‌های تاریک و روشن جاوااسکریپت به آن بپردازیم.

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

اول از قسمت‌هایی که توسط CoffeeScript حل شده‌اند شروع می‌کنیم.

A JavaScript Subset

with یک دستور بسیار زمانبر است و مضر شناخته شده است و نباید از آن استفاده کنید. with با ایجاد یک ساختار خلاصه نویسی، برای جستجو بر روی خصوصیات اشیاء در نظر گرفته شده بود. برای نمونه به جای نوشتن:

dataObj.users.vahid.email = "info@vmt.ir";
می‌توانید به این صورت این کار را انجام دهید:
with(dataObj.users.vahid) {
  email = "info@vmt.ir";
}
مفسر جاوااسکریپت دقیقا نمی‌داند که شما می‌خواهید چه کاری را با with انجام دهید، و به شیء مشخص شده فشار می‌آورد تا اول اسم همه مراجعه شده‌ها را جستجو کند. این عمل واقعا به عملکرد و کارآیی لطمه می‌زند. یعنی مترجم، تمام انواع بهینه سازی‌های JIT را خاموش می‌کند. همچنین پیشنهادهایی مبنی بر حذف کامل آن از نسخه‌های بعدی جاوااسکریپت نیز مطرح شده است.
همه چیز برای عدم استفاده از with در نظر گرفته شده است. CoffeeScript یک قدم جلوتر از همه برداشته و with را از syntax خود حذف کرده است. به عبارت دیگر در صورتیکه شما از آن استفاده کنید، کامپایلر CoffeeScript خطا صادر می‌کند.

Global variables

به طور پیش فرض تمامی برنامه‌های جاوااسکریپت در دامنه global اجرا می‌شوند و تمامی متغیرهایی که ساخته می‌شوند به طور پیش فرض در ناحیه‌ی global قرار می‌گیرند. اگر شما بخواهید متغیری را در ناحیه‌ی local ایجاد کنید، باید از کلمه کلیدی var استفاده کنید.

usersCount = 1;        // Global
var groupsCount = 2;   // Global

(function(){              
  pagesCount = 3;      // Global
  var postsCount = 4;  // Local
})()
اکثر اوقات شما می‌خواهید متغیر local ایی را ایجاد کنید و نه global. توسعه دهندگان باید همیشه به یاد داشته باشند که قبل از مقداردهی اولیه‌ی هر متغیری، کلمه‌ی کلیدی var را قرار دهند یا با انواع و اقسام مشکلات، هنگامی که متغیرها به طور تصادفی با یکدیگر برخورد و یا بازنویسی بر روی یکدیگر انجام می‌دهند، روبرو شوند.
خوشبختانه CoffeeScript به کمک شما می‌آید و به طور کامل انتساب متغیرهای global را به طور ضمنی از بین می‌برد. به عبارت دیگر کلمه کلیدی var در CoffeeScript رزرو شده است و در صورت استفاده خطا صادر می‌شود.
به صورت پیش فرض به طور ضمنی متغیرها local ایجاد می‌شوند و خیلی سخت می‌شود متغیر global ایی را بدون انتساب آن به عنوان خصوصیتی از شیء window ایجاد کرد.
outerScope = true
do ->
  innerScope = true
نتیجه‌ی کامپایل آن می‌شود:
var outerScope;
outerScope = true;
(function() {
  var innerScope;
  return innerScope = true;
})();
همانطور که مشاهده می‌کنید CoffeeScript مقداردهی اولیه متغیر را (با استفاده از var) به صورت خودکار در context ایی که برای اولین بار استفاده شده است انجام می‌دهد. باید مواظب باشید تا از نام متغیر خارجی مجددا استفاده نکنید که این اتفاق ممکن است در کلاس یا تابع با عمق زیاد ایجاد شود. برای مثال، در اینجا به صورت تصادفی متغیر package در یک تابع کلاس بازنویسی شده است:
package = require('./package')

class Test
  build: ->
    # Overwrites outer variable!
    package = @testPackage.compile()

  testPackage: ->
    package.create()
برای ایجاد متغیرهای global باید از انتساب آنها به عنوان خصوصیتی از شیء window استفاده کرد.
  class window.Asset
    constructor: ->
با تضمین متغیرهای global به صورت صریح و روشن به جای به طور ضمنی بودن آنها، CoffeeScript یکی از منابع اصلی ایجاد مشکلات در جاوااسکریپت را حذف کرد‌ه‌است.

Semicolons

جاوااسکریپت اجباری برای نوشتن ";" ندارد، بنابراین ممکن است یک سری از دستورات از قلم بیافتند. با این حال در پشت صحنه‌ی کامپایلر جاوااسکریپت به ";" احتیاج دارد. به طوری که parser جاوااسکریپت به صورت خودکار هر زمانی که نتواند ارزیابی از دستورات داشته باشد، یک بار دیگر با ";" این کار را انجام می‌دهد و درصورت موفقیت، پیام خطایی مبنی بر نبود ";" را صادر می‌کند.
متاسفانه این یک ایده بد است. چرا که ممکن است تغییر رفتاری در کد نوشته شده به وجود آید. به مثال زیر توجه کنید. به نظر کد نوشته شده صحیح است؛ درسته؟
function() {}
(window.options || {}).property
اشتباه است، حداقل با توجه به parser، یک خطای syntax صادر می‌شود. در مورد دوم نیز parser، ";" اضافه نمی‌کند و کد نوشته شده به کد یک خطی تبدیل می‌شوند.
function() {}(window.options || {}).property
حالا شما می‌توانید این موضوع را ببینید که چرا parser خطا داده‌است. وقتی شما در حال نوشتن کد جاوااسکریپتی هستید، باید بعد از هر دستور از ";" استفاده کنید. خوشبختانه در تمام زمانیکه درحال نوشتن کد CoffeeScript هستید، نیازی به نوشتن ";" ندارید. در زمانیکه کد CoffeeScript نوشته شده کامپایل می‌شود، به صورت خودکار ";" را در جای مناسبی قرار می‌دهد.

مطالب
شروع به کار با AngularJS 2.0 و TypeScript - قسمت سوم - غنی سازی کامپوننت‌ها
در قسمت قبل، مقدمه‌ای بر نحوه‌ی تعریف یک کامپوننت در AngularJS 2.0 عنوان شد و همچنین نحوه‌ی بوت استرپ و آغاز اینگونه برنامه‌ها بررسی گردید. در این قسمت می‌خواهیم امکانات پیشرفته‌تری از کامپوننت‌ها را بررسی کنیم.


روش‌های مختلف تعریف خاصیت template در یک کامپوننت

در قسمت قبل، روش تعریف inline یک template را مشاهده کردید:
template:`
          <div><h1>{{pageTitle}}</h1>
               <div>My First Component</div>
          </div>
 `
در اینجا رشته‌ی قالب نهایی این View، در همان تعاریف متادیتای Component قرار گرفته‌است (روش inline). اگر این رشته تک سطری باشد، از روش متداول ذکر "" برای تعریف رشته‌ها در جاوا اسکریپت استفاده می‌شود و اگر این رشته چند سطری باشد، از back tick مربوط به ES 6 مانند مثال فوق کمک گرفته خواهد شد. استفاده از back ticks و رشته‌های چند سطری، نحوه‌ی تعریف قالب‌های inline را خواناتر می‌کند.
هر چند این روش تعریف قالب‌ها، مزیت سادگی و امکان مشاهده‌ی View را به همراه کدهای مرتبط با آن، در یک فایل میسر می‌کند، اما به دلیل رشته‌ای بودن، مزیت کار کردن با ادیتورهای وب، مانند داشتن intellisense، فرمت خودکار کدها و بررسی syntax را از دست خواهیم داد و با بیشتر شدن حجم این رشته، این مشکلات بیشتر نمایان خواهند شد.
به همین جهت قابلیت دیگری به نام linked template نیز در اینجا درنظر گرفته شده‌است:
 templateUrl: 'product-list.component.html'
در این حالت، محتوای قالب، به یک فایل html مجزا منتقل شده و سپس لینک آن در خاصیت دیگری از متادیتای Component به نام templateUrl ذکر می‌شود.


ساخت کامپوننت نمایش لیست محصولات

در ادامه می‌خواهیم کامپوننتی را طراحی کنیم که آرایه‌ای از محصولات را نمایش می‌دهد. در اینجا مرسوم است هر ویژگی برنامه، در یک پوشه‌ی مجزا قرار گیرد. به همین جهت در ادامه‌ی مثال قسمت قبل که پوشه‌ی app را به ریشه‌ی پروژه اضافه کردیم و سپس main.ts راه انداز و کامپوننت ریشه‌ی سایت یا app.component.ts را در آن تعریف کردیم، در داخل همین پوشه‌ی app، پوشه‌ی جدیدی را به نام products اضافه می‌کنیم. سپس به این پوشه‌ی جدید محصولات، فایل جدیدی را به نام product-list.component.html اضافه کنید. از این فایل جهت تعریف قالب کامپوننت لیست محصولات استفاده خواهیم کرد. در اینجا نیز مرسوم است نام قالب یک Component را به صورت نام ویژگی ختم شده‌ی به کلمه‌ی Component، با پسوند html تعریف کنیم.


پس از اضافه شدن فایل product-list.component.html، محتوای آن‌را به نحو ذیل تغییر دهید:
<div class='panel panel-default'>
    <div class='panel-heading'>
        {{pageTitle}}
    </div>
    <div class='panel-body'>
        <div class='row'>
            <div class='col-md-2'>Filter by:</div>
            <div class='col-md-4'>
                <input type='text' />
            </div>
        </div>
        <div class='row'>
            <div class='col-md-6'>
                <h3>Filtered by: </h3>
            </div>
        </div>
        <div class='table-responsive'>
            <table class='table'>
                <thead>
                    <tr>
                        <th>
                            <button class='btn btn-primary'>
                                Show Image
                            </button>
                        </th>
                        <th>Product</th>
                        <th>Code</th>
                        <th>Available</th>
                        <th>Price</th>
                        <th>5 Star Rating</th>
                    </tr>
                </thead>
                <tbody>
 
                </tbody>
            </table>
        </div>
    </div>
</div>
در اینجا قصد داریم داخل پنل بوت استرپ 3، لیستی از محصولات را به صورت یک جدول نمایش دهیم. همچنین می‌خواهیم قابلیت جستجوی داخل این لیست را نیز فراهم کنیم. فعلا شکل کلی این قالب را به نحو فوق تهیه می‌کنیم. قسمت tbody جدول آن را که قرار است لیست محصولات را رندر کند، در ادامه‌ی بحث تکمیل خواهیم کرد.
تنها نکته‌ی AngularJS 2.0 قالب فوق، اتصال به pageTitle است که نمونه‌ای از آن‌را در قسمت قبل با معرفی اولین کامپوننت مشاهده کرده‌اید.

در ادامه نیاز است برای این قالب و view، یک کامپوننت را طراحی کنیم که متشکل است از یک کلاس TypeScript ایی مزین شده به Component. بنابراین فایل ts جدیدی را به نام product-list.component.ts به پوشه‌ی App\products اضافه کنید؛ با این محتوا:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
}


با جزئیات نحوه‌ی تعریف یک کامپوننت در قسمت قبل در حین معرفی کامپوننت‌ها آشنا شدیم. در اینجا کلاس ProductListComponent با واژه‌ی کلیدی export همراه است تا توسط module loader برنامه قابلیت بارگذاری را پیدا کند. همچنین خاصیت عمومی pageTitle نیز در آن تعریف شده‌است تا در قالب مرتبط مورد استفاده قرار گیرد.
سپس این کلاس، با decorator ویژه‌ای به نام Component مزین شده‌است تا AngularJS 2.0 بداند که هدف از تعریف آن، ایجاد یک کامپوننت جدید است. مقدار selector آن که تشکیل دهنده‌ی یک تگ HTML سفارشی متناظر با آن خواهد شد، به pm-products تنظیم شده‌است و اینبار بجای تعریف inline قالب آن به صورت یک رشته، از خاصیت templateUrl جهت معرفی مسیر فایل html قالبی که پیشتر آماده کردیم، کمک گرفته شده‌است.


نمایش کامپوننت لیست محصولات در صفحه‌ی اصلی سایت

خوب، تا اینجا یک کامپوننت جدید را به نام لیست محصولات، ایجاد کردیم؛ اما چگونه باید آن‌را نمایش دهیم؟
در قسمت قبل که کامپوننت ریشه‌ی برنامه یا AppComponent را تعریف کردیم، نام selector آن را pm-app درنظر گرفتیم و در نهایت این directive سفارشی را به نحو ذیل در body صفحه‌ی اصلی سایت نمایش دادیم:
    <div>
        @RenderBody()
        <pm-app>Loading App...</pm-app>
    </div>
اما این روش، تنها برای root component سایت مناسب است. برای سایر کامپوننت‌های غیر ریشه‌ای (یعنی تمام کامپوننت‌ها)، سه مرحله‌ی زیر باید طی شوند:
الف) تگ سفارشی این دایرکتیو جدید را به کامپوننت ریشه‌ی سایت یا همان AppComponent اضافه می‌کنیم. بنابراین فایل app.component.ts را گشوده و سپس selector کامپوننت لیست محصولات را به قالب آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
همانطور که مشاهده می‌کنید، تگ جدید pm-products بر اساس نام selector کامپوننت لیست محصولات، به قالب کامپوننت ریشه‌ی سایت اضافه شده‌است.
ب) تا اینجا یک دایرکتیو جدید را به نام pm-products به یک کامپوننت دیگر اضافه کرده‌ایم. اما این کامپوننت نمی‌داند که اطلاعات آن‌را باید از کجا تامین کند. برای این منظور خاصیت جدیدی را به نام directives به لیست خاصیت‌های Component ریشه‌ی سایت اضافه می‌کنیم. این خاصیت، آرایه‌ای از دایرکتیوهای سفارشی را قبول می‌کند:
 directives: [ProductListComponent]
ج) بلافاصله که این تغییر را اعمال کنید، در ادیتور TypeScript ایی موجود، ذیل کلمه‌ی ProductListComponent خط قرمز کشیده خواهد شد. چون هنوز مشخص نکرده‌ایم که این شیء جدید باید از کدام ماژول تامین شود و ناشناخته‌است. بنابراین import مربوطه را به ابتدای فایل اضافه می‌کنیم:
import { Component } from 'angular2/core';
import { ProductListComponent } from './products/product-list.component';
 
@Component({
    selector: 'pm-app',
    template:`
    <div><h1>{{pageTitle}}</h1>
        <pm-products></pm-products>
    </div>
    `,
    directives: [ProductListComponent]
})
export class AppComponent {
    pageTitle: string = "DNT AngularJS 2.0 APP";
}
کدهای فوق، کد نهایی کامپوننت ریشه‌ی سایت هستند که به آن selector جدیدی به نام pm-products اضافه شده‌است. سپس directive متناظر آن به لیست دایرکتیوهای کامپوننت جاری اضافه شده و در نهایت این دایرکتیو، از ماژول مرتبط با آن import شده‌است.

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

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


یک نکته
اگر برنامه را اجرا کردید و خروجی را مشاهده نکردید، مطمئن شوید که فایل‌های ts شما کامپایل شده‌اند. فشردن دکمه‌ی ctrl+s مجدد در این فایل‌ها، سبب کامپایل مجدد آن‌ها می‌شوند و یا انتخاب گزینه‌ی Build و سپس ReBuild solution نیز همینکار را انجام می‌دهد.


غنی سازی کامپوننت‌های AngularJS 2.0 با data-binding

در AngularJS 2.0 عملیات binding، کار مدیریت ارتباطات بین یک کلاس کامپوننت و قالب آن‌را انجام می‌دهد. نمونه‌ای از آن‌را پیشتر با خاصیت pageTitle و سپس نمایش آن در قالب کامپوننت متناظر با آن کلاس، مشاهده کرده‌اید. همچنین در اینجا یک قالب می‌تواند متدهای داخل کلاس کامپوننت خود را توسط رخدادها نیز فراخوانی کند.
به نحوه‌ی نمایش {{pageTitle}} اصطلاحا interpolation می‌گویند. در اینجا خاصیت pageTitle اطلاعات خود را از کلاس کامپوننت دریافت می‌کند. به این نوع binding، انقیاد یک طرفه یا one-way binding نیز گفته می‌شوند؛ از خاصیت کلاس شروع شده و به قالب خاتمه می‌یابد.
ویژگی interpolation فراتر است از صرفا نمایش یک خاصیت و می‌تواند حاوی محاسبات نیز باشد:
{{'Title: ' + pageTitle}}
{{2*20+1}}
و یا حتی در آن می‌توان متدی از کلاس کامپوننت را نیز فراخوانی کرد. در مثال زیر فرض شده‌است که متد getTitle، در کلاس متناظر با کامپوننت این قالب، تعریف شده‌است:
{{'Title: ' + getTitle()}}
کار interpolation درج عبارت محاسبه شده‌ی نهایی بین المان‌های html است؛ مانند:
 <h1>{{pageTitle}}</h1>
و یا حتی می‌توان این مقدار نهایی را به خواص المان‌های html نیز نسبت داد:
 <h1 innerText={{pageTitle}}></h1>
در این مثال خاصیت innerText المان h1 توسط interpolation مقدار دهی شده‌است.

بنابراین به صورت خلاصه هر زمانیکه نیاز به نمایش اطلاعات فقط خواندنی (one-way binding) داریم، ابتدا خاصیتی را در کلاس کامپوننت تعریف کرده و سپس مقدار این خاصیت را توسط interpolation، در قالب کامپوننت درج می‌کنیم. حین استفاده از interpolation نیازی به ذکر "" نیست.
در مورد مباحث تکمیلی binding در قسمت‌های بعدی بیشتر بحث خواهیم کرد.


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

دایرکتیوها به صورت المان‌ها و یا ویژگی‌های سفارشی HTML، قابلیت توسعه‌ی امکانات پیش فرض آن‌را دارند. در اینجا می‌توان دایرکتیوهای سفارشی خود را تولید کرد (مانند pm-products فوق) و یا از دایرکتیوهای توکار AngularJS 2.0 استفاده کرد. برای مثال ngIf* و ngFor* جزو structural directives توکار AngularJS 2.0 هستند. ستاره‌ای که پیش از نام این دایرکتیوها قرار گرفته‌است، آن‌‌ها را در گروه structural directives قرار می‌دهد.
کار دایرکتیوهای ساختاری، تغییر ساختار یا همان view کامپوننت‌ها است؛ با افزودن، حذف و یا تغییر المان‌های HTML تعریف شده‌ی در صفحه.

بررسی ngIf*

فایل قالب product-list.component.html را گشوده و تعریف جدول آن‌را به نحو ذیل تغییر دهید:
 <table class='table' *ngIf='products && products.length'>
کار ngIf* نمایش یا عدم نمایش قسمتی از DOM یا document object model بر اساس برآورده شدن منطقی است که توسط آن بررسی می‌شود. اگر حاصل عبارتی که به ngIf* انتساب داده می‌شود به false تعبیر شود، آن المان و فرزندان آن از DOM حذف می‌شوند و اگر این عبارت به true تعبیر شود، آن المان و فرزندانش مجددا به DOM اضافه خواهند شد.
برای نمونه عبارت انتساب داده شده‌ی به ngIf* در مثال فوق به این معنا است که اگر خاصیت و آرایه‌ی products در کلاس کامپوننت این قالب تعریف شده بود و همچنین دارای اعضایی نیز بود، آنگاه این جدول را نمایش بده.
برای آزمایش آن، فایل product-list.component.ts را گشوده و خاصیت عمومی آرایه‌ی products را به نحو ذیل به آن اضافه کنید:
import { Component } from 'angular2/core';
 
@Component({
    selector: 'pm-products',
    templateUrl: 'app/products/product-list.component.html'
})
export class ProductListComponent {
    pageTitle: string = 'Product List';
    products: any[] = [
        {
            "productId": 2,
            "productName": "Garden Cart",
            "productCode": "GDN-0023",
            "releaseDate": "March 18, 2016",
            "description": "15 gallon capacity rolling garden cart",
            "price": 32.99,
            "starRating": 4.2,
            "imageUrl": "app/assets/images/garden_cart.png"
        },
        {
            "productId": 5,
            "productName": "Hammer",
            "productCode": "TBX-0048",
            "releaseDate": "May 21, 2016",
            "description": "Curved claw steel hammer",
            "price": 8.9,
            "starRating": 4.8,
            "imageUrl": "app/assets/images/rejon_Hammer.png"
        }
    ];
}
فعلا چون اینترفیسی را برای شیء محصول تعریف نکرده‌ایم، نوع این آرایه را any یا همان حالت پیش فرض جاوا اسکریپت تعریف می‌کنیم.
همچنین فعلا در اینجا اطلاعات را بجای دریافت از سرور، توسط آرایه‌ی مشخصی از اشیاء تعریف کرده‌ایم. این موارد را در قسمت‌های بعدی بهبود خواهیم بخشید.

اکنون که خاصیت عمومی products تعریف شده‌است، امکان استفاده‌ی از ngIf* ایی که پیشتر تعریف کردیم، میسر شده‌است. در این حالت اگر برنامه را اجرا کنید، قسمت table header تصویر قبلی نمایش سایت، هنوز نمایان است. یعنی ngIf* تعریف شده کار می‌کند؛ چون خاصیت products تعریف شده‌است و همچنین دارای اعضایی است.
برای آزمایش بیشتر، خاصیت products را کامنت کنید و یکبار نیز فایل ts آن‌را ذخیره کنید تا فایل js متناظر با آن کامپایل شود. سپس مجددا برنامه را اجرا کنید. در این حالت دیگر نباید هدر جدول نمایان باشد؛ چون products تعریف نشده‌است.


بررسی ngFor*

تا اینجا بر اساس داشتن لیستی از محصولات یا عدم آن، جدول متناظری را نمایش داده و یا مخفی کردیم. اما این جدول هنوز فاقد ردیف‌های نمایش اعضای آرایه‌ی products است.
برای این منظور مجددا فایل قالب product-list.component.html را گشوده و سپس بدنه‌ی جدول را به نحو ذیل تکمیل کنید:
<tbody>
    <tr *ngFor='#product of products'>
        <td></td>
        <td>{{ product.productName }}</td>
        <td>{{ product.productCode }}</td>
        <td>{{ product.releaseDate }}</td>
        <td>{{ product.price }}</td>
        <td>{{ product.starRating }}</td>
    </tr>
</tbody>
یکی دیگر از دایرکتیوهای ساختاری، ngFor* نام دارد. کار آن تکرار قسمتی از DOM، به ازای تک تک عناصر لیست انتساب داده شده‌ی به آن است.
بنابراین ابتدا قسمتی از عناصر HTML را طوری کنار هم قرار می‌دهیم که جمع آن‌ها یک تک آیتم را تشکیل دهند. سپس با استفاده از ngFor* به AngularJS 2.0 اعلام می‌کنیم که این قطعه را به ازای عناصر لیست دریافتی، تکرار و رندر کند.
برای نمونه در مثال فوق می‌خواهیم ردیف‌های جدول تکرار شوند. بنابراین هر ردیف را به عنوان یک قطعه‌ی تکرار شونده‌ی توسط ngFor* مشخص می‌کنیم. به این ترتیب این ردیف و عناصر فرزند آن، به ازای تک تک محصولات موجود در آرایه‌ی products، تکرار خواهند شد.
علامت # در اینجا (product#) یک متغیر محلی را تعریف می‌کند که تنها در قالب جاری قابل استفاده خواهد بود و همچنین فقط در فرزندان tr تعریف شده قابل دسترسی هستند.
به علاوه در اینجا بجای in از of استفاده شده‌است. این of از ES 6 گرفته شده‌است. زمانیکه از حلقه‌ی جدید for...of استفاده می‌شود، متغیر محلی product حاوی یک عنصر از لیست product خواهد بود؛ اما اگر از حلقه‌ی قدیمی for...in استفاده می‌شد، تنها ایندکس عددی این عناصر در دسترس قرار می‌گرفتند. به همین جهت است که در این حلقه، اکنون product.productName به نام محصول آن عنصر آرایه‌ی دریافتی اشاره می‌کند و قابل استفاده است.

تا اینجا اگر برنامه را اجرا کنید، چنین خروجی را مشاهده خواهید کرد:


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: MVC5Angular2.part3.zip


خلاصه‌ی بحث

از inline templateها جهت معرفی قالب‌های کوتاه استفاده می‌شود. در اینجا از "" برای معرفی قالب یک سطری و یا از back tickهای ES 6، برای تعریف قالب‌های چندسطری استفاده خواهد شد. برای قالب‌های مفصل‌تر، بهتر است Linked templateها استفاده شود؛ با پشتیبانی کامل ادیتورهای موجود از لحاظ تکمیل و بررسی کدها.
برای استفاده از یک کامپوننت در کامپوننتی دیگر، نام selector آن‌را به صورت یک المان جدید HTML در قالب دیگری ذکر کرده و سپس با استفاده از خاصیت directives، نام کلاس متناظر با آن‌را نیز ذکر می‌کنیم. همچنین کار import ماژول آن نیز باید در ابتدای فایل صورت گیرد.
جهت غنی سازی قالب‌ها و کامپوننت‌ها و نمایش اطلاعات فقط خواندنی می‌توان از binding یک طرفه‌ی ویژه‌ای به نام interpolation استفاده کرد. کار آن اتصال یک خاصیت عمومی کلاس کامپوننت، به قالب آن است. interpolation توسط {{}} تعریف می‌شود و می‌تواند شامل محاسبات نیز باشد.
همچنین در ادامه‌ی بحث، نحوه‌ی کار با دو دایرکتیو توکار ساختاری AngularJS 2.0 را نیز بررسی کردیم. این دایرکتیوهای ساختاری نیاز است با ستاره شروع شوند و عبارت انتساب داده شده‌ی به آن‌ها باید داخل "" قرار گیرد (برخلاف interpolation که نیازی به اینکار ندارد). از ngIf* برای حذف یا افزودن یک المان و فرزندان آن از/به DOM استفاده می‌شود. اگر عبارت منتسب به آن به true ارزیابی شود، این المان از صفحه حذف خواهد شد. از ngFor* برای تکرار المانی مشخص به همراه فرزندان آن به تعداد اعضای لیستی که برای آن تعیین می‌گردد، استفاده می‌شود. متغیر محلی این پیمایشگر با # مشخص شده و حلقه‌ی آن با of بجای in تعریف می‌شود.
مطالب
بررسی چند نکته در مورد ارث بری کلاس‌ها در #C
مقدمه
وراثت، بین کلاس‌های والد (Parent) و فرزند (Child) ارتباط ایجاد می‌کند. در این مطلب، با یک مثال ساده، نکات مختلفی را بررسی خواهیم کرد.

در ابتدا کلاس‌هایی را با نام parent و child، به شکل زیر ایجاد می‌کنیم:
public class Parent
{
  public Parent()
  {
            Console.WriteLine("Parent Constructor");
  }
  public void Print()
  {
            Console.WriteLine("Parent Print");
  }
} public class Child : Parent { public Child() { Console.WriteLine("Child Constructor"); } public void Print() { Console.WriteLine("Child Print"); } }
با کامپایل کد فوق، هشدار (نه خطا) زیر توسط ویژوال استودیو صادر خواهد شد:

هشدارفوق این نکته را تذکر می‌دهد که متد Print تعریف شده در کلاس Child، پیاده سازی متد Print را در کلاس والد، مخفی (Hide) می‌کند. به همین خاطر پیشنهاد می‌کند که اگر واقعا قصد چنین کاری را داریم (نادیده گرفتن پیاده سازی print کلاس والد) از کلمه کلیدی (keyword) new استفاده کنیم. بدین شکل:
 public new void Print()
  {
     Console.WriteLine("Child Print");
  }
حال با نمونه سازی کلاس‌های فوق، رفتار سازنده و متد Print را بررسی می‌کنیم:
Console.WriteLine("====Parent====");
Parent parent = new Parent();
parent.Print();

Console.WriteLine("====Child====");
Child child = new Child();
child.Print();

Console.WriteLine("====Parent Via Child====");
Parent pc = new Child();
pc.Print();
در قسمت اول نمونه سازی از والد، نکته خاصی وجود ندارد. در ابتدا سازنده و سپس فراخوانی متد Print اتفاق خواهد افتاد.
در قسمت دوم نمونه سازی از فرزند، ابتدا سازنده والد و سپس سازنده فرزند فراخوانی خواهند.
در بخش سوم، یک نمونه فرزند را از نوع والد، ایجاد کرد‌هایم .( () Parent pc=new Child). در این بخش ابتدا سازنده والد و بعد از آن سازنده فرزند، فراخوانی می‌شود و با فراخوانی متد Print، متد والد اجرا خواهد شد.


استفاده از Virtual  و Override
اگر بدنبال این باشیم که در قسمت سوم متد Print فرزند فراخوانی شود، مفاهیم virtual و override به کمک ما خواهند آمد:
  public class Parent
{
  public Parent()
  {
Console.WriteLine("Parent Constructor");
  }

  public virtual void  Print()
  {
Console.WriteLine("Parent Print");
  }
}

public class Child : Parent
{
  public Child()
  {
Console.WriteLine("Child Constructor");
  }

  public override void Print()
  {
Console.WriteLine("Child Print");
  }
}
با تعریف متد از نوع virtual، امکان تحریف رفتار پیش فرض متد را توسط فرزند‌ها، مهیا خواهیم کرد. فرزندان نیز با override کردن متد والد، پیاده سازی خود را اعمال می‌کنند.
اگر خروجی کد بالا را با قسمت قبل مقایسه کنید، متوجه خواهید شد که در قسمت سوم فرزند، رفتار متد والد را تحریف/بازنویسی (override) کرده است ( پیاده سازی فرزند اجرا شده است).


سازنده‌های استاتیک (Static Constructor)
سازنده‌های استاتیک برای مقدار دهی به داده‌های استاتیک و یا انجام عملیاتی که تنها قرار است یکبار انجام شوند مورد استفاده قرار میگیرند. این سازنده‌ها بصورت اتوماتیک قبل از ساخت نمونه و مقداردهی اعضای استاتیک و قبل از سازنده‌های غیر استاتیک اجرا می‌شوند.
   public class Parent
{
  static Parent()
  {
Console.WriteLine("Parent static Constructor");
  }
  public Parent()
  {
Console.WriteLine("Parent Constructor");
  }

  public virtual void  Print()
  {
Console.WriteLine("Parent Print");
  }
}

public class Child : Parent
{
  static Child()
  {
Console.WriteLine("Child static Constructor");
  }
  public Child()
  {
Console.WriteLine("Child Constructor");
  }

  public override void Print()
  {
Console.WriteLine("Child Print");
  }
در بخش سوم در ابتدا سازنده استاتیک فرزند و سپس سازنده استاتیک والد فراخوانی خواهند شد و ترتیب اجرای سایر متد‌ها و سازنده‌ها مثل قبل است.




  جمع بندی
* اگر نمونه‌ای از یک فرزند را ایجاد کنیم، ابتدا سازنده‌ی والد فراخوانی خواهد شد و پس از آن سازنده‌ی کلاس فرزند.
* اگر قصد تحریف رفتار متد والد را در فرزندان داریم، می‌توانیم این متد‌ها را در کلاس والد بصورت virtual تعریف کنیم.
مطالب
شروع به کار با EF Core 1.0 - قسمت 12 - بررسی تنظیمات ارث بری روابط
پیشنیاز: «تنظیمات ارث بری کلاس‌ها در EF Code first»

در مطلب پیشنیاز فوق، تنظیمات روابط ارث بری را تا EF 6.x، می‌توانید مطالعه کنید. در EF Core 1.0 RTM، فقط رابطه‌ی TPH که در آن تمام کلاس‌های سلسه مراتب ارث بری، به یک جدول در بانک اطلاعاتی نگاشت می‌شوند، پشتیبانی می‌شود. سایر روش‌های ارث بری که در EF 6.x وجود دارند، مانند TPT و TPC، قرار است به نگارش‌های پس از 1.0 RTM آن اضافه شوند:
- لیست مواردی که قرار است به نگارش‌های بعدی اضافه شوند
- پیگیری وضعیت پیاده سازی TPT
- پیگیری وضعیت پیاده سازی TPC


طراحی یک کلاس پایه، بدون تنظیمات ارث بری روابط

مرسوم است که یک کلاس ویژه را به نام BaseEntity، به شکل زیر تعریف کنند؛ که اهدف آن حداقل سه مورد ذیل است:
الف) کاهش ذکر فیلدهای تکراری در سایر کلاس‌های دومین برنامه، مانند فیلد Id
ب) نشانه گذاری موجودیت‌های برنامه، جهت یافتن سریع آن‌ها توسط Reflection (برای مثال افزودن خودکار موجودیت‌ها به Context برنامه با یافتن تمام کلاس‌هایی که از نوع BaseEntity هستند)
ج) مقدار دهی خودکار یک سری از فیلدهای ویژه، مانند زمان افزوده شدن رکورد و آخرین زمان ویرایش شدن رکورد و امثال آن
public class BaseEntity
{
   public int Id { set; get; }
   public DateTime? DateAdded { set; get; }
   public DateTime? DateUpdated { set; get; }
}
و پس از آن هر موجودیت برنامه به این شکل خلاصه شده و نشانه گذاری می‌شود:
public class Person : BaseEntity
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
}
حالت پیش فرض ارث بری‌ها در EF Core، همان حالت TPH است که در ادامه توضیح داده خواهد شد. اما هدف ما در اینجا تنظیم هیچکدام از حالت‌های ارث بری نیست. هدف صرفا کاهش تعداد فیلدهای تکراری ذکر شده‌ی در کلاس‌های دومین برنامه است. بنابراین جهت لغو تنظیمات ارث بری EF Core، نیاز است یک چنین تنظیمی را انجام داد:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Ignore<BaseEntity>();
با فراخوانی متد Ignore بر روی کلاس پایه‌ی تهیه شده، این کلاس دیگر وارد تنظیمات روابط EF Core نمی‌شود و در جداول نهایی، فیلدهای آن به صورت معمول در کنار سایر فیلدهای جداول مشتق شده‌ی از آن‌ها قرار می‌گیرند.

مشکل! اگر بر روی کلاس پایه‌ی تعریف شده تنظیماتی را اعمال کنید (هر نوع تنظیمی را)، با توجه به فراخوانی متد Ignore، این تنظیمات نیز ندید گرفته خواهند شد.
اگر علاقمند بودید تا این تنظیمات را به تمام کلاس‌های مشتق شده‌ی از BaseEntity به صورت خودکار اعمال کنید، روش کار به صورت ذیل است:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Ignore<BaseEntity>();
   foreach (var entityType in modelBuilder.Model.GetEntityTypes())
   {
     var dateAddedProperty = entityType.FindProperty("DateAdded");
     dateAddedProperty.ValueGenerated = ValueGenerated.OnAdd;
     dateAddedProperty.SqlServer().DefaultValueSql = "getdate()";
     var dateUpdatedProperty = entityType.FindProperty("DateUpdated");
     dateUpdatedProperty.ValueGenerated = ValueGenerated.OnAddOrUpdate;
     dateUpdatedProperty.SqlServer().ComputedColumnSql = "getdate()";
   }
کاری که در اینجا انجام شده، تنظیم خاصیت DateAdded کلاس پایه، به حالت ValueGeneratedOnAdd و تنظیم خاصیت DateUpdated کلاس پایه به حالت ValueGeneratedOnAddOrUpdate با مقدار پیش فرض getdate است. این مفاهیم را در مطلب «شروع به کار با EF Core 1.0 - قسمت 5 - استراتژهای تعیین کلید اصلی جداول و ایندکس‌ها» پیشتر بررسی کردیم.
خلاصه‌ی آن نیز به این صورت است:
الف) نیازی نیست تا در حین ثبت اطلاعات موجودیت‌های خود، فیلدهای DateAdded و یا DateUpdated را مقدار دهی کنید.
ب) فیلد DateAdded فقط در زمان اولین بار ثبت در بانک اطلاعاتی، به صورت خودکار توسط متد getdate مقدار دهی می‌شود.
ج) فیلد DateUpdated در هر بار فراخوانی متد SaveChanges  (یعنی در هر دو حالت ثبت و یا به روز رسانی) به صورت خودکار توسط متد getdate مقدار دهی می‌شود.

تذکر! بدیهی است متد getdate، یک متد بومی سمت SQL Server است و این روش خاص تعیین مقدار پیش فرض فیلدها، فقط با SQL Server کار می‌کند. همچنین این getdate، به معنای دریافت تاریخ و ساعت سروری است که SQL Server بر روی آن نصب شده‌است و نه سروری که برنامه‌ی وب شما در آن قرار دارد و برنامه کوچکترین دخالتی را در مقدار دهی این مقادیر نخواهد داشت.
در قسمت‌های بعدی که مباحث Tracking را بررسی خواهیم کرد، روش دیگری را برای طراحی کلاس‌های پایه و مقدار دهی خواص ویژه‌ی آن‌ها مطرح می‌کنیم که مستقل است از نوع بانک اطلاعاتی مورد استفاده.


بررسی تنظیمات رابطه‌ی  Table per Hierarchy یا TPH

رابطه‌ی TPH یا تشکیل یک جدول بانک اطلاعاتی، به ازای تمام کلاس‌های دخیل در سلسه مراتب ارث بری تعریف شده، بسیار شبیه است به حالت BaseEntity فوق که در آن نیز ارث بری تعریف شده، در نهایت منجر به تشکیل یک جدول، در سمت بانک اطلاعاتی می‌گردد. با این تفاوت که در حالت TPH، فیلد جدیدی نیز به نام Discriminator، به تعریف نهایی جدول ایجاد شده، اضافه می‌شود. از فیلد Discriminator جهت درج نام کلاس‌های متناظر با هر رکورد، استفاده شده است. به این ترتیب EF در حین کار با اشیاء، دقیقا می‌داند که چگونه باید خواص متناظر با کلاس‌های مختلف را مقدار دهی کند و نوع ردیف درج شده‌ی در بانک اطلاعاتی چیست؟
باید دقت داشت که تنظیمات TPH، شیوه برخورد پیش فرض EF Core با ارث بری کلاس‌ها است و نیاز به هیچگونه تنظیم اضافه‌تری را ندارد. اما اگر علاقمند بودید تا نام فیلد خودکار Discriminator و مقادیری را که در آن درج می‌شوند، سفارشی سازی کنید، روش کار صرفا توسط Fluent API میسر است و به صورت زیر می‌باشد:
public class Blog
{
   public int BlogId { get; set; }
   public string Url { get; set; }
}

public class RssBlog : Blog
{
   public string RssUrl { get; set; }
}

class MyContext : DbContext
{
   public DbSet<Blog> Blogs { get; set; }
   protected override void OnModelCreating(ModelBuilder modelBuilder)
   {
      modelBuilder.Entity<Blog>()
       .HasDiscriminator<string>("blog_type")
       .HasValue<Blog>("blog_base")
       .HasValue<RssBlog>("blog_rss");
   }
}
در اینجا نام فیلد Discriminator، به blog_type، مقدار نوع متناظر با کلاس Blog، به blog_base و مقدار نوع متناظر با کلاس RssBlog، به blog_rss تنظیم شده‌است.
اگر این تنظیمات سفارشی صورت نگیرند، از نام‌های پیش فرض نوع‌ها برای مقدار دهی ستون Discriminator، مانند تصویر ذیل استفاده خواهد شد:


برای کوئری نوشتن در این حالت می‌توان از متد الحاقی OfType جهت فیلتر کردن اطلاعات بر اساس کلاسی خاص، کمک گرفت:
 var blog1 = db.Blogs.OfType<RssBlog>().FirstOrDefault(x => x.RssUrl == "………");
مطالب
React 16x - قسمت 11 - طراحی یک گرید - بخش 1 - کامپوننت صفحه بندی
در طی چند قسمت قصد داریم مثال قسمت قبل را که کار نمایش لیستی از فیلم‌ها را انجام می‌دهد، تبدیل به یک کامپوننت گرید کنیم که دارای امکانات صفحه بندی، فیلتر کردن و مرتب سازی اطلاعات است.


بررسی ساختار کامپوننت Pagination

شبیه به کامپوننت Like که در قسمت قبل ایجاد کردیم، می‌خواهیم کامپوننت جدید Pagination نیز به طور کامل از اشیاء movie مستقل باشد؛ تا در آینده بتوان از آن در جاهای دیگری نیز استفاده کرد. به همین جهت فایل جدید src\components\common\pagination.jsx را ایجاد کرده و سپس با استفاده از میانبرهای imrc و cc در VSCode، ساختار ابتدایی این کامپوننت را ایجاد می‌کنیم. هرچند می‌توان این کامپوننت را به صورت «Stateless Functional Component» نیز طراحی کرد؛ چون state و متد دیگری بجز render نخواهد داشت و تمام اطلاعات خودش را از والد خود دریافت می‌کند.
سپس به کامپوننت movies مراجعه کرده و این کامپوننت خالی را import می‌کنیم:
import Pagination from "./common/pagination";
پس از آن به متد رندر کامپوننت movies مراجعه کرده و پس از بسته شدن tag المان جدول، قصد داریم این کامپوننت صفحه بندی را نمایش دهیم. به همین جهت المان آن‌را در این محل قرار می‌دهیم تا بتوانیم اینترفیس ابتدایی آن‌را پیش از پیاده سازی آن، طراحی کنیم:
<Pagination
  itemsCount={this.state.movies.length}
  pageSize={this.state.pageSize}
  onPageChange={this.handlePageChange}
/>
این کامپوننت نیاز به تعداد کل آیتم‌های فیلم‌ها را دارد؛ به علاوه‌ی اندازه‌ی صفحه، یا همان تعداد ردیفی که قرار است در هر صفحه نمایش داده شود. به این ترتیب کامپوننت صفحه بندی می‌تواند تعداد صفحات مورد نیاز را محاسبه کرده و سپس آن‌ها را رندر کند.
  state = {
    movies: getMovies(),
    pageSize: 4
  };

به همین جهت دو ویژگی itemsCount و pageSize را پیش از هرکاری به تعریف المان صفحه بندی اضافه کرده‌ایم تا داده‌های ورودی آن مشخص شوند. ویژگی itemsCount از تعداد اعضای آرایه‌ی movies حاصل می‌شود و pageSize را به عنوان یک خاصیت جدید شیء منتسب به state تعریف و با عدد 4 مقدار دهی کرده‌ایم.
همچنین در لیست صفحاتی که توسط این کامپوننت رندر می‌شود، باید با کلیک بر روی هر کدام، اطلاعات آن صفحه رندر شود. به همین جهت در اینجا ویژگی onPageChange تعریف شده و به متد جدید handlePageChange در کامپوننت movies، متصل گردیده تا عدد page را دریافت کرده و به آن واکنش نشان دهد:
  handlePageChange = page => {
    console.log("handlePageChange", page);
  };
تا اینجا اینترفیس کامپوننت صفحه بندی را پیش از پیاده سازی آن تعریف کردیم (تعیین ورودی‌ها و خروجی آن). در مرحله‌ی بعد، این کامپوننت را تکمیل می‌کنیم.


نمایش شماره صفحات گرید، در کامپوننت صفحه بندی

برای رندر کامپوننت صفحه بندی، از کلاس‌های مخصوص اینکار که در بوت استرپ تعریف شده‌اند، استفاده می‌کنیم که ساختار کلی آن به صورت زیر است و از یک المان nav که داخل آن ul ای با کلاس pagination و liهایی با کلاس page-item هستند، تشکیل می‌شود. هر li، به همراه یک anchor است؛ با کلاس page-link تا لینک به صفحه‌ای خاص را ارائه دهد که در اینجا بجای لینک، از کلیک بر روی آن‌ها استفاده خواهیم کرد:
import React, { Component } from "react";

class Pagination extends Component {
  render() {
    return (
      <nav>
        <ul className="pagination">
          <li className="page-item">
            <a className="page-link">1</a>
          </li>
        </ul>
      </nav>
    );
  }
}

export default Pagination;

تا اینجا اگر برنامه را ذخیره کرده و اجرا کنید، عدد 1 را در پایین جدول فیلم‌ها مشاهده خواهید کرد:


اکنون باید رندر این liها را بر اساس ورودی‌های این کامپوننت که پیشتر معرفی کردیم، یعنی pageSize و itemsCount، پویا کنیم. به همین جهت نیاز به آرایه‌ای داریم که بر اساس این ورودی‌ها، شماره‌ی صفحات مانند [1,2,3] را ارائه دهد تا بر روی آن متد Array.map را فراخوانی کرده و liهای مورد نیاز را به صورت پویا رندر کنیم:
class Pagination extends Component {
  // ...
  getPageNumbersArray() {
    const { itemsCount, pageSize } = this.props;
    const pagesCount = Math.ceil(itemsCount / pageSize);
    if (pagesCount === 1) {
      return null;
    }

    const pages = new Array();
    for (let i = 1; i <= pagesCount; i++) {
      pages.push(i);
    }
    return pages;
  }
}
در اینجا بر اساس ورودی‌ها، تعداد صفحات محاسبه شده و سپس بر اساس آن‌ها آرایه‌ای از این شماره صفحه‌ها تشکیل و بازگشت داده می‌شود. همچنین اگر تعداد صفحات 1 بود، می‌خواهیم این کامپوننت چیزی را رندر نکند. به همین جهت در اینجا null بازگشت داده شده‌است. دلیل استفاده‌ی از Math.ceil که کوچکترین عدد صحیح بزرگتر یا مساوی خروجی را بازگشت می‌دهد، نیز همین مورد است. توسط این متد، خروجی float دریافتی به integer تبدیل شده و سپس قابلیت مقایسه‌ی با 1 را پیدا می‌کند. برای مثال اگر تعداد ردیف‌های صفحه را به 10 تنظیم کنیم، خروجی این تقسیم در این مثال، 0.9 خواهد بود که شرط بررسی pagesCount === 1 را برآورده نمی‌کند. به همین جهت توسط متد Math.ceil، این خروجی به عدد 1 تقریب زده شده و سبب بازگشت نال از این متد خواهد شد.
سپس به کمک متد map، اعضای این آرایه را تبدیل به لیست liهای نمایش شماره صفحات می‌کنیم. در اینجا key هر li را نیز به شماره صفحه که منحصربفرد است، تنظیم کرده‌ایم:
class Pagination extends Component {
  render() {
    const pages = this.getPageNumbersArray();
    if (!pages) {
      return null;
    }

    return (
      <nav>
        <ul className="pagination">
          {pages.map(page => (
            <li key={page} className="page-item">
              <a className="page-link">{page}</a>
            </li>
          ))}
        </ul>
      </nav>
    );
  }

پس از ذخیره‌ی این کامپوننت و بارگذاری مجدد برنامه در مرورگر، شماره‌ی صفحات رندر شده، در پایین جدول مشخص هستند:


با داشتن 9 فیلم در آرایه‌ی movies و نمایش 4 فیلم به ازای هر صفحه، به 3 صفحه خواهیم رسید که به درستی در اینجا رندر شده‌است. یکبار هم برای آزمایش بیشتر، مقدار pageSize را در کامپوننت movies به 10 تنظیم کنید. در این حالت کامپوننت صفحه بندی نباید رندر شود.


مدیریت انتخاب شماره‌های صفحات

در این قسمت می‌خواهیم مدیریت onPageChange={this.handlePageChange} را که به تعریف المان صفحه بندی در کامپوننت movies اضافه کردیم، تکمیل کنیم. برای این منظور در کامپوننت صفحه بندی، قسمت anchor را به صورت زیر تغییر می‌دهیم تا با کلیک بر روی آن، رخداد onPageChange صادر شود:
<a
   onClick={() => this.props.onPageChange(page)}
   className="page-link"
   style={{ cursor: "pointer" }}
>
   {page}
</a>

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

اکنون می‌خواهیم اگر صفحه‌ای انتخاب شد، شماره‌ی آن صفحه با رنگی دیگر نمایش داده شود. در بوت استرپ برای اینکار تنها کافی است کلاس active را به className هر li اضافه کنیم و برعکس. یعنی اگر page ای مساوی صفحه‌ی جاری انتخاب شده بود (currentPage در اینجا)، آنگاه کلاس page-item active، به المان li اضافه شود. بنابراین در این کامپوننت نیاز است عدد currentPage را نیز دریافت کنیم. به همین جهت ویژگی currentPage را به تعریف المان Pagination در کامپوننت movies اضافه می‌کنیم:
<Pagination
  itemsCount={this.state.movies.length}
  pageSize={this.state.pageSize}
  onPageChange={this.handlePageChange}
  currentPage={this.state.currentPage}
/>
این ویژگی نیز مقدار خودش را از state به روز شده دریافت می‌کند:
class Movies extends Component {
  state = {
    movies: getMovies(),
    pageSize: 4,
    currentPage: 1
  };
به روز رسانی state نیز در متد handlePageChange که به ویژگی onPageChange متصل است، بر اساس page دریافتی از کامپوننت صفحه بندی، رخ می‌دهد:
  handlePageChange = page => {
    console.log("handlePageChange", page);
    this.setState({currentPage: page});
  };
بنابراین هرگاه که بر روی یک شماره صفحه در کامپوننت صفحه بندی کلیک می‌شود، رخ‌داد onPageChange متصل به تعریف المان Pagination درج شده‌ی در کامپوننت movies، روی داده و به همراه آن شماره صفحه‌ای، به متد رخ‌دادگران متصل به آن در کامپوننت movies که در اینجا handlePageChange نام دارد، ارسال می‌شود. در این متد state کامپوننت به روز شده و این امر سبب فراخوانی مجدد متد رندر می‌شود که در انتهای آن کامپوننت Pagination درج شده‌است. بنابراین به روز رسانی state، سبب رندر مجدد کامپوننت صفحه بندی با currentPage جدیدی که به آن رسیده‌است، خواهد شد.

پس از این تغییرات، به کامپوننت صفحه بندی مراجعه کرده و بر اساس currentPage دریافتی، کلاس active را به المان li اعمال می‌کنیم:
<li
  key={page}
  className={
    page === this.props.currentPage
    ? "page-item active"
    : "page-item"
  }
>
پس از اعمال این تغییرات، اکنون برنامه در مرورگر به صورت زیر به نظر می‌رسد:


در اولین بار نمایش برنامه، عدد 1 در حالت انتخاب شده قرار دارد؛ چون مقدار currentPage موجود در state، همان عدد 1 است. پس از آن با کلیک بر روی اعداد دیگر، با به روز رسانی state، مقدار currentPage تغییر کرده و کامپوننت صفحه بندی نسبت به آن واکنش نشان می‌دهد.


نمایش صفحه بندی شده‌ی اطلاعات

تا اینجا لیستی که نمایش داده می‌شود، حاوی تمام اطلاعات آرایه‌ی this.state.movies است و بر اساس شماره‌ی صفحه‌ی انتخابی، تغییر نمی‌کند. به همین جهت با استفاده از متد slice، تکه‌ای از آرایه‌ی movies را که بر اساس شماره صفحه‌ی انتخابی و تعداد ردیف‌ها در هر صفحه نیاز است نمایش داده شود، انتخاب کرده و بازگشت می‌دهیم:
  paginate() {
    const first = (this.state.currentPage - 1) * this.state.pageSize;
    const last = first + this.state.pageSize;
    return this.state.movies.slice(first, last);
  }
اکنون در متد رندر کامپوننت movies، بجای کار با کل آرایه‌ی this.state.movies، آرایه‌ی جدید slice شده را توسط متد paginate دریافت کرده:
  render() {
    const { length: count } = this.state.movies;

    if (count === 0) return <p>There are no movies in the database.</p>;

    const movies = this.paginate();
و سپس در همین متد رندر، فراخوانی this.state.movies.map را به movies.map تغییر می‌دهیم، تا تنها لیست مرتبط با هر صفحه‌ی انتخابی نمایش داده شود.

پس از ذخیره‌ی تغییرات و بارگذاری مجدد برنامه، اکنون می‌توان نمایش صفحه بندی شده‌ی اطلاعات را شاهد بود:



بررسی صحت نوع پارامترهای ارسالی به کامپوننت‌ها

تا اینجا فرض بر این است که مصرف کننده‌ی کامپوننت صفحه بندی، دقیقا همان ویژگی‌هایی را که ما طراحی کرده‌ایم، با همان نام‌ها و همان نوع‌ها را حتما به آن ارسال می‌کند. همچنین اگر افزونه‌ی eslint را هم در VSCode نصب کرده باشید، به همراه نصب وابستگی‌های زیر در خط فرمان:
> npm i -g typescript eslint tslint eslint-plugin-react-hooks jshint babel-eslint eslint-plugin-react eslint-plugin-mocha

 به ازای هر خاصیت props استفاده شده‌ی در کامپوننت صفحه بندی، اخطاری را مانند «'currentPage' is missing in props validation eslint(react/prop-types)» مشاهده خواهید کرد:



که عنوان می‌کند props validation این خاصیت استفاده شده، فراموش شده‌است.
در نگارش‌های قبلی React، امکانات بررسی نوع‌های ارسالی به کامپوننت‌ها، جزئی از بسته‌ی اصلی آن بود؛ اما از نگارش 15 به بعد، به بسته‌ی مستقلی منتقل شده‌است که باید به صورت جداگانه‌ای در ریشه‌ی پروژه نصب شود:
> npm i prop-types --save

البته اگر TypeScript را بر روی سیستم خود نصب کرده باشید، دیگر نیازی به نصب بسته‌ی npm فوق را ندارید و prop-types، جزئی از آن است که عموما در یک چنین مسیری قرار دارد و برای کار کردن با آن، تنها ذکر import مرتبط با PropType در ماژول‌های برنامه کافی بوده و برنامه در این حالت بدون مشکل کامپایل می‌شود:
 C:/Users/{username}/AppData/Local/Microsoft/TypeScript/3.6/node_modules/@types/prop-types/index

اکنون در ابتدای فایل کامپوننت صفحه بندی، تعریف زیر را اضافه می‌کنیم:
 import PropTypes from "prop-types";
سپس در انتهای این فایل، اعتبارسنجی props آن‌را تعریف خواهیم کرد:
Pagination.propTypes = {
  itemsCount: PropTypes.number.isRequired,
  pageSize: PropTypes.number.isRequired,
  currentPage: PropTypes.number.isRequired,
  onPageChange: PropTypes.func.isRequired
};

export default Pagination;
همانطور که مشاهده می‌کنید، در اینجا خاصیت جدید propTypes (دقیقا با همین نگارش؛ در غیراینصورت بررسی نوع‌ها کار نخواهد کرد)، به تعریف کلاس Pagination اضافه شده‌است (پس از تعریف کلاس کامپوننت به صورت مستقل اضافه می‌شود).
سپس مقدار این خاصیت جدید را به شیءای تنظیم می‌کنیم که نام خواص آن، دقیقا همان نام خواص و رویدادهای props استفاده شده‌ی در این کامپوننت است. در ادامه توسط PropTypes ای که در ابتدای ماژول import می‌شود، کار تعریف نوع این خواص و اجباری بودن آن‌ها را می‌توان مشخص کرد که برای مثال در اینجا سه خاصیت تعریف شده از نوع عددی و اجباری بوده و onPageChange، از نوع func است.
پس از اضافه کردن Pagination.propTypes و مقدار دهی آن، خطاهای eslint ای که در تصویر فوق مشاهده کردید، برطرف می‌شوند. همچنین اگر فراخوان کامپوننت Pagination مثلا بجای itemsCount یک رشته‌ی فرضی را وارد کند (برای آزمایش آن در کامپوننت movies، در تعریف المان Pagination، مقدار itemsCount را یک رشته وارد کنید)، چنین خطایی در مرورگر ظاهر خواهد شد که عنوان می‌کند itemsCount یک عدد را می‌پذیرد و نوع ورودی آن اشتباه است:

البته این خطا فقط در حالت development مشاهده می‌شود و در حالت توزیع برنامه، خیر.

بنابراین تعریف propTypes یک best practice ایجاد کامپوننت‌های React است که نه فقط بررسی نوع‌ها را فعال می‌کند، بلکه می‌تواند به عنوان مستندات آن نیز در جهت تعیین props مورد نیاز، همچنین نوع و اجباری بودن آن‌ها، اطلاعات کاملی را ارائه کند.


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید: sample-11.zip
مطالب
آشنایی با الگوی Adapter
  قبل از آشنایی با الگوی Adapter،ابتدا با تعریف الگوهای ساختاری آشنا می‌شویم که به شرح ذیل می‌باشد:

الگوهای ساختاری (Structural Patterns):
    از الگوهای ساختاری برای ترکیب کلاسها و اشیاء (Objects)،در جهت ایجاد ساختارهای بزرگتر استفاده می‌شود.به بیان ساده‌تر الگوهای ساختاری با ترکیب کلاسها و آبجکتها،قابلیت‌های کلاسهای غیر مرتبط را در قالب یک Interface(منظور ظاهر) در اختیار Client (منظور کلاس یا متد استفاده کننده می‌باشد) قرار می‌دهند.الگوهای ساختاری با استفاده از ارث بری به ترکیب Interfaceها پرداخته و آنها را پیاده سازی می‌نمایند.
استفاده از الگوهای ساختاری برای توسعه کتابخانه هایی (Library) که مستقل از یکدیگر می‌باشند،اما در کنار هم مورد استفاده قرار می‌گیرند،بسیار مفید است.

در ادامه به الگوی Adapter که یکی از الگوهای ساختاری است،می پردازیم.الگوی  Adapter انواع مختلفی دارد که فهرست آنها به شرح ذیل می‌باشد:
1- Pluggable  Adapter - 4 Two way  Adapter- 3 Object Adapter - 2 Class Adapter

در این مقاله Class Adapter و Object Adapter را مورد بررسی قرار می‌دهیم و اگر عمری باقی باشد در مقاله بعدی Two-way Adapter و Pluggable Adapter را بررسی می‌کنیم.
قبل از پرداختن به هر یک از Adapter‌ها با یکسری واژه آشنا می‌شویم،که در سرتاسر مقاله ممکن است از آنها استفاده شود.
Interface: منظور از Interface در اینجا، ظاهر یا امکاناتی است که یک کلاس می‌تواند ارائه دهد.
Client: منظور متد یا کلاسی است که از Interface مورد انتظار،استفاده می‌نمایید.

Intent (هدف)
     هدف از ارائه الگوی Adapter ،تبدیل Interface یک Class به Interface ی که مورد انتظار Client است، می‌باشد.در واقع الگوی Adapter روشی است که بوسیله آن می‌توان کلاسهای با Interface متفاوت را در یک سیستم کنار یکدیگر مورد استفاده قرار داد. به بیان ساده‌تر هرگاه بخواهیم از کلاسهای ناهمگون یا نامنطبق (کلاسهای غیر مرتبط) در یک سیستم استفاده کنیم،راه حل مناسب استفاده از الگوی Adapter می‌باشد.

Adapter را به عنوان Wrapper می‌شناسند.الگوی Adapter از سه Component مهم تشکیل شده است،که عبارتند از: Target،Adapter و Adaptee. 
Target:کلاس یا Interface ی است که توسط Client مورد استفاده قرار می‌گیرد، و Client از طریق آن درخواستهای خود را بیان می‌کند. در واقع Functionality موجود در کلاس Target به جهت پاسخگویی به درخواست‌های Client فراهم گردیده است.
Adaptee: کلاسی است، دارای قابلیتهای مورد نیاز Client بطوریکه Interface اش با Interface مورد انتظار Client (یعنی Target)سازگار نیست. و Client برای استفاده از امکانات کلاس Adaptee و سازگاری با Interface مورد انتظارش نیاز به یک Wrapper همانند کلاسAdapter دارد.
Adapter: کلاسی است که قابلیتها و امکانات کلاس Adaptee را با Interface مورد انتظار Client یعنی Target سازگار می‌کند، تا Client بتواند از امکانات کلاس Adaptee جهت رفع نیاز‌های خود استفاده نماید. به بیان ساده‌تر Adapter کلاسی هست که برای اتصال دو کلاس نامتجانس (منظور دو کلاسی که هم جنس نمی‌باشند یا از نظر Interface بطور کامل با یکدیگر غیر مرتبط هستند) مورد استفاده قرار می‌گیرد.

در ادامه به بررسی اولین الگوی Adapter یعنی Class Adapter می‌پردازیم:
Class Adapter: 
در این روش کلاس Adapter از ارث بری چند گانه استفاده می‌کند و Interface مرتبط به Adaptee را به Interface مرتبط به Target سازگار می‌نماید.
برای درک تعریف بالا مثالی را بررسی می‌کنیم، در ابتدا شکل زیر را مشاهده نمایید:

در شکل ملاحظه می‌کنید، متد SpecificationRequet واقع در Adaptee می‌تواند نیاز Client را برطرف نماید، اما Client،چیزی را که مشاهده می‌کند اینترفیس Itarget می‌باشد، به عبارتی Client بطور مستقیم نمی‌تواند با Adaptee ارتباط برقرار کند، بنابراین اگر بخواهیم از طریق Itarget نیاز Client را برطرف نماییم، لازم است کلاسی بین Itarget و Adaptee به جهت تبادل اطلاعات ایجاد کنیم، که Adapter نامیده می‌شود. حال در روش Class Adapter، کلاس Adapter  جهت تبادل اطلاعات بین ITarget و Adaptee هر دو را در خود Implement می‌نماید، به عبارتی از هر دو مشتق (Inherit) می‌شود.
در ادامه شکل بالا را بصورت کد پیاده سازی می‌نماییم.
class Adaptee
    {
        public void SpecificationRequest()
        {
            Console.WriteLine("SpecificationRequest() is called");
        }
    }
interface ITarget
    {
        void Request();

    }
class Adapter:Adaptee, ITarget
    {
        public void Request()
        {
            SpecificationRequest();
        }
    }
class MainApp
{
    static void Main()
    {
        ITarget target = new Adapter();
        target.Request();

        Console.ReadKey();
    }
}
سادگی کد، روش Class Adapter را قابل درک می‌نماید،نکته مهم در کد بالا،متد Request در کلاس Adapter و نحوه فراخوانی متد SpecificationRequest در آن می‌باشد.
شکل زیر که از سایت Wikipedia گرفته شده است،به خوبی نحوه فراخوانی  را مشخص می‌نماید:


روش Object Adapter:
می دانیم در زبان برنامه نویسی #C هر کلاس فقط می‌تواند از یک کلاس دیگر Inherit شود، به طوری که هر کلاس نمی‌تواند بیش از یک کلاس Parent داشته باشد، بنابراین اگر Client شما بخواهد از امکانات و قابلیت‌های چندین کلاس Adaptee استفاده نماید، روش Class Adapter نمی‌تواند پاسخگوی نیازتان باشد، بلکه می‌بایست از روش Object Adapter استفاده نمایید.
شکل زیر بیانگر روش Object Adapter می‌باشد:

همانطور که در شکل ملاحظه می‌کنید، در این روش کلاس Adapter به جای Inherit نمودن از کلاس Adaptee، آبجکتی از کلاس Adaptee را در خود ایجاد می‌نماید، بنابراین با این روش شما می‌توانید به چندین Adaptee از طریق کلاس Adapter دسترسی داشته باشید.
پیاده سازی کدی شکل بالا به شرح ذیل می‌باشد:
class Adaptee
    {
        public void SpecificRequest()
        {
            MessageBox.Show("Called SpecificRequest()");
        }
    }
interface ITarget
    {
        void Request();

    }
class Adapter: ITarget
    {
        private Adaptee _adptee = new Adaptee();

        public void Request()
        {
            _adptee.SpecificationRequest();
        }
    }
class MainApp
{
    static void Main()
    {
        ITarget target = new Adapter();
        target.Request();

        Console.ReadKey();
    }
}
برای درک تفاوت Class Adapter و Object Adapter ، پیاده سازی کلاس Adapter را مشاهده نمایید، که در کد بالا به جای Inherit نمودن از کلاس Adaptee ، آبجکت آن را ایجاد نمودیم. واضح است که Object Adapter انعطاف پذیرتر نسبت به Class Adapter می‌باشد.
امیدوارم مطلب فوق مفید واقع شود
مطالب
ارتقاء به ASP.NET Core 1.0 - قسمت 7 - کار با فایل‌های config
یکی دیگر از تغییرات ASP.NET Core با نگارش‌های قبلی آن، تغییرات اساسی در مورد نحوه‌ی کار با تنظیمات برنامه و فایل‌های مرتبط با آن‌ها است. در ASP.NET Core می‌توانید:
- تنظیمات برنامه را از چندین منبع مختلف خوانده و آن‌ها را یکی کنید.
- تنظیمات را بر اساس تنظیمات مختلف محیطی برنامه، بارگذاری کنید.
- امکان نگاشت اطلاعات خوانده شده‌ی از فایل‌های کانفیگ به کلاس‌ها پیش بینی شده‌است.
- امکان بارگذاری مجدد فایل‌های کانفیگ درصورت تغییر، بدون ری‌استارت کل برنامه وجود دارد.
- امکان تزریق وابستگی‌های تنظیمات برنامه، به قسمت‌های مختلف آن پیش بینی شده‌است.


نصب پیشنیاز خواندن تنظیمات برنامه از یک فایل JSON

برای شروع به کار با خواندن تنظیمات برنامه در ASP.NET Core، نیاز است ابتدا بسته‌ی نیوگت Microsoft.Extensions.Configuration.Json را نصب کنیم.
برای این منظور بر روی گره references کلیک راست کرده و گزینه‌ی manage nuget packages را انتخاب کنید. سپس در برگه‌ی browse آن Microsoft.Extensions.Configuration.Json را جستجو کرده و نصب نمائید:


البته همانطور که در تصویر مشاهده می‌کنید، اگر صرفا Microsoft.Extensions.Configuration را جستجو کنید (بدون ذکر JSON)، بسته‌های مرتبط با خواندن فایل‌های کانفیگ از نوع XML و یا حتی INI را هم خواهید یافت.
انجام این مراحل معادل هستند با افزودن یک سطر ذیل به فایل project.json برنامه:
{
    "dependencies": {
         //same as before  
         "Microsoft.Extensions.Configuration.Json": "1.0.0"
    },

 
افزودن یک فایل کانفیگ JSON دلخواه

بر روی پروژه کلیک راست کرده و از طریق منوی add->new item یک فایل خالی جدید را به نام appsettings.json ایجاد کنید (نام این فایل دلخواه است)؛ با این محتوا:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    }
}
در نگارش‌های پیشین ASP.NET که از web.config برای تعریف تنظیمات برنامه استفاده می‌شد، حالت پیش فرض ذکر تنظیمات برنامه می‌توانست تنها یک سطحی و با ساختار ذیل باشد (البته امکان کدنویسی و نوشتن مداخل سفارشی هم وجود داشت؛ ولی حالت پیش فرض appSettings، تنها key/valueهای یک سطحی هستند):
<appSettings>
   <add key="Logging-IncludeScopes" value="false" />
   <add key="Logging-Level-Default" value="verbose" />
   <add key="Logging-Level-System" value="Information" />
   <add key="Logging-Level-Microsoft" value="Information" />
</appSettings>
اما اکنون یک فایل JSON را با هر تعداد سطح مورد نیاز می‌توان تعریف و استفاده کرد و برای اینکار نیازی به نوشتن کدهای سفارشی تعریف مداخل خاص، وجود ندارد.
در فایل JSON فوق، نمونه‌ای از key/valueها، آرایه‌ها و اطلاعات چندین سطحی را مشاهده می‌کنید.


خواندن فایل تنظیمات appsettings.json در برنامه

پس از نصب پیشنیاز خواندن فایل‌های کانفیگ از نوع JSON، به فایل آغازین برنامه مراجعه کرده و سازنده‌ی جدیدی را به آن اضافه کنید:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddJsonFile("appsettings.json");
        Configuration = builder.Build();
    }
در اینجا نحوه‌ی خواندن فایل کانفیگ جدید appsettings.json را مشاهده می‌کنید. چند نکته در اینجا حائز اهمیت هستند:
الف) این خواندن، در سازنده‌ی کلاس آغازین برنامه و پیش از تمام تنظیمات دیگر باید انجام شود.
ب) جهت در معرض دید قرار دادن اطلاعات خوانده شده، آن‌را به یک خاصیت عمومی انتساب داده‌ایم.
ج) متد SetBasePath جهت مشخص کردن محل یافتن فایل appsettings.json ذکر شده‌است. این اطلاعات را می‌توان از سرویس توکار IHostingEnvironment و خاصیت ContentRootPath آن دریافت کرد. همانطور که ملاحظه می‌کنید، این تزریق وابستگی نیز به صورت خودکار توسط ASP.NET Core مدیریت می‌شود.


دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot

تا اینجا موفق شدیم تا تنظیمات خوانده شده را به خاصیت عمومی Configuration از نوع IConfigurationRoot انتساب دهیم. اما ساختار ذخیره شده‌ی در این اینترفیس به چه صورتی است؟


همانطور که مشاهده می‌کنید، هر سطح از سطح قبلی آن با : جدا شده‌است. همچنین اعضای آرایه، دارای ایندکس‌های 0: و 1: و 2: هستند. بنابراین برای خواندن این اطلاعات می‌توان نوشت:
var key1 = Configuration["Key1"];
var user1 = Configuration["Auth:Users:0"];
var authUsers = Configuration.GetSection("Auth:Users").GetChildren().Select(x => x.Value).ToArray();
var loggingIncludeScopes = Configuration["Logging:IncludeScopes"];
var loggingLoggingLogLevelDefault = Configuration["Logging:LogLevel:Default"];
خاصیت Configuration نیز در نهایت بر اساس key/valueها کار می‌کند و این keyها اگر چند سطحی بودند، با : از هم جدا می‌شوند و اگر نیاز به دسترسی اعضای خاصی از آرایه‌ها وجود داشت می‌توان آن ایندکس خاص را در انتهای زنجیره ذکر کرد. همچنین در اینجا نحوه‌ی استخراج تمام اعضای یک آرایه را نیز مشاهده می‌کنید.

یک نکته: خاصیت Configuration، دارای متد GetValue نیز هست که توسط آن می‌توان نوع مقدار دریافتی و یا حتی مقدار پیش فرضی را در صورت عدم وجود این key، مشخص کرد:
 var val = Configuration.GetValue<int>("key-name", defaultValue: 10);
در متد GetValue، آرگومان جنریک آن، یک کلاس را نیز می‌پذیرد. یعنی می‌توان خواص تو در توی مشخص شده‌ی با : را به یک کلاس نیز نگاشت کرد. در اینجا مقدار کلید معرفی شده، اولین سطحی خواهد بود که باید این اطلاعات از آن استخراج و نگاشت شوند.


سرویس IConfigurationRoot قابل تزریق است

در قسمت قبل، سرویس‌ها و تزریق وابستگی‌ها را بررسی کردیم. نکته‌ی جالبی را که می‌توان به آن اضافه کرد، قابلیت تزریق خاصیت عمومی
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
به تمام قسمت‌های برنامه است. برای نمونه در همان مثال قسمت قبل، قصد داریم تنظیمات برنامه را در لایه سرویس آن خوانده و مورد استفاده قرار دهیم. برای اینکار باید مراحل ذیل طی شوند:
الف) اعلام موجودیت IConfigurationRoot به IoC Container
اگر از استراکچرمپ استفاده می‌کنید، باید مشخص کنید، زمانیکه IConfigurationRoot درخواست شد، آن‌را چگونه باید از خاصیت مرتبط با آن دریافت کند:
var container = new Container();
container.Configure(config =>
{
    config.For<IConfigurationRoot>().Singleton().Use(() => Configuration);
و یا اگر از همان IoC Container توکار ASP.NET Core استفاده می‌کنید، روش انجام این‌کار در متد ConfigureServices به صورت زیر است:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IConfigurationRoot>(provider => { return Configuration; });
طول عمر آن هم singleton مشخص شده‌است تا تنها یکبار وهله سازی و سپس کش شود (مناسب برای کار با تنظیمات سراسری برنامه).

ب) فایل project.json کتابخانه‌ی Core1RtmEmptyTest.Services را گشوده و وابستگی Microsoft.Extensions.Configuration.Abstractions را به آن اضافه کنید:
{ 
    "dependencies": {
        //same as before 
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0"
    }
این وابستگی امکان دسترسی به اینترفیس IConfigurationRoot را در اسمبلی‌های دیگر میسر می‌کند.

ج) سپس فایل MessagesService.cs را گشوده و این اینترفیس را به سازنده‌ی سرویس MessagesService تزریق می‌کنیم:
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
 
    public MessagesService(IConfigurationRoot configurationRoot)
    {
        _configurationRoot = configurationRoot;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        return $"DNT {key1}";
    }
}
در ادامه، نحوه‌ی استفاده‌ی از آن، همانند نکاتی است که در قسمت «دسترسی به تنظیمات خوانده شده توسط اینترفیس IConfigurationRoot» عنوان شد.
اکنون اگر برنامه را اجرا کنید، با توجه به اینکه میان افزار Run از این سرویس سفارشی استفاده می‌کند:
public void Configure(
    IApplicationBuilder app,
    IHostingEnvironment env,
    IMessagesService messagesService)
{ 
    app.Run(async context =>
    {
        var siteName = messagesService.GetSiteName();
        await context.Response.WriteAsync($"Hello {siteName}");
    });
}
چنین خروجی را خواهیم داشت:



خواندن تنظیمات از حافظه

الزاما نیازی به استفاده از فایل‌های JSON و یا XML در اینجا وجود ندارد. ابتدایی‌ترین حالت کار با بسته‌ی Microsoft.Extensions.Configuration، متد AddInMemoryCollection آن است که در اینجا می‌توان لیستی از key/value‌ها را ذکر کرد:
var builder = new ConfigurationBuilder()
                    .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                });
 و نحوه‌ی کار با آن نیز همانند قبل است:
 var theValue = Configuration["the-key"];


امکان بازنویسی تنظیمات انجام شده، بسته به شرایط محیطی

در اینجا محدود به یک فایل JSON و یک فایل تنظیمات برنامه، نیستیم. برای کار با ConfigurationBuilder می‌توان از Fluent interface آن استفاده کرد و به هر تعدادی که نیاز بود، متدهای خواندن از فایل‌های کانفیگ دیگر را اضافه کرد:
public class Startup
{
    public IConfigurationRoot Configuration { set; get; }
 
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
                            .SetBasePath(env.ContentRootPath)
                            .AddInMemoryCollection(new[]
                                {
                                    new KeyValuePair<string,string>("the-key", "the-value"),
                                })
                            .AddJsonFile("appsettings.json", reloadOnChange: true, optional: false)
                            .AddJsonFile($"appsettings.{env}.json", optional: true);
        Configuration = builder.Build();
    }
و نکته‌ی مهم اینجا است که تنظیمات فایل دوم، تنظیمات مشابه فایل اول را بازنویسی می‌کند.
برای مثال در اینجا آخرین AddJsonFile تعریف شده، بنابر متغیر محیطی فعلی به appsettings.development.json تفسیر شده و در صورت وجود این فایل (با توجه به optional بودن آن) اطلاعات آن دریافت گردیده و اطلاعات مشابه فایل appsettings.json قبلی را بازنویسی می‌کند.


امکان دسترسی به متغیرهای محیطی سیستم عامل

در انتهای زنجیره‌ی ConfigurationBuilder می‌توان متد AddEnvironmentVariables را نیز ذکر کرد:
 var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
این متد سبب می‌شود تا تمام اطلاعات قسمت Environment سیستم عامل، به مجموعه‌ی تنظیمات جاری اضافه شوند (در صورت نیاز) که نمونه‌ای از آن‌را در تصویر ذیل مشاهده می‌کنید:



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

کار کردن با key/valueهای رشته‌ای، هرچند روش پایه‌ای استفاده‌ی از تنظیمات برنامه است، اما آنچنان refactoring friendly نیست. در ASP.NET Core امکان تعریف تنظیمات strongly typed نیز پیش بینی شده‌است. برای این منظور باید مراحل زیر طی شوند:
به عنوان نمونه تنظیمات فرضی smtp ذیل را به انتهای فایل appsettings.json اضافه کنید:
{
    "Key1": "Value1",
    "Auth": {
        "Users": [ "Test1", "Test2", "Test3" ]
    },
    "Logging": {
        "IncludeScopes": false,
        "LogLevel": {
            "Default": "Debug",
            "System": "Information",
            "Microsoft": "Information"
        }
    },
    "Smtp": {
        "Server": "0.0.0.1",
        "User": "user@company.com",
        "Pass": "123456789",
        "Port": "25"
    }
}
مثال جاری که بر اساس ASP.NET Core Web Application و با قالب خالی آن ایجاد شده‌است، دارای نام فرضی Core1RtmEmptyTest است. در همین پروژه بر روی پوشه‌ی src کلیک راست کرده و گزینه‌ی Add new project را انتخاب کنید و سپس یک پروژه‌ی جدید از نوع NET Core -> Class library. را به آن با نام Core1RtmEmptyTest.ViewModels اضافه کنید (تصویر ذیل).


در این کتابخانه‌ی جدید که محل نگهداری ViewModelهای برنامه خواهد بود، کلاس معادل قسمت smtp فایل config فوق را اضافه کنید:
namespace Core1RtmEmptyTest.ViewModels
{
    public class SmtpConfig
    {
        public string Server { get; set; }
        public string User { get; set; }
        public string Pass { get; set; }
        public int Port { get; set; }
    }
}
از این جهت این کلاس را در یک library جداگانه قرار داده‌ایم تا بتوان از آن در لایه‌ی سرویس و همچنین خود برنامه استفاده کرد. اگر این کلاس را در برنامه‌ی اصلی قرار می‌دادیم، امکان دسترسی به آن در لایه‌ی سرویس میسر نمی‌شد.
سپس به پروژه‌ی Core1RtmEmptyTest مراجعه کرده و بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید، تا اسمبلی آن‌را بتوان در پروژه‌ی جاری استفاده کرد.
انجام اینکار معادل است با افزودن یک سطر ذیل به فایل project.json پروژه:
{
    "dependencies": {
        // same as before        
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*"
    },
اکنون با فرض وجود تنظیمات خواندن فایل appsettings.json در سازنده‌ی کلاس آغازین برنامه، نیاز است بسته‌ی نیوگت Microsoft.Extensions.Configuration.Binder را نصب کنید:


و سپس در کلاس آغازین برنامه و متد ConfigureServices آن، نحوه‌ی نگاشت قسمت Smtp فایل کانفیگ را مشخص کنید:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
   services.Configure<SmtpConfig>(options => Configuration.GetSection("Smtp").Bind(options));
در اینجا مشخص شده‌است که کار وهله سازی کلاس SmtpConfig بر اساس اطلاعات قسمت smtp فایل کانفیگ تامین می‌شود. متغیر Configuration ایی که در اینجا استفاده شده‌است همان خاصیت عمومی public IConfigurationRoot Configuration کلاس آغازین برنامه است.

سپس برای استفاده از این تنظیمات strongly typed (برای نمونه در لایه سرویس برنامه)، ابتدا ارجاعی را به پروژه‌ی Core1RtmEmptyTest.ViewModels به لایه‌ی سرویس برنامه اضافه می‌کنیم (بر روی گره references آن کلیک راست کنید. در اینجا گزینه‌ی add reference را انتخاب کرده و سپس Core1RtmEmptyTest.ViewModels را انتخاب کنید).
در ادامه نیاز است بسته‌ی نیوگت جدیدی را به نام Microsoft.Extensions.Options به لایه‌ی سرویس برنامه اضافه کنیم. به این ترتیب قسمت وابستگی‌های فایل project.json این لایه چنین شکلی را پیدا می‌کند:
    "dependencies": {
        "Core1RtmEmptyTest.ViewModels": "1.0.0-*",
        "Microsoft.Extensions.Configuration.Abstractions": "1.0.0",
        "Microsoft.Extensions.Options": "1.0.0",
        "NETStandard.Library": "1.6.0"
    }
پس از ذخیره سازی این کلاس و بازیابی خودکار وابستگی‌های آن، اکنون برای دسترسی به این تنظیم باید از اینترفیس ویژه‌ی IOptions استفاده کرد (به همین جهت بسته‌ی جدید نیوگت Microsoft.Extensions.Options را نصب کردیم):
public interface IMessagesService
{
    string GetSiteName();
}
 
public class MessagesService : IMessagesService
{
    private readonly IConfigurationRoot _configurationRoot;
    private readonly IOptions<SmtpConfig> _settings;
 
    public MessagesService(IConfigurationRoot configurationRoot, IOptions<SmtpConfig> settings)
    {
        _configurationRoot = configurationRoot;
        _settings = settings;
    }
 
    public string GetSiteName()
    {
        var key1 = _configurationRoot["Key1"];
        var server = _settings.Value.Server;
        return $"DNT {key1} - {server}";
    }
}
همانطور که ملاحظه می‌کنید <IOptions<SmtpConfig به سازنده‌ی کلاس تزریق شده‌است و سپس از طریق خاصیت Value آن می‌توان به تمام اطلاعات کلاس SmtpConfig به شکل strongly typed دسترسی یافت.

اکنون اگر برنامه را جرا کنید، این خروجی را می‌توان مشاهده کرد (که در آن آدرس Server دریافت شده‌ی از فایل کانفیگ نیز مشخص است):


البته همانطور که در قسمت قبل نیز عنوان شد، این تزریق وابستگی‌ها در تمام قسمت‌های برنامه کار می‌کند. برای مثال در کنترلرها هم می‌توان <IOptions<SmtpConfig را به همین نحو تزریق کرد.


نحوه‌ی واکنش به تغییرات فایل‌های کانفیگ

در نگارش‌های قبلی ASP.NET، هر تغییری در فایل web.config، سبب ری‌استارت شدن کل برنامه می‌شد که این مساله نیز خود سبب بروز مشکلات زیادی مانند از دست رفتن سشن تمام کاربران می‌شد.
در ASP.NET Core، برنامه‌ی وب ما دیگر متکی به فایل web.config نبوده و همچنین می‌توان چندین و چند نوع فایل config داشت. به علاوه در اینجا متدهای مرتبط معرفی فایل‌های کانفیگ دارای پارامتر مخصوص reloadOnChange نیز هستند:
 .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
این پارامتر در صورت true بودن، به صورت خودکار سبب بارگذاری مجدد اطلاعات فایل کانفیگ می‌شود (بدون ری‌استارت کل برنامه).