بازنویسی متد مقدار دهی اولیه‌ی کاربر ادمین در ASP.NET Core Identity‌ توسط متد HasData در EF Core
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: چهار دقیقه

فرض کنید قصد داریم متد «SeedDatabaseWithAdminUserAsync» را توسط روش جدید «مقدار دهی اولیه‌ی بانک اطلاعاتی توسط Entity framework Core» بازنویسی کنیم. در ادامه مراحل اینکار را مرور خواهیم کرد.


اضافه کردن نقش پیش‌فرض Admin

اولین تغییری که در اینجا مورد نیاز است، افزودن نقش پیش‌فرض Admin است. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی Role برنامه را به نحو زیر انجام می‌دهیم:
namespace ASPNETCoreIdentitySample.DataLayer.Mappings
{
    public class RoleConfiguration : IEntityTypeConfiguration<Role>
    {
        private readonly SiteSettings _siteSettings;
        private readonly ILookupNormalizer _keyNormalizer;

        public RoleConfiguration(SiteSettings siteSettings, ILookupNormalizer keyNormalizer)
        {
            _siteSettings = siteSettings ?? throw new ArgumentNullException(nameof(siteSettings));
            _keyNormalizer = keyNormalizer ?? throw new ArgumentNullException(nameof(keyNormalizer));
        }

        public void Configure(EntityTypeBuilder<Role> builder)
        {
            builder.ToTable("AppRoles");

            var adminUserSeed = _siteSettings.AdminUserSeed;
            builder.HasData(
                new Role
                {
                    Id = 1,
                    Name = adminUserSeed.RoleName,
                    NormalizedName = _keyNormalizer.NormalizeName(adminUserSeed.RoleName),
                    ConcurrencyStamp = Guid.NewGuid().ToString()
                });
        }
    }
}
در حین افزودن new Role، دو نکته مدنظر قرار گرفته‌اند:
الف) NormalizedName از طریق سرویس ILookupNormalizer در ASP.NET Core Identity تامین می‌شود.
ب) SiteSettings در اینجا جهت دریافت نام نقش Admin از فایل appsettings.json، تعریف شده‌است (تامین مقدار پیش‌فرض).


اضافه کردن کاربر پیش‌فرض Admin

برای اضافه کردن کاربر پیش‌فرض ادمین، علاوه بر سرویس ILookupNormalizer که کار تامین مقادیر NormalizedEmail و NormalizedUserName را به عهده دارد، نیاز به سرویس IPasswordHasher نیز می‌باشد. از آن برای تامین مقدار فیلد PasswordHash، بر اساس سرویس هش کردن پسوردهای توکار ASP.NET Core Identity، استفاده می‌کنیم. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی User برنامه را به نحو زیر انجام می‌دهیم:
    public class UserConfiguration : IEntityTypeConfiguration<User>
    {
        private readonly SiteSettings _siteSettings;
        private readonly ILookupNormalizer _keyNormalizer;
        private readonly IPasswordHasher<User> _passwordHasher;

        public UserConfiguration(
            SiteSettings siteSettings,
            ILookupNormalizer keyNormalizer,
            IPasswordHasher<User> passwordHasher)
        {
            _siteSettings = siteSettings ?? throw new ArgumentNullException(nameof(siteSettings));
            _keyNormalizer = keyNormalizer ?? throw new ArgumentNullException(nameof(keyNormalizer));
            _passwordHasher = passwordHasher ?? throw new ArgumentNullException(nameof(passwordHasher));
        }

        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.ToTable("AppUsers");

            var adminUserSeed = _siteSettings.AdminUserSeed;
            builder.HasData(
                new User
                {
                    Id = 1,
                    UserName = adminUserSeed.Username,
                    NormalizedUserName = _keyNormalizer.NormalizeName(adminUserSeed.Username),
                    Email = adminUserSeed.Email,
                    NormalizedEmail = _keyNormalizer.NormalizeEmail(adminUserSeed.Email),
                    EmailConfirmed = true,
                    IsEmailPublic = true,
                    LockoutEnabled = true,
                    TwoFactorEnabled = false,
                    PasswordHash = _passwordHasher.HashPassword(null, adminUserSeed.Password),
                    ConcurrencyStamp = Guid.NewGuid().ToString(),
                    SecurityStamp = string.Empty,
                    IsActive = true
                });
        }
    }


انتساب دادن نقش Admin، به کاربر Admin

تا اینجا نقش ادمین و کاربر ادمین را به صورت مجزا ایجاد کردیم. مرحله‌ی آخر، انتساب این نقش، به کاربر ادمین است که بر اساس Id این دو صورت می‌گیرد. برای این منظور توسط یک IEntityTypeConfiguration جدید، تنظیمات موجودیت سفارشی UserRole برنامه را به نحو زیر انجام می‌دهیم:
    public class UserRoleConfiguration : IEntityTypeConfiguration<UserRole>
    {
        public void Configure(EntityTypeBuilder<UserRole> builder)
        {
            builder.HasOne(userRole => userRole.Role)
                   .WithMany(role => role.Users)
                   .HasForeignKey(userRole => userRole.RoleId);

            builder.HasOne(userRole => userRole.User)
                   .WithMany(user => user.Roles)
                   .HasForeignKey(userRole => userRole.UserId);

            builder.ToTable("AppUserRoles");

            builder.HasData(
                new UserRole
                {
                    UserId = 1,
                    RoleId = 1
                });
        }
    }


اتصال IEntityTypeConfiguration به DbContext برنامه

IEntityTypeConfigurationهای تهیه شده، دارای سازنده‌هایی هستند که تعدادی سرویس را دریافت می‌کنند. روش معرفی آن‌ها به این صورت است:

الف) تامین دو سرویس ILookupNormalizer و IPasswordHasher در IDesignTimeDbContextFactory تهیه شده:
    public class ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
    {
        public ApplicationDbContext CreateDbContext(string[] args)
        {
            // ....
            services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
            services.TryAddScoped<IPasswordHasher<User>, PasswordHasher<User>>();
در حین انجام عملیات Migration، کار به افزودن و اجرای IEntityTypeConfigurationهای تهیه شده می‌رسد و این سرویس‌ها قرار است از DbContext تامین شوند؛ به همین جهت نیاز است روش تامین این سرویس‌ها را در همینجا معرفی کنیم.

ب) پس از تامین این سرویس‌ها، روش معرفی آن‌ها به متدهای modelBuilder.ApplyConfiguration در متد OnModelCreating، توسط متد this.GetService به صورت زیر است. متد this.GetService از فضای نام Microsoft.EntityFrameworkCore.Infrastructure تامین می‌شود و فقط یکبار در زمان Migration از طریق IDesignTimeDbContextFactory تامین خواهد شد:
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace ASPNETCoreIdentitySample.DataLayer.Context
{
    public class ApplicationDbContext :
        IdentityDbContext<User, Role, int, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>,
        IUnitOfWork
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options) { }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            var siteSettings = this.GetService<IOptionsSnapshot<SiteSettings>>()?.Value;
            var lookupNormalizer = this.GetService<ILookupNormalizer>();
            var passwordHasher =  this.GetService<IPasswordHasher<User>>()

            modelBuilder.ApplyConfiguration(new RoleConfiguration(siteSettings, lookupNormalizer));
            modelBuilder.ApplyConfiguration(new UserConfiguration(siteSettings, lookupNormalizer, passwordHasher));
            modelBuilder.ApplyConfiguration(new UserUsedPasswordConfiguration(siteSettings, passwordHasher));
// ...

پس از این تغییرات اگر دستور Migration زیر را صادر کنیم:
dotnet ef migrations --startup-project ../ASPNETCoreIdentitySample/ add V1
مقدار دهی اولیه‌ی نقش و کاربر ادمین و همچنین اتصال این دو را در فایل Migrations\ApplicationDbContextModelSnapshot.cs تولید شده، می‌توان مشاهده کرد.
  • #
    ‫۵ سال قبل، پنجشنبه ۱۴ شهریور ۱۳۹۸، ساعت ۲۰:۰۲
    برای مواردی که مقدارخروجی یک متد ثابت نباشد مثل مقدار هش شده پسورد یا مقدار NewGuid باعث می‌شود در هر مایگریشن جدید کوئری آپدیت ایجاد شود. بعد از ایجاد مایگریشن، مقادیر خروجی رو از فایل مایگریشن جایگزین متد ها کردم و متد‌ها رو کامنت کردم.
    • #
      ‫۵ سال قبل، جمعه ۱۵ شهریور ۱۳۹۸، ساعت ۰۲:۵۷
      با توجه به این نکته و همچنین مطلب «امکان رمزنگاری اطلاعات شخصی کاربران در ASP.NET» استفاده از روش HasData (مطلب جاری) برای کار با Identity اشتباه است و در برابر تغییرات آینده‌ی این کتابخانه مقاوم نیست. از همان روش امن SeedDatabaseWithAdminUserAsync باید استفاده شود.
      • #
        ‫۵ سال قبل، جمعه ۱۵ شهریور ۱۳۹۸، ساعت ۰۸:۱۳
        یک نکته دیگه،  اگر از متد ApplyConfigurationsFromAssembly که از نسخه 2.2 به بعد در دسترس است برای شناسایی IEntityTypeConfiguration استفاده کنیم، فقط کلاس هایی که سازنده اونها بدون پارامتر هستند شناسایی می‌شوند.    #
        سوال چطور می‌تونم داخل کلاس EntityTypeConfiguration به serviceProvider  دسترسی پیدا کنم؟
        • #
          ‫۵ سال قبل، جمعه ۱۵ شهریور ۱۳۹۸، ساعت ۱۱:۵۹
          - برای تزریق وابستگی‌ها طراحی نشده. زمانیکه از Activator.CreateInstance برای وهله سازی خودکار استفاده می‌کند، خارج از سیستم تزریق وابستگی‌ها عمل می‌کند.
          - یک روش آن همان کاری است که در این مثال انجام شده (به صورت دستی):
          protected override void OnModelCreating(ModelBuilder modelBuilder)
          {
             var passwordHasher =  this.GetService<IPasswordHasher<User>>();
  • #
    ‫۵ سال قبل، چهارشنبه ۲۰ شهریور ۱۳۹۸، ساعت ۰۰:۵۳
    پیاده سازی کلاس ApplicationDbContextFactory برای دریافت سرویس‌های موردنیاز چه صورت است؟