24 — מרחיבים את פרופיל המשתמש


שדות אישיים ועריכת פרופיל; הרחבה קטנה באמצעות Code First

נוסיף לחשבון שלנו שם לתצוגה, עיר ותחביב. נבנה טופס לעריכת הפרופיל ונבדוק שהפרטים נשמרים גם אחרי יציאה וכניסה מחדש.

הפרק הקודם: 23 — הרשמה וכניסה עם Microsoft Identity · הפרק הבא: 25 — מי רשאי לערוך? בעלות והרשאות

במסך מגע אפשר להחליק ימינה לפרק הבא ושמאלה לפרק הקודם.

מה נלמד

  • להרחיב מחלקה קיימת בתכונות.
  • לעדכן פרופיל באמצעות UserManager.
  • לבחור משתמש לפי הכניסה, ולא לפי מזהה שהגולש שולח.

מתחילים מסונכרנים

פתחו את תיקיית הפרויקט הנכונה, בדקו שאין שינוי לא שמור והפעילו Pull. אם Git מציג התנגשות, פנו למורה; אל תמחקו עבודה ואל תבצעו Force Push.

מוסיפים מעט מידע

AppUser יורשת מ־IdentityUser. מחליפים את הטיפוס באותם מקומות שבהם נרשמה תשתית Identity ומוסיפים מיגרציה למסד החשבונות בלבד. פרופיל קורא את המשתמש המחובר באמצעות GetUserAsync(User). אין שדה UserId בטופס. [Authorize] דורש כניסה לפני פתיחת הפרופיל.

TempData משמשת להודעת הצלחה אחרי הפניה; היא אינה המקום שבו הפרופיל נשמר. השדות נשמרים במסד דרך UpdateAsync.

פקודות השלב

הפקודות מורצות משורש המאגר, שבו נמצאת התיקייה RazorTaba, בטרמינל של Visual Studio או VS Code.

dotnet ef migrations add ExtendProfile --context AuthDbContext --output-dir Migrations/Auth --project RazorTaba
dotnet ef database update --context AuthDbContext --project RazorTaba

השינויים בקוד

RazorTaba/Models/AppUser.cs

קובץ חדש. צרו את RazorTaba/Models/AppUser.cs והדביקו את התוכן הבא:

using Microsoft.AspNetCore.Identity;

namespace RazorTaba.Models;

/// <summary>חשבון Identity עם תוספת קטנה של פרטי פרופיל.</summary>
public class AppUser : IdentityUser
{
    public string DisplayName { get; set; } = "";
    public string City { get; set; } = "";
    public bool LikesNature { get; set; }
}

RazorTaba/Data/AuthDbContext.cs

הוסיפו בראש הקובץ using RazorTaba.Models; והחליפו את IdentityUser ב־AppUser בתוך IdentityDbContext. כך מסד החשבונות מכיר את המחלקה שהרחבנו:

לפני


using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace RazorTaba.Data;

/// <summary>מסד החשבונות נפרד ממסד החיות, כדי להוסיף כניסה בלי לשכתב את הליבה.</summary>
public class AuthDbContext(DbContextOptions<AuthDbContext> options) : IdentityDbContext<IdentityUser>(options)
{
}

אחרי

using RazorTaba.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace RazorTaba.Data;

/// <summary>מסד החשבונות נפרד ממסד החיות, כדי להוסיף כניסה בלי לשכתב את הליבה.</summary>
public class AuthDbContext(DbContextOptions<AuthDbContext> options) : IdentityDbContext<AppUser>(options)
{
}

RazorTaba/Program.cs

הוסיפו בראש הקובץ using RazorTaba.Models;. בקריאה ל־AddDefaultIdentity החליפו רק את טיפוס המשתמש מ־IdentityUser ל־AppUser; הגדרות הכניסה נשארות כפי שהיו:

לפני


using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using RazorTaba.Data;

⁞
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite($"Data Source={databasePath}"));

string accountsPath = Path.Combine(builder.Environment.ContentRootPath, "Accounts.db");
builder.Services.AddDbContext<AuthDbContext>(options => options.UseSqlite($"Data Source={accountsPath}"));
builder.Services.AddDefaultIdentity<IdentityUser>(options =>
{
    // תרגול מקומי: אין דרישה לשליחת דוא"ל או לספק חיצוני.
    options.SignIn.RequireConfirmedAccount = false;
    options.User.RequireUniqueEmail = true;

אחרי

using RazorTaba.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Identity;
using RazorTaba.Data;

⁞
builder.Services.AddDbContext<AppDbContext>(options => options.UseSqlite($"Data Source={databasePath}"));

string accountsPath = Path.Combine(builder.Environment.ContentRootPath, "Accounts.db");
builder.Services.AddDbContext<AuthDbContext>(options => options.UseSqlite($"Data Source={accountsPath}"));
builder.Services.AddDefaultIdentity<AppUser>(options =>
{
    // תרגול מקומי: אין דרישה לשליחת דוא"ל או לספק חיצוני.
    options.SignIn.RequireConfirmedAccount = false;
    options.User.RequireUniqueEmail = true;

RazorTaba/Pages/Account/Register.cshtml.cs

הוסיפו בראש הקובץ using RazorTaba.Models; והחליפו את IdentityUser ב־AppUser בשלושת המקומות המודגשים: בשני המנהלים וביצירת המשתמש החדש. שמות המשתנים ושאר פעולת ההרשמה נשארים כפי שהיו:

לפני


using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>יוצר חשבון דרך Identity; לא שומר סיסמה בטבלת חיות או בקוד.</summary>
public class RegisterModel(UserManager<IdentityUser> users, SignInManager<IdentityUser> signIn) : PageModel
{
    [BindProperty, Required, EmailAddress]
    public string Email { get; set; } = "";
    [BindProperty, Required, DataType(DataType.Password)]
⁞

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid) return Page();
        var user = new IdentityUser { UserName = Email, Email = Email };
        var result = await users.CreateAsync(user, Password);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors) ModelState.AddModelError("", error.Description);

אחרי

using RazorTaba.Models;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>יוצר חשבון דרך Identity; לא שומר סיסמה בטבלת חיות או בקוד.</summary>
public class RegisterModel(UserManager<AppUser> users, SignInManager<AppUser> signIn) : PageModel
{
    [BindProperty, Required, EmailAddress]
    public string Email { get; set; } = "";
    [BindProperty, Required, DataType(DataType.Password)]
⁞

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid) return Page();
        var user = new AppUser { UserName = Email, Email = Email };
        var result = await users.CreateAsync(user, Password);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors) ModelState.AddModelError("", error.Description);

RazorTaba/Pages/Account/Login.cshtml.cs

הוסיפו בראש הקובץ using RazorTaba.Models; והחליפו את IdentityUser ב־AppUser בתוך SignInManager. שאר תהליך הכניסה נשאר כפי שהיה:

לפני


using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>מאמת סיסמה באמצעות Identity ומאפשר רק חזרה לכתובת מקומית.</summary>
public class LoginModel(SignInManager<IdentityUser> signIn) : PageModel
{
    [BindProperty, Required, EmailAddress]
    public string Email { get; set; } = "";
    [BindProperty, Required, DataType(DataType.Password)]

אחרי

using RazorTaba.Models;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>מאמת סיסמה באמצעות Identity ומאפשר רק חזרה לכתובת מקומית.</summary>
public class LoginModel(SignInManager<AppUser> signIn) : PageModel
{
    [BindProperty, Required, EmailAddress]
    public string Email { get; set; } = "";
    [BindProperty, Required, DataType(DataType.Password)]

RazorTaba/Pages/Account/Logout.cshtml.cs

גם בקובץ היציאה הוסיפו using RazorTaba.Models; והחליפו את IdentityUser ב־AppUser בתוך SignInManager. פעולת היציאה עצמה נשארת כפי שהייתה:

לפני


using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>יציאה מתבצעת באמצעות POST, לא באמצעות קישור GET.</summary>
public class LogoutModel(SignInManager<IdentityUser> signIn) : PageModel
{
    public IActionResult OnGet() => RedirectToPage("/Index");
    public async Task<IActionResult> OnPostAsync()
    {

אחרי

using RazorTaba.Models;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace RazorTaba.Pages.Account;

/// <summary>יציאה מתבצעת באמצעות POST, לא באמצעות קישור GET.</summary>
public class LogoutModel(SignInManager<AppUser> signIn) : PageModel
{
    public IActionResult OnGet() => RedirectToPage("/Index");
    public async Task<IActionResult> OnPostAsync()
    {

RazorTaba/Pages/Account/Profile.cshtml.cs

קובץ חדש. צרו את RazorTaba/Pages/Account/Profile.cshtml.cs והדביקו את התוכן הבא:

using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorTaba.Models;

namespace RazorTaba.Pages.Account;

/// <summary>עריכת הפרופיל של המשתמש המחובר בלבד.</summary>
[Authorize]
public class ProfileModel(UserManager<AppUser> users) : PageModel
{
    [BindProperty, Required(ErrorMessage = "כתבו שם לתצוגה"), StringLength(60)]
    public string DisplayName { get; set; } = "";
    [BindProperty, StringLength(60)]
    public string? City { get; set; }
    [BindProperty]
    public bool LikesNature { get; set; }
    [TempData]
    public string? Message { get; set; }

    public async Task<IActionResult> OnGetAsync()
    {
        var user = await users.GetUserAsync(User);
        if (user is null) return Challenge();
        DisplayName = user.DisplayName;
        City = user.City;
        LikesNature = user.LikesNature;
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid) return Page();
        var user = await users.GetUserAsync(User);
        if (user is null) return Challenge();
        user.DisplayName = DisplayName;
        user.City = City ?? "";
        user.LikesNature = LikesNature;
        var result = await users.UpdateAsync(user);
        if (!result.Succeeded)
        {
            foreach (var error in result.Errors) ModelState.AddModelError("", error.Description);
            return Page();
        }
        Message = "הפרופיל נשמר";
        return RedirectToPage();
    }
}

RazorTaba/Pages/Account/Profile.cshtml

קובץ חדש. צרו את RazorTaba/Pages/Account/Profile.cshtml והדביקו את התוכן הבא:

@page
@model RazorTaba.Pages.Account.ProfileModel
@{ ViewData["Title"] = "הפרופיל שלי"; }
<h1>הפרופיל שלי</h1>
@if (Model.Message is not null) { <p class="alert alert-success">@Model.Message</p> }
<form method="post" class="card p-4 col-lg-6">
    <div asp-validation-summary="All" class="text-danger" role="alert"></div>
    <label asp-for="DisplayName">שם לתצוגה</label><input asp-for="DisplayName" class="form-control mb-3" />
    <label asp-for="City">עיר</label><input asp-for="City" class="form-control mb-3" />
    <label class="mb-3"><input asp-for="LikesNature" /> אוהבים טבע</label>
    <button class="btn btn-primary">שמירת פרופיל</button>
</form>

RazorTaba/Pages/Shared/_LoginPartial.cshtml

בקובץ הקיים בצעו את השינוי הבא. סימני + ו־- מציינים שינוי ואינם חלק מהקוד:

--- a/RazorTaba/Pages/Shared/_LoginPartial.cshtml
+++ b/RazorTaba/Pages/Shared/_LoginPartial.cshtml
@@ -1,6 +1,7 @@
 @if (User.Identity?.IsAuthenticated == true)
 {
+    <a asp-page="/Account/Profile">הפרופיל שלי</a>
     <span class="small">שלום, @User.Identity.Name</span>
     <form method="post" asp-page="/Account/Logout" class="m-0">
         <button class="btn btn-sm btn-outline-secondary">יציאה</button>
     </form>

מריצים ובודקים

  1. משתמש לא מחובר מופנה לכניסה בפתיחת הפרופיל.
  2. שנו שם, עיר וסימון תחביב; צאו והיכנסו וודאו שנשמרו.
  3. שם ריק אינו נשמר.
  4. חשבון אחר אינו מקבל את פרטי הפרופיל הראשון.

משימה אישית ובדיקת הבנה

התאימו את כותרת התחביב לנושא שלכם. הסבירו מדוע אין צורך לשאול בטופס איזה משתמש רוצים לערוך.

בסיום שינוי משמעותי: בדקו את ה־diff, בצעו Stage, כתבו הודעת commit שמתארת מה שיניתם, ובצעו Push. ודאו ב־GitHub שהשינוי הגיע. נדרשים לפחות 50 commits משמעותיים לאורך השנה, גם למי שעובד תמיד באותו מחשב נייד.

AI יכול לעזור להבין הודעת שגיאה ולנפות תקלה. אתם אחראים לכתוב, לבדוק ולהסביר את הקוד; אין להפעיל סוכן שיכתוב את הפרויקט.


המשך