مطالب
پردازش‌های Async در Entity framework 6
اجرای Async اعمال نسبتا طولانی، در برنامه‌های مبتنی بر داده، عموما این مزایا را به همراه دارد:

الف) مقیاس پذیری سمت سرور

در اعمال سمت سرور متداول، تردهای متعددی جهت پردازش درخواست‌های کلاینت‌ها تدارک دیده می‌شوند. هر زمانیکه یکی از این تردها، یک عملیات blocking را انجام می‌دهد (مانند دسترسی به شبکه یا اعمال I/O)، ترد مرتبط با آن تا پایان کار این عملیات معطل خواهد شد. با بالا رفتن تعداد کاربران یک برنامه و در نتیجه بیشتر شدن تعداد درخواست‌هایی که سرور باید پردازش کند، تعداد تردهای معطل مانده نیز به همین ترتیب بیشتر خواهند شد. مشکل اصلی اینجا است که نمونه سازی تردها بسیار هزینه بر است (با اختصاص 1MB of virtual memory space) و منابع سرور محدود. با زیاد شدن تعداد تردهای معطل اعمال I/O یا شبکه، سرور مجبور خواهد شد بجای استفاده مجدد از تردهای موجود، تردهای جدیدی را ایجاد کند. همین مساله سبب بالا رفتن بیش از حد مصرف منابع و حافظه برنامه می‌گردد. یکی از روش‌های رفع این مشکل بدون نیاز به بهبودهای سخت افزاری، تبدیل اعمال blocking نامبرده شده به نمونه‌های non-blocking است. به این ترتیب ترد پردازش کننده‌ی این اعمال Async بلافاصله آزاد شده و سرور می‌تواند از آن جهت پردازش درخواست دیگری استفاده کند؛ بجای اینکه ترد جدیدی را وهله سازی نماید.

ب) بالا بردن پاسخ دهی کلاینت‌ها

کلاینت‌ها نیز اگر مدام درخواست‌های blocking را به سرور جهت دریافت پاسخی ارسال کنند، به زودی به یک رابط کاربری غیرپاسخگو خواهند رسید. برای رفع این مشکل نیز می‌توان از توانمندی‌های Async دات نت 4.5 جهت آزاد سازی ترد اصلی برنامه یا همان ترد UI استفاده کرد.

و ... تمام این‌ها یک شرط را دارند. نیاز است یک چنین API خاصی که اعمال Async واقعی را پشتیبانی می‌کنند، فراهم شده باشد. بنابراین صرفا وجود متد Task.Run، به معنای اجرای واقعی Async یک متد خاص نیست. برای این منظور ADO.NET 4.5 به همراه متدهای Async ویژه کار با بانک‌های اطلاعاتی است و پس از آن Entity framework 6 از این زیر ساخت استفاده کرده‌است که در ادامه جزئیات آن‌را بررسی خواهیم کرد.


پیشنیازها

برای کار با امکانات جدید Async موجود در EF 6 نیاز است از VS 2012 به بعد که به همراه کامپایلری است که واژه‌های کلیدی async و await را پشتیبانی می‌کند و همچنین دات نت 4.5 استفاده کرد. چون ADO.NET 4.5 اعمال async واقعی را پشتیبانی می‌کند، دات نت 4 در اینجا قابل استفاده نخواهد بود.


متدهای الحاقی جدید Async در EF 6.x

جهت متدهای الحاقی متداول EF مانند ToList، Max، Min و غیره، نمونه‌های Async آن‌ها نیز اضافه شده‌اند:
 QueryableExtensions:
AllAsync
AnyAsync
AverageAsync
ContainsAsync
CountAsync
FirstAsync
FirstOrDefaultAsync
ForEachAsync
LoadAsync
LongCountAsync
MaxAsync
MinAsync
SingleAsync
SingleOrDefaultAsync
SumAsync
ToArrayAsync
ToDictionaryAsync
ToListAsync

DbSet:
FindAsync

DbContext:
SaveChangesAsync

Database:
ExecuteSqlCommandAsync
بنابراین اولین قدم تبدیل کدهای قدیمی به Async، استفاده از متدهای الحاقی فوق است.


چند مثال


فرض کنید، مدل‌های برنامه، رابطه‌ی one-to-many ذیل را بین یک کاربر و مقالات او دارند:
    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public virtual ICollection<BlogPost> BlogPosts { get; set; }
    }

    public class BlogPost
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Content { get; set; }

        [ForeignKey("UserId")]
        public virtual User User { get; set; }
        public int UserId { get; set; }
    }
همچنین Context برنامه نیز جهت در معرض دید قرار دادن این کلاس‌ها، به نحو ذیل تشکیل شده‌است:
    public class MyContext : DbContext
    {
        public DbSet<User> Users { get; set; }
        public DbSet<BlogPost> BlogPosts { get; set; }

        public MyContext()
            : base("Connection1")
        {
            this.Database.Log = sql => Console.Write(sql);
        }
    }
بر این اساس مثالی که دو رکورد را در جداول کاربران و مقالات به صورت async ثبت می‌کند، به نحو ذیل خواهد بود:
        private async Task<User> addUserAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user = context.Users.Add(new User
                {
                    Name = "Vahid"
                });
                context.BlogPosts.Add(new BlogPost
                {
                    Content = "Test",
                    Title = "Test",
                    User = user
                });
                await context.SaveChangesAsync(cancellationToken);
                return user;
            }
        }

چند نکته جهت یادآوری مباحث Async

- به امضای متد واژه‌ی کلیدی async اضافه شده‌است، زیرا در بدنه‌ی آن از کلمه‌ی کلیدی await استفاده کرده‌ایم (لازم و ملزوم هستند).
- به انتهای نام متد، کلمه‌ی Async اضافه شده‌است. این مورد ضروری نیست؛ اما به یک استاندارد و قرارداد تبدیل شده‌است.
- مدل Async دات نت 4.5 مبتنی بر Taskها است. به همین جهت اینبار خروجی‌های توابع نیاز است از نوع Task باشند و آرگومان جنریک آن‌ها، بیانگر نوع مقداری که باز می‌گردانند.
- تمام متدهای الحاقی جدیدی که نامبرده شدند، دارای پارامتر اختیاری لغو عملیات نیز هستند. این مورد را با مقدار دهی cancellationToken در کدهای فوق ملاحظه می‌کنید.
نمونه‌ای از نحوه‌ی مقدار دهی این پارامتر در ASP.NET MVC به صورت زیر می‌تواند باشد:
 [AsyncTimeout(8000)]
public async Task<ActionResult> Index(CancellationToken cancellationToken)
در اینجا به امضای اکشن متد جاری، async اضافه شده‌است و خروجی آن نیز به نوع Task تغییر یافته است. همچنین یک پارامتر cancellationToken نیز تعریف شده‌است. این پارامتر به صورت خودکار توسط ASP.NET MVC پس از زمانیکه توسط ویژگی AsyncTimeout تعیین شده‌است، تنظیم خواهد شد. به این ترتیب، اعمال async در حال اجرا به صورت خودکار لغو می‌شوند.
- برای اجرا و دریافت نتیجه‌ی متدهای Async دار EF، نیاز است از واژه‌ی کلیدی await استفاده گردد.

استفاده کننده نیز می‌تواند متد addUserAsync را به صورت زیر فراخوانی کند:
 var user = await addUserAsync();
Console.WriteLine("user id: {0}", user.Id);

شبیه به همین اعمال را نیز جهت به روز رسانی و یا حذف اطلاعات خواهیم داشت:
        private async Task<User> updateAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user1 = await context.Users.FindAsync(cancellationToken, 1);
                if (user1 != null)
                    user1.Name = "Vahid N.";

                await context.SaveChangesAsync(cancellationToken);
                return user1;
            }
        }

        private async Task<int> deleteAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var user1 = await context.Users.FindAsync(cancellationToken, 1);
                if (user1 != null)
                    context.Users.Remove(user1);

                return await context.SaveChangesAsync(cancellationToken);
            }
        }

کدهای Async تقلبی!

به قطعه کد ذیل دقت کنید:
         public async Task<List<TEntity>> GetAllAsync()
   {
    return await Task.Run(() => _tEntities.ToList());
   }
این متد از یکی از Generic repositoryهای فله‌ای رها شده در اینترنت انتخاب شده‌است.
به این نوع متدها که از Task.Run برای فراخوانی متدهای همزمان قدیمی مانند ToList جهت Async جلوه دادن آن‌ها استفاده می‌شود، کدهای Async تقلبی می‌گویند! این عملیات هر چند در یک ترد دیگر انجام می‌شود اما هم سربار ایجاد یک ترد جدید را به همراه دارد و هم عملیات ToList آن کاملا blocking است.
معادل صحیح Async واقعی این عملیات را در ذیل مشاهده می‌کنید:
        private async Task<List<User>> getUsersAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                return await context.Users.ToListAsync(cancellationToken);
            }
        }
متد ToListAsync یک متد Async واقعی است و نه شبیه سازی شده توسط Task.Run. متدهای Async واقعی کار با شبکه و اعمال I/O، از ترد استفاده نمی‌کنند و توسط سیستم عامل به نحو بسیار بهینه‌ای اجرا می‌گردند.
برای مثال پشت صحنه‌ی متد الحاقی SaveChangesAsync به یک چنین متدی ختم می‌شود:
 internal override async Task<long> ExecuteAsync(
//...
rowsAffected = await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(continueOnCapturedContext: false);
//...
متد ExecuteNonQueryAsync جزو متدهای ADO.NET 4.5 است و برای اجرا نیاز به هیچ ترد جدیدی ندارد.
و یا برای شبیه سازی ToListAsync با ADO.NET 4.5 و استفاده از متدهای Async واقعی آن، به یک چنین کدهایی نیاز است: 
    var connectionString = "........";
    var sql = @"......"";
    var users = new List<User>();
 
    using (var cnx = new SqlConnection(connectionString))
    {
      using (var cmd = new SqlCommand(sql, cnx))
      {
       await cnx.OpenAsync(); 
       using (var reader = await cmd.ExecuteReaderAsync(CommandBehavior.CloseConnection))
       {
        while (await reader.ReadAsync())
        {
          var user = new User
          {
           Id = reader.GetInt32(0), 
           Name = reader.GetString(1), 
          };
         users.Add(user);
        }
       }
      }
    }


محدودیت پردازش موازی اعمال در EF

در متد ذیل، دو Task غیرهمزمان تعریف شده‌اند و سپس با await Task.WhenAll درخواست اجرای همزمان و موازی آن‌ها را کرده‌ایم:
        // multiple operations
        private static async Task loadAllAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            using (var context = new MyContext())
            {
                var task1 = context.Users.ToListAsync(cancellationToken);
                var task2 = context.BlogPosts.ToListAsync(cancellationToken);

                await Task.WhenAll(task1, task2);
                // use task1.Result
            }
        }
این متد ممکن است اجرا شود؛ یا در بعضی از مواقع با استثنای ذیل خاتمه یابد:
  An unhandled exception of type 'System.NotSupportedException' occurred in mscorlib.dll
 Additional information: A second operation started on this context before a previous asynchronous operation completed.
Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context.
Any instance members are not guaranteed to be thread safe.
متن استثنای ارائه شده بسیار مفید است و توضیحات کامل را به همراه دارد. در EF در طی یک Context اگر عملیات Async شروع شده‌ای خاتمه نیافته باشد، مجاز به شروع یک عملیات Async دیگر، به موازت آن نخواهیم بود. برای رفع این مشکل یا باید از چندین Context استفاده کرد و یا await Task.WhenAll را حذف کرده و بجای آن واژه‌ی کلیدی await را همانند معمول، جهت صبر کردن برای دریافت نتیجه‌ی یک عملیات غیرهمزمان استفاده کنیم.
نظرات مطالب
معرفی ASP.NET Identity
کلاس کاربر:
public class AppUser : IdentityUser
{
    public string Email { get; set; }
    public string ConfirmationToken { get; set; }
    public bool IsConfirmed { get; set; }

    public virtual UserProfile Profile { get; set; }
}

کلاس پروفایل کاربر:
public class UserProfile
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public DateTime? Birthday { get; set; }

    public byte[] Avatar { get; set; }
}

کلاس کانتکست دیتابیس:
public class SampleDbContext : IdentityDbContext
{
    public SampleDbContext() : base("DefaultConnection") { }

    static SampleDbContext()
    {
        Database.SetInitializer(new DropCreateDatabaseIfModelChanges<SampleDbContext>());
    }

    public DbSet<UserProfile> UserProfiles { get; set; }
    public DbSet<Customer> Customers { get; set; }
    public DbSet<Product> Products { get; set; }
    ...
}


این کلاس‌ها می‌تونن تو لایه دیگری مثل Domain Models تعریف بشن.
مطالب
EF Code First #2

در قسمت قبل با تنظیمات و قراردادهای ابتدایی EF Code first آشنا شدیم، هرچند این تنظیمات حجم کدنویسی ابتدایی راه اندازی سیستم را به شدت کاهش می‌دهند، اما کافی نیستند. در این قسمت نگاهی سطحی و مقدماتی خواهیم داشت بر امکانات مهیا جهت تنظیم ویژگی‌های مدل‌های برنامه در EF Code first.

تنظیمات EF Code first توسط اعمال متادیتای خواص

اغلب متادیتای مورد نیاز جهت اعمال تنظیمات EF Code first در اسمبلی System.ComponentModel.DataAnnotations.dll قرار دارند. بنابراین اگر مدل‌های خود را در اسمبلی و پروژه class library جداگانه‌ای تعریف و نگهداری می‌کنید (مثلا به نام DomainClasses)، نیاز است ابتدا ارجاعی را به این اسمبلی به پروژه جاری اضافه نمائیم. همچنین تعدادی دیگر از متادیتای قابل استفاده در خود اسمبلی EntityFramework.dll قرار دارند. بنابراین در صورت نیاز باید ارجاعی را به این اسمبلی نیز اضافه نمود.
همان مثال قبل را در اینجا ادامه می‌دهیم. دو کلاس Blog و Post در آن تعریف شده (به این نوع کلاس‌ها POCO – the Plain Old CLR Objects نیز گفته می‌شود)، به همراه کلاس Context که از کلاس DbContext مشتق شده است. ابتدا دیتابیس قبلی را دستی drop کنید. سپس در کلاس Blog، خاصیت public int Id را مثلا به public int MyTableKey تغییر دهید و پروژه را اجرا کنید. برنامه بلافاصله با خطای زیر متوقف می‌شود:

One or more validation errors were detected during model generation:
\tSystem.Data.Entity.Edm.EdmEntityType: : EntityType 'Blog' has no key defined.

زیرا EF Code first در این کلاس خاصیتی به نام Id یا BlogId را نیافته‌است و امکان تشکیل Primary key جدول را ندارد. برای رفع این مشکل تنها کافی است ویژگی Key را به این خاصیت اعمال کنیم:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }

همچنین تعدادی ویژگی دیگر مانند MaxLength و Required را نیز می‌توان بر روی خواص کلاس اعمال کرد:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
public class Blog
{
[Key]
public int MyTableKey { set; get; }

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

[Required]
public string AuthorName { set; get; }

public IList<Post> Posts { set; get; }
}
}

این ویژگی‌ها دو مقصود مهم را برآورده می‌سازند:
الف) بر روی ساختار بانک اطلاعاتی تشکیل شده تاثیر دارند:

CREATE TABLE [dbo].[Blogs](
[MyTableKey] [int] IDENTITY(1,1) NOT NULL,
[Title] [nvarchar](100) NULL,
[AuthorName] [nvarchar](max) NOT NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY CLUSTERED
(
[MyTableKey] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

همانطور که ملاحظه می‌کنید در اینجا طول فیلد Title به 100 تنظیم شده است و همچنین فیلد AuthorName اینبار NOT NULL است. به علاوه primary key نیز بر اساس ویژگی Key اعمالی تعیین شده است.
البته برای اجرای کدهای تغییر کرده مدل، فعلا بانک اطلاعاتی قبلی را دستی می‌توان حذف کرد تا بتوان به ساختار جدید رسید. در مورد جزئیات مبحث DB Migration در قسمت‌های بعدی مفصلا بحث خواهد شد.

ب) اعتبار سنجی اطلاعات پیش از ارسال کوئری به بانک اطلاعاتی
برای مثال اگر در حین تعریف وهله‌ای از کلاس Blog، خاصیت AuthorName مقدار دهی نگردد، پیش از اینکه رفت و برگشتی به بانک اطلاعاتی صورت گیرد، یک validation error را دریافت خواهیم کرد. یا برای مثال اگر طول اطلاعات خاصیت Title بیش از 100 حرف باشد نیز مجددا در حین ثبت اطلاعات، یک استثنای اعتبار سنجی را مشاهده خواهیم کرد. البته امکان تعریف پیغام‌های خطای سفارشی نیز وجود دارد. برای این حالت تنها کافی است پارامتر ErrorMessage این ویژگی‌ها را مقدار دهی کرد. برای مثال:
[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }

نکته‌ی مهمی که در اینجا وجود دارد، وجود یک اکوسیستم هماهنگ و سازگار است. این نوع اعتبار سنجی هم با EF Code first هماهنگ است و هم برای مثال در ASP.NET MVC به صورت خودکار جهت اعتبار سنجی سمت سرور و کلاینت یک مدل می‌تواند مورد استفاده قرار گیرد و مفاهیم و روش‌های مورد استفاده در آن نیز یکی است.


تنظیمات EF Code first به کمک Fluent API

اگر علاقمند به استفاده از متادیتا، جهت تعریف قیود و ویژگی‌های خواص کلاس‌های مدل خود نیستید، روش دیگری نیز در EF Code first به نام Fluent API تدارک دیده شده است. در اینجا امکان تعریف همان ویژگی‌ها توسط کدنویسی نیز وجود دارد، به علاوه اعمال قیود دیگری که توسط متادیتای مهیا قابل تعریف نیستند.
محل تعریف این قیود، کلاس Context که از کلاس DbContext مشتق شده است، می‌باشد و در اینجا، کار با تحریف متد OnModelCreating شروع می‌شود:

using System.Data.Entity;
using EF_Sample01.Models;

namespace EF_Sample01
{
public class Context : DbContext
{
public DbSet<Blog> Blogs { set; get; }
public DbSet<Post> Posts { set; get; }

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>().HasKey(x => x.MyTableKey);
modelBuilder.Entity<Blog>().Property(x => x.Title).HasMaxLength(100);
modelBuilder.Entity<Blog>().Property(x => x.AuthorName).IsRequired();

base.OnModelCreating(modelBuilder);
}
}
}

به کمک پارامتر modelBuilder، امکان دسترسی به متدهای تنظیم کننده ویژگی‌های خواص یک مدل یا موجودیت وجود دارد. در اینجا چون می‌توان متدها را به صورت یک زنجیره به هم متصل کرد و همچنین حاصل نهایی شبیه به جمله بندی انگلیسی است، به آن Fluent API یا API روان نیز گفته می‌شود.
البته در این حالت امکان تعریف ErrorMessage وجود ندارد و برای این منظور باید از همان data annotations استفاده کرد.


نحوه مدیریت صحیح تعاریف نگاشت‌ها به کمک Fluent API

OnModelCreating محل مناسبی جهت تعریف حجم انبوهی از تنظیمات کلاس‌های مختلف مدل‌های برنامه نیست. در حد سه چهار سطر مشکلی ندارد اما اگر بیشتر شد بهتر است از روش زیر استفاده شود:

using System.Data.Entity;
using EF_Sample01.Models;
using System.Data.Entity.ModelConfiguration;

namespace EF_Sample01
{
public class BlogConfig : EntityTypeConfiguration<Blog>
{
public BlogConfig()
{
this.Property(x => x.Id).HasColumnName("MyTableKey");
this.Property(x => x.RowVersion).HasColumnType("Timestamp");
}
}


با ارث بری از کلاس EntityTypeConfiguration،‌ می‌توان به ازای هر کلاس مدل، تنظیمات را جداگانه انجام داد. به این ترتیب اصل SRP یا Single responsibility principle نقض نخواهد شد. سپس برای استفاده از این کلاس‌های Config تک مسئولیتی به نحو زیر می‌توان اقدام کرد:

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new BlogConfig());




نحوه تنظیمات ابتدایی نگاشت کلاس‌ها به بانک اطلاعاتی در EF Code first

الزامی ندارد که EF Code first حتما با یک بانک اطلاعاتی از نو تهیه شده بر اساس پیش فرض‌های آن کار کند. در اینجا می‌توان از بانک‌های اطلاعاتی موجود نیز استفاده کرد. اما در این حالت نیاز خواهد بود تا مثلا نام جدولی خاص با کلاسی مفروض در برنامه، یا نام فیلدی خاص که مطابق استانداردهای نامگذاری خواص در سی شارپ تعریف نشده، با خاصیتی در یک کلاس تطابق داده شوند. برای مثال اینبار تعاریف کلاس Blog را به نحو زیر تغییر دهید:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace EF_Sample01.Models
{
[Table("tblBlogs")]
public class Blog
{
[Column("MyTableKey")]
public int Id { set; get; }

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

[Required(ErrorMessage = "لطفا نام نویسنده را مشخص نمائید")]
public string AuthorName { set; get; }

public IList<Post> Posts { set; get; }

[Timestamp]
public byte[] RowVersion { set; get; }
}
}

در اینجا فرض بر این است که نام جدول متناظر با کلاس Blog در بانک اطلاعاتی مثلا tblBlogs است و نام خاصیت Id در بانک اطلاعاتی مساوی فیلدی است به نام MyTableKey. چون نام خاصیت را مجددا به Id تغییر داده‌ایم، دیگر ضرورتی به ذکر ویژگی Key وجود نداشته است. برای تعریف این دو از ویژگی‌های Table و Column جهت سفارشی سازی نام‌های خواص و کلاس استفاده شده است.
یا اگر در کلاس خود خاصیتی محاسبه شده بر اساس سایر خواص، تعریف شده است و قصد نداریم آن‌را به فیلدی در بانک اطلاعاتی نگاشت کنیم، می‌توان از ویژگی NotMapped برای مزین سازی و تعریف آن کمک گرفت.
به علاوه اگر از نام پیش فرض کلید خارجی تشکیل شده خرسند نیستید می‌توان به کمک ویژگی ForeignKey، نسبت به تعریف مقداری جدید مطابق تعاریف یک بانک اطلاعاتی موجود، اقدام کرد.
همچنین خاصیت دیگری به نام RowVersion در اینجا اضافه شده که با ویژگی TimeStamp مزین گردیده است. از این خاصیت ویژه برای بررسی مسایل همزمانی ثبت اطلاعات در EF استفاده می‌شود. به علاوه بانک اطلاعاتی می‌تواند به صورت خودکار آن‌را در حین ثبت مقدار دهی کند.
تمام این تغییرات را به کمک Fluent API نیز می‌توان انجام داد:

modelBuilder.Entity<Blog>().ToTable("tblBlogs");
modelBuilder.Entity<Blog>().Property(x => x.Id).HasColumnName("MyTableKey");
modelBuilder.Entity<Blog>().Property(x => x.RowVersion).HasColumnType("Timestamp");



تبدیل پروژه‌های قدیمی EF به کلاس‌های EF Code first به صورت خودکار

روش متداول کار با EF از روز اول آن، مهندسی معکوس خودکار اطلاعات یک بانک اطلاعاتی و تبدیل آن به یک فایل EDMX بوده است. هنوز هم می‌توان از این روش در اینجا نیز بهره جست. برای مثال اگر قصد دارید یک پروژه قدیمی را تبدیل به نمونه جدید Code first کنید، یا یک بانک اطلاعاتی موجود را مهندسی معکوس کنید، بر روی پروژه در Solution explorer کلیک راست کرده و گزینه Add|New Item را انتخاب کنید. سپس از صفحه ظاهر شده، ADO.NET Entity data model را انتخاب کرده و در ادامه گزینه «Generate from database» را انتخاب کنید. این روال مرسوم کار با EF Database first است.
پس از اتمام کار به entity data model designer مراجعه کرده و بر روی صفحه کلیک راست نمائید. از منوی ظاهر شده گزینه «Add code generation item» را انتخاب کنید. سپس در صفحه باز شده از لیست قالب‌های موجود، گزینه «ADO.NET DbContext Generator» را انتخاب نمائید. این گزینه به صورت خودکار اطلاعات فایل EDMX قدیمی یا موجود شما را تبدیل به کلاس‌های مدل Code first معادل به همراه کلاس DbContext معرف آن‌ها خواهد کرد.

روش دیگری نیز برای انجام اینکار وجود دارد. نیاز است افزونه‌ی به نام Entity Framework Power Tools را دریافت کنید. پس از نصب، از منوی Entity Framework آن گزینه‌ی «Reverse Engineer Code First» را انتخاب نمائید. در اینجا می‌توان مشخصات اتصال به بانک اطلاعاتی را تعریف و سپس نسبت به تولید خودکار کدهای مدل‌ها و DbContext مرتبط اقدام کرد.



استراتژی‌های مقدماتی تشکیل بانک اطلاعاتی در EF Code first

اگر مثال این سری را دنبال کرده باشید، مشاهده کرده‌اید که با اولین بار اجرای برنامه، یک بانک اطلاعاتی پیش فرض نیز تولید خواهد شد. یا اگر تعاریف ویژگی‌های یک فیلد را تغییر دادیم، نیاز است تا بانک اطلاعاتی را دستی drop کرده و اجازه دهیم تا بانک اطلاعاتی جدیدی بر اساس تعاریف جدید مدل‌ها تشکیل شود که ... هیچکدام از این‌ها بهینه نیستند.
در اینجا دو استراتژی مقدماتی را در حین آغاز یک برنامه می‌توان تعریف کرد:

System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseIfModelChanges<Context>());
// or
System.Data.Entity.Database.SetInitializer(new DropCreateDatabaseAlways<Context>());

می‌توان بانک اطلاعاتی را در صورت تغییر اطلاعات یک مدل به صورت خودکار drop کرده و نسبت به ایجاد نمونه‌ای جدید اقدام کرد (DropCreateDatabaseIfModelChanges)؛ یا در حین آزمایش برنامه همیشه (DropCreateDatabaseAlways) با شروع برنامه، ابتدا باید بانک اطلاعاتی drop شده و سپس نمونه جدیدی تولید گردد.
محل فراخوانی این دستور هم باید در نقطه آغازین برنامه، پیش از وهله سازی اولین DbContext باشد. مثلا در برنامه‌های وب در متد Application_Start فایل global.asax.cs یا در برنامه‌های WPF در متد سازنده کلاس App می‌توان بانک اطلاعاتی را آغاز نمود.
البته الزامی به استفاده از کلاس‌های DropCreateDatabaseIfModelChanges یا DropCreateDatabaseAlways وجود ندارد. می‌توان با پیاده سازی اینترفیس IDatabaseInitializer از نوع کلاس Context تعریف شده در برنامه، همان عملیات را شبیه سازی کرد یا سفارشی نمود:

public class MyInitializer : IDatabaseInitializer<Context>
{
public void InitializeDatabase(Context context)
{
if (context.Database.Exists() ||
context.Database.CompatibleWithModel(throwIfNoMetadata: false))
context.Database.Delete();

context.Database.Create();
}
}

سپس برای استفاده از این کلاس در ابتدای برنامه، خواهیم داشت:

System.Data.Entity.Database.SetInitializer(new MyInitializer());


نکته:
اگر از یک بانک اطلاعاتی موجود استفاده می‌کنید (محیط کاری) و نیازی به پیش فرض‌های EF Code first ندارید و همچنین این بانک اطلاعاتی نیز نباید drop شود یا تغییر کند، می‌توانید تمام این پیش فرض‌ها را با دستور زیر غیرفعال کنید:

Database.SetInitializer<Context>(null);

بدیهی است این دستور نیز باید پیش از ایجاد اولین وهله از شیء DbContext فراخوانی شود.


همچنین باید درنظر داشت که در آخرین نگارش‌های پایدار EF Code first، این موارد بهبود یافته‌اند و مبحثی تحت عنوان DB Migration ایجاد شده است تا نیازی نباشد هربار بانک اطلاعاتی drop شود و تمام اطلاعات از دست برود. می‌توان صرفا تغییرات کلاس‌ها را به بانک اطلاعاتی اعمال کرد که به صورت جداگانه، در قسمتی مجزا بررسی خواهد شد. به این ترتیب دیگر نیازی به drop بانک اطلاعاتی نخواهد بود. به صورت پیش فرض در صورت از دست رفتن اطلاعات یک استثناء را سبب خواهد شد (که توسط برنامه نویس قابل تنظیم است) و در حالت خودکار یا دستی با تنظیمات ویژه قابل اعمال است.



تنظیم استراتژی‌های آغاز بانک اطلاعاتی در فایل کانفیگ برنامه

الزامی ندارد که حتما متد Database.SetInitializer را دستی فراخوانی کنیم. با اندکی تنظیم فایل‌های app.config و یا web.config نیز می‌توان نوع استراتژی مورد استفاده را تعیین کرد:

<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="MyNamespace.MyInitializerClass, MyAssembly" />
</appSettings>

<appSettings>
<add key="DatabaseInitializerForType MyNamespace.MyDbContextClass, MyAssembly"
value="Disabled" />
</appSettings>

یکی از دو حالت فوق باید در قسمت appSettings فایل کانفیگ برنامه تنظیم شود. حالت دوم برای غیرفعال کردن پروسه آغاز بانک اطلاعاتی و اعمال تغییرات به آن، بکار می‌رود.
برای نمونه در مثال جاری، جهت استفاده از کلاس MyInitializer فوق، می‌توان از تنظیم زیر نیز استفاده کرد:

<appSettings>
<add key="DatabaseInitializerForType EF_Sample01.Context, EF_Sample01"
value="EF_Sample01.MyInitializer, EF_Sample01" />
</appSettings>



اجرای کدهای ویژه در حین تشکیل یک بانک اطلاعاتی جدید

امکان سفارشی سازی این آغاز کننده‌های پیش فرض نیز وجود دارد. برای مثال:

public class MyCustomInitializer : DropCreateDatabaseIfModelChanges<Context>
{
protected override void Seed(Context context)
{
context.Blogs.Add(new Blog { AuthorName = "Vahid", Title = ".NET Tips" });
context.Database.ExecuteSqlCommand("CREATE INDEX IX_title ON tblBlogs (title)");
base.Seed(context);
}
}

در اینجا با ارث بری از کلاس DropCreateDatabaseIfModelChanges یک آغاز کننده سفارشی را تعریف کرده‌ایم. سپس با تحریف متد Seed آن می‌توان در حین آغاز یک بانک اطلاعاتی، تعدادی رکورد پیش فرض را به آن افزود. کار ذخیره سازی نهایی در متد base.Seed انجام می‌شود.
برای استفاده از آن اینبار در حین فراخوانی متد System.Data.Entity.Database.SetInitializer، از کلاس MyCustomInitializer استفاده خواهیم کرد.
و یا توسط متد context.Database.ExecuteSqlCommand می‌توان دستورات SQL را مستقیما در اینجا اجرا کرد. عموما دستوراتی در اینجا مدنظر هستند که توسط ORMها پشتیبانی نمی‌شوند. برای مثال تغییر collation یک ستون یا افزودن یک ایندکس و مواردی از این دست.


سطح دسترسی مورد نیاز جهت فراخوانی متد Database.SetInitializer

استفاده از متدهای آغاز کننده بانک اطلاعاتی نیاز به سطح دسترسی بر روی بانک اطلاعاتی master را در SQL Server دارند (زیرا با انجام کوئری بر روی این بانک اطلاعاتی مشخص می‌شود، آیا بانک اطلاعاتی مورد نظر پیشتر تعریف شده است یا خیر). البته این مورد حین کار با SQL Server CE شاید اهمیتی نداشته باشد. بنابراین اگر کاربری که با آن به بانک اطلاعاتی متصل می‌شویم سطح دسترسی پایینی دارد نیاز است Persist Security Info=True را به رشته اتصالی اضافه کرد. البته این مورد را پس از انجام تغییرات بر روی بانک اطلاعاتی جهت امنیت بیشتر حذف کنید (یا به عبارتی در محیط کاری Persist Security Info=False باید باشد).

Server=(local);Database=yourDatabase;User ID=yourDBUser;Password=yourDBPassword;Trusted_Connection=False;Persist Security Info=True


تعیین Schema و کاربر فراخوان دستورات SQL

در EF Code first به صورت پیش فرض همه چیز بر مبنای کاربری با دسترسی مدیریتی یا dbo schema در اس کیوال سرور تنظیم شده است. اما اگر کاربر خاصی برای کار با دیتابیس تعریف گردد که در هاست‌های اشتراکی بسیار مرسوم است، دیگر از دسترسی مدیریتی dbo خبری نخواهد بود. اینبار نام جداول ما بجای dbo.tableName مثلا someUser.tableName می‌باشند و عدم دقت به این نکته، اجرای برنامه را غیرممکن می‌سازد.
برای تغییر و تعیین صریح کاربر متصل شده به بانک اطلاعاتی اگر از متادیتا استفاده می‌کنید، روش زیر باید بکارگرفته شود:

[Table("tblBlogs", Schema="someUser")]    
public class Blog

و یا در حالت بکارگیری Fluent API به نحو زیر قابل تنظیم است:

modelBuilder.Entity<Blog>().ToTable("tblBlogs", schemaName:"someUser");






مطالب
اعتبارسنجی مبتنی بر کوکی‌ها در 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 نیز می‌تواند مفید باشد و به همین نحو کار می‌کنند.



کدهای کامل این مطلب را از اینجا می‌توانید دریافت کنید.
مطالب
Claim Based Identity
بنده در حال توسعه‌ی یک CMS هستم و این کار را برای یادگیری MVC انجام میدم. الان هم تقریبا رسیدم به اواخر کار و انشالله اگه کار تمام شد، نرم افزار را به صورت سورس باز منتشر می‌کنم. الان رسیدم به قسمت مدیریت کاربران. همانطور که می‌دانید ASP.NET در نسخه‌های جدید خودش بر خلاف نسخه‌های قدیمی که از SQL Membership استفاده می‌کرد الان از سیستم Identity بهره می‌برد، که انشالله در نوشتارهای بعدی به موضوع Identity به تفصیل خواهیم پرداخت. در حقیقت  سیستم Identity یک نوع Claim Based Identity هست. اما حالا ببینیم که این Claim چی هست؟ اما قبل از آن یک سری اصطلاحات را که به درک بهتر مفهوم کمک می‌کند، باید تعریف کنیم:

Relying Party (RP) = Application

Service Providers (SP) = Application 

RP  یا SP یک Application می‌باشد که از Claim استفاده می‌کند. واژه‌ی Relying Party بدین دلیل انتخاب شده که Application روی یک Issuer به منظور تامین اطلاعات در مورد یک Identity تکیه می‌کند.

Subject = User

Principal = User

واژه‌ی Subject یا Principal در حقیقت یک User می‌باشد. این واژه زمانی معنا پیدا می‌کند که شما به User به عنوان یک Subject برای کنترل دسترسی، شخصی سازی (Personalization) و ... بنگرید. در Net Framework. به جای Subject از Principal استفاده می‌شود.

Security Token Service (STC) = Issuer

از دید فنی، STC یک رابط درون یک Issuer می‌باشد که درخواست‌ها (Request) را تأیید و اقدام به ساخت Issues Security Token (توکن‌های مسائل امنیتی) می‌نماید. توکن‌های مسائل امنیتی شامل یک سری Claim می‌باشند.

Identity Provider (IDP) = Issuer 

تامین کننده Identity یک Issuer یا Token Issuer می‌باشد که وظیفه‌ی تعیین اعتبار (Validation) کاربر مانند نام کاربری، رمز عبور و ... را بر عهده دارد.


Active Client = Smart or rich Client

Passive Client = Browser 

یک Active Client می‌تواند از یک کتابخانه‌ی پیچیده مانند WCF به منظور پیاده سازی پروتکل‌هایی که بتوانند یک Security Token را درخواست و پاس دهند، استفاده نمایند. به منظور پشتیبانی از مرورگرهای مختلف، سناریوهای passive از پروتکل‌های ساده‌تری به منظور درخواست و پاس دادن یک Security Token که بر روی پروتکل Http تکیه دارد استفاده می‌کند (Http Post - Http Get).


و اما Claim چیست؟

در حقیقت Claim عبارتست از یک بیانیه یا شرح که یک Subject در مورد خودش یا Subject دیگری می‌سازد. این بیانیه می‌تواند در مورد یک نام، هویت، کلید، گروه، حق دسترسی و یا یک قابلیت باشد. Claim‌ها بوسیله یک Provider صادر و سپس بسته بندی (Package) شده و بوسیله‌ی یک Issuer صادر می‌شوند که این Issuer عموما با نام Security Token Service شناخته می‌شود. 

به منظور آشنایی با این مبحث میتوانید به اینجا و اینجا مراجعه نمایید.

این نوشتار مقدمه‌ای بود بر مباحث ASP.NET Identity. بنده در حال ترجمه‌ی سه فصل آخر کتاب Pro ASP.NET Mvc 5 Platform که اختصاص به مبحث Identity دارد هستم. فصل 13 تقریبا تمام شده و انشالله بزودی آن‌را منتشر میکنم.

نظرات مطالب
سفارشی سازی ASP.NET Core Identity - قسمت دوم - سرویس‌های پایه
ارتقاء به ASP.NET Core Identity 3.0  

امضای سازنده‌ی تعدادی از سرویس‌های توکار ASP.NET Core Identity 3.0 تغییر کرده و شامل پارامترهای جدیدی به صورت زیر هستند:
    public class ApplicationSignInManager :
        SignInManager<User>,
        IApplicationSignInManager
    {
   // ...

        public ApplicationSignInManager(
            IApplicationUserManager userManager,
            IHttpContextAccessor contextAccessor,
            IUserClaimsPrincipalFactory<User> claimsFactory,
            IOptions<IdentityOptions> optionsAccessor,
            ILogger<ApplicationSignInManager> logger,
            IAuthenticationSchemeProvider schemes,
            IUserConfirmation<User> confirmation)
            : base((UserManager<User>)userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation)
        {

    public class ApplicationUserManager :
        UserManager<User>,
        IApplicationUserManager
    {
   // ...

        public ApplicationUserManager(
            IApplicationUserStore store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<User> passwordHasher,
            IEnumerable<IUserValidator<User>> userValidators,
            IEnumerable<IPasswordValidator<User>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IServiceProvider services,
            ILogger<ApplicationUserManager> logger,
            IHttpContextAccessor contextAccessor,
            IUnitOfWork uow,
            IUsedPasswordsService usedPasswordsService)
            : base(
                (UserStore<User, Role, ApplicationDbContext, int, UserClaim, UserRole, UserLogin, UserToken, RoleClaim>)store,
                  optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
        {

    public class ConfirmEmailDataProtectorTokenProvider<TUser> : DataProtectorTokenProvider<TUser> where TUser : class
    {
        public ConfirmEmailDataProtectorTokenProvider(
            IDataProtectionProvider dataProtectionProvider,
            IOptions<ConfirmEmailDataProtectionTokenProviderOptions> options,
            ILogger<DataProtectorTokenProvider<TUser>> logger)
            : base(dataProtectionProvider, options, logger)
        {

    public class CustomSecurityStampValidator : SecurityStampValidator<User>
    {
   // ...
   
        public CustomSecurityStampValidator(
            IOptions<SecurityStampValidatorOptions> options,
            IApplicationSignInManager signInManager,
            ISystemClock clock,
            ISiteStatService siteStatService,
            ILoggerFactory logger)
            : base(options, (SignInManager<User>)signInManager, clock, logger)
        {
مطالب
نمایش بلادرنگ اعلامی به تمام کاربران در هنگام درج یک رکورد جدید
در ادامه می‌خواهیم اعلام عمومی نمایش افزوده شدن یک پیام جدید را بعد از ثبت رکوردی جدید، به تمامی کاربران متصل به سیستم ارسال کنیم. پیش نیاز مطلب جاری موارد زیر می‌باشند:
namespace ShowAlertSignalR.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Title { get; set; }
        public string Description { get; set; }
        public float Price { get; set; }
        public Category Category { get; set; }

    }

    public enum Category
    {
        [Display(Name = "دسته بندی اول")]
        Cat1,
        [Display(Name = "دسته بندی دوم")]
        Cat2,
        [Display(Name = "دسته بندی سوم")]
        Cat3
    }
}
در اینجا مدل ما شامل عنوان، توضیح، قیمت و یک enum برای دسته‌بندی یک محصول ساده می‌باشد.
کلاس context نیز به صورت زیر می‌باشد:
namespace ShowAlertSignalR.Models
{
    public class ProductDbContext : DbContext
    {
        public ProductDbContext() : base("productSample")
        {
            Database.Log = sql => Debug.Write(sql);
        }
        public DbSet<Product> Products { get; set; }
    }
}
همانطور که در ابتدا عنوان شد، می‌خواهیم بعد از ثبت یک رکورد جدید، پیامی عمومی به تمامی کاربران متصل به سایت نمایش داده شود. در کد زیر اکشن متد Create را مشاهده می‌کنید: 
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index");
            }

            return View(product);
        }
می‌توانیم از ViewBag برای اینکار استفاده کنیم؛ به طوریکه یک پارامتر از نوع bool برای متد Index تعریف کرده و سپس مقدار آن را درون این شیء ViewBag انتقال دهیم، این متغییر بیانگر حالتی است که آیا اطلاعات جدیدی برای نمایش وجود دارد یا خیر؟ بنابراین اکشن متد Index را به اینصورت تعریف می‌کنیم:
public ActionResult Index(bool notifyUsers = false)
        {
            ViewBag.NotifyUsers = notifyUsers;
            return View(db.Products.ToList());
        }
در اینجا مقدار پیش‌فرض این متغیر، false می‌باشد. یعنی اطلاعات جدیدی برای نمایش موجود نمی‌باشد. در نتیجه اکشن متد Create را به صورتی تغییر می‌دهیم که بعد از درج رکورد موردنظر و هدایت کاربر به صفحه‌ی Index، مقدار این متغییر به true تنظیم شود:
[HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create(Product product)
        {
            if (ModelState.IsValid)
            {
                db.Products.Add(product);
                db.SaveChanges();
                return RedirectToAction("Index", new { notifyUsers = true });
            }

            return View(product);
        }
قدم بعدی ایجاد یک هاب SignalR می‌باشد:
namespace ShowAlertSignalR.Hubs
{
    public class NotificationHub : Hub
    {
        public void SendNotification()
        {
            Clients.Others.ShowNotification();
        }
    }
}
در ادامه کدهای سمت کلاینت را برای هاب فوق، داخل ویوی Index اضافه می‌کنیم:
@section scripts
{
    
    <script src="~/Scripts/jquery.signalR-2.0.2.min.js"></script>
    <script src="~/signalr/hubs"></script>
    <script>

        var notify = $.connection.notificationHub;
        notify.client.showNotification = function() {
            $('#result').append("<div class='alert alert-info alert-dismissable'>" +
                "<button type='button' class='close' data-dismiss='alert' aria-hidden='true'>&times;</button>" +
            "رکورد جدیدی هم اکنون ثبت گردید، برای مشاهده آن صفحه را بروزرسانی کنید" + "</div>");
        };
        $.connection.hub.start().done(function() {
            @{
                if (ViewBag.NotifyUsers)
                {
                    <text>notify.server.sendNotification();</text>
                }
            }
        });
    </script>
}
همانطور که در کدهای فوق مشاهده می‌کنید، بعد از اینکه اتصال با موفقیت برقرار شد (درون متد done) شرط چک کردن متغییر NotifyUsers را بررسی کرده‌ایم. یعنی در این حالت اگر مقدار آن true بود، متد درون هاب را فراخوانی کرده‌ایم. در نهایت پیام به یک div با آی‌دی result اضافه شده است.
لازم به ذکر است برای حالت‌های حذف و به‌روزرسانی نیز روال کار به همین صورت می‌باشد.
سورس مثال جاری : ShowAlertSignalR.zip
مطالب
سفارشی سازی ASP.NET Core Identity - قسمت چهارم - User Claims
از نگارش‌های پیشین ASP.NET، هنوز هم اطلاعات شیء User مانند User.Identity.Name در ASP.NET Core نیز در دسترس هستند. به این ترتیب زمانیکه کاربری به سیستم وارد شد، دیگر نیازی نیست تا جهت یافتن Name او، از بانک اطلاعاتی کوئری گرفت. خاصیت Name یاد شده به صورت خودکار از کوکی رمزنگاری شده‌ی او دریافت شده و در اختیار برنامه قرار می‌گیرد. این Name در ASP.NET Core Identity، اصطلاحا یک User Claim پیش‌فرض نام دارد و به صورت خودکار ایجاد و مقدار دهی می‌شود. اکنون این سؤال مطرح می‌شود که آیا می‌توان خاصیت دیگری را به شیء User.Identity اضافه کرد؟


جدول AppUserClaims


جدول AppUserClaims، جزو جداول اصلی ASP.NET Core Identity است و هدف آن ذخیره‌ی اطلاعات ویژه‌ی کاربران و بازیابی ساده‌تر آن‌ها از طریق کوکی‌های آن‌ها است (همانند User.Identity.Name). زمانیکه کاربری به سیستم وارد می‌شود، بر اساس UserId او، تمام رکوردهای User Claims متعلق به او از این جدول واکشی شده و به صورت خودکار به کوکی او اضافه می‌شوند.

در پروژه‌ی DNT Identity از این جدول استفاده نمی‌شود. چون اطلاعات User Claims مورد نیاز آن، هم اکنون در جدول AppUsers موجود هستند. به همین جهت افزودن این نوع User Claimها به جدول AppUserClaims، به ازای هر کاربر، کاری بیهوده است. سناریویی که استفاده‌ی از این جدول را با مفهوم می‌کند، ذخیره سازی تنظیمات ویژه‌ی هرکاربر است (خارج از فیلدهای جدول کاربران). برای مثال اگر سایتی را چندزبانه طراحی کردید، می‌توانید یک User Claim سفارشی جدید را برای این منظور ایجاد و زبان انتخابی کاربر را به عنوان یک رکورد جدید مخصوص آن در این جدول ویژه ثبت کنید. مزیت آن این است که واکشی و افزوده شدن اطلاعات آن به کوکی شخص، به صورت خودکار توسط فریم ورک صورت گرفته و در حین مرور صفحات توسط کاربر، دیگر نیازی نیست تا اطلاعات زبان انتخابی او را از بانک اطلاعاتی واکشی کرد.
بنابراین برای ذخیره سازی تنظیمات با کارآیی بالای ویژه‌ی هرکاربر، جدول جدیدی را ایجاد نکنید. جدول User Claim برای همین منظور درنظر گرفته شده‌است و پردازش اطلاعات آن توسط فریم ورک صورت می‌گیرد.


ASP.NET Core Identity چگونه اطلاعات جدول AppUserClaims را پردازش می‌کند؟

ASP.NET Identity Core در حین لاگین کاربر به سیستم، از سرویس SignInManager خودش استفاده می‌کند که با نحوه‌ی سفارشی سازی آن پیشتر در قسمت دوم این سری آشنا شدیم. سرویس SignInManager پس از لاگین شخص، از یک سرویس توکار دیگر این فریم ورک به نام UserClaimsPrincipalFactory جهت واکشی اطلاعات User Claims و همچنین Role Claims و افزودن آن‌ها به کوکی رمزنگاری شده‌ی شخص، استفاده می‌کند.
بنابراین اگر قصد افزودن User Claim سفارشی دیگری را داشته باشیم، می‌توان همین سرویس توکار UserClaimsPrincipalFactory را سفارشی سازی کرد (بجای اینکه الزاما رکوردی را به جدول AppUserClaims اضافه کنیم).

اطلاعات جالبی را هم می‌توان از پیاده سازی متد CreateAsync آن استخراج کرد:
  public virtual async Task<ClaimsPrincipal> CreateAsync(TUser user)
1) userId شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserIdClaimType به کوکی او اضافه می‌شود.
2) userName شخص پس از لاگین از طریق User Claims ایی با نوع Options.ClaimsIdentity.UserNameClaimType به کوکی او اضافه می‌شود.
3) security stamp او (آخرین بار تغییر اطلاعات اکانت کاربر) نیز یک Claim پیش‌فرض است.
4) اگر نقش‌هایی به کاربر انتساب داده شده باشند، تمام این نقش‌ها واکشی شده و به عنوان یک Claim جدید به کوکی او اضافه می‌شوند.
5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).

بنابراین حداقل مدیریت Claims این 5 مورد خودکار است و اگر برای مثال نیاز به Id کاربر لاگین شده را داشتید، نیازی نیست تا آن‌را از بانک اطلاعاتی واکشی کنید. چون این اطلاعات هم اکنون در کوکی او موجود هستند.


سفارشی سازی کلاس UserClaimsPrincipalFactory جهت افزودن User Claims سفارشی

تا اینجا دریافتیم که کلاس UserClaimsPrincipalFactory کار مدیریت Claims پیش‌فرض این فریم ورک را برعهده دارد. در ادامه از این کلاس ارث بری کرده و متد CreateAsync آن‌را جهت افزودن Claims سفارشی خود بازنویسی می‌کنیم. این پیاده سازی سفارشی را در کلاس ApplicationClaimsPrincipalFactory می‌توانید مشاهده کنید:
        public override async Task<ClaimsPrincipal> CreateAsync(User user)
        {
            var principal = await base.CreateAsync(user).ConfigureAwait(false); 
            addCustomClaims(user, principal);
            return principal;
        }

        private static void addCustomClaims(User user, IPrincipal principal)
        {
            ((ClaimsIdentity) principal.Identity).AddClaims(new[]
            {
                new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), ClaimValueTypes.Integer),
                new Claim(ClaimTypes.GivenName, user.FirstName ?? string.Empty),
                new Claim(ClaimTypes.Surname, user.LastName ?? string.Empty),
                new Claim(PhotoFileName, user.PhotoFileName ?? string.Empty, ClaimValueTypes.String),
            });
        }
در حین بازنویسی متد CreateAsync، ابتدا base.CreateAsync را فراخوانی کرده‌ایم، تا اخلالی در عملکرد این فریم ورک رخ ندهد و هنوز هم همان مواردی که در قسمت قبل توضیح داده شد، به صورت پیش فرض به کوکی شخص اضافه شوند. سپس در متد addCustomClaims، تعدادی Claim سفارشی خاص خودمان را اضافه کرده‌ایم.
برای مثال نام، نام خانوادگی و نام تصویر شخص به صورت Claimهایی جدید به کوکی او اضافه می‌شوند. در این حالت دیگر نیازی نیست تا به ازای هر کاربر، جدول AppUserClaims را ویرایش کرد و اطلاعات جدیدی را افزود و یا تغییر داد. همینقدر که کاربر به سیستم لاگین کند، شیء User او به متد Create کلاس UserClaimsPrincipalFactory ارسال می‌شود. به این ترتیب می‌توان به تمام خواص این کاربر دسترسی یافت و در صورت نیاز آن‌ها را به صورت Claimهایی به کوکی او افزود.

پس از تدارک کلاس ApplicationClaimsPrincipalFactory‌، تنها کاری را که باید در جهت معرفی و جایگرینی آن انجام داد، تغییر ذیل در کلاس IdentityServicesRegistry است:
services.AddScoped<IUserClaimsPrincipalFactory<User>, ApplicationClaimsPrincipalFactory>();
services.AddScoped<UserClaimsPrincipalFactory<User, Role>, ApplicationClaimsPrincipalFactory>();
یکبار ApplicationClaimsPrincipalFactory را به عنوان پیاده سازی کننده‌ی IUserClaimsPrincipalFactory معرفی کرده‌ایم. همچنین یکبار هم سرویس توکار UserClaimsPrincipalFactory را به سرویس سفارشی خودمان هدایت کرده‌ایم. به این ترتیب مطمئن خواهیم شد که همواره از ApplicationClaimsPrincipalFactory ما استفاده خواهد شد (حتی اگر UserClaimsPrincipalFactory اصلی از سیستم تزریق وابستگی‌ها درخواست شود).
 

چگونه به اطلاعات User Claims در سرویس‌های برنامه دسترسی پیدا کنیم؟

برای دسترسی به اطلاعات User Claims نیاز به دسترسی به HttpContext جاری را داریم. در این مورد و تزریق سرویس IHttpContextAccessor جهت تامین آن، در مطلب «بررسی روش دسترسی به HttpContext در ASP.NET Core» پیشتر بحث شده‌است.
به علاوه در کلاس IdentityServicesRegistry، تزریق وابستگی‌های سفارشی‌تری نیز صورت گرفته‌است:
services.AddScoped<IPrincipal>(provider =>
    provider.GetService<IHttpContextAccessor>()?.HttpContext?.User ?? ClaimsPrincipal.Current);
در اینجا اگر نیاز به اطلاعات Claims شیء User را داشتید، می‌توانید اینترفیس IPrincipal را هم بجای IHttpContextAccessor، به سازنده‌ی کلاس مدنظر خود تزریق کنید.


چگونه اطلاعات User Claims سفارشی را دریافت کنیم؟

برای کار ساده‌تر با Claims یک کلاس کمکی به نام IdentityExtensions به پروژه اضافه شده‌است و متدهایی مانند دو متد ذیل را می‌توانید در آن مشاهده کنید:
        public static string FindFirstValue(this ClaimsIdentity identity, string claimType)
        {
            return identity?.FindFirst(claimType)?.Value;
        }

        public static string GetUserClaimValue(this IIdentity identity, string claimType)
        {
            var identity1 = identity as ClaimsIdentity;
            return identity1?.FindFirstValue(claimType);
        }
در اینجا نحوه‌ی استخراج اطلاعات را از شیء User و یا User.Identity مشاهده می‌کنید. تنها کافی است claimType ایی را درخواست کرده و سپس مقدار آن‌را از کوکی شخص به نحو فوق واکشی کنیم.
برای نمونه متد GetUserDisplayName این کلاس کمکی، از همان Claims سفارشی که در کلاس ApplicationClaimsPrincipalFactory تعریف کردیم، اطلاعات خود را استخراج می‌کند و اگر در View ایی خواستید این اطلاعات را نمایش دهید، می‌توانید بنویسید:
 @User.Identity.GetUserDisplayName()


چگونه پس از ویرایش اطلاعات کاربر، اطلاعات کوکی او را نیز به روز کنیم؟

در پروژه قسمتی وجود دارد جهت ویرایش اطلاعات کاربران (UserProfileController). اگر کاربری برای مثال نام و نام خانوادگی خود را ویرایش کرد، می‌خواهیم بلافاصله متد GetUserDisplayName اطلاعات صحیح و به روزی را از کوکی او دریافت کند. برای اینکار یا می‌توان او را وادار به لاگین مجدد کرد (تا پروسه‌ی رسیدن به متد CreateAsync کلاس ApplicationClaimsPrincipalFactory طی شود) و یا روش بهتری نیز وجود دارد:
 // reflect the changes, in the current user's Identity cookie
await _signInManager.RefreshSignInAsync(user).ConfigureAwait(false);
در اینجا تنها کافی است متد RefreshSignInAsync را مجددا بر اساس اطلاعات ویرایش شده‌ی کاربر، فراخوانی کنیم تا کوکی او را بلافاصله به روز کند و این روش نیازی به اجبار به لاگین مجدد کاربر را ندارد.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.
مطالب
توسعه سیستم مدیریت محتوای DNTCms - قسمت دوم

در مقاله‌ی قبل توانستیم یک سری از مدل‌های مربوط به وبلاگ را آماده کنیم. در ادامه به تکمیل آن و همچین آغاز تهیه‌ی مدل‌های مربوط به اخبار و پیغام خصوصی می‌پردازیم.
همکاران این قسمت:
سلمان معروفی

مدل گزارش دهی

    /// <summary>    
    /// Repersents a Report template for every cms section
    /// </summary>
    public class Report
    {
        #region Ctor
        /// <summary>
        /// Create one instance for <see cref="Report"/>
        /// </summary>
        public Report()
        {
            ReportedOn = DateTime.Now;
            Id = SequentialGuidGenerator.NewSequentialGuid();
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier for Report
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// gets or sets reason of report
        /// </summary>
        public virtual string Reason { get; set; }
        /// <summary>
        /// gets or sets section that is reported
        /// </summary>
        public virtual ReportSection Section { get; set; }
        /// <summary>
        /// gets or sets sectionid that is reported
        /// </summary>
        public virtual long SectionId { get; set; }
        /// <summary>
        /// gets or sets type of report
        /// </summary>
        public virtual ReportType Type{ get; set; }
        /// <summary>
        /// gets or sets report's datetime
        /// </summary>
        public virtual DateTime ReportedOn { get; set; }
        /// <summary>
        /// indicate this report is read by admin
        /// </summary>
        public virtual bool IsRead { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets id of user that is reporter
        /// </summary>
        public virtual long ReporterId { get; set; }
        /// <summary>
        /// gets or sets id of user that is reporter
        /// </summary>
        public virtual User Reporter { get; set; }
        #endregion
    }

/// <summary>
    /// Represents Report Section
    /// </summary>
   public  enum  ReportSection
    {
        News,
        Poll,
        Announcement,
        ForumTopic,
        BlogComment,
        BlogPost,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        User,
      ...
    }

/// <summary>
    /// Represents Type of Report
    /// </summary>
    public enum  ReportType
    {
        Spam,
        Abuse,
        Advertising,
       ...
    }

قصد داریم در این سیستم به کاربران خاصی دسترسی گزارش دادن در بخش‌های مختلف را بدهیم. این دسترسی‌ها در بخش تنظیمات سیستم قابل تغییر خواهند بود (برای مثال براساس امتیاز ، براساس تعداد پست و ... ) . این امکان می‌تواند برای مدیریت سیستم مفید باشد.
برای سیستم گزارش دهی به مانند سیستم امتیاز دهی عمل خواهیم کرد. در کلاس Report، خصوصیت ReportSection  از نوع داده‌ی شمارشی می‌باشد که در بالا تعریف آن نیز آماده است و مشخص کننده‌ی بخش‌هایی می‌باشد که لازم است امکان گزارش دهی داشته باشند. خصوصیت Type هم که از نوع شمارشی ReportType می‌باشد، مشخص کننده‌ی نوع گزارشی است که داده شده است. 
علاوه بر نوع گزارش، می‌توان دلیل گزارش را هم ذخیره کرد که برای این منظور خصوصیت Reason در نظر گرفته شده‌است. خصوصیت IsRead هم برای مدیریت این گزارشات در پنل مدیریت در نظر گرفته شده است. اگر در مقاله‌ی قبل دقت کرده باشید، متوجه وجود خصوصیتی به نام ReportsCount در کلاس BaseContent و  BaseComment خواهید شد که برای نشان دادن تعداد گزارش‌هایی است که برای آن مطلب یا نظر داده شده است، استفاده می‌شود.

کلاس پایه فایل‌های ضمیمه

 /// <summary>
    /// Represents a base class for every attachment
    /// </summary>
    public abstract class BaseAttachment
    {
        #region Ctor

        public BaseAttachment()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            AttachedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// sets or gets identifier for attachment
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// sets or gets name for attachment
        /// </summary>
        public virtual string FileName { get; set; }
        /// <summary>
        /// sets or gets type of attachment
        /// </summary>
        public virtual string ContentType { get; set; }
        /// <summary>
        /// sets or gets size of attachment
        /// </summary>
        public virtual long Size { get; set; }
        /// <summary>
        /// sets or gets Extention of attachment
        /// </summary>
        public virtual string Extension { get; set; }
        /// <summary>
        /// sets or gets bytes of data
        /// </summary>
        //public byte[] Data { get; set; }
        /// <summary>
        /// sets or gets Creation Date
        /// </summary>
        public virtual DateTime AttachedOn { get; set; }
        /// <summary>
        /// gets or sets counts of download this file
        /// </summary>
        public virtual long DownloadsCount { get; set; }
        /// <summary>
        /// gets or sets datetime that is modified
        /// </summary>
        public virtual DateTime ModifiedOn { get; set; }
        /// <summary>
        /// gets or sets section that this file attached there
        /// </summary>
        public virtual AttachmentSection Section { get; set; }
        /// <summary>
        /// gets or sets information of user agent 
        /// </summary>
        public virtual string Agent { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual long OwnerId { get; set; }
        /// <summary>
        /// sets or gets identifier of attachment's owner
        /// </summary>
        public virtual User Owner { get; set; }
        #endregion
    }



    public enum  AttachmentSection
    {
        News,
        Announcement,
        ForumTopic,
        Conversation,
        BlogComment,
        NewsComment,
        PollComment,
        AnnouncementComment,
        ForumPost,
        BlogPost,
        Group,
        ...
    }

کلاس بالا اکثر خصوصیات لازم برای مدل Attachment ما را در خود دارد. قصد داریم از ارث بری TPH برای مدیریت فایل‌های ضمیمه استفاده کنیم. در سیستم بسته‌ی ما، تنها کاربران احراز هویت شده می‌توانند فایل ضمیمه کنند و برای همین منظور OwnerId را که همان ارسال کننده‌ی فایل می‌باشد، به صورت Nullable در نظر نگرفته‌ایم.
یک سری از مشخصات که نیاز به توضیح اضافی ندارند، ولی خصوصیت AttachmentSection که از نوع شمارشی AttachmentSection است، برای دسترسی راحت کاربر به فایل‌های ارسالی خود در پنل کاربری در نظر گرفته شده است. برای بخش‌های (وبلاگ - اخبار - نظرسنجی‌ها - آگهی‌ها - انجمن)  که نیاز به Privacy خاصی نیست و احراز هویت کفایت می‌کند، مدل زیر را در نظر گرفته ایم:

مدل فایل‌های ضمیمه عمومی

 /// <summary>
    /// Repersent the attachment for file
    /// </summary>
    public class Attachment : BaseAttachment
    {
       
    }
  مدل بالا صرفا برای بخش‌های مذکور کفایت خواهد کرد. در ادامه مقالات، برای بخش‌هایی مانند پیغام خصوصی، گروه‌هایی که کاربران ایجاد می‌کنند، برای انتشار تجربیات خود و هر بخشی که اضافه شود و نیاز به Privacy داشته باشد، نیاز خواهند بود تا مدل Attachment آنها با خود بخش هم در ارتباط باشد و تمام خصوصیت آنها که اکثرا کلید خارجی خواهند بود به صورت Nullable تعریف شوند.
مدل اخبار
 /// <summary>
    /// Represents one news item 
    /// </summary>
    public class NewsItem : BaseContent
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="NewsItem"/>
        /// </summary>
        public NewsItem()
        {
            Rating = new Rating();
            PublishedOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// indicating that this news show on sidebar
        /// </summary>
        public virtual bool ShowOnSideBar { get; set; }
        /// <summary>
        /// indicate this NewsItem is approved by admin if NewsItem.Moderate==true
        /// </summary>
        public virtual bool IsApproved { get; set; }

        #endregion

        #region NavigationProperties

        /// <summary>
        /// gets or sets  newsitem's Reviews
        /// </summary>
        public ICollection<NewsComment> Comments { get; set; }

        #endregion
    }

                  کلاس بالا نشان دهنده‌ی اشتراک‌های ما خواهند بود. این مدل ما هم از کلاس پایه‌ی BaseContent بحث شده در مقاله‌ی قبل، ارث بری کرده و علاوه بر آن دو خصوصیت دیگر تحت عنوان IsApproved برای اعمال مدیریتی در نظر گرفته شده است (اگر در بخش تنظیمات سیستم اخبار، مدیریت تصمیم گرفته باشد تا اخبار جدید به اشتراک گذاشته شده با تأیید مدیریتی منتشر شوند) و خصوصیت ShowOnSideBar هم به عنوان یک تنظیم مدیریتی برای خبر خاصی در نظر گرفته شده که لازم است به صورت sticky در سایدبار نمایش داده شود.
برای اخبار نیز امکان ارسال نظر خواهیم داشت که برای این منظور لیستی از مدل زیر (NewsComment) در مدل بالا تعریف شده است .

مدل نظرات اخبار 

 public class NewsComment : BaseComment
    {
        #region Ctor
        public NewsComment()
        {
            Rating = new Rating();
            CreatedOn = DateTime.Now;

        }
        #endregion

        #region NavigationProperties

        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual long? ReplyId { get; set; }
        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual NewsComment Reply { get; set; }
        /// <summary>
        /// gets or sets body of blog NewsItem's comment
        /// </summary>
        public virtual ICollection<NewsComment> Children { get; set; }
        /// <summary>
        /// gets or sets NewsItem that this comment sent to it
        /// </summary>
        public virtual NewsItem NewsItem { get; set; }
        /// <summary>
        /// gets or sets NewsItem'Id that this comment sent to it
        /// </summary>
        public virtual long NewsItemId { get; set; }
        #endregion
    }

                           مدل بالا نشان دهنده‌ی نظرات داده شده‌ی برای اخبار می‌باشند که از کلاس BaseComment بحث شده در مقاله‌ی قبل ارث بری کرده و ساختار درختی آن نیز مشخص است و همچنین برای اعمال ارتباط یک به چند نیز خصوصیتی تحت عنوان NewsItem  با کلید NewsItemId در این کلاس در نظر گرفته شده است.

مدل‌های پیغام خصوصی
/// <summary>
    /// Indicate one conversation
    /// </summary>
    public class Conversation
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="Conversation"/>
        /// </summary>
        public Conversation()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            SentOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// represents this conversaion is seen
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets subject of this conversation
        /// </summary>
        public virtual string Subject { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime SentOn { get; set; }
        /// <summary>
        /// indicate this record deleted by sender
        /// </summary>
        public virtual bool DeletedBySender { get; set; }
        /// <summary>
        /// indicate this record deleted by receiver
        /// </summary>
        public virtual bool DeletedByReceiver { get; set; }
        /// <summary>
        /// gets or sets Messagescount that Unread  by sender of this conversation
        /// </summary>
        public virtual int UnReadSenderMessagesCount { get; set; }
        /// <summary>
        /// gets or sets Messagescount that Unread  by receiver of this conversation
        /// </summary>
        public virtual int UnReadReceiverMessagesCount { get; set; }
        /// <summary>
        /// gets or sets Messagescount of this conversation for increase performance
        /// </summary>
        public virtual int MessagesCount { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets if of  user that start this conversation
        /// </summary>
        public virtual long SenderId { get; set; }
        /// <summary>
        /// gets or sets user that start this conversation
        /// </summary>
        public virtual User Sender { get; set; }
        /// <summary>
        /// gets or sets id of  user that is recipient
        /// </summary>
        public virtual long ReceiverId { get; set; }
        /// <summary>
        /// gets or sets   user that is recipient
        /// </summary>
        public virtual User Receiver { get; set; }
        /// <summary>
        /// get or set Messages of this conversation
        /// </summary>
        public virtual ICollection<ConversationReply> Messages { get; set; }
        /// <summary>
        /// get or set Attachments that attached in this conversation
        /// </summary>
        public virtual ICollection<ConversationAttachment> Attachments { get; set; }
        #endregion

مدل بالا نشان دهنده‌ی گفتگوی بین دو کاربر می‌باشد. هر گفتگو امکان دارد با موضوع خاصی ایجاد شود و مسلما یک کاربر به‌عنوان دریافت کننده و کاربر دیگری بعنوان ارسال کننده خواهد بود. برای این منظور خصوصیات Receiver و Sender که از نوع User هستند را در این کلاس در نظر گرفته‌ایم.
خصوصیات DeletedBySender و DeletedByReceiver هم برای این در نظر گفته شده‌اند که اگر یک طرف این گفتگو خواهان حذف آن باشد، برای آن کاربر حذف نرم انجام دهیم و فعلا برای کاربر مقابل قابل دسترسی باشد.
UnReadSenderMessagesCount و UnReadReceiverMessagesCount هم برای بالا بردن کارآیی سیستم در نظر گفته شده‌اند و در واقع تعداد پیغام‌های خوانده نشده در یک گفتگو به صورت متمایز برای هر دو طرف، ذخیره می‌شود. هر گفتگو شامل یکسری پیغام رد و بدل شده خواهد بود که بدین منظور لیستی از ConversationReply‌ها را در مدل بالا تعریف کرده‌ایم.
در هر گفتگو یکسری فایل هم ممکن است ضمیمه شود ، برای این منظور هم یک لیستی از کلاس ConversationAttachment در مدل گفتگو تعریف شده است که در ادامه پیاده سازی کلاس ConversationAttachment را هم خواهیم دید.   
مدل  ConversationReply به شکل زیر می‌باشد:

  /// <summary>
    /// Represents One Reply to Conversation
    /// </summary>
    public class ConversationReply
    {
        #region Ctor
        /// <summary>
        /// create one instance of <see cref="ConversationReply"/>
        /// </summary>
        public ConversationReply()
        {
            Id = SequentialGuidGenerator.NewSequentialGuid();
            SentOn = DateTime.Now;
        }
        #endregion

        #region Properties
        /// <summary>
        /// gets or sets identifier of record
        /// </summary>
        public virtual Guid Id { get; set; }
        /// <summary>
        /// represents this conversaionReply is seen
        /// </summary>
        public virtual bool IsRead { get; set; }
        /// <summary>
        /// gets or sets body of this conversationReply
        /// </summary>
        public virtual string Body { get; set; }
        /// <summary>
        /// gets or sets Date that this record added
        /// </summary>
        public virtual DateTime SentOn { get; set; }
        #endregion

        #region NavigationProperties
        /// <summary>
        /// gets or sets  Parent's Id Of this ConversationReply
        /// </summary>
        public virtual Guid? ParentId { get; set; }
        /// <summary>
        /// gets or sets Parent Of this ConversationReply
        /// </summary>
        public virtual ConversationReply Parent { get; set; }
        /// <summary>
        /// get or set Children Of this ConversationReply
        /// </summary>
        public virtual ICollection<ConversationReply> Children { get; set; }
        /// <summary>
        /// gets or sets if of  user that start this conversationReply
        /// </summary>
        public virtual long SenderId { get; set; }
        /// <summary>
        /// gets or sets user that start this conversationReply
        /// </summary>
        public virtual User Sender { get; set; }
        /// <summary>
        /// gets or sets Conversation that this message sent in it 
        /// </summary>
        public virtual Conversation Conversation{ get; set; }
        /// <summary>
        /// gets or sets Id of Conversation that this message sent in it 
        /// </summary>
        public virtual Guid ConversationId { get; set; }
        #endregion
    }

مدل بالا نشان دهنده‌ی پیغام‌های داده شده در یک گفتگو با موضوعی خاص می‌باشد. ساختار درختی آن هم برای ایجاد امکان جواب دهی برای پیغام‌ها در نظر گرفته شده است (الزامی نیست). هر پیغام در یک گفتگو ارسال شده و یک ارسال کننده نیز دارد که برای این منظور به ترتیب دو خصوصیت Conversation از نوع کلاس Conversation و Sender از نوع User در نظر گرفته‌ایم.  
با توجه به وجود Privacy در گفتگو نیاز است تا مدل فایل ضمیمه بخش گفتگو‌ها به شکل زیر باشد:

/// <summary>
    /// Represents the attachment That attached in Conversation
    /// </summary>
    public class ConversationAttachment : BaseAttachment
    {
        #region NavigationProperties

        public virtual Conversation Conversation { get; set; }
        public virtual Guid? ConversationId { get; set; }
        #endregion
    }

همانطور که کمی بالاتر بحث شد، قصد اعمال ارث بری TPH را برای مدیریت فایل‌های ضمیمه داریم. برای این منظور مدل بالا نیز از کلاس BaseAttachment ارث بری کرده و دو خصوصیت اضافه هم برای اعمال ارتباط یک به چند با گفتگو خواهد داشت. توجه کنید که ConversationId به صورت Nullable تعریف شده‌است.

نتیجه این قسمت

مطالب
سفارشی سازی ASP.NET Core Identity - قسمت پنجم - سیاست‌های دسترسی پویا
ASP.NET Core Identity به همراه دو قابلیت جدید است که پیاده سازی سطوح دسترسی پویا را با سهولت بیشتری میسر می‌کند:
الف) Policies
ب) Role Claims


سیاست‌های دسترسی یا Policies در ASP.NET Core Identity

ASP.NET Core Identity هنوز هم از مفهوم Roles پشتیبانی می‌کند. برای مثال می‌توان مشخص کرد که اکشن متدی و یا تمام اکشن متدهای یک کنترلر تنها توسط کاربران دارای نقش Admin قابل دسترسی باشند. اما نقش‌ها نیز در این سیستم جدید تنها نوعی از سیاست‌های دسترسی هستند.
[Authorize(Roles = ConstantRoles.Admin)]
public class RolesManagerController : Controller
برای مثال در اینجا دسترسی به امکانات مدیریت نقش‌های سیستم، به نقش ثابت و از پیش تعیین شده‌ی Admin منحصر شده‌است و تمام کاربرانی که این نقش به آن‌ها انتساب داده شود، امکان استفاده‌ی از این قابلیت‌ها را خواهند یافت.
اما نقش‌های ثابت، بسیار محدود و غیر قابل انعطاف هستند. برای رفع این مشکل مفهوم جدیدی را به نام Policy اضافه کرده‌اند.
[Authorize(Policy="RequireAdministratorRole")]
public IActionResult Get()
{
   /* .. */
}
سیاست‌های دسترسی بر اساس Requirements و یا نیازهای سیستم تعیین می‌شوند و تعیین نقش‌ها، تنها یکی از قابلیت‌های آن‌ها هستند.
برای مثال اگر بخواهیم تک نقش Admin را به صورت یک سیاست دسترسی جدید تعریف کنیم، روش کار به صورت ذیل خواهد بود:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
services.AddAuthorization(options =>
    {
        options.AddPolicy("RequireAdministratorRole", policy => policy.RequireRole("Admin"));
    });
}
در تنظیمات متد AddAuthorization، یک سیاست دسترسی جدید تعریف شده‌است که جهت برآورده شدن نیازمندی‌های آن، کاربر سیستم باید دارای نقش Admin باشد که نمونه‌ای از نحوه‌ی استفاده‌ی از آن‌را با ذکر [Authorize(Policy="RequireAdministratorRole")] ملاحظه کردید.
و یا بجای اینکه چند نقش مجاز به دسترسی منبعی را با کاما از هم جدا کنیم:
 [Authorize(Roles = "Administrator, PowerUser, BackupAdministrator")]
می‌توان یک سیاست دسترسی جدید را به نحو ذیل تعریف کرد که شامل تمام نقش‌های مورد نیاز باشد و سپس بجای ذکر Roles، از نام این Policy جدید استفاده کرد:
options.AddPolicy("ElevatedRights", policy => policy.RequireRole("Administrator", "PowerUser", "BackupAdministrator"));
به این صورت
[Authorize(Policy = "ElevatedRights")]
public IActionResult Shutdown()
{
   return View();
}

سیاست‌های دسترسی تنها به نقش‌ها محدود نیستند:
services.AddAuthorization(options =>
{
   options.AddPolicy("EmployeeOnly", policy => policy.RequireClaim("EmployeeNumber"));
});
برای مثال می‌توان دسترسی به یک منبع را بر اساس User Claims یک کاربر به نحوی که ملاحظه می‌کنید، محدود کرد:
[Authorize(Policy = "EmployeeOnly")]
public IActionResult VacationBalance()
{
   return View();
}


سیاست‌های دسترسی پویا در ASP.NET Core Identity

مهم‌ترین مزیت کار با سیاست‌های دسترسی، امکان سفارشی سازی و تهیه‌ی نمونه‌های پویای آن‌ها هستند؛ موردی که با نقش‌های ثابت سیستم قابل پیاده سازی نبوده و در نگارش‌های قبلی، جهت پویا سازی آن، یکی از روش‌های بسیار متداول، تهیه‌ی فیلتر Authorize سفارشی سازی شده بود. اما در اینجا دیگر نیازی نیست تا فیلتر Authorize را سفارشی سازی کنیم. با پیاده سازی یک AuthorizationHandler جدید و معرفی آن به سیستم، پردازش سیاست‌های دسترسی پویای به منابع، فعال می‌شود.
پیاده سازی سیاست‌های پویای دسترسی شامل مراحل ذیل است:
1- تعریف یک نیازمندی دسترسی جدید
public class DynamicPermissionRequirement : IAuthorizationRequirement
{
}
ابتدا باید یک نیازمندی دسترسی جدید را با پیاده سازی اینترفیس IAuthorizationRequirement ارائه دهیم. این نیازمندی مانند روشی که در پروژه‌ی DNT Identity بکار گرفته شده‌است، خالی است و صرفا به عنوان نشانه‌ای جهت یافت AuthorizationHandler استفاده کننده‌ی از آن استفاده می‌شود. در اینجا در صورت نیاز می‌توان یک سری خاصیت اضافه را تعریف کرد تا آن‌ها را به صورت پارامترهایی ثابت به AuthorizationHandler ارسال کند.

2- پیاده سازی یک AuthorizationHandler استفاده کننده‌ی از نیازمندی دسترسی تعریف شده
پس از اینکه نیازمندی DynamicPermissionRequirement را تعریف کردیم، در ادامه باید یک AuthorizationHandler استفاده کننده‌ی از آن را تعریف کنیم:
    public class DynamicPermissionsAuthorizationHandler : AuthorizationHandler<DynamicPermissionRequirement>
    {
        private readonly ISecurityTrimmingService _securityTrimmingService;

        public DynamicPermissionsAuthorizationHandler(ISecurityTrimmingService securityTrimmingService)
        {
            _securityTrimmingService = securityTrimmingService;
            _securityTrimmingService.CheckArgumentIsNull(nameof(_securityTrimmingService));
        }

        protected override Task HandleRequirementAsync(
             AuthorizationHandlerContext context,
             DynamicPermissionRequirement requirement)
        {
            var mvcContext = context.Resource as AuthorizationFilterContext;
            if (mvcContext == null)
            {
                return Task.CompletedTask;
            }

            var actionDescriptor = mvcContext.ActionDescriptor;
            var area = actionDescriptor.RouteValues["area"];
            var controller = actionDescriptor.RouteValues["controller"];
            var action = actionDescriptor.RouteValues["action"];

            if(_securityTrimmingService.CanCurrentUserAccess(area, controller, action))
            {
                context.Succeed(requirement);
            }
            else
            {
                context.Fail();
            }

            return Task.CompletedTask;
        }
    }
کار با ارث بری از AuthorizationHandler شروع شده و آرگومان جنریک آن، همان نیازمندی است که پیشتر تعریف کردیم. از این آرگومان جنریک جهت یافتن خودکار AuthorizationHandler متناظر با آن، توسط ASP.NET Core Identity استفاده می‌شود. بنابراین در اینجا DynamicPermissionRequirement تهیه شده صرفا کارکرد علامتگذاری را دارد.
در کلاس تهیه شده باید متد HandleRequirementAsync آن‌را بازنویسی کرد و اگر در این بین، منطق سفارشی ما context.Succeed را فراخوانی کند، به معنای برآورده شدن سیاست دسترسی بوده و کاربر جاری می‌تواند به منبع درخواستی، بلافاصله دسترسی یابد و اگر context.Fail فراخوانی شود، در همینجا دسترسی کاربر قطع شده و HTTP status code مساوی 401 (عدم دسترسی) را دریافت می‌کند.

منطق سفارشی پیاده سازی شده نیز به این صورت است:
نام ناحیه، کنترلر و اکشن متد درخواستی کاربر از مسیریابی جاری استخراج می‌شوند. سپس توسط سرویس سفارشی ISecurityTrimmingService تهیه شده، بررسی می‌کنیم که آیا کاربر جاری به این سه مؤلفه دسترسی دارد یا خیر؟

3- معرفی سیاست دسترسی پویای تهیه شده به سیستم
معرفی سیاست کاری پویا و سفارشی تهیه شده، شامل دو مرحله‌ی زیر است:
        private static void addDynamicPermissionsPolicy(this IServiceCollection services)
        {
            services.AddScoped<IAuthorizationHandler, DynamicPermissionsAuthorizationHandler>();
            services.AddAuthorization(opts =>
            {
                opts.AddPolicy(
                    name: ConstantPolicies.DynamicPermission,
                    configurePolicy: policy =>
                    {
                        policy.RequireAuthenticatedUser();
                        policy.Requirements.Add(new DynamicPermissionRequirement());
                    });
            });
        }
ابتدا باید DynamicPermissionsAuthorizationHandler تهیه شده را به سیستم تزریق وابستگی‌ها معرفی کنیم.
سپس یک Policy جدید را با نام دلخواه DynamicPermission تعریف کرده و نیازمندی علامتگذار خود را به عنوان یک policy.Requirements جدید، اضافه می‌کنیم. همانطور که ملاحظه می‌کنید یک وهله‌ی جدید از DynamicPermissionRequirement در اینجا ثبت شده‌است. همین وهله به متد HandleRequirementAsync نیز ارسال می‌شود. بنابراین اگر نیاز به ارسال پارامترهای بیشتری به این متد وجود داشت، می‌توان خواص مرتبطی را به کلاس DynamicPermissionRequirement نیز اضافه کرد.
همانطور که مشخص است، در اینجا یک نیازمندی را می‌توان ثبت کرد و نه Handler آن‌را. این Handler از سیستم تزریق وابستگی‌ها، بر اساس آرگومان جنریک AuthorizationHandler پیاده سازی شده، به صورت خودکار یافت شده و اجرا می‌شود (بنابراین اگر Handler شما اجرا نشد، مطمئن شوید که حتما آن‌را به سیستم تزریق وابستگی‌ها معرفی کرده‌اید).

پس از آن هر کنترلر یا اکشن متدی که از این سیاست دسترسی پویای تهیه شده استفاده کند:
[Authorize(Policy = ConstantPolicies.DynamicPermission)]
[DisplayName("کنترلر نمونه با سطح دسترسی پویا")]
public class DynamicPermissionsSampleController : Controller
به صورت خودکار توسط DynamicPermissionsAuthorizationHandler مدیریت می‌شود.


سرویس ISecurityTrimmingService چگونه کار می‌کند؟

کدهای کامل ISecurityTrimmingService را در کلاس SecurityTrimmingService می‌توانید مشاهده کنید.
پیشنیاز درک عملکرد آن، آشنایی با دو قابلیت زیر هستند:
الف) «روش یافتن لیست تمام کنترلرها و اکشن متدهای یک برنامه‌ی ASP.NET Core»
دقیقا از همین سرویس توسعه داده شده‌ی در مطلب فوق، در اینجا نیز استفاده شده‌است؛ با یک تفاوت تکمیلی:
public interface IMvcActionsDiscoveryService
{
    ICollection<MvcControllerViewModel> MvcControllers { get; }
    ICollection<MvcControllerViewModel> GetAllSecuredControllerActionsWithPolicy(string policyName);
}
از متد GetAllSecuredControllerActionsWithPolicy جهت یافتن تمام اکشن متدهایی که مزین به ویژگی Authorize هستند و دارای Policy مساوی DynamicPermission می‌باشند، در کنترلر DynamicRoleClaimsManagerController برای لیست کردن آن‌ها استفاده می‌شود. اگر این اکشن متد مزین به ویژگی DisplayName نیز بود (مانند مثال فوق و یا کنترلر نمونه DynamicPermissionsSampleController)، از مقدار آن برای نمایش نام این اکشن متد استفاده خواهد شد.
بنابراین همینقدر که تعریف ذیل یافت شود، این اکشن متد نیز در صفحه‌ی مدیریت سطوح دسترسی پویا لیست خواهد شد.
 [Authorize(Policy = ConstantPolicies.DynamicPermission)]

ابتدا به مدیریت نقش‌های ثابت سیستم می‌رسیم. سپس به هر نقش می‌توان یک ‍Claim جدید را با مقدار area:controller:action انتساب داد.
به این ترتیب می‌توان به یک نقش، تعدادی اکشن متد را نسبت داد و سطوح دسترسی به آن‌ها را پویا کرد. اما ذخیره سازی آن‌ها چگونه است و چگونه می‌توان به اطلاعات نهایی ذخیره شده دسترسی پیدا کرد؟


مفهوم جدید Role Claims در ASP.NET Core Identity

تا اینجا موفق شدیم تمام اکشن متدهای دارای سیاست دسترسی سفارشی سازی شده‌ی خود را لیست کنیم، تا بتوان آن‌ها را به صورت دلخواهی انتخاب کرد و سطوح دسترسی به آن‌ها را به صورت پویا تغییر داد. اما این اکشن متدهای انتخاب شده را در کجا و به چه صورتی ذخیره کنیم؟
برای ذخیره سازی این اطلاعات نیازی نیست تا جدول جدیدی را به سیستم اضافه کنیم. جدول جدید AppRoleClaims به همین منظور تدارک دیده شده‌است.



وقتی کاربری عضو یک نقش است، به صورت خودکار Role Claims آن نقش را نیز به ارث می‌برد. هدف از نقش‌ها، گروه بندی کاربران است. توسط Role Claims می‌توان مشخص کرد این نقش‌ها چه کارهایی را می‌توانند انجام دهند. اگر از قسمت قبل بخاطر داشته باشید، سرویس توکار UserClaimsPrincipalFactory دارای مرحله‌ی 5 ذیل است:
«5) اگر یک نقش منتسب به کاربر دارای Role Claim باشد، این موارد نیز واکشی شده و به کوکی او به عنوان یک Claim جدید اضافه می‌شوند. در ASP.NET Identity Core نقش‌ها نیز می‌توانند Claim داشته باشند (امکان پیاده سازی سطوح دسترسی پویا).»
به این معنا که با لاگین شخص به سیستم، تمام اطلاعات مرتبط به او که در جدول AppRoleClaims وجود دارند، به کوکی او به صورت خودکار اضافه خواهند شد و دسترسی به آن‌ها فوق العاده سریع است.

در کنترلر DynamicRoleClaimsManagerController، یک Role Claim Type جدید به نام DynamicPermissionClaimType اضافه شده‌است و سپس ID اکشن متدهای انتخابی را به نقش جاری، تحت Claim Type عنوان شده، اضافه می‌کند (تصویر فوق). این ID به صورت area:controller:action طراحی شده‌است. به همین جهت است که در  DynamicPermissionsAuthorizationHandler همین سه جزء از سیستم مسیریابی استخراج و در سرویس SecurityTrimmingService مورد بررسی قرار می‌گیرد:
 return user.HasClaim(claim => claim.Type == ConstantPolicies.DynamicPermissionClaimType &&
claim.Value == currentClaimValue);
در اینجا user همان کاربرجاری سیستم است. HasClaim جزو متدهای استاندارد آن است و Type انتخابی، همان نوع سفارشی مدنظر ما است. currentClaimValue دقیقا همان ID اکشن متد جاری است که توسط کنار هم قرار دادن area:controller:action تشکیل شده‌است.
متد HasClaim هیچگونه رفت و برگشتی را به بانک اطلاعاتی ندارد و اطلاعات خود را از کوکی شخص دریافت می‌کند. متد user.IsInRole نیز به همین نحو عمل می‌کند.


Tag Helper جدید SecurityTrimming

اکنون که سرویس ISecurityTrimmingService را پیاده سازی کرده‌ایم، از آن می‌توان جهت توسعه‌ی SecurityTrimmingTagHelper نیز استفاده کرد:
        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            context.CheckArgumentIsNull(nameof(context));
            output.CheckArgumentIsNull(nameof(output));

            // don't render the <security-trimming> tag.
            output.TagName = null;

            if(_securityTrimmingService.CanCurrentUserAccess(Area, Controller, Action))
            {
                // fine, do nothing.
                return;
            }

            // else, suppress the output and generate nothing.
            output.SuppressOutput();
        }
عملکرد آن نیز بسیار ساده است. اگر کاربر، به area:controller:action جاری دسترسی داشت، این Tag Helper کاری را انجام نمی‌دهد. اگر خیر، متد SuppressOutput را فراخوانی می‌کند. این متد سبب خواهد شد، هر آنچه که داخل تگ‌های این TagHelper قرار گرفته، در صفحه رندر نشوند و از خروجی آن حذف شوند. به این ترتیب، کاربر به اطلاعاتی که به آن دسترسی ندارد (مانند لینک به مدخلی خاص را) مشاهده نخواهد کرد. به این مفهوم security trimming می‌گویند.
نمونه‌ای از کاربرد آن‌را در ReportsMenu.cshtml_ می‌توانید مشاهده کنید:
            <security-trimming asp-area="" asp-controller="DynamicPermissionsTest" asp-action="Products">
                <li>
                    <a asp-controller="DynamicPermissionsTest" asp-action="Products" asp-area="">
                        <span class="left5 fa fa-user" aria-hidden="true"></span>
                        گزارش از لیست محصولات
                    </a>
                </li>
            </security-trimming>
در اینجا اگر کاربر جاری به کنترلر DynamicPermissionsTest و اکشن متد Products آن دسترسی پویا نداشته باشد، محتوای قرارگرفته‌ی داخل تگ security-trimming را مشاهده نخواهد کرد.

برای آزمایش آن یک کاربر جدید را به سیستم DNT Identity اضافه کنید. سپس آن‌را در گروه نقشی مشخص قرار دهید (منوی مدیریتی،‌گزینه‌ی مدیریت نقش‌های سیستم). سپس به این گروه دسترسی به تعدادی از آیتم‌های پویا را بدهید (گزینه‌ی مشاهده و تغییر لیست دسترسی‌های پویا). سپس با این اکانت جدید به سیستم وارد شده و بررسی کنید که چه تعدادی از آیتم‌های منوی «گزارشات نمونه» را می‌توانید مشاهده کنید (تامین شده‌ی توسط ReportsMenu.cshtml_).


مدیریت اندازه‌ی حجم کوکی‌های ASP.NET Core Identity

همانطور که ملاحظه کردید، جهت بالابردن سرعت دسترسی به اطلاعات User Claims و Role Claims، تمام اطلاعات مرتبط با آن‌ها، به کوکی کاربر وارد شده‌ی به سیستم، اضافه می‌شوند. همین مساله در یک سیستم بزرگ با تعداد صفحات بالا، سبب خواهد شد تا حجم کوکی کاربر از 5 کیلوبایت بیشتر شده و توسط مرورگرها مورد قبول واقع نشوند و عملا سیستم از کار خواهد افتاد.
برای مدیریت یک چنین مساله‌ای، امکان ذخیره سازی کوکی‌های شخص در داخل بانک اطلاعاتی نیز پیش بینی شده‌است. زیر ساخت آن‌را در مطلب «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» پیشتر در این سایت مطالعه کردید و در پروژه‌ی DNT Identity بکارگرفته شده‌است.
اگر به کلاس IdentityServicesRegistry مراجعه کنید، یک چنین تنظیمی در آن قابل مشاهده است:
 var ticketStore = provider.GetService<ITicketStore>();
identityOptionsCookies.ApplicationCookie.SessionStore = ticketStore; // To manage large identity cookies
در ASP.NET Identity Core، امکان تدارک SessionStore سفارشی برای کوکی‌ها نیز وجود دارد. این SessionStore  باید با پیاده سازی اینترفیس ITicketStore تامین شود. دو نمونه پیاده سازی ITicketStore را در لایه سرویس‌های پروژه می‌توانید مشاهده کنید:
الف) DistributedCacheTicketStore
ب) MemoryCacheTicketStore

اولی از همان زیرساخت «تنظیمات کش توزیع شده‌ی مبتنی بر SQL Server در ASP.NET Core» استفاده می‌کند و دومی از IMemoryCache توکار ASP.NET Core برای پیاده سازی مکان ذخیره سازی محتوای کوکی‌های سیستم، بهره خواهد برد.
باید دقت داشت که اگر حالت دوم را انتخاب کنید، با شروع مجدد برنامه، تمام اطلاعات کوکی‌های کاربران نیز حذف خواهند شد. بنابراین استفاده‌ی از حالت ذخیره سازی آن‌ها در بانک اطلاعاتی منطقی‌تر است.


نحوه‌ی تنظیم سرویس ITicketStore را نیز در متد setTicketStore می‌توانید مشاهده کنید و در آن، در صورت انتخاب حالت بانک اطلاعاتی، ابتدا تنظیمات کش توزیع شده، صورت گرفته و سپس کلاس DistributedCacheTicketStore به عنوان تامین کننده‌ی ITicketStore به سیستم تزریق وابستگی‌ها معرفی می‌شود.
همین اندازه برای انتقال محتوای کوکی‌های کاربران به سرور کافی است و از این پس تنها اطلاعاتی که به سمت کلاینت ارسال می‌شود، ID رمزنگاری شده‌ی این کوکی است، جهت بازیابی آن از بانک اطلاعاتی و استفاده‌ی خودکار از آن در برنامه.


کدهای کامل این سری را در مخزن کد DNT Identity می‌توانید ملاحظه کنید.