Esercizio - Personalizzare Identity

Completato

Nell'unità precedente si è appreso come funziona la personalizzazione in ASP.NET Core Identity. In questa unità verrà esteso il modello di dati Identity e verranno apportate le modifiche corrispondenti all'interfaccia utente.

Personalizzare l’interfaccia utente dell'account utente

In questa sezione si creeranno e si personalizzeranno i file dell'interfaccia utente di Identity da usare in sostituzione della libreria di classi Razor predefinita.

  1. Aggiungere al progetto i file di registrazione utente da modificare:

    dotnet aspnet-codegenerator identity --dbContext RazorPagesPizzaAuth --files "Account.Manage.EnableAuthenticator;Account.Manage.Index;Account.Register;Account.ConfirmEmail"
    

    Nel comando precedente:

    • L'opzione --dbContext fornisce allo strumento informazioni sulla classe derivata da DbContext esistente denominata RazorPagesPizzaAuth.
    • L'opzione --files specifica un elenco delimitato da punto e virgola di file univoci da aggiungere all'area Identity.
      • Account.Manage.Index è la pagina di gestione del profilo. Questa pagina viene modificata più avanti in questa unità.
      • Account.Register è la pagina di registrazione dell'utente. Anche questa pagina viene modificata più avanti in questa unità.
      • Per Account.Manage.EnableAuthenticator e Account.ConfirmEmail è stato eseguito lo scaffolding ma non sono stati modificati in questa unità.

    Suggerimento

    Eseguire il comando seguente dalla radice del progetto per visualizzare i valori validi per l'opzione --files: dotnet aspnet-codegenerator identity --listFiles

    I file seguenti vengono aggiunti alla directory Areas/Identity:

    • Pages/
      • _ViewImports.cshtml
      • Account/
        • _ViewImports.cshtml
        • ConfirmEmail.cshtml
        • ConfirmEmail.cshtml.cs
        • Register.cshtml
        • Register.cshtml.cs
        • Manage/
          • _ManageNav.cshtml
          • _ViewImports.cshtml
          • EnableAuthenticator.cshtml
          • EnableAuthenticator.cshtml.cs
          • Index.cshtml
          • Index.cshtml.cs
          • ManageNavPages.cs

Estendere IdentityUser

Viene assegnato un nuovo requisito per archiviare i nomi degli utenti. Poiché la classe IdentityUser predefinita non contiene proprietà per nome e cognome, è necessario estendere la classe RazorPagesPizzaUser.

Modificare Areas/Identity/Data/RazorPagesPizzaUser.cs nel modo seguente:

  1. Aggiungere le proprietà FirstName e LastName:

    using System.ComponentModel.DataAnnotations;
    using Microsoft.AspNetCore.Identity;
    
    namespace RazorPagesPizza.Areas.Identity.Data;
    
    public class RazorPagesPizzaUser : IdentityUser
    {
        [Required]
        [MaxLength(100)]
        public string FirstName { get; set; } = string.Empty;
    
        [Required]
        [MaxLength(100)]
        public string LastName { get; set; } = string.Empty;
    }
    

    Le proprietà nel frammento di codice precedente rappresentano colonne aggiuntive da creare nella tabella AspNetUsers sottostante. Entrambe le proprietà sono obbligatorie e pertanto annotate con l'attributo [Required]. Inoltre, l'attributo [MaxLength] indica che è consentita una lunghezza massima di 100 caratteri. Il tipo di dati della colonna della tabella sottostante viene definito di conseguenza. Viene assegnato un valore predefinito di string.Empty dal momento che in questo progetto è abilitato il contesto che ammette i valori Null e le proprietà sono stringhe che non ammettono i valori Null.

  2. Aggiungere l'istruzione using seguente all'inizio di questo file.

    using System.ComponentModel.DataAnnotations;
    

    Il codice precedente risolve gli attributi di annotazione dei dati applicati alle proprietà FirstName e LastName.

Aggiornare il database

Ora che sono state apportate le modifiche al modello, è necessario apportare le modifiche opportune al database.

  1. Assicurarsi che tutte le modifiche vengano salvate.

  2. Creare e applicare una migrazione di EF Core per aggiornare l'archivio dati sottostante:

    dotnet ef migrations add UpdateUser
    dotnet ef database update
    

    La migrazione di EF Core UpdateUser ha applicato uno script delle modifiche DDL allo schema della tabella AspNetUsers. In particolare sono state aggiunte le colonne FirstName e LastName, come illustrato nell'estratto dell'output della migrazione riportato di seguito:

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (37ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [FirstName] nvarchar(100) NOT NULL DEFAULT N'';
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (36ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE [AspNetUsers] ADD [LastName] nvarchar(100) NOT NULL DEFAULT N'';
    
  3. Esaminare il database per analizzare l'effetto della migrazione di UpdateUser di EF Core sullo schema della tabella AspNetUsers.

    Nel riquadro SQL Server espandere il nodo Colonne nella tabella dbo.AspNetUsers.

    Screenshot dello schema della tabella AspNetUsers.

    Le proprietà FirstName e LastName nella classe RazorPagesPizzaUser corrispondono alle colonne FirstName e LastName nell'immagine precedente. Il tipo di dati nvarchar(100) è stato assegnato alle due colonne a causa degli attributi [MaxLength(100)]. È stato aggiunto il vincolo non Null perché FirstName e LastName nella classe sono stringhe che non ammettono i valori Null. Le righe esistenti mostrano stringhe vuote nelle nuove colonne.

Personalizzare il modulo di registrazione utente

Sono state aggiunte nuove colonne per FirstName e LastName. A questo punto, è necessario modificare l'interfaccia utente per visualizzare i campi corrispondenti nel modulo di registrazione.

  1. In Areas/Identity/Pages/Account/Register.cshtml aggiungere il markup evidenziato seguente:

    <form id="registerForm" asp-route-returnUrl="@Model.ReturnUrl" method="post">
        <h2>Create a new account.</h2>
        <hr />
        <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
        <div class="form-floating mb-3">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" placeholder="name@example.com" />
            <label asp-for="Input.Email">Email</label>
            <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
    

    Con il markup precedente, al modulo di registrazione utente vengono aggiunte le caselle di testo First name (Nome) e Last name (Cognome).

  2. In Areas/Identity/Pages/Account/Register.cshtml.cs aggiungere il supporto per le caselle di testo del nome.

    1. Aggiungere le proprietà FirstName e LastName alla classe annidata InputModel:

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      

      Gli attributi [Display] definiscono il testo dell'etichetta da associare alle caselle di testo.

    2. Modificare il metodo OnPostAsync in modo da impostare le proprietà FirstName e LastName sull'oggetto RazorPagesPizza. Aggiungere le righe evidenziate seguenti:

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
      {
          returnUrl ??= Url.Content("~/");
          ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();
          if (ModelState.IsValid)
          {
              var user = CreateUser();
      
              user.FirstName = Input.FirstName;
              user.LastName = Input.LastName;
              
              await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
              await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);
              var result = await _userManager.CreateAsync(user, Input.Password);
      
      

      La modifica precedente imposta le proprietà FirstName e LastName sull'input dell'utente dal modulo di registrazione.

Personalizzare l'intestazione del sito

Aggiornare Pages/Shared/_LoginPartial.cshtml in modo da visualizzare il nome e il cognome raccolti durante la registrazione dell'utente. Sono necessarie le righe evidenziate nel frammento di codice seguente:

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    RazorPagesPizzaUser? user = await UserManager.GetUserAsync(User);
    var fullName = $"{user?.FirstName} {user?.LastName}";

    <li class="nav-item">
        <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello, @fullName!</a>
    </li>

UserManager.GetUserAsync(User) restituisce un oggetto RazorPagesPizzaUser che ammette i valori Null. L'operatore ?. condizionale Null viene usato per accedere alle proprietà FirstName e LastName solo se l'oggetto RazorPagesPizzaUser non è Null.

Personalizzare il modulo di gestione dei profili

I nuovi campi sono stati aggiunti al modulo di registrazione utente, ma è necessario aggiungerli anche al modulo di gestione dei profili in modo che gli utenti esistenti possano modificarli.

  1. In Areas/Identity/Pages/Account/Manage/Index.cshtml aggiungere il markup evidenziato seguente. Salva le modifiche.

    <form id="profile-form" method="post">
        <div asp-validation-summary="ModelOnly" class="text-danger" role="alert"></div>
        <div class="form-floating mb-3">
            <input asp-for="Input.FirstName" class="form-control" />
            <label asp-for="Input.FirstName"></label>
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Input.LastName" class="form-control" />
            <label asp-for="Input.LastName"></label>
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-floating mb-3">
            <input asp-for="Username" class="form-control" disabled />
            <label asp-for="Username" class="form-label"></label>
        </div>
    
  2. In Areas/Identity/Pages/Account/Manage/Index.cshtml.cs apportare le modifiche seguenti per supportare le caselle di testo del nome.

    1. Aggiungere le proprietà FirstName e LastName alla classe annidata InputModel:

      public class InputModel
      {
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "First name")]
          public string FirstName { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 1)]
          [Display(Name = "Last name")]
          public string LastName { get; set; }
      
          /// <summary>
          ///     This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used
          ///     directly from your code. This API may change or be removed in future releases.
          /// </summary>
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. Incorporare le modifiche evidenziate nel metodo LoadAsync:

      private async Task LoadAsync(RazorPagesPizzaUser user)
      {
          var userName = await _userManager.GetUserNameAsync(user);
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
      
          Username = userName;
      
          Input = new InputModel
          {
              PhoneNumber = phoneNumber,
              FirstName = user.FirstName,
              LastName = user.LastName
          };
      }
      

      Il codice precedente supporta il recupero del nome e del cognome per visualizzarli nelle caselle di testo corrispondenti del modulo di gestione dei profili.

    3. Incorporare le modifiche evidenziate nel metodo OnPostAsync. Salva le modifiche.

      public async Task<IActionResult> OnPostAsync()
      {
          var user = await _userManager.GetUserAsync(User);
          if (user == null)
          {
              return NotFound($"Unable to load user with ID '{_userManager.GetUserId(User)}'.");
          }
      
          if (!ModelState.IsValid)
          {
              await LoadAsync(user);
              return Page();
          }
      
          user.FirstName = Input.FirstName;
          user.LastName = Input.LastName;
          await _userManager.UpdateAsync(user);
      
          var phoneNumber = await _userManager.GetPhoneNumberAsync(user);
          if (Input.PhoneNumber != phoneNumber)
          {
              var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber);
              if (!setPhoneResult.Succeeded)
              {
                  StatusMessage = "Unexpected error when trying to set phone number.";
                  return RedirectToPage();
              }
          }
      
          await _signInManager.RefreshSignInAsync(user);
          StatusMessage = "Your profile has been updated";
          return RedirectToPage();
      }
      

      Il codice precedente supporta l'aggiornamento del nome e del cognome nella tabella AspNetUsers del database.

Configurare il mittente del messaggio di posta elettronica di conferma

La prima volta che è stata testata l'app, è stato registrato un utente e quindi è stato fatto clic su un collegamento per simulare la conferma dell'indirizzo di posta elettronica dell'utente. Per inviare il messaggio di posta elettronica di conferma effettivo, è necessario creare un'implementazione di IEmailSender e registrarla nel sistema di inserimento delle dipendenze. Per semplicità, in questa unità l'implementazione non invia effettivamente messaggi di posta elettronica a un server SMTP (Simple Mail Transfer Protocol). ma scriverà semplicemente il contenuto del messaggio di posta elettronica nella console.

  1. Dal momento il messaggio di posta elettronica verrà visualizzato come testo normale nella console, è consigliabile modificare il messaggio generato in modo da escludere il testo con codifica HTML. In Areas/Identity/Pages/Account/Register.cshtml.cs individuare il codice seguente:

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");
    

    Sostituirlo con:

    await _emailSender.SendEmailAsync(Input.Email, "Confirm your email",
        $"Please confirm your account by visiting the following URL:\r\n\r\n{callbackUrl}");
    
  2. Nel riquadro Esplora risorse, fare clic con il pulsante destro del mouse sulla cartella RazorPagesPizza\Services e creare un nuovo file denominato EmailSender.cs. Aprire il file e aggiungere il codice seguente:

    using Microsoft.AspNetCore.Identity.UI.Services;
    namespace RazorPagesPizza.Services;
    
    public class EmailSender : IEmailSender
    {
        public EmailSender() {}
    
        public Task SendEmailAsync(string email, string subject, string htmlMessage)
        {
            Console.WriteLine();
            Console.WriteLine("Email Confirmation Message");
            Console.WriteLine("--------------------------");
            Console.WriteLine($"TO: {email}");
            Console.WriteLine($"SUBJECT: {subject}");
            Console.WriteLine($"CONTENTS: {htmlMessage}");
            Console.WriteLine();
    
            return Task.CompletedTask;
        }
    }
    

    Il codice precedente crea un'implementazione di IEmailSender che scrive il contenuto del messaggio nella console. In un'implementazione reale SendEmailAsync si connette a un servizio di posta elettronica esterno o a un'altra azione per inviare messaggi di posta elettronica.

  3. In Program.cs aggiungere le righe evidenziate:

    using Microsoft.AspNetCore.Identity;
    using Microsoft.EntityFrameworkCore;
    using RazorPagesPizza.Areas.Identity.Data;
    using Microsoft.AspNetCore.Identity.UI.Services;
    using RazorPagesPizza.Services;
    
    var builder = WebApplication.CreateBuilder(args);
    var connectionString = builder.Configuration.GetConnectionString("RazorPagesPizzaAuthConnection");
    builder.Services.AddDbContext<RazorPagesPizzaAuth>(options => options.UseSqlServer(connectionString)); 
    builder.Services.AddDefaultIdentity<RazorPagesPizzaUser>(options => options.SignIn.RequireConfirmedAccount = true)
          .AddEntityFrameworkStores<RazorPagesPizzaAuth>();
    
    // Add services to the container.
    builder.Services.AddRazorPages();
    builder.Services.AddTransient<IEmailSender, EmailSender>();
    
    var app = builder.Build();
    

    Quella precedente registra EmailSender come IEmailSender nel sistema di inserimento delle dipendenze.

Testare le modifiche apportate al modulo di registrazione

È tutto. A questo punto verranno testate le modifiche apportate al modulo di registrazione e al messaggio di posta elettronica di conferma.

  1. Assicurarsi di avere salvato tutte le modifiche.

  2. Nel riquadro del terminale compilare il progetto ed eseguire l'app con il comando dotnet run.

  3. Passare all'app nel browser. Selezionare Disconnetti se si è ancora connessi.

  4. Selezionare Register (Registra) e usare il modulo aggiornato per registrare un nuovo utente.

    Nota

    I vincoli di convalida sui campi First name (Nome) e Last name (Cognome) riflettono le annotazioni dei dati nelle proprietà FirstName e LastName di InputModel.

  5. Dopo la registrazione, si viene reindirizzati alla schermata Conferma registrazione. Nel riquadro del terminale scorrere verso l'alto per individuare l'output della console simile al seguente:

    Email Confirmation Message
    --------------------------
    TO: jana.heinrich@contoso.com
    SUBJECT: Confirm your email
    CONTENTS: Please confirm your account by visiting the following URL:
    
    https://localhost:7192/Identity/Account/ConfirmEmail?<query string removed>
    

    Per passare all'URL, premere CTRL+clic. Viene visualizzata la schermata di conferma.

    Nota

    Se si usa GitHub Codespaces, potrebbe essere necessario aggiungere -7192 alla prima parte dell'URL inoltrato. Ad esempio: scaling-potato-5gr4j4-7192.preview.app.github.dev.

  6. Selezionare Login (Accedi) e accedere con il nuovo utente. L'intestazione dell'app ora contiene Hello, [Nome] [Cognome]!.

  7. Nel riquadro SQL Server in VS Code fare clic on il pulsante destro del mouse sul database RazorPagesPizza e selezionare Nuova query. Nella scheda visualizzata immettere la query seguente e premere CTRL+MAIUSC+E per eseguirla.

    SELECT UserName, Email, FirstName, LastName
    FROM dbo.AspNetUsers
    

    Verrà visualizzata una scheda con risultati simili ai seguenti:

    UserName E-mail FirstName LastName
    kai.klein@contoso.com kai.klein@contoso.com
    jana.heinrich@contoso.com jana.heinrich@contoso.com Jana Heinrich

    Il primo utente registrato prima dell'aggiunta di FirstName e LastName allo schema. Di conseguenza, il record della tabella AspNetUsers associato non contiene dati in quelle colonne.

Testare le modifiche apportate al modulo di gestione dei profili

È anche opportuno testare le modifiche apportate al modulo di gestione del profilo.

  1. Nell'app Web accedere con l'account del primo utente creato.

  2. Selezionare il collegamento Hello, ! per passare al modulo di gestione del profilo.

    Nota

    Il collegamento non viene visualizzato correttamente perché la riga della tabella AspNetUsers per questo utente non contiene valori per FirstName e LastName.

  3. Immettere valori validi per il nome e il cognome. Seleziona Salva.

    L'intestazione dell'app viene aggiornata in Hello, [Nome] [Cognome]!.

  4. Per arrestare l’app, premere CTRL+C nel riquadro del terminale in VS Code.

Riepilogo

In questa unità si è personalizzato Identity in modo da archiviare informazioni utente personalizzate. Si è anche personalizzato il messaggio di posta elettronica di conferma. Nell'unità successiva verrà illustrato come implementare l'autenticazione a più fattori in Identità.