توسعه-مدل-Asp.Net-Identity-2.0Reviewed by کارشناسان.نت on October 03Rating:5

همانطور که مستحضر هستید ورژن دوم Asp.net Identity منتشر شده است و ورژن 3 آن در حال تهیه می باشد . شما می توانید در عکس زیر تاریخچه مدیریت کاربران در Asp.Net را مشاهده کنید .

 

Asp.Net Identity یک مدل بر پایه Claim و پیاده سازی شده به کمک ابزار میانی Owin می باشد , شما می توانید از این مدل در انواع Template ها مثل Asp.Net WebForm , Asp.Net MVC , SPA , ... استفاده کنید.

مدل پیش فرض پیاده سازی شده با Entity FrameWork میباشد در فضای نام Microsoft.AspNet.Identity.EntityFramework قرار دارد شما می توانید

, پیاده سازی های دیگر از این مدل را در ادرس www.asp.net/identity پیدا کنید .

 

همچنین برای مطالعه سورس مدل می توانید از ادرس www.github.com/aspnet/Identity استفاده کنید .

و البته 6 عدد فیلم اموزشی مایکروسافت هم در Customizing ASPNET Authentication with Identity قرار دارد .

 

این مدل پیش فرض دارای دو کمبود می باشد :

 الف -

مدل Asp.net Identity یک مدل بر پایه Role می باشد , یعنی در برنامه یک سری نقش یا دسترسی یا مجوز وجود دارد که به انها Role می گویند و هر کاربر دارای صفر یا چند نقش می باشد .

 

برای وبسایتهای کوچک این مدل , مدل مناسبی می باشد اما برای وبسایتهای متوسط و بزرگ با هزاران کاربر و صد ها Role پاسخگو نخواهد بود , مثلا فرض کنید شما می خواهید دسترسی یک یا چند Role را از کاربران بگیرید , برای این منظور باید این Role ها را از تک تک هزاران کاربر حذف گردد .

 

برای حل این مشکل می توان از راهکار Group - Role - User استفاده کرد , در این مدل به جای ارتباط مستقیم User با Role هایش از یک واسط به اسم Group استفاده می کنیم. در این مدل :

 

هر Group می تواند شامل صفر یا User n باشد و از طرف دیگر شامل صفر یا Role n باشد .

 

هر User می تواند شامل صفر یا Group n باشد .

 

هر Role می تواند شامل صفر یا Group n باشد .

 

ب -

کلید اصلی (PrimaryKey) پیش فرض در این مدل از تایپ Guid می باشد, اینکه چرا Guid به عنوان پیش فرض انتخاب شده است و نه Int (همه ما عادت داریم به فیلد Id از تایپ Int و نوع Auto Identity برای تمام Entity ها و جداول) ؟

 

به طور کلی این سیاست و مقتضیات مایکروسافت بوده است و نه لزوما " روش بهینه , برای توضیحات بیشتر مایکروسافت برای این انتخاب این پاسخ را در Stack OverFlow مطالعه بفرمایید .

 

اما به طور کلی انتخاب Guid به عنوان کلید اصلی ممکن است موجب مشکلات Performance در سطح دیتابیس شود , برای مطالعه بیشتر این پاسخ را در Stack OverFlow مطالعه بفرمایید .

 

خوشبختانه برای حل این مشکل , مدل Asp.net Identity با Generic Interface ها طراحی شده و در نتیجه به راحتی می توان تایپ کلید اصلی را از String به Int تغییر داد .

برای حل دو مشکل الف و ب , دو مقاله بسیار خوب بر روی اینترنت موجود است , اولی با عنوان ASP.NET Identity 2.0: Implementing Group-Based Permissions Management برای حل مشکل الف و دومی با عنوان

ASP.NET Identity 2.0 Extending Identity Models and Using Integer Keys Instead of Strings برای حل مشکل ب .

در این مقاله من قصد توضیح مجدد این دو مقاله را ندارم , بلکه هدف ترکیب دو مقاله و ساخت مدل Group Base و با کلید اصلی Int میباشد و در ضمن بهینه کردن و رفع مشکلات مدل مقاله الف به منظور دستیابی به یک Template اماده برای Asp.Net MVC 5 در ویژوال استدیو 2013 تحت دات نت 4.5.2 .

در قدم اول :

من ابتدا از مقاله دوم شروع کردم برای تبدیل تایپ کلید اصلی به Int , در متن مقاله یک Nuget وجود داره که من ازش استفاده نکردم و به جای اون , یک وبسایت از نوع MVC با Authenticaion Mode از نوع پیش فرض (Individal User Account) طبق عکس های زیر ساختم :

و سپس همانند شکل زیر با کلیک بر روی دکمه Individal User Account  , Change Authtentication را انتخاب می کنیم , سپس تمام مراحل موجود در مقاله را طی کردم و قسمتهایی که کد ها در مقاله ذکر نشده بود , از کد موجود در GitHub استفاده کردم .

 مدل Asp.Net Identity از یک سری Generic InterFace مثل

IUser<TKey>
//و
IRole<TKey>

تشکیل شده است , در نتیجه شما برای استفاده از کلید اصلی از تایپ Int کافی است در تمام کدهای پروژه جدیدتان به جای استفاده از TKey از Int استفاده کنید

 

مثل قطعه کد زیر :

public class ApplicationRole : IdentityRole<int, ApplicationUserRole>, IRole<int>

{

public string Description { get; set; }

public ApplicationRole() { }

public ApplicationRole(string name): this()

{

this.Name = name;

}

public ApplicationRole(string name, string description) : this(name)

{

this.Description = description;

}

}

خوب مطلب اصلی این مقاله همین Find و Replace کردن Tkey با Int می باشد .

حال به سراغ مشکل اول و مقاله ASP.NET Identity 2.0: Implementing Group-Based Permissions Management  می رویم . همانطور که قبلا" توضیح دادم Asp.Net Identity 2.0 به کمک Entity FrameWork پیاده سازی شده است , در نتیجه شما یک Context به نام ApplicationDbContext  در فایل IdentityModels.cs دارید که اینترفیس IdentityDbContext را پیاده سازی می کند , همانند یک Context معمولی شما می توانید هر Entity جدیدی که نیاز داشتین به این Context اضافه کنید از جمله Entity Group را , این لپ مطلب این مقاله است .

در این مقاله نیز من Nuget نصب نکردم و تمام مراحل مقاله را انجام دادم , اما مدل این مقاله بر اساس کلید پیش فرض از نوع String می باشد , در نتیجه تنها تفاوت من در هر مرحله افزودن کد , تبدیل TKey به Int بود ابتدا برای کلاس ApplicationGroup به شکل زیر:

public class ApplicationGroup

{

public ApplicationGroup()

{

this.ApplicationRoles = new List<ApplicationGroupRole>();

this.ApplicationUsers = new List<ApplicationUserGroup>();

}

public ApplicationGroup(string name) : this()

{

this.Name = name;

}

public ApplicationGroup(string name, string description): this(name)

{

this.Description = description;

}

public int Id { get; set; }

public string Name { get; set; }

public string Description { get; set; }

public virtual ICollection<ApplicationGroupRole> ApplicationRoles { get; set; }

public virtual ICollection<ApplicationUserGroup> ApplicationUsers { get; set; }

}

همانطور که مشاهده می فرمایید تفاوت این کد با کد موجود در مقاله تبدیل کلید اصلی به نوع Int می باشد , شما الباقی مقاله را نیز به همین ترتیب تبدیل می کنید . (سورس کامل برنامه در اخر مقاله موجود است)

 

در پایان شما یک مدل User Group Role بر پایه کلید اصلی Int خواهید داشت که تمام مزایای ذکر شده در اول مقاله را داراست .

 

خوب حالا تازه رسیدیم به موضوع اصلی این مقاله :

 

مدل بدست امده اگر چه ظاهرا" بر پایه Group می باشد , اما در اصل همچنان تمام و کمال مبتنی بر Role می باشد , اما چرا ؟

 

علت اصلی در کلاس ApplicationGroupManager طراحی شده می باشد , ابتدا نگاهی به کد این Class در شکل زیر بیاندازید :

public class ApplicationGroupManager

{

private ApplicationGroupStore _groupStore;

private ApplicationDbContext _db;

private ApplicationUserManager _userManager;

private ApplicationRoleManager _roleManager;

public ApplicationGroupManager()

{

_db = HttpContext.Current

.GetOwinContext().Get<ApplicationDbContext>();

_userManager = HttpContext.Current

.GetOwinContext().GetUserManager<ApplicationUserManager>();

_roleManager = HttpContext.Current

.GetOwinContext().Get<ApplicationRoleManager>();

_groupStore = new ApplicationGroupStore(_db);

}

public IQueryable<ApplicationGroup> Groups

{

get

{

return _groupStore.Groups;

}

}

public async Task<IdentityResult> CreateGroupAsync(ApplicationGroup group)

{

await _groupStore.CreateAsync(group);

return IdentityResult.Success;

}

public IdentityResult CreateGroup(ApplicationGroup group)

{

_groupStore.Create(group);

return IdentityResult.Success;

}

public IdentityResult SetGroupRoles(int groupId, params string[] roleNames)

{

// Clear all the roles associated with this group:

var thisGroup = this.FindById(groupId);

thisGroup.ApplicationRoles.Clear();

_db.SaveChanges();

 

 

// Add the new roles passed in:

var newRoles = _roleManager.Roles.Where(r => roleNames.Any(n => n == r.Name));

foreach (var role in newRoles)

{

thisGroup.ApplicationRoles.Add(new ApplicationGroupRole

{

ApplicationGroupId = groupId,

ApplicationRoleId = role.Id

});

}

_db.SaveChanges();

 

 

// Reset the roles for all affected users:

foreach (var groupUser in thisGroup.ApplicationUsers)

{

this.RefreshUserGroupRoles(groupUser.ApplicationUserId);

}

return IdentityResult.Success;

}

public async Task<IdentityResult> SetGroupRolesAsync(int groupId, params string[] roleNames)

{

// Clear all the roles associated with this group:

var thisGroup = await this.FindByIdAsync(groupId);

thisGroup.ApplicationRoles.Clear();

await _db.SaveChangesAsync();

 

 

// Add the new roles passed in:

var newRoles = _roleManager.Roles

.Where(r => roleNames.Any(n => n == r.Name));

foreach (var role in newRoles)

{

thisGroup.ApplicationRoles.Add(new ApplicationGroupRole

{

ApplicationGroupId = groupId,

ApplicationRoleId = role.Id

});

}

await _db.SaveChangesAsync();

 

 

// Reset the roles for all affected users:

foreach (var groupUser in thisGroup.ApplicationUsers)

{

await this.RefreshUserGroupRolesAsync(groupUser.ApplicationUserId);

}

return IdentityResult.Success;

}

public async Task<IdentityResult> SetUserGroupsAsync(int userId, params int[] groupIds)

{

// Clear current group membership:

var currentGroups = await this.GetUserGroupsAsync(userId);

foreach (var group in currentGroups)

{

group.ApplicationUsers

.Remove(group.ApplicationUsers

.FirstOrDefault(gr => gr.ApplicationUserId == userId

));

}

await _db.SaveChangesAsync();

 

 

// Add the user to the new groups:

foreach (int groupId in groupIds)

{

var newGroup = await this.FindByIdAsync(groupId);

newGroup.ApplicationUsers.Add(new ApplicationUserGroup

{

ApplicationUserId = userId,

ApplicationGroupId = groupId

});

}

await _db.SaveChangesAsync();

 

 

await this.RefreshUserGroupRolesAsync(userId);

return IdentityResult.Success;

}

public IdentityResult SetUserGroups(int userId, params int[] groupIds)

{

// Clear current group membership:

var currentGroups = this.GetUserGroups(userId);

foreach (var group in currentGroups)

{

group.ApplicationUsers

.Remove(group.ApplicationUsers

.FirstOrDefault(gr => gr.ApplicationUserId == userId

));

}

_db.SaveChanges();

 

 

// Add the user to the new groups:

foreach (var groupId in groupIds)

{

var newGroup = this.FindById(groupId);

newGroup.ApplicationUsers.Add(new ApplicationUserGroup

{

ApplicationUserId = userId,

ApplicationGroupId = groupId

});

}

_db.SaveChanges();

 

 

this.RefreshUserGroupRoles(userId);

return IdentityResult.Success;

}

public IdentityResult RefreshUserGroupRoles(int userId)

{

var user = _userManager.FindById(userId);

if (user == null)

{

throw new ArgumentNullException("User");

}

// Remove user from previous roles:

var oldUserRoles = _userManager.GetRoles(userId);

if (oldUserRoles.Count > 0)

{

_userManager.RemoveFromRoles(userId, oldUserRoles.ToArray());

}

 

 

// Find teh roles this user is entitled to from group membership:

var newGroupRoles = this.GetUserGroupRoles(userId);

 

 

// Get the damn role names:

var allRoles = _roleManager.Roles.ToList();

var addTheseRoles = allRoles

.Where(r => newGroupRoles.Any(gr => gr.ApplicationRoleId == r.Id

));

var roleNames = addTheseRoles.Select(n => n.Name).ToArray();

 

 

// Add the user to the proper roles

_userManager.AddToRoles(userId, roleNames);

 

 

return IdentityResult.Success;

}

public async Task<IdentityResult> RefreshUserGroupRolesAsync(int userId)

{

var user = await _userManager.FindByIdAsync(userId);

if (user == null)

{

throw new ArgumentNullException("User");

}

// Remove user from previous roles:

var oldUserRoles = await _userManager.GetRolesAsync(userId);

if (oldUserRoles.Count > 0)

{

await _userManager.RemoveFromRolesAsync(userId, oldUserRoles.ToArray());

}

 

 

// Find the roles this user is entitled to from group membership:

var newGroupRoles = await this.GetUserGroupRolesAsync(userId);

 

 

// Get the damn role names:

var allRoles = await _roleManager.Roles.ToListAsync();

var addTheseRoles = allRoles

.Where(r => newGroupRoles.Any(gr => gr.ApplicationRoleId == r.Id

));

var roleNames = addTheseRoles.Select(n => n.Name).ToArray();

 

 

// Add the user to the proper roles

await _userManager.AddToRolesAsync(userId, roleNames);

 

 

return IdentityResult.Success;

}

public async Task<IdentityResult> DeleteGroupAsync(int groupId)

{

var group = await this.FindByIdAsync(groupId);

if (group == null)

{

throw new ArgumentNullException("User");

}

var currentGroupMembers = (await this.GetGroupUsersAsync(groupId)).ToList();

// remove the roles from the group:

group.ApplicationRoles.Clear();

 

 

// Remove all the users:

group.ApplicationUsers.Clear();

 

 

// Remove the group itself:

_db.ApplicationGroups.Remove(group);

 

 

await _db.SaveChangesAsync();

 

 

// Reset all the user roles:

foreach (var user in currentGroupMembers)

{

await this.RefreshUserGroupRolesAsync(user.Id);

}

return IdentityResult.Success;

}

public IdentityResult DeleteGroup(int groupId)

{

var group = this.FindById(groupId);

if (group == null)

{

throw new ArgumentNullException("User");

}

 

 

var currentGroupMembers = this.GetGroupUsers(groupId).ToList();

// remove the roles from the group:

group.ApplicationRoles.Clear();

 

 

// Remove all the users:

group.ApplicationUsers.Clear();

 

 

// Remove the group itself:

_db.ApplicationGroups.Remove(group);

 

 

_db.SaveChanges();

 

 

// Reset all the user roles:

foreach (var user in currentGroupMembers)

{

this.RefreshUserGroupRoles(user.Id);

}

return IdentityResult.Success;

}

public async Task<IdentityResult> UpdateGroupAsync(ApplicationGroup group)

{

await _groupStore.UpdateAsync(group);

foreach (var groupUser in group.ApplicationUsers)

{

await this.RefreshUserGroupRolesAsync(groupUser.ApplicationUserId);

}

return IdentityResult.Success;

}

public IdentityResult UpdateGroup(ApplicationGroup group)

{

_groupStore.Update(group);

foreach (var groupUser in group.ApplicationUsers)

{

this.RefreshUserGroupRoles(groupUser.ApplicationUserId);

}

return IdentityResult.Success;

}

public IdentityResult ClearUserGroups(int userId)

{

return this.SetUserGroups(userId, new int[] { });

}

public async Task<IdentityResult> ClearUserGroupsAsync(int userId)

{

return await this.SetUserGroupsAsync(userId, new int[] { });

}

public async Task<IEnumerable<ApplicationGroup>> GetUserGroupsAsync(int userId)

{

var result = new List<ApplicationGroup>();

var userGroups = (from g in this.Groups

where g.ApplicationUsers

.Any(u => u.ApplicationUserId == userId)

select g).ToListAsync();

return await userGroups;

}

public IEnumerable<ApplicationGroup> GetUserGroups(int userId)

{

var result = new List<ApplicationGroup>();

var userGroups = (from g in this.Groups

where g.ApplicationUsers

.Any(u => u.ApplicationUserId == userId)

select g).ToList();

return userGroups;

}

public async Task<IEnumerable<ApplicationRole>> GetGroupRolesAsync(int groupId)

{

var grp = await _db.ApplicationGroups

.FirstOrDefaultAsync(g => g.Id == groupId);

var roles = await _roleManager.Roles.ToListAsync();

var groupRoles = (from r in roles

where grp.ApplicationRoles

.Any(ap => ap.ApplicationRoleId == r.Id)

select r).ToList();

return groupRoles;

}

public IEnumerable<ApplicationRole> GetGroupRoles(int groupId)

{

var grp = _db.ApplicationGroups.FirstOrDefault(g => g.Id == groupId);

var roles = _roleManager.Roles.ToList();

var groupRoles = from r in roles

where grp.ApplicationRoles

.Any(ap => ap.ApplicationRoleId == r.Id)

select r;

return groupRoles;

}

public IEnumerable<ApplicationUser> GetGroupUsers(int groupId)

{

var group = this.FindById(groupId);

var users = new List<ApplicationUser>();

foreach (var groupUser in group.ApplicationUsers)

{

var user = _db.Users.Find(groupUser.ApplicationUserId);

users.Add(user);

}

return users;

}

public async Task<IEnumerable<ApplicationUser>> GetGroupUsersAsync(int groupId)

{

var group = await this.FindByIdAsync(groupId);

var users = new List<ApplicationUser>();

foreach (var groupUser in group.ApplicationUsers)

{

var user = await _db.Users

.FirstOrDefaultAsync(u => u.Id == groupUser.ApplicationUserId);

users.Add(user);

}

return users;

}

public IEnumerable<ApplicationGroupRole> GetUserGroupRoles(int userId)

{

var userGroups = this.GetUserGroups(userId);

var userGroupRoles = new List<ApplicationGroupRole>();

foreach (var group in userGroups)

{

userGroupRoles.AddRange(group.ApplicationRoles.ToArray());

}

return userGroupRoles;

}

public async Task<IEnumerable<ApplicationGroupRole>> GetUserGroupRolesAsync(int userId)

{

var userGroups = await this.GetUserGroupsAsync(userId);

var userGroupRoles = new List<ApplicationGroupRole>();

foreach (var group in userGroups)

{

userGroupRoles.AddRange(group.ApplicationRoles.ToArray());

}

return userGroupRoles;

}

public async Task<ApplicationGroup> FindByIdAsync(int id)

{

return await _groupStore.FindByIdAsync(id);

}

public ApplicationGroup FindById(int id)

{

return _groupStore.FindById(id);

}

}

در پس این Class طولانی , اصل کاریی که انجام می شود , یک فاجعه به تمام معنا است . اگر بخواهم خیلی خلاصه و ساده بگویم , به علت این که این مدل Identity همچنان یک مدل بر پایه Role است (یعنی بعد از SignIn (ورود کاربر به  سایت), زمانی که قرار است Role های کاربر از دیتابیس دریافت شود تا هنگام Authrize از آنها استفاده شود , این واکشی به طور مستقیم از جدول Role ها انجام خواهد شد و هیچ رابطه ایی با جدول Group موجود نخواهد بود.)

 

برای توضیح بیشتر , فرض کنید قرار است کاربر عضو یک Group جدید شود , برای این امر به طور خیلی خلاصه باید موارد زیر انجام شود :

 

  •     تمام Group های که User عضو انها است از دیتابیس دریافت می شود .
  •     User از تک تک این Group ها حذف می شود .
  •     به ازای تک تک Group های از قبل عضو و Group جدید , Group از دیتابیس دریافت شده و Userعضو ان می شود .
  •     تمام Role های User از دیتابیس دریافت می شود , User از این Role ها حذف می شود .
  •     تمام Group هایی که User عضو آنهاست از دیتابیس دریفت شده و به ازای تک تک انها تمام Role های عضو Group ها از دیتابیس دریافت می شود .
  •     به ازای تمام Role های مرحله قبل , User عضو آنها می شود .

 

خوب همانطور که متوجه شدید برای یک عملیات ساده عضو کردن یک User در یک Group , چه میزان عملیات اضافی باید انجام شود , حال شما فرض کنید اگر به جای یک کاربر , 1000 کاربر داشته باشین و یا به جای یک گروه , چندین گروه وجود داشته باشد و هر گروه چند صد Role داشته باشد ؟!!!

 

خوب Entity FrameWork خودش به میزان کافی کند است , حالا با این همه عملیات اضافی لغت فاجعه , لغت بسیار به جایی می باشد .

 

خوب این عملیات اضافی برای چه انجام می شود ؟

 

همانطور که در چند خط بالاتر توضیح دادم , هنگام SignIn به منظور Authrize کردن, Role هابه طور مستقیم از جدول Role ها دریافت می شود و هیچ رابطه ایی با جدول Group موجود نخواهد بود , در نتیجه هنگام عملیاتی نظیر حذف و اضافه User از Group , شما مجبور خواهید بود رابطه بین جدول Role و User را هم Update کنید , تا جدول Role برابر با Group های کاربر , به روز باشد , این عملیات Update همان عملیات اضافی است .

 

اولین راه حلی که برای حل این مشکل به ذهن من رسید Override کردن متد GetRolesAsync در کلاس ApplicationUserManager بود , شما در این متد می توانید به جای برگرداندن Role های مرتبط با Role, User های مرتبط با Group های مرتبط با User را برگردانید .

 

ایده بسیار ساده و شفاف است , اما متاسفانه در عمل با شکست مواجه شد . وقتی من این متد را Override کردم , وبسایت بدون خطا بالا امد , اما بعد از SignIn , وبسایت تا ابد در حال لود باقی می ماند , بدون پیام خطا و بدون TimeOut . شما می توانید قطعه کد زیر را ازمایش کنید :

public override Task<IList<string>> GetRolesAsync(int userId)

{

return new Task<IList<string>>(() => base.GetRolesAsync(userId).Result);

}

 اگر کسی علت این مشکل را می داند یا بعد از اجرای این کد با موفقیت SignIn کرد , ممنون میشم کامنت بذاره .

 

ویرایش جدید : این مشکل به علت DeadLock قطعه کد آزمایشی است , توضیحات بیشتر: جلوگیری از DeadLock در برنامه‌های Async , بنابراین علاوه بر راه حل دوم , شما در همینجا نیز می توانید , Role های مرتبط با Group را دریافت کنید .

 

اما راه حل دوم : در Asp.net Identity , بعد از عملیات SignIn و دریافت Role های User به کمک متد GetRolesAsync , این User و Role هایش در شی System.Web.HttpContext.Current.User ذخیره می شوند , این شی مکان مناسبی است تا شما Role های کاربر را با Role های مرتبط با Group های کاربر عوض کنید .

 

 من اینکار را در Event با نام AcquireRequestState در فایل Global.asax همانند قطعه کد زیر انجام دادم :

public class MvcApplication : System.Web.HttpApplication

{

public MvcApplication()

{

AcquireRequestState += OnAcquireRequestState;

}

private void OnAcquireRequestState(object sender, EventArgs eventArgs)

{

if (!System.Web.HttpContext.Current.User.Identity.IsAuthenticated) return;

if (System.Web.HttpContext.Current.Cache != null)

{

if (System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.User.Identity.Name + "roles"] == null ||

System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.User.Identity.Name + "roles"].ToString().Trim() =="")

{

var context = new Context();

var temp = context.GetUserRoles(System.Web.HttpContext.Current.User.Identity.Name);

System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.User.Identity.Name + "roles"] =

temp;

}

SetUserRoles(((IEnumerable)System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.User.Identity.Name + "roles"]).Cast<object>()

.Select(x => x.ToString())

.ToArray());

}

else

{

var context = new Context();

SetUserRoles(context.GetUserRoles(System.Web.HttpContext.Current.User.Identity.Name)

.Select(x => x.ToString())

.ToArray());

}

}

private void SetUserRoles(string[] roles)

{

System.Web.HttpContext.Current.User =

new GenericPrincipal(System.Web.HttpContext.Current.User.Identity,roles);

}

 

protected void Application_Start()

{

AreaRegistration.RegisterAllAreas();

FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);

RouteConfig.RegisterRoutes(RouteTable.Routes);

BundleConfig.RegisterBundles(BundleTable.Bundles);

}

}

این قطعه کد حاوی دو نکته می باشد :

1 - هر Request که از سمت Client به وبسایت شما ارسال بشود , موجب می شود که رویداد AcquireRequestState یکبار فراخونی گردد در نتیجه من برای اینکه به ازای هر Request به دیتابیس متصل نشوم , لیست Role ها را در یک Cache منحصر فرد به ازای هر کاربر ذخیره کردم , حتما" از خود می پرسید چرا به جای Cache از Session استفاده نکرده ام ؟ به دو علت , یکی عدم دسترسی مطمئن به شی Session به ازای هر Request و دومی به علت کارایی بهتر Cache نسبت به Session , در نتیجه متد LogOff در فایل AccountController.cs به شکل زیر نوشته ام :

// POST: /Account/LogOff

[HttpPost]

[ValidateAntiForgeryToken]

public ActionResult LogOff()

{

System.Web.HttpContext.Current.Session.Abandon();

System.Web.HttpContext.Current.Cache[System.Web.HttpContext.Current.User.Identity.Name + "roles"] = "";

AuthenticationManager.SignOut();

return RedirectToAction("Index", "Home");

}

 2 - من برای بالا بردن سرعت LogIn به جای اینکه Role های مرتبط را از ApplicationDbContextخود مدل دریافت کنم , مستقیما" از لایه دیتا یک Stored Procedure را فراخوانی می کنم , در نتیجه Context موجود در این قطعه کد , لایه دیتا من می باشد , نه Context موجود در Asp.Net Identity .(لینک دانلود سورس درپایان مقاله می باشد .)

بعد از انجام این راه حل کلاس ApplicationGroupManager که در چند خط بالاتر فاجعه نام گرفته بود , از فاجعه بودن در می آید و تبدیل می شود به قطعه کد زیر :

public class ApplicationGroupManager

{

private ApplicationGroupStore _groupStore;

private ApplicationDbContext _db;

private ApplicationUserManager _userManager;

private ApplicationRoleManager _roleManager;

public ApplicationGroupManager()

{

_db = HttpContext.Current

.GetOwinContext().Get<ApplicationDbContext>();

_userManager = HttpContext.Current

.GetOwinContext().GetUserManager<ApplicationUserManager>();

_roleManager = HttpContext.Current

.GetOwinContext().Get<ApplicationRoleManager>();

_groupStore = new ApplicationGroupStore(_db);

}

public ApplicationGroupManager(ApplicationDbContext dbContext,ApplicationUserManager userManager,ApplicationRoleManager roleManager)

{

_db = dbContext;

_userManager = userManager;

_roleManager = roleManager;

_groupStore = new ApplicationGroupStore(_db);

}

public IQueryable<ApplicationGroup> Groups

{

get

{

return _groupStore.Groups;

}

}

public async Task<IdentityResult> CreateGroupAsync(ApplicationGroup group)

{

await _groupStore.CreateAsync(group);

return IdentityResult.Success;

}

public IdentityResult CreateGroup(ApplicationGroup group)

{

_groupStore.Create(group);

return IdentityResult.Success;

}

public IdentityResult SetGroupRoles(int groupId, params int[] roleIds)

{

// Clear all the roles associated with this group:

var thisGroup = this.FindById(groupId);

thisGroup.ApplicationRoles.Clear();

 

// Add the new roles passed in:

foreach (var item in roleIds)

{

thisGroup.ApplicationRoles.Add(new ApplicationGroupRole

{

ApplicationGroupId = groupId,

ApplicationRoleId = item

});

}

_db.SaveChanges();

return IdentityResult.Success;

}

public async Task<IdentityResult> SetGroupRolesAsync(int groupId, params int[] roleIds)

{

// Clear all the roles associated with this group:

var thisGroup = await this.FindByIdAsync(groupId);

thisGroup.ApplicationRoles.Clear();

foreach (var item in roleIds)

{

thisGroup.ApplicationRoles.Add(new ApplicationGroupRole

{

ApplicationGroupId = groupId,

ApplicationRoleId = item

});

}

await _db.SaveChangesAsync();

return IdentityResult.Success;

}

public async Task<IdentityResult> SetUserGroupsAsync(int userId, params int[] groupIds)

{

// Clear current group membership:

var currentUser = await _userManager.FindByIdAsync(userId);

currentUser.ApplicationGroups.Clear();

// Add the user to the new groups:

foreach (int groupId in groupIds)

{

currentUser.ApplicationGroups.Add(new ApplicationUserGroup

{

ApplicationUserId = userId,

ApplicationGroupId = groupId

});

}

await _db.SaveChangesAsync();

return IdentityResult.Success;

}

public IdentityResult SetUserGroups(int userId, params int[] groupIds)

{

// Clear current group membership:

var currentUser = _userManager.FindById(userId);

currentUser.ApplicationGroups.Clear();

// Add the user to the new groups:

foreach (var groupId in groupIds)

{

currentUser.ApplicationGroups.Add(new ApplicationUserGroup

{

ApplicationUserId = userId,

ApplicationGroupId = groupId

});

}

_db.SaveChanges();

return IdentityResult.Success;

}

public async Task<IdentityResult> DeleteGroupAsync(int groupId)

{

var group = await this.FindByIdAsync(groupId);

if (group == null)

{

throw new ArgumentNullException("User");

}

// remove the roles from the group:

group.ApplicationRoles.Clear();

 

// Remove all the users:

group.ApplicationUsers.Clear();

 

// Remove the group itself:

_db.ApplicationGroups.Remove(group);

await _db.SaveChangesAsync();

return IdentityResult.Success;

}

public IdentityResult DeleteGroup(int groupId)

{

var group = this.FindById(groupId);

if (group == null)

{

throw new ArgumentNullException("User");

}

// remove the roles from the group:

group.ApplicationRoles.Clear();

 

// Remove all the users:

group.ApplicationUsers.Clear();

// Remove the group itself:

_db.ApplicationGroups.Remove(group);

_db.SaveChanges();

return IdentityResult.Success;

}

public async Task<IdentityResult> UpdateGroupAsync(ApplicationGroup group)

{

await _groupStore.UpdateAsync(group);

return IdentityResult.Success;

}

public IdentityResult UpdateGroup(ApplicationGroup group)

{

_groupStore.Update(group);

return IdentityResult.Success;

}

public IdentityResult ClearUserGroups(int userId)

{

return this.SetUserGroups(userId, new int[] { });

}

public async Task<IdentityResult> ClearUserGroupsAsync(int userId)

{

return await this.SetUserGroupsAsync(userId, new int[] { });

}

public async Task<IEnumerable<ApplicationGroup>> GetUserGroupsAsync(int userId)

{

var userGroups = (from g in this.Groups

where g.ApplicationUsers

.Any(u => u.ApplicationUserId == userId)

select g).ToListAsync();

return await userGroups;

}

public IEnumerable<ApplicationGroup> GetUserGroups(int userId)

{

var userGroups = (from g in this.Groups

where g.ApplicationUsers

.Any(u => u.ApplicationUserId == userId)

select g).ToList();

return userGroups;

}

public async Task<IEnumerable<ApplicationRole>> GetGroupRolesAsync(int groupId)

{

var grp = await _db.ApplicationGroups

.FirstOrDefaultAsync(g => g.Id == groupId);

var roles = await _roleManager.Roles.ToListAsync();

var groupRoles = (from r in roles

where grp.ApplicationRoles

.Any(ap => ap.ApplicationRoleId == r.Id)

select r).ToList();

return groupRoles;

}

public IEnumerable<ApplicationRole> GetGroupRoles(int groupId)

{

var grp = _db.ApplicationGroups.FirstOrDefault(g => g.Id == groupId);

var roles = _roleManager.Roles.ToList();

var groupRoles = from r in roles

where grp.ApplicationRoles

.Any(ap => ap.ApplicationRoleId == r.Id)

select r;

return groupRoles;

}

public IEnumerable<ApplicationUser> GetGroupUsers(int groupId)

{

var group = this.FindById(groupId);

var users = new List<ApplicationUser>();

foreach (var groupUser in group.ApplicationUsers)

{

var user = _db.Users.Find(groupUser.ApplicationUserId);

users.Add(user);

}

return users;

}

public async Task<ApplicationGroup> FindByIdAsync(int id)

{

return await _groupStore.FindByIdAsync(id);

}

public ApplicationGroup FindById(int id)

{

return _groupStore.FindById(id);

}

}

اگر شما به تعداد خطوط این کلاس ApplicationGroupManager جدید نسبت به قبلی دقت کنید , متوجه می شوید که چه تعداد کد که همه انها کوئری روی دیتابیس بوده اند حذف شده است , در نتیجه سرعت مدیریت کاربران سایت شما فوق العاده بیشتر خواهد بود. برای توضیح بیشتر در قطعه کد بالا , برای اضافه کردن یک User به یک Group (متد SetUserGroupsAsync) دیگر نیازی به Update رابطه Role با User نمی باشد , در نتیجه  تنها کافی است رابطه User با Group ویرایش شود و هیچ نیازی به ویرایش رابطه با Role نمی باشد .

البته لازم به توضیح است که تنها کلاس ApplicationGroupManager تغییر نکرده بلکه Controller ها و موارد دیگر هم متناسب با این ApplicationGroupManager جدید تغییر کرده اند که لینک سورس کامل در پایان مقاله برای دانلود هست .

خوب ما به هدف اصلی مقاله رسیدیم یعنی مدل Asp.Net Identity را توسعه دادیم و حال بدون نیاز به تغییر در کد الگوهای Asp.net می توان از این مدل استفاده نمود , مثلا در الگوی MVC شما می توانید طبق روال عادی از Attribute Authorize استفاده کنید .

 

نکات اموزشی Asp.Net Identity مرتبط با Entity Frame Work :

 اولین موردی که در این مدل توجه من رو به خودش جلب کرد , عدم استفاده مستقیم از کلاس ApplicationRole در کلاس ApplicationUser به عنوان رابطه n به n بین User و Role بود , در واقع مایکروسافت از کد زیر در درون کلاس ApplicationUser استفاده کرده است :

public virtual ICollection<ApplicationUserRole> Roles { get; }

به جای کلاس ApplicationRole  از ApplicationUserRole استفاده کرده است , کد کلاس ApplicationUserRole نیز  به شکل زیر است :

// Summary:

// EntityType that represents a user belonging to a role

//

// Type parameters:

// TKey:

public class IdentityUserRole<TKey>

{

public IdentityUserRole();

// Summary:

// RoleId for the role

public virtual TKey RoleId { get; set; }

//

// Summary:

// UserId for the user that is in the role

public virtual TKey UserId { get; set; }

}

دلیل اینکار مایکروسافت , مربوط به مشکل ویرایش رابطه n به n در Entity FrameWork بوده است , هرچند این مشکل با Nuget ایی مثل GraphDiff قابل حل بود , اما راه حل مایکروسافت به نظر جالب تر می آید . مایکروسافت برای حل مشکل از یک کلاس واسط به نام ApplicationUserRole استفاده کرده است , که فقط Id کلاس User و Role را درون خود نگه می دارد , با این روش مشکل ویرایش رابطه n به n حل شده است , اما فواید شی گرائی Entity FrameWork از بین رفته است , یعنی زمانی که شما توقع دارین خروجی کد ()ApplicationUser.Roles.Tolist لیستی از موجودیت های از نوع کلاس ApplicationRole باشد , تنها به لیستی از اشیا از نوع ApplicationUserRole می رسید , که فقط حاوی فیلد Id کلاس ApplicationRole  می باشند .

راه حلی که برای این مشکل به ذهن من رسید , استفاده از یک View دیتابیسی برای برگرداندن تمام Role ها ست و منتسب کردن این View به یک Entity جدید , مثلا به اسم UserRoles .

حالا فراخوانی کد ()UserRoles.Where(ur=>ur.UserId == myUserId).Tolist علاوه بر Id سایر فیلدهای کلاس ApplicationRole را بر می گرداند . اگر دوستان در این مورد هم نظراتشون رو اعلام کنند خیلی ممنون می شم .

 دومین نکته مربوط به Migration می باشد , در مقاله اصلی به جای Migration از متد Database.SetInitializer برای ساخت و مقدار دهی اولیه دیتابیس استفاده کرده است .این روش برای برنامه های کوچک , قابل قبول هست , اما برای سیستم های متوسط و بزرگ روش بهتر استفاده از Migration می باشد .لذا من در فایل IdentityConfig.cs , کلاس ApplicationDbInitializer را از NullDatabaseInitializer ارث برده ام تا عملیات ساخت و مقدار دهی اولیه دیتابیس توسط خود Entity FrameWork انجام نشود.

اما قبل از استفاده از Migration باید راجع به مسئله Context صحبت کنیم , تا اینجا ما متوجه شدیم که Asp.Net برای مدیریت کاربران خود از یک Context ساخته شده از Entity FrameWork استفاده می کند (در برنامه ما ApplicationDbContext نام دارد) , اما خود ما چی ؟ یعنی بهتر بگم Bussines اصلی وبسایت ما بر روی کدام Context باید اجرا شود , Context مرتبط با Asp.Net Identity یا Context جدیدی خاص Bussines سایت ما ؟

من در بعضی جاها دیدم که پیشنهاد شده تا Bussines سایت هم بر روی Context مربوط به Asp.Net Identity قرار گیرد , به نظر من برای وبسایت های کوچک این پیشنهاد خوبی است , اما برای وبسایت های متوسط و بزرگ به 3 دلیل با این امر مخالفم :

1 - به خاطر اصول شی گرایی : یک از اصول اولیه شی گرایی بر پایه مشخص و یکتا بودن وظیفه هر کلاس می باشد , وقتی شما کلاسی به نام ApplicationDbContext دارید بهتر است کلاسی هم به نام BussinesDbContext داشته باشید با وظایف کاملا جدا از هم و مشخص , با این روش , نگهداری برنامه شما نیز ساده تر خواهد بود .

2 - Performance : اگر شما Bussines بزرگی داشته باشین , در نتیجه Model بزرگی خواهید داشت , اگر Context شما همان Context متعلق به Asp.Net باشد , در هر عملیات ساده LogIn کل مدل شما باید در حافظه لود شود و Initialize اولیه Context نسبت به مدل کوچک Asp.Net Identity زمان بیشتری خواهد گرفت , این مشکل به خصوص در سایتهای SPA یا سایت هایی که بعد از ورود کاربر , در Request جداگانه با Ajax, محتوای سایت لود می شود , پر رنگ تر خواهد بود .

3 - امنیت : اگر Context مدیریت کاربران سایت با Context محتوای اصلی سایت یکی باشد , در مواردی مثل استفاده از WebApi یا Breeze شما باید کد بشتری بنویسید تا , مدیریت کاربران به صورت Public در اختیار همه قرار نگیرد .

بنابراین توصیه من استفاده از حداقل 2 Context در برنامه می باشد , در نتیجه در مدل ساخته شده , صرفا برای اموزش Migration با دو یا چند Context از یک Context دیگر به نام MessingerContext استفاده نموده ام .

در مدل نمونه , من از LocalDb به عنوان دیتابیس استفاده کرده ایم , در نتیجه برای عملیات Migration حتما نیاز به وجود فیزیکی فایل دیتابیس با پسوند MDF می باشد .شما برای تست Migraton می توانید یکبار این فایل را در ویژوال استدیو باز و تمام جداول موجود در ان را حذف کنید و سپس عملیات Migration را انجام دهید .برای این منظور ابتدا همانند شکل زیر یک Connection جدید بسازید :

 

سپس همانند شکل زیر :

 

 

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

 

بعد از اجرای عملیات Migraton , فولدری با نام ApplicationDbContextMigrations به پروژه شما اضافه می شود , چون من یکبار Migration انجام داده ام این فولدر در پروژه OO.WebUI وجود دارد لذا قبل از تست خودتان این فولدر را حذف کنید .همچنین فولدر با نام MessingerContextMigrations را از پروژه OO.DataAccess حذف کنید .

 

حال همه چیز برای Migration اماده است . برای فعال کردن Migration برای ApplicationDbContext ابتدا از قسمت Defaults Project کنسول Pakage Manager , پروژه OO.WebUI را انتخاب نمایید سپس دستور زیر را در Pakage Manager Consol وارد کنید :

Enable-Migrations -ContextTypeName OO.WebUI.Models.ApplicationDbContext -MigrationsDirectory:ApplicationDbContextMigrations

 

بعد از ساخت Migration , برای مقدار دهی اولیه به دیتابیس , مقادیر زیر را در متد Seed در فایل Configuration.cs در فولدر ApplicationDbContextMigrations وارد کنید :

protected override void Seed(OO.WebUI.Models.ApplicationDbContext context)

{

var userManager = new ApplicationUserManager(new ApplicationUserStore(context));

var roleManager =new ApplicationRoleManager(new ApplicationRoleStore(context));

const string name = "admin@example.com";

const string password = "Admin@123456";

const string roleName = "Admin";

 

 

//Create Role Admin if it does not exist

var role = roleManager.FindByName(roleName);

if (role == null)

{

role = new ApplicationRole(roleName);

var roleresult = roleManager.Create(role);

}

 

 

var user = userManager.FindByName(name);

if (user == null)

{

user = new ApplicationUser

{

UserName = name,

Email = name,

EmailConfirmed = true

};

var result = userManager.Create(user, password);

result = userManager.SetLockoutEnabled(user.Id, false);

}

 

 

var groupManager = new ApplicationGroupManager(context,userManager,roleManager);

var newGroup = new ApplicationGroup("SuperAdmins", "Full Access to All");

 

 

groupManager.CreateGroup(newGroup);

groupManager.SetUserGroups(user.Id, new int[] { newGroup.Id });

groupManager.SetGroupRoles(newGroup.Id, new int[] { role.Id });

}

برای ساخت قسمتی از دیتابیس که متعلق به ApplicationDbContext می باشد :

update-database -ConfigurationTypeName OO.WebUI.ApplicationDbContextMigrations.Configuration

حال برای MessingerContext  , از قسمت Defaults Project کنسول Pakage Manager پروژه OO.DataAccess را انتخاب نمایید .

برای فعال کردن Migration برای MessingerContext :

Enable-Migrations -ContextTypeName OO.DataAccess.Contexts.MessingerContext -MigrationsDirectory:MessingerContextMigrations

 

برای ساخت اولین Migration :

 

add-migration FirstTime

 

برای ساخت قسمتی از دیتابیس که متعلق بهMessingerContext می باشد :

 

update-database -ConfigurationTypeName OO.DataAccess.MessingerContextMigrations.Configuration

 

فقط دقت فرمایید اگر هر دو Context شما در یک پروژه هستند , در دستور Add Migration باید Context مشخص شود , مثلا به شکل زیر :

 

Add-Migration -ConfigurationTypeName OO.DataAccess.MessingerContextMigrations.Configuration FirstTime

 

خوب دیتابیس شما کامل شد .

در پایان شاید این نکته برای کسانی که تازه برنامه نویسی را شروع کرده باشند لازم باشد , وقتی شما سورس کد را دانلود کردید , احتمالا" بخواهید نام پروژه را عوض کنید و نام وبسایت خود را روی آن بگذارید , این تغییر نام برای برنامه نویسان تازه کار مقداری گیج کننده است لذا من مطالعه این مقاله را که مراحل این کار را توضیح داده توصیه می کنم .

من در سورس کد به دلایل آموزشی از هیچ نوع Container برای IOC استفاده نکردم , شاید نقطه خوب بعدی برای توسعه وبسایتتان , استفاده از Container مورد علاقه تان مثل Unity یا StructureMap یا ... باشد .

دو تا سورس کد برای دانلود وجود دارد , OnLineOstad.rar با حجم کمتر , چون Nuget Pakage هایش همراهش نیست , در نتیجه بعد از باز کردن Solution در ویژوال استدیو برای دریافت Missing Pakage ها باید یکبار Rebuild کنید و OnLineOstadByPackage.rar با حجم بیشتر به همراه Nuget هایش .امیدوارم این پروژه بتواند برای شما مفید باشد .