عمليات Create, Read, Update & Delete في ASP.NET Core Identity

-

في هذا الدرس بنتعلم كيف بنعمل عمليات الاضافة والحذف والتعديل والقراءة (عمليات CRUD )باستخدام  ASP.NET Core Identity

تمام حتي نفهم الموضوع نكمل الشغل على مشروع ASP.NET.Core.Identity: 

انشاء Create Users in Identity

نبدأ بانشاء Model Class باسم Identity داخل مجلد Models باسم User.cs ونضيف الكود التالي الى الملف:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace ASP.NET.Core.Identity.Models
{
    public class User
    {
        [Required]
        public string Name { get; set; }
        [Required]
        [RegularExpression("^[a-zA-Z0-9_.-]+@([a-zA-Z0-9-]+.)+[a-zA-Z]{2,6}$", ErrorMessage = "E-mail is not valid")]
        public string Email { get; set; }
        [Required]
        public string Password { get; set; }
    }
}

استخدام UserManager<T> class

 تتم إدارة Users من خلالclass 

UserManager<T>

حيث T هي class المختارة لتمثيل Users في قاعدة البيانات.

في الجدول التالي بنوضح مكونات هذا class 


الاسم
الوصف
Users
تقوم هذه الخاصية بإرجاع تسلسل يحتوي على المستخدمين المخزنين في قاعدة بيانات الهوية.
FindByIdAsync(id)ييستخدم للبحث عن مستخدم عن طريق رقم محدد specified ID
CreateAsync(user, password)
تخزن هذه الطريقة مستخدم جديد في قاعدة البيانات باستخدام كلمة المرور المحددة
UpdateAsync(user)
يتم تعديل مستخدم موجود في قاعدة بناء على بيانات الهوية Identity .
DeleteAsync(user)
يتم حذف مستخدم من قاعدة بيانات عن طريق الهويةIdentity .
AddToRoleAsync(user, name)
يضيف مستخدم إلى role
RemoveFromRoleAsync(user, name)
يزيل مستخدم من role
GetRolesAsync(user)
يعطي أسماء الأدوار roles التي يكون المستخدم عضوًا فيها
IsInRoleAsync(user, name)
إرجاع "true" هو أن المستخدم عضو في دور محدد specified role، وإلا ترجع خطأ

تمام نتذكر ان انشانا class باسم AppUser والي بنستخدمو لتمثيل المستخدمين في قاعدة البيانات واكيد تم وراثة IdentityUser لهذا class

الجدول التالي يصف خصائص AppUser class  الأكثر فائدة.


الاسمالوصف
Id
يحتوي على Unique Id  للمستخدم
UserName
يحتوي على username الخاص بالمستخدم
Email
يحتوي على البريد الإلكتروني للمستخدم

تمام بهيك بنكون جهزنا الملفات التي بنحتاجها. 

الان دور نعمل Controllers خلونا نضيف Controllers باسم AdminController داخل مجلد Controllers (اذا ما كان المجلد موجد اعمل مجلد بنفس الاسم) ونضيف الكود التالي: 

using ASP.NET.Core.Identity.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace ASP.NET.Core.Identity.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;
        public AdminController(UserManager<AppUser> usrMgr)
        {
            userManager = usrMgr;
        }
        public IActionResult Index()
        {
            return View();
        }
    }
}

نفهم شو عملنا في هذا Controllor

في Controller ، عملنا instance باسم userManager من UserManager من خلال حقن التبعية Dependency Injection. وبعدها في constructor  عملنا تعيين قيمة المتغير المسمى userManager إلى كائن  <UserManager<AppUser من خلال DI

الان خلونا نضيف Action باسم CreateUser والي بنستخدمها لانشاء مستخدم جديد في Identity database. الكود : 

 public ViewResult CreateUser() => View();

هذا action لاستدعاء CreateUser() View

 [HttpPost]
        public async Task<IActionResult> CreateUser(User user)
        {
            if (ModelState.IsValid)
            {
                AppUser appUser = new AppUser
                {
                    UserName = user.Name,
                    Email = user.Email
                };
                IdentityResult result = await userManager.CreateAsync(appUser, user.Password);
                if (result.Succeeded)
                    return RedirectToAction("Index");
                else
                {
                    foreach (IdentityError error in result.Errors)
                        ModelState.AddModelError("", error.Description);
                }
            }
            return View(user);
        }

نفهم الكود: 

اولا هذا ال action من نوع HTTP POST يستقبل Model من نوع User الي المفروض بتحتوي على بيانات المستخدم، وبعدها بنتأكد من ModelState اذا كانت Valid او لا. اذا كانت Valid بنكمل شغل اذا لا بنرجع رسالة خطأ. 

تمام الان اذا كانت ModelState  صالحة بتم عمل instance باسم appUser من AppUser وبنخزن فيها قيم UserName, Email الي راجعه من User Model . الكود الي بيعمل هاذ الاشي : 

AppUser appUser = new AppUser
{
    UserName = user.Name,
    Email = user.Email
};
تمام الخطوة التالية: 
بنستخدم طريقة CreateAsync ل UserManager class الي حكينا عنها في اول الدرس ،واستخدمنا object المسمى userManager لهذا الامر والي عرفناه من خلال Dependency Injection.
في  هما CreateAsync ارسلنا 2 parameters  وهما :
1- AppUser class المسمى appUser 
2- password.
والنتيجة بترجع في فئة IdentityResult الذي يمثل نتيجة العملية.

الجدول التالي وضحنا شو هي الخصائص الي بترجع من IdentityResult

الخاصيةالوصف
Succeeded 
في حال نجاح العملية بتكون النتيجة Succeeded 
Errors 
يتم ارجاع object من نوع IEnumerable بحيث يحتوي على الأخطاء التي حدثت أثناء تنفيذ عملية Identity. ويحتوي IdentityError على خاصية description  تعطي وصفًا لكل خطأ حدث

لاحظ في الكود استخدمنا result.Succeeded للتحقق من ذلك. اذا كانت النتيجة ناجحه برجع لصفحة Index والا بنضيف الاخطأ الى رجعت الى Model State الكود: 
IdentityResult result = await userManager.CreateAsync(appUser, user.Password);
 if (result.Succeeded)
    return RedirectToAction("Index");
else
 {
  foreach (IdentityError error in result.Errors)
  ModelState.AddModelError("", error.Description);
}
أخر سطر اضفنا الامر : 

 return View(user);
هنا سيتم عرض الأخطاء من اجل تصحيحها ومحاولة إنشاء المستخدم مرة أخرى.

انشاء View :
بيانات المستخدم لازم يتم اضافتها عن طريق View لذا خلونا الان نعمل View يحتوي على مجموعة حقول خاصة بالبيانات المطلوب اضافتها وهي UserName, Email. 

اول اشي نتأكد من وجود مجلد باسم Views في التطبيق بعد هيك انقر يمين فوق action المسمى CreateUser وبعدها اختر Add New View 
بعد الاضافة بكون شكل المشروع     


الان نضيف الكود التالي الى View:

@model User @{ ViewData["Title"] = "Create User"; } <h1 class="bg-light text-black">Create User</h1> <div asp-validation-summary="All" class="text-danger"></div> <form method="post">  <div class="form-group">  <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> </div>  <div class="form-group"> <label asp-for="Email"></label> <input asp-for="Email" class="form-control" />  </div>  <div class="form-group">  <label asp-for="Password"></label> <input asp-for="Password" class="form-control" /> </div><button type="submit" class="btn btn-success">Create</button></form>
نفهم الكود اضفنا السطر :
@model User
والي بيعني ان هذا View بستقبل Model من نوع User 
ملاحظة : ما تنسى تضيف NameSpace لهذا Model داخل ملف ViewImports.cshtml_
الكود الخاص بملف ViewImports.cshtml_
@using ASP.NET.Core.Identity
@using ASP.NET.Core.Identity.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

بعدها عملنا كود HTML للاسم والبريد الإلكتروني وتم ربطهم مع Property الموجوده في User Model باستخدام الخاصية asp-for واخر اشي اضفنا button لارسال الطلب الى CreateUser action
  
نجرب الصفحه ونشوف النتيجة:
https://localhost:44347/admin/CreateUser
شكل الصفحة

حاول ادخال بيانات ثم اضغط فوق Create. خلونا نشوف قاعدة البيانات بعد الاضافة 

من Visual Studio  نروح الى 

وبعدها من قاعدة البيانات نروح الى Table المسمى AspNetUsers واعرض البيانات بتكون النتيجة :



بهيك بنكون اضفنا مستخدم الى قاعدة البيانات . 

طيب الان خلونا نعمل عمليات التعديل والحذف على المستخدمين . 

اول اشي بنعملو هو قراءة بيانات المستخدمين وعرضها في صفحه وبعدها بنعمل العمليات. 

قراءة جميع المستخدمين من Read all Users in Identity

حتى نرجع جميع المستخدمين الامر بسيط جدا في Identity باستخدام UserManager class. 
خلونا نعدل على Index action ونضيف الكود التالي: 

 public IActionResult Index()
   {
      return View(userManager.Users);
   }
هذا الكود بيعمل على ارجاع جميع المستخدمين المسجلين في قاعدة البيانات.
حتي نعرض بيانات المستخدمين لازم نعمل View، انقر يمين فوق Index واختر Add New View وبعدها نضيف الكود التالي الى View :

@model IEnumerable<AppUser>
@{
    ViewData["Title"] = "All Identity Users";
}
<h1 class="bg-info text-white">All Identity Users</h1>
<table class="table table-sm table-bordered">
    <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Email</th>
    </tr>
    @foreach (AppUser user in Model)
    {
        <tr>
            <td>@user.Id</td>
            <td>@user.UserName</td>
            <td>@user.Email</td>
        </tr>
    }
</table>

استقبلنا في هذا View متغير model من نوع AppUser على شكل IEnumerable، وبعدها عملنا table لعرض البيانات باستخدام foreach@ وعرضنا البيانات داخل هذا table 

نشغل الصفحة ونشوف النتيجة:
شغل الصفحة بالدخول الى الرابط : 
https://localhost:44347/admin
بتكون النتيجة : 

وبهيك بنكون عرضنا البيانات الموجوده عندنا في قاعدة البيانات. 
تمام الان دور نتعلم كيف ممكن نعمل تعديل على البيانات 

تتحديث بيانات Update Users in Identity

اكيد بنحتاج تعديل على View الخاص بعرض جميع الاسماء والتعديل بكون اضافة عمود يحتوي على Button خاص بالتعديل.
تمام ننتقل الان الى Index View ونعمل تعديل بسيط شوف الكود : 

@model IEnumerable<AppUser>
@{
    ViewData["Title"] = "All Identity Users";
}
<h1 class="bg-light text-black">All Identity Users</h1>
<a asp-action="Create" class="btn btn-secondary">Create a User</a>
<br />
<br />
<table class="table table-sm table-bordered">
    <tr>
    <th>ID</th>
    <th>Name</th>
    <th>Email</th>
    <th>Update</th>
    </tr>
    @foreach (AppUser user in Model)
    {
        <tr>
            <td>@user.Id</td>
            <td>@user.UserName</td>
            <td>@user.Email</td>
            <td>
                <a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@user.Id">
                    Update
                </a>
            </td>

        </tr>
    }
</table>

التعديل بكون على UserName، Email، Password . 

تمام نرجع نوخذ فكره عن البيانات المخزنه في قاعدة البيانات (شوف الصورة):


اكيد لاحظت ان كلمة المرور مشفرة وبالتالي اي تعديل على كلمة المرور لازم يكون مشفر ايضا. ومشان نعمل التشفير بنستخدم IPasswordHasher 

ومشان نستخدم هذا IPasswordHasher  لازم نعرف instance خاص به وبعدها نستخدم dependency في constructor وبناء على هذا الكلام لازم نعدل الكود الى : 

using Microsoft.AspNetCore.Mvc;
using Identity.Models;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
namespace Identity.Controllers
{
    public class AdminController : Controller
    {
        private UserManager<AppUser> userManager;
        private IPasswordHasher<AppUser> passwordHasher;
        public AdminController(UserManager<AppUser> usrMgr, IPasswordHasher<AppUser> passwordHash)
        {
            userManager = usrMgr;
            passwordHasher = passwordHash;
        }
    }
}
تمام الان لازم نعمل action خاص بالتعديل. اضف الكود التالي الى AdminController 

        public async Task<IActionResult> Update(string id)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
                return View(user);
            else
                return RedirectToAction("Index");
        }
 
        [HttpPost]
        public async Task<IActionResult> Update(string id, string email, string password)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
            {
                if (!string.IsNullOrEmpty(email))
                    user.Email = email;
                else
                    ModelState.AddModelError("", "Email cannot be empty");
 
                if (!string.IsNullOrEmpty(password))
                    user.PasswordHash = passwordHasher.HashPassword(user, password);
                else
                    ModelState.AddModelError("", "Password cannot be empty");
 
                if (!string.IsNullOrEmpty(email) && !string.IsNullOrEmpty(password))
                {
                    IdentityResult result = await userManager.UpdateAsync(user);
                    if (result.Succeeded)
                        return RedirectToAction("Index");
                    else
                        Errors(result);
                }
            }
            else
                ModelState.AddModelError("", "User Not Found");
            return View(user);
        }
 
        private void Errors(IdentityResult result)
        {
            foreach (IdentityError error in result.Errors)
                ModelState.AddModelError("", error.Description);
        }
 

لاحظ ان اضفنا نوعين من هذا action

الأول من نوع Get لاستدعاء View الخاص بالتعديل ومن خلاله بتم عرض بيانات المستخدم بناء على Id الخاص بالمستخدم باستخدام الامر FindByIdAsync(id)  والنتيجة بتتخزن في في متغير باسم user من نوع AppUser وبعدها بنتأكد من user اذا ما كان Null هذا بيعني ان تم ايجاد بيانات المستخدم وبعدها بنرجع البيانات الى View على شكل user Model 

والثاني من النوع Post  لاجراء التعديل المطلوب. حيث قمنا بارجاع بيانات المستخدم بناء على Id المرسل باستخدام الامر 

await userManager.FindByIdAsync(id)

وحتي نعمل التعديل المطلوب اول اشي تأكدنا من وجود بيانات راجعه باستخدام الامر 

 if (user != null)

وبعدها عملنا تحقق من القيم المدخلة،

الايميل ما يكون فاضي(الكود) :

if (!string.IsNullOrEmpty(email))
                    user.Email = email;
                else
                    ModelState.AddModelError("", "Email cannot be empty");


والباسورد عملنا تشفير الها

if (!string.IsNullOrEmpty(password))
                    user.PasswordHash = passwordHasher.HashPassword(user, password);
                else
                    ModelState.AddModelError("", "Password cannot be empty");

واذا كانت القيم صحيحه بنستخدم الامر لاجراء التعديل المطلوب

await userManager.UpdateAsync(user);

والنتيجة بتتخزن في متغير باسم result من نوع IdentityResult وحسب النتيجة بتم اتخاذ القرار اذا كان العملية ناجحه بنرجع لصفحة Index واذا في خطأ بنعمل استدعاء ل Errors function  الي بنعمل تخزين الاخطاء في Model وبعدها بنرجع هذه الأخطاء الى View 

انشاء View 

اكيد حتي نعدل على البيانات بنحتاج الى عرض هذه البيانات في شاشة لذا المطلوب انشاء View لهذا action مشان هيك انقر يمين فوق Update action واختر Add new View  وبعدها نضيف الكود التالي : 

@model AppUser
@{
    ViewData["Title"] = "Update User";
}
<h1 class="bg-light text-black">Update User</h1>
<a asp-action="Index" class="btn btn-secondary">Back</a>
<div asp-validation-summary="All" class="text-danger"></div>
<form asp-action="Update" method="post">
    <div class="form-group">
        <label asp-for="Id"></label>
        <input asp-for="Id" class="form-control" disabled />
    </div>
    <div class="form-group">
        <label asp-for="Email"></label>
        <input asp-for="Email" class="form-control" />
    </div>
    <div class="form-group">
        <label for="password">Password</label>
        <input name="password" class="form-control" />
    </div>
    <button type="submit" class="btn btn-primary">Save</button>
</form>
نفهم شو عملنا استقبلنا Model  من نوع AppUser وربطهنا HTML tag مع Property من AppUser باستخدام asp-for واخر اشي عملنا button من خلاله بنعمل استدعاء Update action لتحديث البيانات 
نجرب نشغل الصفحة ونشوف النتيجة :
https://localhost:44347/admin

لاحظ Source HTML الخاص ب Update button حيث تم استدعاء Update action وارسال Id  الخاص بالاسم . 
تمام ننقر فوق Update button وبتكون النتيجة:



حاول تغيير البيانات الموجوده وانقر فوق Save وشوف النتيجة. 

ملاحظة : في المشاريع الحقيقية تغيير الايميل ما بصير هيك لازم يكون في تأكيد يعني ترسل رسالة تأكيد الى البريد المدخل حتى نتأكد ان الشخص هو صاحب الايميل الحقيقي.

حذف المستخدم Delete Users in Identity
اول اشي لازم نعدل على Index View  لاضافة button خاص بالحذف لذا انتقل الى Index View وعدل الكود الى : 
@model IEnumerable<AppUser>
@{
    ViewData["Title"] = "All Identity Users";
}



<h1 class="bg-light text-black">All Identity Users</h1>
<a asp-action="Create" class="btn btn-secondary">Create a User</a>
<br />
<br />
<table class="table table-sm table-bordered">
    <tr>
        <th>ID</th>
        <th>Name</th>
        <th>Email</th>
        <th>Update</th>
        <th>Delete</th>
    </tr>
    @foreach (AppUser user in Model)
    {
<tr>
    <td>@user.Id</td>
    <td>@user.UserName</td>
    <td>@user.Email</td>
    <td>
        <a class="btn btn-sm btn-primary" asp-action="Update" asp-route-id="@user.Id">
            Update
        </a>
    </td>
    <td>
        <form asp-action="Delete" asp-route-id="@user.Id" method="post">
            <button type="submit" class="btn btn-sm btn-danger">
                Delete
            </button>
        </form>
    </td>

</tr>
    }

</table>
لاحظ ان اضفنا هذا button داخل form بهدف استدعاء Delete action واستخدمنا الكود :
asp-action="Delete"
وبعثنا Id  الخاص بالمستخدم الى هذا action باستخدام الامر :
asp-route-id="@user.Id"
واستخدمن الطريقة
 method="post" 
للمزيد حوق الفرق بين Post ,و Get ارجع للدرس الفرق بين Post و Get

انشاء Delete Action 

حتي نحذف من قاعدة البيانات اكيد بنحتاج انشاء Delete action وفي الحذف بنستخدم الامر :

userManager.DeleteAsync(user);
تمام ننتقل الان الى AdminController  ونعمل Delete action ونضيف الكود التالي: 


        [HttpPost]
        public async Task<IActionResult> Delete(string id)
        {
            AppUser user = await userManager.FindByIdAsync(id);
            if (user != null)
            {
                IdentityResult result = await userManager.DeleteAsync(user);
                if (result.Succeeded)
                    return RedirectToAction("Index");
                else
                    Errors(result);
            }
            else
                ModelState.AddModelError("", "User Not Found");
            return View("Index", userManager.Users);
        }

نفهم شو عملنا
اول اشي بنشوف اذا هذا User موجود حيث ارسلنا رقم المستخدم ك Parameter لهذا action واستخدمنا الكود لارجاع بيانات المستخدم:
 AppUser user = await userManager.FindByIdAsync(id);
والنتيجه تخزنت في منغير باسم user من نوع AppUser وبعدها فحصنا هذه النتيجة اذا ما كانت تساوي NULL يعني المستخدم موجود وبناء عليه استدعينا امر الحذف الي هو :

IdentityResult result = await userManager.DeleteAsync(user);
                if (result.Succeeded)
                    return RedirectToAction("Index");
                else
                    Errors(result);
ونتيجة الحذف خزناها في متغير من نوع IdentityResult واذا كانت النتيجة Succeeded بنرجع الى صفحة Index واذا لا بنستدعي Error Function وبعدها بنعرض هاي الاخطاء بشاشة View 
شغل الصفحة وشوف النتيجة :

    https://localhost:44347/admin
حاول تحذف السجل وشوف النتيجة في قاعدة البيانات، اكيد بتكون النتيجة ان حذف السجل بشكل نهائي من قاعدة البيانات.

ملاحظة : الافضل في المشاريع الحقيقة عدم الحذف بهذه الطريقة، حتى تحتفظ ب history للحركات عندك فا بكون الحل اضافة Column من نوع tinyint مثلا  وفي حال الحذف بتغير الى 0 وغير ذلك بكون 1 على سبيل المثال