مطالب
آشنایی با ساختار IIS قسمت پنجم
در مطالب قبلی در مورد ماژولار بودن IIS زیاد صحبت کردیم، ولی اجازه بدهید این مورد را به صورت کاربردی‌تر و موشکافانه‌تر بررسی کنیم. برای اینکه به مشکلی در طول این سری از مطالب برنخورید، IIS را به صورت کامل یعنی full feature نصب نمایید. از بخش control panel>programs & features>Turn Windows features on or off اقدام نمایید و هرچه زیر مجموعه Internet information service هست را برگزینید. در صورتی که از نسخه‌های ویندوز سرور 2008 استفاده می‌کنید از طریق server manager>roles>web server اقدام نمایید.
برای نصب یک ماژول باید دو مرحله را انجام داد:
  1. نصب ماژول
  2. فعال سازی ماژول
نکته ای که در مورد ماژول‌های native وجود دارد این هست که این ماژول‌ها دسترسی بدون محدودیتی به منابع سروری دارند و از این رو حتما باید این نکته را دقت کنید که ماژول native شما از یک منبع مورد اعتماد دریافت شده باشد.

نصب یک native module
برای نصب می‌توانید یکی از سه راه زیر را استفاده کنید:
  1. ویرایش دستی فایل کانفیگ و از نسخه IIS7.5 به بعد هم می‌توانید از configuration editor هم استفاده کنید.
  2. استفاده از محیط گرافیکی IIS
  3. استفاده از خط فرمان با دستور Appcmd
مزیت روش دستی این هست که شما دقیقا می‌دانید در پشت صحنه چه اتفاقی می‌افتد و نتیجه هر کدام از این سه روش، اضافه شدن یک مدخل ورودی به تگ <globalmodules> است. برای اعمال تغییرات، مسیر زیر را بروید:
%windir%\system32\inetsrv\config\applicationhost.config
کسی که نیاز به دسترسی به این مسیر و انجام تغییرات دارد باید در بالاترین سطح مدیریتی سرور باشد.
اگر فایل را باز کنید و تگ globalmodule را پیدا کنید متوجه می‌شوید که تمامی ماژول‌ها در این قسمت معرفی شده‌اند و برای خود یک مدخل ورودی یا همان تگ add را دارند که در آن مسیر فایل dll هم ذکر شده است:
<globalModules> 
 
 <addname="DefaultDocumentModule"image="%windir%\system32\inetsrv\defdoc.dll"/> 
 
 <addname="DirectoryListingModule"image="%windir%\system32\inetsrv\dirlist.dll"/> 

<add name="StaticFileModule"image="%windir%\system32\inetsrv\static.dll"/>
...

 </globalModules>

برای حذف یا جایگزینی یک ماژول به راحتی می‌توانید مدخل ورودی یک ماژول را به صورت دستی حذف نمایید و برای جایگزینی هم بعد از حذف، ماژول خود را معرفی کنید. ولی توجه داشته باشید که این حذف به معنی حذف این ماژول از تمامی اپلیکیشن‌های موجود بر روی IIS هست و سپس اضافه کردن یک ماژول به این بخش. همچنین اگر قصد شما فقط حذف یک ماژول از روی یکی از اپلیکشن‌ها باشد باید از طریق فایل کانفیگ سایت از مسیر تگ‌های <system.webserver><modules> و با استفاده از تگ‌های add و remove به معرفی یک ماژول مختص این اپلیکیشن و یا حذف یک ماژول خاص اقدام نمایید.

PreConditions
این ویژگی می‌تواند در خط معرفی ماژول، مورد استفاده قرار بگیرد. اگر به فایل نگاه کنید می‌بینید که در بعضی خطوط این ویژگی ذکر شده است. تعریف این ویژگی به هسته IIS می‌گوید که این ماژول در چه مواردی و به چه شیوه ای باید به کار گرفته شود.
 <add name="ManagedEngine64" image="%windir%\Microsoft.NET\Framework64\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness64" /> 
 
 <add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness32" /> 
 
<add name="ManagedEngineV4.0_32bit" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness32" /> 
 
<add name="ManagedEngineV4.0_64bit" image="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness64" />

مقادیری که precondition میتواند بگیر شامل موارد زیر هستند:
bitness
این گزینه به دو صورت bitness32 و bitness64 یافت می‌شود. امروزه پردازنده‌های 64 بیتی بسیار متداول شده اند و بسیاری از تولید کنندگان دارند به سمت عرضه ابزارهای 64 بیتی رو می‌آورند و به زودی عرضه‌های 32 بیتی را متوقف می‌کنند و به سمت سیستم عامل‌های 64 بیت سوییچ خواند کرد ولی باز هم هنوز برنامه‌های 32 بیتی زیادی هستند که مورد استفاده قرار می‌گیرند و نمی‌توان آن‌ها را نادیده گرفت. برای همین ویندوزهای 64 بیتی مایکروسافت در کنار محیط 64 بیتی‌شان از یک محیط 32 بیت به اسم WOW64 استفاده می‌کنند. در این حالت این امتیاز به شما داده می‌شود که از پروسه‌های کارگر 32 بیتی در کنار پروسه‌های کارگر 64 بیتی استفاده کنید و PreCondition به bitness32 یا bitness64 تنظیم می‌شود تا از صحت بارگزاری dll در یک محیط درست مطمئن شود. در صورتی که این خصوصیت ذکر نشود یک هندلر 32 بیتی و 64 بیتی و یک module map اجرا می‌شود.

Runtime version
اگر ماژول خاصی برای اجرا به ورژن خاصی از net framwork. نیاز دارد، این ویژگی ذکر می‌شود. در صورتی که ماژولی قصد اجرای بر روی فریم ورک اشتباهی داشته باشد سبب خطا خواهد شد.

Mana gedHandler 
با معرفی IIS7 ما با یک مدل توسعه پذیر روبرو شدیم و می‌توانستیم ماژول‌ها و هندلرهای خود را بنویسیم و مستقیما در Pipeline قرار دهیم ولی سوییچ کردن بین دو بخش کدهای مدیریت شده و native یک عمل سنگین برای سیستم به شمار می‌آید و به منظور کاهش این بار گزینه managedhandler قرار داده شده است تا تعیین کند مواقعی که درخواست نیازی به این ماژول ندارد، این ماژول اجرا نگردد. به عنوان مثال فایل‌های ایستا چون jpg یا html و... شامل این ماژول نخواهند شد. واضح‌ترین مثال در این زمینه Forms Authentication می‌باشد و موقعی اجرا می‌شوند که درخواست فایل‌های aspx شده باشد و اگر یک فایل html را درخواست کنید این ماژول امنیتی روی آن اثری ندارد و عملیات شناسایی هویت روی آن اجرا نمی‌شود و اگر می‌خواهید روی همه فایل‌ها، این عملیات شناسایی انجام شود باید خصوصیت "precondition="managedhandler حذف شود.
در صورتی که تگ module را به صورت زیر بنویسید تمامی ماژول‌ها برای تمامی درخواست‌ها اجرا خواهد شد، صرف نظر از اینکه آیا ماژولی به صورت "precondition="managedmodule مقداردهی شده است یا خیر.
<modules runAllManagedModulesForAllRequests="true"/>
 
The Mode Precondition 
تا به الان گفتیم که چگونه میتوانیم یک ماژول و یا هندلر مدیریت شده‌ای را به Pipeline اضافه کنیم؛ ولی IIS7 به بالا نیاز دارد که تا پروسه‌های کارگر را به روشی خاص به این منظور اجرا کند و فریم ورک دات نت را برای اجرای آن‌ها بارگزاری کند. همچنین به اجرای ماژولی به اسم webengine.dll برای مدیریت  ماژولهای مدیریت شده نیازمند است و خود IIS در مورد کدهای مدیریت شده چیزی متوجه نمی‌شود. پس ما برای اینکه IIS را متوجه این موضوع نمائیم، باید Integrated mode را به آن معرفی کنیم.
در نسخه‌های قبلی IIS یک روش قدیمی برای کدهای مدیریت شده وجود داشت که از طریق اینترفیسی به نام ISAPI صورت می‌گرفت. در این حالت ASPNET_ISAPI.DLL مسئول این کار بود و اگر هنوز هم میخواهید از این dll در نسخه‌های جدیدتر IIS کمک بگیرید باید به جای معرفی classic mode ، integration mode را معرفی کنید.
با برابر کردن precondtion به مقدار "intgretedmode" هندلر یا ماژول شما در یک پول با خصوصیت integrated بارگزاری خواهد شد و اگر مقدار آن "classicmode" باشد در یک پول بدون خاصیت integrated بارگزاری می‌شود.
تفاوت بین دو روش کلاسیک و مجتمع integrated بر سر این هست که در روش جدید، ماژول شما به عنوان یک پلاگین برای IIS دیده نمی‌شود و کد شما را جزئی از کامپوننت‌های خود به شمار می‌آورد. به صورت واضح‌تر در حالت کلاسیک موقعی که درخواستی وارد pipeline میشد ابتدا از کامپوننت‌ها و ماژول‌های داخلی خود IIS عبور داده میشد و بعد فایل ASPNET_ISAPI.DLL جهت پردازش کدهای مدیریت شده صدا زده میشد و با توجه به کدهای شما، بعضی مراحل تکرار میشد؛ مثلا اگر کد شما در مورد Authentication بود و بعد از گذر از مراحل auth داخل خود IIS و بقیه موارد دوباره نوبت کد شما و گذر از مراحل authentication بود. یعنی وجود دو pipeline؛ ولی در حالت مجتمع این دوبار انجام وظیفه از بین رفته است چرا که کدهای شما به طور مستقیم در pipeline قرار دارند و آن‌ها را جزئی از خود می‌داند، نه یک پلاگین که افزون بر فعالیت خودشان، اجرای کدهای شما رو هم بر دوش بکشند.
شکل زیر نمونه ای از حالت کلاسیک را نشان می‌دهد که در آن دو بار عمل auth دارد انجام میگیرد.
 



شکل زیر هم نمونه ای حالت مجتمع هست:

 
در کل امروزه دیگر استفاده از روش کلاسیک راهکار درستی نیست و این ویژگی تنها به عنوان یک سازگاری با نمونه کارهای قدیمی است.
تگ‌هایی که از خصوصت precondition استفاده می‌کنند به شرح زیر هستند:
  • ISAPI filters 
  • globalModules 
  • handlers 
  • modules 

در مورد بقیه تگ‌ها در آینده بیشتر بحث می‌کنیم. بهتر هست مطلب را با توضیح precondition جهت ممانعت از طولانی و طومار شدن در اینجا ببندیم و در قسمت آینده دیگر روش‌های نصب ماژولمان را دنبال کنیم.
اشتراک‌ها
اضافه شدن SHA1 به SQLite 3.17.0

The new sha1(X) function computes the SHA1 hash of the input X, or NULL if X is NULL, while the sha1_query(Y) function evalutes all queries in the SQL statements of Y and returns a hash of their results. 

اضافه شدن SHA1 به SQLite 3.17.0
مطالب
Feature Toggle
در بسیاری از پروژه‌های نرم افزاری ما ممکن است یک امکان (Feature) را برای بازه‌ی زمانی خاصی بنا به درخواست مشتری یا ضوابط خودمان نیاز داشته باشیم و در زمان دیگری یا برای مشتری دیگری نیاز نداشته باشیم و باید قابلیت مورد نظر غیر فعال باشد. یا حتی ممکن است قابلیتی را به تازگی افزوده باشیم، ولی در زمان اجرا خطایی داشته باشد و مجبور باشیم فورا آن را از دسترش خارج کنیم. به این فرایند در اصلاح Feature Toggle میگویند که البته نام‌های دیگری از جمله (feature switch, feature flag, feature flipper, conditional feature ) هم دارد. مارتین فاولر آن را این چنین تعریف میکند:
"Feature Toggling" is a set of patterns which can help a team to deliver new functionality to users rapidly but safely
"Feature Toggling" تکنیک قدرتمندی است که به ما این اجازه را میدهد تا رفتار سیستم را بدون تغییر کد عوض کنیم.
ساده‌ترین الگوی پیاده سازی Feature Toggling چیزی شبیه به نمونه زیر می‌باشد. یک اینترفیس که باید مشخصه یا متدی برای بررسی فعال بودن و نبودن داشته باشد.
 public interface IFeatureToggle {
   bool FeatureEnabled {get;}  
}
برای اینکه اصل قابل تنظیم بودن (Configurable) را هم رعایت کرده باشیم، بررسی فعال بودن کامپوننت را از طریق وب کانفیگ انجام میدهیم.
class ShowMessageToggle : IFeatureToggle  
 {   
    public bool FeatureEnabled {
     get{
           return  bool.Parse(ConfigurationManager.AppSettings["ShowMessageEnabled"]);      
        }
 }
و حالا کافی است در هر جایی که قصد استفاده از آن کلاس را داشته باشیم، فعال بودن و نبودنش را بررسی کنیم.
class Program
 {
 static void Main(string[] args)
   {
     var toggle = new ShowMessageToggle();
     if (toggle.FeatureEnabled)
     {
        Console.WriteLine("This feature is enabled")
     }
     else
     {  
         Console.WriteLine("This feature is disabled");            
     }
   }  
 }
مثال بالا ساده‌ترین نحوه‌ی استفاده از Feature Toggling بود. اما شبیه الگوی IOC که ابزارهای زیادی برای پیاده سازی آن عرضه شده است، برای این الگو هم ابزارهای جالبی تولید شده است که به‌راحتی این قابلیت را در پروژه‌های ما ایجاد و نگهداری میکند. لیستی از این ابزارها و پکیج‌ها را از اینجا میتوانید ببینید.
بطور مثال برای کار با FeatureToggle ابتدا آنرا با دستور زیر نصب میکنیم:
Install-Package FeatureToggle
سپس کلاس مورد نظر را از کلاس پایه SimpleFeatureToggle ارث بری میکنیم.
MyAwesomeFeature : SimpleFeatureToggle {}
در  فایل کانفیگ برنامه یک تنظیم جدید را با نام کلاس مذکور ایجاد میکنیم:
<add key="MyAwesomeFeature " value="true" />
حالا هرجای برنامه نیاز داشتید میتوانید فعال بودن و نبودن قابلیت‌های مختلف را بررسی کنید.
if (!myAwesomeFeature.FeatureEnabled)
{ // code to disable stuff (e.g. UI buttons, etc) }
شما به همین سادگی و سرعت، میتوانید قابلیت Feature Toggle را در پروژه‌هایتان راه اندازی کنید.

لیست منابع
 http://nugetmusthaves.com/Tag/toggle 
http://featureflags.io/dotnet-feature-flags/ 
http://martinfowler.com/articles/feature-toggles.html
مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در ASP.NET Core 2.0 بدون استفاده از سیستم Identity
ASP.NET Core 2.0 به همراه یک AuthenticationMiddleware است که یکی از قابلیت‌های این میان‌افزار، افزودن اطلاعات کاربر و یا همان HttpContext.User به یک کوکی رمزنگاری شده و سپس اعتبارسنجی این کوکی در درخواست‌های بعدی کاربر است. این مورد سبب خواهد شد تا بتوان بدون نیاز به پیاده سازی سیستم کامل ASP.NET Core Identity، یک سیستم اعتبارسنجی سبک، ساده و سفارشی را تدارک دید.



تعریف موجودیت‌های مورد نیاز جهت طراحی یک سیستم اعتبارسنجی

در اینجا کنترل کامل سیستم در اختیار ما است و در این حالت می‌توان طراحی تمام قسمت‌ها را از ابتدا و مطابق میل خود انجام داد. برای مثال سیستم اعتبارسنجی ساده‌ی ما، شامل جدول کاربران و نقش‌های آن‌ها خواهد بود و این دو با هم رابطه‌ی many-to-many دارند. به همین جهت جدول UserRole نیز در اینجا پیش بینی شده‌است.

جدول کاربران

    public class User
    {
        public User()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }

        public string Username { get; set; }

        public string Password { get; set; }

        public string DisplayName { get; set; }

        public bool IsActive { get; set; }

        public DateTimeOffset? LastLoggedIn { get; set; }

        /// <summary>
        /// every time the user changes his Password,
        /// or an admin changes his Roles or stat/IsActive,
        /// create a new `SerialNumber` GUID and store it in the DB.
        /// </summary>
        public string SerialNumber { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
در اینجا SerialNumber فیلدی است که با هر بار ویرایش اطلاعات کاربران باید از طرف برنامه به روز رسانی شود. از آن جهت غیرمعتبر سازی کوکی کاربر استفاده خواهیم کرد. برای مثال اگر خاصیت فعال بودن او تغییر کرد و یا نقش‌های او را تغییر دادیم، کاربر در همان لحظه باید logout شود. به همین جهت چنین فیلدی در اینجا در نظر گرفته شده‌است تا با بررسی آن بتوان وضعیت معتبر بودن کوکی او را تشخیص داد.

جدول نقش‌های کاربران

    public class Role
    {
        public Role()
        {
            UserRoles = new HashSet<UserRole>();
        }

        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<UserRole> UserRoles { get; set; }
    }
البته این سیستم ساده دارای یک سری نقش ثابت و مشخص است.
    public static class CustomRoles
    {
        public const string Admin = nameof(Admin);
        public const string User = nameof(User);
    }
که این نقش‌ها را در ابتدای کار برنامه به بانک اطلاعات اضافه خواهیم کرد.


جدول ارتباط نقش‌ها با کاربران و برعکس

    public class UserRole
    {
        public int UserId { get; set; }
        public int RoleId { get; set; }

        public virtual User User { get; set; }
        public virtual Role Role { get; set; }
    }
وجود این جدول در EF Core جهت تعریف یک رابطه‌ی many-to-many ضروری است.


تعریف Context برنامه و فعالسازی Migrations در EF Core 2.0

DbContext برنامه را به صورت ذیل در یک اسمبلی دیگر اضافه خواهیم کرد:
    public interface IUnitOfWork : IDisposable
    {
        DbSet<TEntity> Set<TEntity>() where TEntity : class;

        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = new CancellationToken());
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken());
    }

    public class ApplicationDbContext : DbContext, IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions options) : base(options)
        { }

        public virtual DbSet<User> Users { set; get; }
        public virtual DbSet<Role> Roles { set; get; }
        public virtual DbSet<UserRole> UserRoles { get; set; }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            // it should be placed here, otherwise it will rewrite the following settings!
            base.OnModelCreating(builder);

            // Custom application mappings
            builder.Entity<User>(entity =>
            {
                entity.Property(e => e.Username).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Username).IsUnique();
                entity.Property(e => e.Password).IsRequired();
                entity.Property(e => e.SerialNumber).HasMaxLength(450);
            });

            builder.Entity<Role>(entity =>
            {
                entity.Property(e => e.Name).HasMaxLength(450).IsRequired();
                entity.HasIndex(e => e.Name).IsUnique();
            });

            builder.Entity<UserRole>(entity =>
            {
                entity.HasKey(e => new { e.UserId, e.RoleId });
                entity.HasIndex(e => e.UserId);
                entity.HasIndex(e => e.RoleId);
                entity.Property(e => e.UserId);
                entity.Property(e => e.RoleId);
                entity.HasOne(d => d.Role).WithMany(p => p.UserRoles).HasForeignKey(d => d.RoleId);
                entity.HasOne(d => d.User).WithMany(p => p.UserRoles).HasForeignKey(d => d.UserId);
            });
        }
    }
در اینجا موجودیت‌های برنامه به صورت DbSet در معرض دید EF Core 2.0 قرار گرفته‌اند. همچنین رابطه‌ی Many-to-Many بین نقش‌ها و کاربران نیز تنظیم شده‌است.
سازنده‌ی کلاس به همراه پارامتر DbContextOptions است تا بتوان آن‌را در آغاز برنامه تغییر داد.


فعالسازی مهاجرت‌ها در EF Core 2.0

EF Core 2.0 برخلاف نگارش‌های قبلی آن به دنبال کلاسی مشتق شده‌ی از IDesignTimeDbContextFactory می‌گردد تا بتواند نحوه‌ی وهله سازی ApplicationDbContext را دریافت کند. در اینجا چون DbContext تعریف شده دارای یک سازنده‌ی با پارامتر است، EF Core 2.0 نمی‌داند که چگونه باید آن‌را در حین ساخت مهاجرت‌ها و اعمال آن‌ها، وهله سازی کند. کار کلاس ApplicationDbContextFactory ذیل دقیقا مشخص سازی همین مساله است:
    /// <summary>
    /// Only used by EF Tooling
    /// </summary>
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            var basePath = Directory.GetCurrentDirectory();
            Console.WriteLine($"Using `{basePath}` as the BasePath");
            var configuration = new ConfigurationBuilder()
                                    .SetBasePath(basePath)
                                    .AddJsonFile("appsettings.json")
                                    .Build();
            var builder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var connectionString = configuration.GetConnectionString("DefaultConnection");
            builder.UseSqlServer(connectionString);
            return new ApplicationDbContext(builder.Options);
        }
    }
کاری که در اینجا انجام شده، خواندن DefaultConnection از قسمت ConnectionStrings فایل appsettings.json است:
{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=(LocalDB)\\MSSQLLocalDB;Initial Catalog=ASPNETCore2CookieAuthenticationDB;Integrated Security=True;MultipleActiveResultSets=True;"
  },
  "LoginCookieExpirationDays": 30
}
و سپس استفاده‌ی از آن جهت تنظیم رشته‌ی اتصالی متد UseSqlServer و در آخر وهله سازی ApplicationDbContext.
کار یافتن این کلاس در حین تدارک و اعمال مهاجرت‌ها توسط EF Core 2.0 خودکار بوده و باید محل قرارگیری آن دقیقا در اسمبلی باشد که DbContext برنامه در آن تعریف شده‌است.


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

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

سرویس کاربران
    public interface IUsersService
    {
        Task<string> GetSerialNumberAsync(int userId);
        Task<User> FindUserAsync(string username, string password);
        Task<User> FindUserAsync(int userId);
        Task UpdateUserLastActivityDateAsync(int userId);
    }
کار این سرویس ابتدایی کاربران، یافتن یک یک کاربر بر اساس Id او و یا کلمه‌ی عبور و نام کاربری او است. از این امکانات در حین لاگین و یا اعتبارسنجی کوکی کاربر استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس نقش‌های کاربران
    public interface IRolesService
    {
        Task<List<Role>> FindUserRolesAsync(int userId);
        Task<bool> IsUserInRole(int userId, string roleName);
        Task<List<User>> FindUsersInRoleAsync(string roleName);
    }
از این سرویس برای یافتن نقش‌های کاربر لاگین شده‌ی به سیستم و افزودن آن‌ها به کوکی رمزنگاری شده‌ی اعتبارسنجی او استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس آغاز بانک اطلاعاتی

    public interface IDbInitializerService
    {
        void Initialize();
        void SeedData();
    }
از این سرویس در آغاز کار برنامه برای اعمال خودکار مهاجرت‌های تولیدی و همچنین ثبت نقش‌های آغازین سیستم به همراه افزودن کاربر Admin استفاده خواهیم کرد.
پیاده سازی کامل این سرویس را در اینجا می‌توانید مشاهده کنید.

سرویس اعتبارسنجی کوکی‌های کاربران

یکی از قابلیت‌های میان‌افزار اعتبارسنجی ASP.NET Core 2.0، رخ‌دادی است که در آن اطلاعات کوکی دریافتی از کاربر، رمزگشایی شده و در اختیار برنامه جهت تعیین اعتبار قرار می‌گیرد:
    public interface ICookieValidatorService
    {
        Task ValidateAsync(CookieValidatePrincipalContext context);
    }

    public class CookieValidatorService : ICookieValidatorService
    {
        private readonly IUsersService _usersService;
        public CookieValidatorService(IUsersService usersService)
        {
            _usersService = usersService;
            _usersService.CheckArgumentIsNull(nameof(usersService));
        }

        public async Task ValidateAsync(CookieValidatePrincipalContext context)
        {
            var userPrincipal = context.Principal;

            var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
            if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
            if (serialNumberClaim == null)
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
            if (!int.TryParse(userIdString, out int userId))
            {
                // this is not our issued cookie
                await handleUnauthorizedRequest(context);
                return;
            }

            var user = await _usersService.FindUserAsync(userId).ConfigureAwait(false);
            if (user == null || user.SerialNumber != serialNumberClaim.Value || !user.IsActive)
            {
                // user has changed his/her password/roles/stat/IsActive
                await handleUnauthorizedRequest(context);
            }

            await _usersService.UpdateUserLastActivityDateAsync(userId).ConfigureAwait(false);
        }

        private Task handleUnauthorizedRequest(CookieValidatePrincipalContext context)
        {
            context.RejectPrincipal();
            return context.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
کار این سرویس، تعیین اعتبار موارد ذیل است:
- آیا کوکی دریافت شده دارای اطلاعات HttpContext.User است؟
- آیا این کوکی به همراه اطلاعات فیلد SerialNumber است؟
- آیا این کوکی به همراه Id کاربر است؟
- آیا کاربری که بر اساس این Id یافت می‌شود غیرفعال شده‌است؟
- آیا کاربری که بر اساس این Id یافت می‌شود دارای SerialNumber یکسانی با نمونه‌ی موجود در بانک اطلاعاتی است؟

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


تنظیمات ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0

تنظیمات کامل ابتدایی میان‌افزار اعتبارسنجی کاربران در ASP.NET Core 2.0 را در فایل Startup.cs می‌توانید مشاهده کنید.

ابتدا سرویس‌های برنامه معرفی شده‌اند:
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IUnitOfWork, ApplicationDbContext>();
            services.AddScoped<IUsersService, UsersService>();
            services.AddScoped<IRolesService, RolesService>();
            services.AddScoped<ISecurityService, SecurityService>();
            services.AddScoped<ICookieValidatorService, CookieValidatorService>();
            services.AddScoped<IDbInitializerService, DbInitializerService>();

سپس تنظیمات مرتبط با ترزیق وابستگی‌های ApplicationDbContext برنامه انجام شده‌است. در اینجا رشته‌ی اتصالی، از فایل appsettings.json خوانده شده و سپس در اختیار متد UseSqlServer قرار می‌گیرد:
            services.AddDbContext<ApplicationDbContext>(options =>
            {
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection"),
                    serverDbContextOptionsBuilder =>
                        {
                            var minutes = (int)TimeSpan.FromMinutes(3).TotalSeconds;
                            serverDbContextOptionsBuilder.CommandTimeout(minutes);
                            serverDbContextOptionsBuilder.EnableRetryOnFailure();
                        });
            });

در ادامه تعدادی Policy مبتنی بر نقش‌های ثابت سیستم را تعریف کرده‌ایم. این کار اختیاری است اما روش توصیه شده‌ی در ASP.NET Core، کار با Policyها است تا کار مستقیم با نقش‌ها. Policy‌ها انعطاف پذیری بیشتری را نسبت به نقش‌ها ارائه می‌دهند و در اینجا به سادگی می‌توان چندین نقش و یا حتی Claim را با هم ترکیب کرد و به صورت یک Policy ارائه داد:
            // Only needed for custom roles.
            services.AddAuthorization(options =>
                    {
                        options.AddPolicy(CustomRoles.Admin, policy => policy.RequireRole(CustomRoles.Admin));
                        options.AddPolicy(CustomRoles.User, policy => policy.RequireRole(CustomRoles.User));
                    });

قسمت اصلی تنظیمات میان افزار اعتبارسنجی مبتنی بر کوکی‌ها در اینجا قید شده‌است:
            // Needed for cookie auth.
            services
                .AddAuthentication(options =>
                {
                    options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                })
                .AddCookie(options =>
                {
                    options.SlidingExpiration = false;
                    options.LoginPath = "/api/account/login";
                    options.LogoutPath = "/api/account/logout";
                    //options.AccessDeniedPath = new PathString("/Home/Forbidden/");
                    options.Cookie.Name = ".my.app1.cookie";
                    options.Cookie.HttpOnly = true;
                    options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                    options.Cookie.SameSite = SameSiteMode.Lax;
                    options.Events = new CookieAuthenticationEvents
                    {
                        OnValidatePrincipal = context =>
                        {
                            var cookieValidatorService = context.HttpContext.RequestServices.GetRequiredService<ICookieValidatorService>();
                            return cookieValidatorService.ValidateAsync(context);
                        }
                    };
                });
ابتدا مشخص شده‌است که روش مدنظر ما، اعتبارسنجی مبتنی بر کوکی‌ها است و سپس تنظیمات مرتبط با کوکی رمزنگاری شده‌ی برنامه مشخص شده‌اند. تنها قسمت مهم آن CookieAuthenticationEvents است که نحوه‌ی پیاده سازی آن‌را با معرفی سرویس ICookieValidatorService پیشتر بررسی کردیم. این قسمت جائی است که پس از هر درخواست به سرور اجرا شده و کوکی رمزگشایی شده، در اختیار برنامه جهت اعتبارسنجی قرار می‌گیرد.

کار نهایی تنظیمات میان افزار اعتبارسنجی در متد Configure با فراخوانی UseAuthentication صورت می‌گیرد. اینجا است که میان افزار، به برنامه معرفی خواهد شد:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
   app.UseAuthentication();

همچنین پس از آن، کار اجرای سرویس آغاز بانک اطلاعاتی نیز انجام شده‌است تا نقش‌ها و کاربر Admin را به سیستم اضافه کند:
            var scopeFactory = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>();
            using (var scope = scopeFactory.CreateScope())
            {
                var dbInitializer = scope.ServiceProvider.GetService<IDbInitializerService>();
                dbInitializer.Initialize();
                dbInitializer.SeedData();
            }


پیاده سازی ورود و خروج به سیستم

پس از این مقدمات به مرحله‌ی آخر پیاده سازی این سیستم اعتبارسنجی می‌رسیم.

پیاده سازی Login
در اینجا از سرویس کاربران استفاده شده و بر اساس نام کاربری و کلمه‌ی عبور ارسالی به سمت سرور، این کاربر یافت خواهد شد.
در صورت وجود این کاربر، مرحله‌ی نهایی کار، فراخوانی متد الحاقی HttpContext.SignInAsync است:
        [AllowAnonymous]
        [HttpPost("[action]")]
        public async Task<IActionResult> Login([FromBody]  User loginUser)
        {
            if (loginUser == null)
            {
                return BadRequest("user is not set.");
            }

            var user = await _usersService.FindUserAsync(loginUser.Username, loginUser.Password).ConfigureAwait(false);
            if (user == null || !user.IsActive)
            {
                await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
                return Unauthorized();
            }

            var loginCookieExpirationDays = _configuration.GetValue<int>("LoginCookieExpirationDays", defaultValue: 30);
            var cookieClaims = await createCookieClaimsAsync(user).ConfigureAwait(false);
            await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                cookieClaims,
                new AuthenticationProperties
                {
                    IsPersistent = true, // "Remember Me"
                    IssuedUtc = DateTimeOffset.UtcNow,
                    ExpiresUtc = DateTimeOffset.UtcNow.AddDays(loginCookieExpirationDays)
                });

            await _usersService.UpdateUserLastActivityDateAsync(user.Id).ConfigureAwait(false);

            return Ok();
        }
مهم‌ترین کار متد HttpContext.SignInAsync علاوه بر تنظیم طول عمر کوکی کاربر، قسمت createCookieClaimsAsync است که به صورت ذیل پیاده سازی شده‌است:
        private async Task<ClaimsPrincipal> createCookieClaimsAsync(User user)
        {
            var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
            identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
            identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
            identity.AddClaim(new Claim("DisplayName", user.DisplayName));

            // to invalidate the cookie
            identity.AddClaim(new Claim(ClaimTypes.SerialNumber, user.SerialNumber));

            // custom data
            identity.AddClaim(new Claim(ClaimTypes.UserData, user.Id.ToString()));

            // add roles
            var roles = await _rolesService.FindUserRolesAsync(user.Id).ConfigureAwait(false);
            foreach (var role in roles)
            {
                identity.AddClaim(new Claim(ClaimTypes.Role, role.Name));
            }

            return new ClaimsPrincipal(identity);
        }
در اینجا است که اطلاعات اضافی کاربر مانند Id او یا نقش‌های او به کوکی رمزنگاری شده‌ی تولیدی توسط HttpContext.SignInAsync اضافه شده و در دفعات بعدی و درخواست‌های بعدی او، به صورت خودکار توسط مرورگر به سمت سرور ارسال خواهند شد. این کوکی است که امکان کار با MyProtectedApiController و یا MyProtectedAdminApiController را فراهم می‌کند. اگر شخص لاگین کرده باشد، بلافاصله قابلیت دسترسی به امکانات محدود شده‌ی توسط فیلتر Authorize را خواهد یافت. همچنین در این مثال چون کاربر Admin لاگین می‌شود، امکان دسترسی به Policy مرتبطی را نیز خواهد یافت:
[Route("api/[controller]")]
[Authorize(Policy = CustomRoles.Admin)]
public class MyProtectedAdminApiController : Controller

پیاده سازی Logout

متد الحاقی HttpContext.SignOutAsync کار Logout کاربر را تکمیل می‌کند.
        [AllowAnonymous]
        [HttpGet("[action]"), HttpPost("[action]")]
        public async Task<bool> Logout()
        {
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
            return true;
        }


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

در فایل index.html ، نمونه‌ای از متدهای لاگین، خروج و فراخوانی اکشن متدهای محافظت شده را مشاهده می‌کنید. این روش برای برنامه‌های تک صفحه‌ای وب یا SPA نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
ترفندها در SQL Server 2014 DML Triggers

SQL Server 2014 DML Triggers are often a point of contention between Developers and DBAs, between those who customize a database application and those who provides it. They are often the first database objects investigated when the performance degrades. They seem easy to write, but writing efficient Trigger, though complex have a very important characteristic: they allow solving problems that cannot be managed in any other application layer. Therefore, if you cannot work without them, in this article you will learn tricks and best practices for writing and managing them efficiently. 

ترفندها در SQL Server 2014 DML Triggers
مطالب
Blazor 5x - قسمت پنجم - مبانی Blazor - بخش 2 - کامپوننت‌ها
انتقال محتوای کامپوننت Index به یک کامپوننت جدید و تعریف مسیریابی و مدخل منوی آن

پیش از ادامه‌ی مثال قسمت قبل، قصد داریم تمام کدهای موجود در فایل Pages\Index.razor را به یک فایل اختصاصی آن‌ها منتقل کرده و مسیریابی و منوی آن‌را تکمیل کنیم. به همین جهت در پوشه‌ی Pages، یک پوشه‌ی جدید را به نام LearnBlazor ایجاد کرده و درون آن، فایل خالی BindProp.razor را ایجاد می‌کنیم. سپس تمام محتوای فایل Pages\Index.razor را cut کرده و به درون فایل جدید Pages\LearnBlazor\BindProp.razor، منتقل و Paste می‌کنیم.
پس از این تغییرات، در فایل Pages\Index.razor، مهم‌ترین سطر آن، همان اولین سطر تعریف مسیریابی آن خواهد بود و هر محتوای دلخواهی که علاقمند بودید:
@page "/"

<h1>Hello, world!</h1>
در ادامه چون می‌خواهیم گزینه‌ی منوی جدیدی را برای BindProp.razor تعریف کنیم، سطر اول آن‌را به صورت زیر تغییر می‌دهیم:
@page "/bindprop"
با اینکار، این کامپوننت صرفنظر از محل قرارگیری آن که اکنون در پوشه‌ی Pages\LearnBlazor است، در مسیر https://localhost:5001/bindprop قابل دسترسی خواهد شد. اما چگونه باید مدخل منوی جدیدی را برای آن تعریف کرد؟ برای اینکار به فایل Shared\NavMenu.razor مراجعه کرده و دقیقا شبیه به ساختار مداخل منوهای Home ، Counter و غیره، مدخل جدیدی را برای آن تعریف می‌کنیم:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="bindprop">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Bind Properties
    </NavLink>
</li>
در اینجا برچسب مدخل جدید تعریف شده، Bind Properties است و href لینک به آن، دقیقا به مسیریابی تعریف شده‌ی در فایل BindProp.razor اشاره می‌کند.



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

در قسمت قبل، نمایش ردیفی لیست اتاق‌های تعریف شده را مشاهده کردید. در این قسمت می‌خواهیم هر اتاق تعریف شده را در یک card جداگانه نمایش دهیم. هدف این است که در ابتدا به یک UI متداول شلوغ برسیم و بعد شروع کنیم به Refactoring این UI پیچیده، به کامپوننت‌های کوچک‌تر تشکیل دهنده‌ی آن، جهت مدیریت ساده‌تر این UI و درک بهتر آن. بنابراین در ابتدا با یک کامپوننت کلی شلوغ، شروع خواهیم کرد.
به همین جهت فایل جدید Pages\LearnBlazor\DemoHotel.razor را برای نمایش لیست اتاق‌های موجود اضافه می‌کنیم. سپس محتوای آن‌را به صورت زیر تغییر خواهیم داد:
@page "/demoHotel"

<h3>Hotel Rooms</h3>
<div class="border p-2 mt-2" style="background-color:azure">
    <h2 class="text-info">Rooms List</h2>
    <div class="row container">
        @foreach (var room in Rooms)
        {
            <div class="bg-light border p-2 col-5 ml-2">
                <h4 class="text-secondary">Room - @room.Id</h4>

                @room.Name<br />
                @room.Price.ToString("c")<br />
                <input type="checkbox" @bind-value="room.IsActive" checked="@(room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
                <span>This room is @(room.IsActive?"Active": "InActive")</span>

                @if (room.IsActive)
                {
                    @foreach (var roomProp in room.RoomProps)
                    {
                        <p>@roomProp.Name - @roomProp.Value</p>
                    }
                }

                <input type="button" class="btn btn-danger" value="Delete" />
                <input type="button" class="btn btn-success" value="Edit" />
            </div>
        }
    </div>
</div>
- قسمت کدهای آن که در اینجا ذکر نشده (code@)، با قسمت کدهای کامپوننت Pages\LearnBlazor\BindProp.razor که در قسمت قبل تهیه کردیم، یکی است و هدف از آن، ارائه‌ی List<BlazorRoom> Rooms است که در کدهای razor جاری استفاده شده‌است.
- سپس مسیریابی منتهی به این کامپوننت، به آدرس demoHotel/ تنظیم شده‌است. این مسیریابی را در کامپوننت Shared\NavMenu.razor به صورت زیر مورد استفاده قرار خواهیم داد تا مدخل منوی جدیدی برای آن تهیه شود:
<li class="nav-item px-3">
    <NavLink class="nav-link" href="demoHotel">
      <span class="oi oi-list-rich" aria-hidden="true"></span> Demo Hotel
    </NavLink>
</li>
- در این کامپوننت، با ایجاد حلقه‌ای بر روی لیست اتاق‌ها، مشخصات هر کدام نمایش داده می‌شود. همچنین در اینجا اگر اتاق در حال نمایش فعال باشد، لیست خواص آن نیز درج خواهد شد. به علاوه دو دکمه‌ی جدید حذف و ویرایش نیز در انتهای هر برگه اضافه شده‌است:



تبدیل دکمه‌های حذف و ویرایش هر اتاق به یک کامپوننت جدید

اکنون می‌خواهیم کامپوننت شلوغ Pages\LearnBlazor\DemoHotel.razor را به چند زیر کامپوننت بشکنیم تا هر کدام وظایف خاص خود را انجام دهند و در نهایت به یک UI قابل درک‌تر برسیم. برای مثال می‌خواهیم دکمه‌های حذف و ویرایش هر اتاق را به یک کامپوننت جدید منتقل کنیم تا هم این UI خلوت‌تر شود و هم اگر در قسمت دیگری از برنامه نیاز به یک چنین دکمه‌هایی بود، بتوان از آن کامپوننت اختصاصی، استفاده‌ی مجدد کرد.
برای این منظور ابتدا پوشه‌ی جدید Pages\LearnBlazor\LearnBlazor‍Components را افزوده و سپس در داخل آن، فایل جدید کامپوننت EditDeleteButton.razor را نیز ایجاد می‌کنیم. در این فایل جدید در ابتدا کدهای دو دکمه‌ی تعریف شده را از کامپوننت DemoHotel.razor انتخاب و cut کرده و سپس در این فایل جدید paste می‌کنیم. در این کامپوننت جدید، نیازی به تعریف page@ و مسیریابی آن نیست. به این معنا که این کامپوننت، یک کامپوننت اشتراکی است و routable نیست و قرار است در داخل یک کامپوننت دیگر مورد استفاده قرار گیرد.
بنابراین تا اینجا محتوای کامپوننت EditDeleteButton.razor فقط از دو سطر زیر تشکیل می‌شود:
<input type="button" class="btn btn-danger" value="Delete" />
<input type="button" class="btn btn-success" value="Edit" />
در ادامه برای درج این کامپوننت در حلقه‌ی نمایشی آن در کامپوننت DemoHotel، باید به صورت زیر عمل کرد که به فضای نام کامل این کامپوننت اشاره می‌کند:
<BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton></BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents.EditDeleteButton>
برای اینکه مجبور به تعریف یک چنین نام طولانی نباشیم، می‌توان فضای نام پوشه‌ی آن‌را در انتهای فایل Imports.razor_ قرار داد:
@using BlazorServerSample.Pages.LearnBlazor.LearnBlazorComponents
البته اگر قرار نیست از این کامپوننت در سایر کامپوننت‌ها استفاده شود و فقط یک محل استفاده را دارد، می‌توان این using را در بالای تعاریف فایل DemoHotel.razor نیز قرار داد.

اکنون می‌توان تعریف مدخل کامپوننت را به صورت زیر خلاصه کرد:
<EditDeleteButton></EditDeleteButton>


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

فرض کنید قصد داریم دکمه‌های ویرایش و حذف را تنها به کاربران ادمین نمایش دهیم. به همین جهت نیاز است بتوان پارامتری مانند IsAdmin را به کامپوننت EditDeleteButton ارسال کرد. برای اینکار کامپوننت Pages\LearnBlazor\LearnBlazor‍Components\EditDeleteButton.razor را به صورت زیر ویرایش می‌کنیم:
@if (IsAdmin)
{
    <input type="button" class="btn btn-danger" value="Delete" />
    <input type="button" class="btn btn-success" value="Edit" />
}

@code
{
    [Parameter]
    public bool IsAdmin { get; set; }
}
در اینجا خواص عمومی مزین شده‌ی با ویژگی Parameter، به عنوان پارامتر ورودی کامپوننت عمل می‌کنند. برای نمونه بر اساس مقدار خاصیت IsAdmin، توسط یک if@ تصمیم خواهیم گرفت که آیا قسمتی از UI نمایش داده شود یا خیر؟

پس از تعریف این پارامتر ورودی، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<EditDeleteButton IsAdmin="true"></EditDeleteButton>


انتقال هر اتاق به کامپوننت مجزای خاص خودش

در ادامه می‌خواهیم محتوای حلقه‌ی foreach (var room in Rooms)@ کامپوننت DemoHotel را به طور کامل cut کرده و در یک کامپوننت جدید paste کنیم تا به حلقه‌ای خواناتر و با مسئولیت‌های کمتری برسیم. نگهداری کدهایی که قسمت‌های مختلف آن از هم ایزوله شده‌اند و دامنه‌ی تغییرات آن‌ها کاملا مشخص و محدود است، در طول زمان بسیار ساده‌تر از نگهداری کدهای UI ای در هم تنیده‌است.
به همین جهت ابتدا کامپوننت جدید Pages\LearnBlazor\LearnBlazor‍Components\IndividualRoom.razor را ایجاد می‌کنیم و سپس، هر آنچه داخل حلقه‌ی foreach یاد شده قرار دارد را انتخاب و cut کرده و درون این کامپوننت جدید paste می‌کنیم:
<div class="bg-light border p-2 col-5 offset-1">
    <h4 class="text-secondary">Room - @Room.Id</h4>

    @Room.Name<br />
    @Room.Price.ToString("c")<br />
    <input type="checkbox" @bind-value="Room.IsActive" checked="@(Room.IsActive?"checked":null)" /> &nbsp; Is Active<br />
    <span>This room is @(Room.IsActive?"Active": "InActive")</span>

    @if (Room.IsActive)
    {
        @foreach (var roomProp in Room.RoomProps)
        {
            <p>@roomProp.Name - @roomProp.Value</p>
        }
    }

    <EditDeleteButton IsAdmin="true"></EditDeleteButton>
</div>

@code
{
    [Parameter]
    public BlazorRoom Room { get; set; }
}
در اینجا پس از paste کدهای داخل حلقه، نیاز به یک پارامتر ورودی که همان شیء Room در حال رندر است، خواهد بود. به همین جهت پارامتر آن‌را تعریف کرده و همچنین کدهای موجود را نیز اندکی ویرایش می‌کنیم، تا از نام این پارامتر جدید استفاده کند.

پس از این تغییر، کدهای حلقه‌ی foreach کامپوننت DemoHotel.razor به صورت زیر خلاصه می‌شوند. در اینجا روش ارسال یک شیء را به پارامتر Room نیز مشاهده می‌کنید (البته ذکر @ در اینجا الزامی نیست و می‌شد از روش مقدار دهی "Room="room نیز استفاده کرد):
<div class="row container">
  @foreach (var room in Rooms)
  {
    <IndividualRoom Room="@room"></IndividualRoom>
  }
</div>
در اینجا می‌توان سلسه مراتب کامپوننت‌ها را مشاهده کرد. کامپوننت DemoHotel، کامپوننت IndividualRoom را فراخوانی می‌کند و این کامپوننت نیز کامپوننت EditDeleteButton را مورد استفاده قرار می‌دهد.


یک تمرین: نمایش لیست امکانات رفاهی هتل

پس از نمایش لیست اتاق‌های یک هتل، قصد داریم لیست امکانات رفاهی آن‌را نیز نمایش دهیم:


 مدل این امکانات را به صورت زیر به پوشه‌ی Models برنامه اضافه می‌کنیم:
namespace BlazorServerSample.Models
{
    public class BlazorAmenity
    {
        public int Id { get; set; }

        public string Name { get; set; }

        public string Description { get; set; }
    }
}
از آنجائیکه قصد داریم لیست آن‌ها را در همان کامپوننت DemoHotel.razor نمایش دهیم، این لیست را به صورت زیر تشکیل می‌دهیم:
@code{

    List<BlazorAmenity> AmenitiesList = new List<BlazorAmenity>();
    // ...

    protected override void OnInitialized()
    {
        base.OnInitialized();

        // ...

        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 111,
            Name = "Gym",
            Description = "24x7 gym room is available."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 222,
            Name = "Swimming Pool",
            Description = "Pool room is open from 6am to 10pm."
        });
        AmenitiesList.Add(new BlazorAmenity
        {
            Id = 333,
            Name = "Free Brakfast",
            Description = "Enjoy free breakfast at out hotel."
        });
    }
}
در ابتدا فیلد List<BlazorAmenity> AmenitiesList جهت دسترسی به لیست امکانات رفاهی تعریف شده و سپس آن‌را در رویدادگردان OnInitialized، مقدار دهی اولیه کرده‌ایم. در مورد این متدهای چرخه‌ی حیات، در قسمت بعدی بیشتر بحث خواهیم کرد.

اکنون برای نمایش تک تک عناصر این لیست، ابتدا یک کامپوننت منحصر به یک BlazorAmenity را به نام Pages\LearnBlazor\LearnBlazor‍Components\IndividualAmenity.razor ایجاد می‌کنیم با این محتوا:
<div class="bg-light border p-2 col-5 offset-1 mt-2">
    <h4 class="text-secondary">Amenity - @Amenity.Id</h4>

    @Amenity.Name<br />
    @Amenity.Description<br />
</div>

@code
{
    [Parameter]
    public BlazorAmenity Amenity { get; set; }
}
این کامپوننت، یک شیء BlazorAmenity را به عنوان پارامتر دریافت کرده و سپس Id، نام و توضیحات آن‌را نمایش می‌دهد.

در آخر پس از تعریف کامپوننت IndividualAmenity.razor، روش استفاده‌ی از آن در کامپوننت DemoHotel به صورت زیر است:
<div class="col-12 mt-4">
    <h4 class="text-info">Hotel Amenities</h4>
</div>
@foreach (var amenity in AmenitiesList)
{
    <IndividualAmenity Amenity="@amenity"></IndividualAmenity>
}
در اینجا بر روی لیست امکانات، یک حلقه را تشکیل داده و سپس توسط کامپوننت IndividualAmenity، هر کدام از امکانات را جداگانه نمایش داده‌ایم.

کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید: Blazor-5x-Part-05.zip
مطالب
شمسی سازی Date-Picker توکار Angular Material 6x
Angular Material به همراه یک کامپوننت Date-Picker بسیار شکیل و حرفه‌ای است اما ... از تقویم شمسی پشتیبانی نمی‌کند. در این مطلب می‌خواهیم با تدارک یک DateAdapter سفارشی، این مشکل را برطرف کنیم تا در نهایت به یک چنین Date-Picker شمسی برسیم:



تاریخچه‌ی تغییرات کامپوننت Date-Picker

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

DateAdapter شمسی تهیه شده از کتابخانه‌ی jalali-moment برای تبدیل تاریخ‌ها استفاده می‌کند. بنابراین ابتدا نیاز است این وابستگی را نصب کرد:
 npm install jalali-moment --save


افزودن DateAdapter شمسی به پروژه

برای افزودن DateAdapter شمسی تهیه شده، فایل جدید app\shared\material.persian-date.adapter.ts را به برنامه اضافه کرده و به صورت زیر تکمیل کنید:
import { DateAdapter } from "@angular/material";
import * as jalaliMoment from "jalali-moment";

export const PERSIAN_DATE_FORMATS = {
  parse: {
    dateInput: "jYYYY/jMM/jDD"
  },
  display: {
    dateInput: "jYYYY/jMM/jDD",
    monthYearLabel: "jYYYY jMMMM",
    dateA11yLabel: "jYYYY/jMM/jDD",
    monthYearA11yLabel: "jYYYY jMMMM"
  }
};

export class MaterialPersianDateAdapter extends DateAdapter<jalaliMoment.Moment> {

  constructor() {
    super();
    super.setLocale("fa");
  }

  getYear(date: jalaliMoment.Moment): number {
    return this.clone(date).jYear();
  }

  getMonth(date: jalaliMoment.Moment): number {
    return this.clone(date).jMonth();
  }

  getDate(date: jalaliMoment.Moment): number {
    return this.clone(date).jDate();
  }

  getDayOfWeek(date: jalaliMoment.Moment): number {
    return this.clone(date).day();
  }

  getMonthNames(style: "long" | "short" | "narrow"): string[] {
    switch (style) {
      case "long":
      case "short":
        return jalaliMoment.localeData("fa").jMonths().slice(0);
      case "narrow":
        return jalaliMoment.localeData("fa").jMonthsShort().slice(0);
    }
  }

  getDateNames(): string[] {
    const valuesArray = Array(31);
    for (let i = 0; i < 31; i++) {
      valuesArray[i] = String(i + 1);
    }
    return valuesArray;
  }

  getDayOfWeekNames(style: "long" | "short" | "narrow"): string[] {
    switch (style) {
      case "long":
        return jalaliMoment.localeData("fa").weekdays().slice(0);
      case "short":
        return jalaliMoment.localeData("fa").weekdaysShort().slice(0);
      case "narrow":
        return ["ی", "د", "س", "چ", "پ", "ج", "ش"];
    }
  }

  getYearName(date: jalaliMoment.Moment): string {
    return this.clone(date).jYear().toString();
  }

  getFirstDayOfWeek(): number {
    return jalaliMoment.localeData("fa").firstDayOfWeek();
  }

  getNumDaysInMonth(date: jalaliMoment.Moment): number {
    return this.clone(date).jDaysInMonth();
  }

  clone(date: jalaliMoment.Moment): jalaliMoment.Moment {
    return date.clone().locale("fa");
  }

  createDate(year: number, month: number, date: number): jalaliMoment.Moment {
    if (month < 0 || month > 11) {
      throw Error(
        `Invalid month index "${month}". Month index has to be between 0 and 11.`
      );
    }
    if (date < 1) {
      throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
    }
    const result = jalaliMoment()
      .jYear(year).jMonth(month).jDate(date)
      .hours(0).minutes(0).seconds(0).milliseconds(0)
      .locale("fa");

    if (this.getMonth(result) !== month) {
      throw Error(`Invalid date ${date} for month with index ${month}.`);
    }
    if (!result.isValid()) {
      throw Error(`Invalid date "${date}" for month with index "${month}".`);
    }
    return result;
  }

  today(): jalaliMoment.Moment {
    return jalaliMoment().locale("fa");
  }

  parse(value: any, parseFormat: string | string[]): jalaliMoment.Moment | null {
    if (value && typeof value === "string") {
      return jalaliMoment(value, parseFormat, "fa");
    }
    return value ? jalaliMoment(value).locale("fa") : null;
  }

  format(date: jalaliMoment.Moment, displayFormat: string): string {
    date = this.clone(date);
    if (!this.isValid(date)) {
      throw Error("JalaliMomentDateAdapter: Cannot format invalid date.");
    }
    return date.format(displayFormat);
  }

  addCalendarYears(date: jalaliMoment.Moment, years: number): jalaliMoment.Moment {
    return this.clone(date).add(years, "jYear");
  }

  addCalendarMonths(date: jalaliMoment.Moment, months: number): jalaliMoment.Moment {
    return this.clone(date).add(months, "jmonth");
  }

  addCalendarDays(date: jalaliMoment.Moment, days: number): jalaliMoment.Moment {
    return this.clone(date).add(days, "jDay");
  }

  toIso8601(date: jalaliMoment.Moment): string {
    return this.clone(date).format();
  }

  isDateInstance(obj: any): boolean {
    return jalaliMoment.isMoment(obj);
  }

  isValid(date: jalaliMoment.Moment): boolean {
    return this.clone(date).isValid();
  }

  invalid(): jalaliMoment.Moment {
    return jalaliMoment.invalid();
  }

  deserialize(value: any): jalaliMoment.Moment | null {
    let date;
    if (value instanceof Date) {
      date = jalaliMoment(value);
    }
    if (typeof value === "string") {
      if (!value) {
        return null;
      }
      date = jalaliMoment(value).locale("fa");
    }
    if (date && this.isValid(date)) {
      return date;
    }
    return super.deserialize(value);
  }
}
کار این Adapter و یا «وفق دهنده» این است که مشخص می‌کند، هفته‌ی ایرانی از چه روزی شروع می‌شود. نام روزهای هفته‌ی ایرانی چیست؟ برچسب‌های نام ماه‌های ایرانی چگونه باید تامین شوند و در کل جهت وفق دادن تقویم میلادی اصلی با تقویم شمسی، چه اجزایی باید به سیستم معرفی شوند تا این تقویم توکار بدون مشکل مانند قبل کار کند.
 
معرفی وفق دهنده‌ی شمسی به پروژه

پس از تعریف MaterialPersianDateAdapter و همچنین PERSIAN_DATE_FORMATS، برای معرفی آن‌ها به برنامه، فایل app\shared\material.module.ts را گشوده و به صورت زیر تغییر دهید:
import { NgModule } from "@angular/core";
import {  DateAdapter,  MAT_DATE_FORMATS,  MAT_DATE_LOCALE } from "@angular/material";

import { MaterialPersianDateAdapter, PERSIAN_DATE_FORMATS } from "./material.persian-date.adapter";

@NgModule({
  providers: [
    { provide: DateAdapter, useClass: MaterialPersianDateAdapter, deps: [MAT_DATE_LOCALE] },
    { provide: MAT_DATE_FORMATS, useValue: PERSIAN_DATE_FORMATS }
  ]
})
export class MaterialModule {
}
کار این تعاریف، تعویض DateAdapter اصلی میلادی، با نمونه‌ی شمسی است. همچنین فرمت نمایشی برچسب‌ها را نیز جایگزین می‌کند.

پس از آن اگر mat-datepicker را به نحو متداولی به صفحه اضافه کنیم:
<mat-form-field>
    <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl">
    <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle>
    <mat-datepicker #picker6></mat-datepicker>
</mat-form-field>
یک چنین خروجی حاصل خواهد شد:




چند مثال تکمیلی از کاربردهای کامپوننت mat-datepicker

1) استفاده از تاریخ میلادی رسیده‌ی از سمت سرور و نمایش آن
<mat-form-field>
    <input matInput [matDatepicker]="picker6" placeholder="json gregorian input" [(ngModel)]="dateControl">
    <mat-datepicker-toggle matSuffix [for]="picker6"></mat-datepicker-toggle>
    <mat-datepicker #picker6></mat-datepicker>
</mat-form-field>
با این کدها:
@Component()
export class PersianDatepickerComponent {

  jsonDate = "2018-01-08T20:21:29.4674496";
  dateControl = this.jsonDate;
}
در اینجا jsonDate همان رشته‌ی تاریخی است که از سمت سرور دریافت شده و میلادی است. با انتساب آن به ngModel، به صورت خودکار شمسی نمایش داده خواهد شد:




2) تعیین تاریخ آغاز تقویم و نمایش آن در حالت انتخاب سال
<mat-form-field>
    <input matInput [matDatepicker]="picker2" placeholder="startAt 2017-01-01 and startView=year">
    <mat-datepicker-toggle matSuffix [for]="picker2"></mat-datepicker-toggle>
    <mat-datepicker #picker2 startView="year" [startAt]="startDate"></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent  {

  startDate = moment("2017-01-01", "YYYY-MM-DD"); // = moment.from("2017-01-01", "en");
  
}
در این مثال خاصیت startAt را به یک تاریخ میلادی متصل کرده‌ایم و همچنین خاصیت startView به year تنظیم شده‌است که یک چنین خروجی را در بار اول نمایش تقویم ایجاد می‌کند:




3) تعیین باز‌ه‌ی تاریخی قابل انتخاب توسط کاربر
<mat-form-field>
    <input matInput [matDatepicker]="picker3" [min]="minDate" [max]="maxDate" placeholder="min: 2017-10-02 and max: 1396-07-29">
    <mat-datepicker-toggle matSuffix [for]="picker3"></mat-datepicker-toggle>
    <mat-datepicker #picker3></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent  {

  minDate = moment.from("2017-10-02", "en"); // = moment('2017-10-02', 'YYYY-MM-DD');
  maxDate = moment.from("1396-07-29", "fa"); // = moment('1396-07-29', 'jYYYY-jMM-jDD');
}
همانطور که ملاحظه می‌کنید کتابخانه‌ی jalali-moment می‌تواند تاریخ شمسی و یا میلادی را توسط متد from آن دریافت کند و هر دو حالت در اینجا پس از انتساب به خواص min و max تقویم، به خوبی کار کرده و سبب محدود ساختن بازه‌ی قابل انتخاب توسط کاربر می‌شوند.



در این تصویر روزهای خاکستری، قابل انتخاب نیستند و غیرفعال شده‌اند (چون min به 10 مهر و max به 29 مهر تنظیم شده‌است).


4) غیرفعال کردن روزهای قابل انتخاب بر اساس یک منطق سفارشی
<mat-form-field>
    <input matInput [matDatepicker]="picker4" [matDatepickerFilter]="myFilter" placeholder="Date validation - Datepicker Filter">
    <mat-datepicker-toggle matSuffix [for]="picker4"></mat-datepicker-toggle>
    <mat-datepicker #picker4></mat-datepicker>
</mat-form-field>
با این کدها:
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent {

  myFilter = (d: moment.Moment): boolean => {
    const day: number = d.day();
    // Prevent Thursday and Friday from being selected.
    return day !== 5 && day !== 4;
  }
}
در اینجا روزهای پنج‌شنبه و جمعه در تقویم نمایش داده شده، بر اساس تعریف matDatepickerFilter سفارشی، دیگر قابل انتخاب نیستند:



5) کار با رخ‌دادهای تقویم
<mat-form-field>
    <input matInput [matDatepicker]="picker5" (dateInput)="onInput($event)" (dateChange)="onChange($event)"
        placeholder="dateInput and dateChange events">
    <mat-datepicker-toggle matSuffix [for]="picker5"></mat-datepicker-toggle>
    <mat-datepicker #picker5></mat-datepicker>
</mat-form-field>
با این کدها:
import { MatDatepickerInputEvent } from "@angular/material";
import * as moment from "jalali-moment";

@Component()
export class PersianDatepickerComponent {

  onInput(event: MatDatepickerInputEvent<moment.Moment>) {
    console.log("OnInput: ", event.value);
  }

  onChange(event: MatDatepickerInputEvent<moment.Moment>) {
    const x = moment(event.value).format("jYYYY/jMM/jDD");
    console.log("OnChange: ", x);
  }
}
در اینجا نحوه‌ی واکنش نشان دادن به رخ‌دادهای dateInput و dateChange کامپوننت mat-datepicker را ملاحظه می‌کنید:


در اینجا، onInput، با ورود دستی اطلاعات به textbox کامپوننت، فعال می‌شود و onChange، در صورت انتخاب یک تاریخ از تقویم.


کدهای کامل این مثال را از اینجا می‌توانید دریافت کنید.
اشتراک‌ها
ایجاد UI شناور و زیبا

One of the major new capabilities for developers in Windows 10 version 1511 is the final release of Windows.UI.Composition. This was in preview in the 10240 SDK but has now reached full release in this new update. 

ایجاد UI شناور و زیبا