Ejercicio: personalización de Identity

Completado

De forma predeterminada, Identity representa un usuario con una clase IdentityUser. Una manera de extender los datos que se capturan en el momento del registro es crear una clase derivada de IdentityUser. En esta unidad, se crea una clase derivada llamada ContosoPetsUser. ContosoPetsUser contendrá las propiedades para almacenar el nombre de pila y los apellidos del usuario.

Clase IdentityUser derivada.

También se requieren cambios en la interfaz de usuario para recopilar información adicional de perfil de usuario. En los pasos siguientes se explica el proceso de recopilación del nombre de pila y los apellidos del usuario registrado.

Personalización de los datos de la cuenta de usuario

  1. Agregue los archivos de registro del usuario que se van a modificar en el proyecto:

    dotnet aspnet-codegenerator identity \
        --dbContext ContosoPetsAuth \
        --files "Account.Manage.EnableAuthenticator;Account.Manage.Index;Account.Register" \
        --userClass ContosoPetsUser \
        --force
    

    En el comando anterior:

    • La opción --dbContext proporciona a la herramienta conocimientos de la clase derivada de DbContext existente llamada ContosoPetsAuth.
    • La opción --files especifica una lista delimitada por signos de punto y coma de archivos únicos que se van a agregar al área de Identity.
    • La opción --userClass da como resultado la creación de una clase derivada de IdentityUser llamada ContosoPetsUser.
    • La opción --force hace que se sobrescriban los archivos existentes en el área Identity.

    Sugerencia

    Ejecute el siguiente comando desde la raíz del proyecto para ver los valores válidos de la opción --files:

    dotnet aspnet-codegenerator identity --listFiles
    

    Los archivos siguientes se agregan al directorio Areas/Identity:

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

    Además, el archivo Data/ContosoPetsAuth.cs, que existía antes de ejecutar el comando anterior, se ha sobrescrito porque se ha usado la opción --force. La declaración de clase ContosoPetsAuth ahora hace referencia al tipo de usuario recién creado ContosoPetsUser:

    public class ContosoPetsAuth : IdentityDbContext<ContosoPetsUser>
    

    A la página de Razor EnableAuthenticator se le ha aplicado scaffolding, aunque no se modificará en el módulo hasta más adelante.

  2. En el método Configure de Areas/Identity/IdentityHostingStartup.cs, la llamada a AddDefaultIdentity debe tener constancia del nuevo tipo de usuario de Identity. Incorpore el cambio resaltado siguiente y guarde el archivo.

    services.AddDefaultIdentity<ContosoPetsUser>()
        .AddDefaultUI()
        .AddEntityFrameworkStores<ContosoPetsAuth>();
    
  3. Actualice Pages/Shared/_LoginPartial.cshtml para incorporar los cambios resaltados siguientes. Guarde los cambios.

    @using Microsoft.AspNetCore.Identity
    @using ContosoPets.Ui.Areas.Identity.Data
    @inject SignInManager<ContosoPetsUser> SignInManager
    @inject UserManager<ContosoPetsUser> UserManager
    
    <ul class="navbar-nav">
    

    Los cambios anteriores actualizan el tipo de usuario pasado a SignInManager<T> y UserManager<T> en las directivas @inject. En lugar del tipo predeterminado IdentityUser, ahora se hace referencia al usuario ContosoPetsUser. Se ha agregado la directiva @using para resolver las referencias de ContosoPetsUser.

    Pages/Shared/_LoginPartial.cshtml se encuentra físicamente fuera del área Identity. Por lo tanto, la herramienta de scaffolding no ha actualizado automáticamente el archivo. Los cambios apropiados se han realizado manualmente.

    Sugerencia

    Como alternativa a la edición manual del archivo _LoginPartial.cshtml, este se puede eliminar antes de ejecutar la herramienta de scaffolding. El archivo _LoginPartial.cshtml se volverá a crear con referencias a la clase nueva ContosoPetsUser.

  4. Actualice Areas/Identity/Data/ContosoPetsUser.cs para admitir el almacenamiento y la recuperación de los datos adicionales de perfil de usuario. Realice los cambios siguientes:

    1. Agregue las propiedades FirstName y LastName:

      public class ContosoPetsUser : IdentityUser
      {
          [Required]
          [MaxLength(100)]
          public string FirstName { get; set; }
      
          [Required]
          [MaxLength(100)]
          public string LastName { get; set; }
      }
      

      Las propiedades del fragmento anterior representan columnas adicionales que se van a crear en la tabla subyacente AspNetUsers. Ambas propiedades son necesarias y, por tanto, se anotan con el atributo [Required]. El atributo [Required] también da como resultado una restricción distinta de NULL en la columna de la tabla de base de datos subyacente. Además, el atributo [MaxLength] indica que se permite una longitud máxima de 100 caracteres. El tipo de datos de la columna de la tabla subyacente se define en consecuencia.

    2. Agregue la instrucción using siguiente en la parte superior del archivo. Guarde los cambios.

      using System.ComponentModel.DataAnnotations;
      

      El código anterior resuelve los atributos de anotación de datos aplicados a las propiedades FirstName y LastName.

Actualización de la base de datos

  1. Cree y aplique una migración de EF Core para actualizar el almacén de datos subyacente:

    dotnet ef migrations add UpdateUser && \
        dotnet ef database update
    

    La migración de EF Core UpdateUser ha aplicado un script de cambios de DDL al esquema de la tabla AspNetUsers. En concreto, se han agregado las columnas FirstName y LastName, tal y como se muestra en el fragmento siguiente de resultado de migración:

    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (1,005ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      ALTER TABLE "AspNetUsers" ADD "FirstName" character varying(100) NOT NULL DEFAULT '';
    info: Microsoft.EntityFrameworkCore.Database.Command[20101]
        Executed DbCommand (517ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
        ALTER TABLE "AspNetUsers" ADD "LastName" character varying(100) NOT NULL DEFAULT '';
    
    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'';
    

    Complete los pasos siguientes para analizar el impacto de la migración de EF Core UpdateUser en el esquema de la tabla AspNetUsers. Obtendrá información sobre el impacto que puede extender el modelo de datos de Identity en el almacén de datos subyacente.

  1. Ejecute el comando siguiente para ver el esquema de la tabla:

    db -c '\d "AspNetUsers"'
    

    Se muestra el resultado siguiente:

                                        Table "public.AspNetUsers"
            Column        |           Type           | Collation | Nullable |        Default
    ----------------------+--------------------------+-----------+----------+-----------------------
     Id                   | text                     |           | not null |
     UserName             | character varying(256)   |           |          |
     NormalizedUserName   | character varying(256)   |           |          |
     Email                | character varying(256)   |           |          |
     NormalizedEmail      | character varying(256)   |           |          |
     EmailConfirmed       | boolean                  |           | not null |
     PasswordHash         | text                     |           |          |
     SecurityStamp        | text                     |           |          |
     ConcurrencyStamp     | text                     |           |          |
     PhoneNumber          | text                     |           |          |
     PhoneNumberConfirmed | boolean                  |           | not null |
     TwoFactorEnabled     | boolean                  |           | not null |
     LockoutEnd           | timestamp with time zone |           |          |
     LockoutEnabled       | boolean                  |           | not null |
     AccessFailedCount    | integer                  |           | not null |
     FirstName            | character varying(100)   |           | not null | ''::character varying
     LastName             | character varying(100)   |           | not null | ''::character varying
    

    Las propiedades FirstName y LastName de la clase ContosoPetsUser corresponden a las columnas FirstName y LastName del resultado anterior. Se ha asignado un tipo de datos character varying(100) a cada una de las dos columnas debido a los atributos [MaxLength(100)]. Se ha agregado la restricción distinta a NULL debido a los atributos [Required].

  2. Desplácese hacia abajo en el shell de comandos hasta que se muestre la información de índice siguiente:

    Indexes:
        "PK_AspNetUsers" PRIMARY KEY, btree ("Id")
        "UserNameIndex" UNIQUE, btree ("NormalizedUserName")
        "EmailIndex" btree ("NormalizedEmail")
    

    El índice PK_AspNetUsers muestra que la columna Id es el identificador único de una cuenta de usuario.

  3. Presione la tecla q para salir del visor de texto en el shell de comandos.

  1. Ejecute el comando siguiente para ver el esquema de la tabla:

    db -Q "SELECT COLUMN_NAME, IS_NULLABLE, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH AS MAX_LENGTH FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME='AspNetUsers'" -Y 20
    

    Se muestra el resultado siguiente:

    COLUMN_NAME          IS_NULLABLE DATA_TYPE            MAX_LENGTH
    -------------------- ----------- -------------------- -----------
    Id                   NO          nvarchar                     450
    UserName             YES         nvarchar                     256
    NormalizedUserName   YES         nvarchar                     256
    Email                YES         nvarchar                     256
    NormalizedEmail      YES         nvarchar                     256
    EmailConfirmed       NO          bit                         NULL
    PasswordHash         YES         nvarchar                      -1
    SecurityStamp        YES         nvarchar                      -1
    ConcurrencyStamp     YES         nvarchar                      -1
    PhoneNumber          YES         nvarchar                      -1
    PhoneNumberConfirmed NO          bit                         NULL
    TwoFactorEnabled     NO          bit                         NULL
    LockoutEnd           YES         datetimeoffset              NULL
    LockoutEnabled       NO          bit                         NULL
    AccessFailedCount    NO          int                         NULL
    FirstName            NO          nvarchar                     100
    LastName             NO          nvarchar                     100
    

    Las propiedades FirstName y LastName de la clase ContosoPetsUser corresponden a las columnas FirstName y LastName del resultado anterior. Se ha asignado un tipo de datos nvarchar(100) a cada una de las dos columnas debido a los atributos [MaxLength(100)]. Se ha agregado la restricción distinta a NULL debido a los atributos [Required]. Las filas existentes muestran cadenas vacías en las nuevas columnas.

  2. Ejecute el comando siguiente para ver la clave principal de la tabla:

    db -i $setupWorkingDirectory/list-aspnetusers-pk.sql -Y 15
    

    El siguiente resultado muestra que la columna Id es el identificador único de una cuenta de usuario:

    Table           Column          Primary key
    --------------- --------------- ---------------
    AspNetUsers     Id              PK_AspNetUsers
    

Personalización del formulario de registro de usuarios

  1. En Areas/Identity/Pages/Account/Register.cshtml, agregue el marcado resaltado siguiente:

    <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
        <h4>Create a new account.</h4>
        <hr />
        <div asp-validation-summary="All" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Input.FirstName"></label>
            <input asp-for="Input.FirstName" class="form-control" />
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Input.LastName"></label>
            <input asp-for="Input.LastName" class="form-control" />
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Input.Email"></label>
            <input asp-for="Input.Email" class="form-control" />
            <span asp-validation-for="Input.Email" class="text-danger"></span>
        </div>
    

    Con el marcado anterior, se agregan los cuadros de texto Nombre de pila y Apellidos al formulario de registro del usuario.

  2. En Areas/Identity/Pages/Account/Register.cshtml.cs, agregue compatibilidad para los cuadros de texto de nombre.

    1. Agregue las propiedades FirstName y LastName a la clase anidada 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; }
      
          [Required]
          [EmailAddress]
          [Display(Name = "Email")]
          public string Email { get; set; }
      
          [Required]
          [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
          [DataType(DataType.Password)]
          [Display(Name = "Password")]
          public string Password { get; set; }
      
          [DataType(DataType.Password)]
          [Display(Name = "Confirm password")]
          [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
          public string ConfirmPassword { get; set; }
      }
      

      Los atributos [Display] definen el texto de la etiqueta que se va a asociar con los cuadros de texto.

    2. Modifique el método OnPostAsync para establecer las propiedades FirstName y LastName en el objeto ContosoPetsUser. Realice los cambios resaltados siguientes:

      public async Task<IActionResult> OnPostAsync(string returnUrl = null)
      {
          returnUrl = returnUrl ?? Url.Content("~/");
          if (ModelState.IsValid)
          {
              var user = new ContosoPetsUser
              {
                  FirstName = Input.FirstName,
                  LastName = Input.LastName,
                  UserName = Input.Email,
                  Email = Input.Email,
              };
              var result = await _userManager.CreateAsync(user, Input.Password);
              if (result.Succeeded)
              {
      

      El cambio anterior establece las propiedades FirstName y LastName en la entrada del usuario del formulario de registro.

Personalización del encabezado del sitio

Actualice Pages/Shared/_LoginPartial.cshtml para mostrar el nombre de pila y los apellidos recopilados durante el registro del usuario. Se necesitan las líneas resaltadas en el fragmento de código siguiente:

@using Microsoft.AspNetCore.Identity
@using ContosoPets.Ui.Areas.Identity.Data
@inject SignInManager<ContosoPetsUser> SignInManager
@inject UserManager<ContosoPetsUser> UserManager

<ul class="navbar-nav">
@if (SignInManager.IsSignedIn(User))
{
    ContosoPetsUser 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>
    <li class="nav-item">
        <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Page("/Index", new { area = "" })">
            <button id="logout" type="submit" class="nav-link btn btn-link text-dark">Logout</button>
        </form>
    </li>
}
else
{
    <li class="nav-item">
        <a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">Register</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">Login</a>
    </li>
}
</ul>

Personalización del formulario de administración de perfiles

  1. En Areas/Identity/Pages/Account/Manage/Index.cshtml, agregue el marcado resaltado siguiente. Guarde los cambios.

    <form id="profile-form" method="post">
        <div asp-validation-summary="All" class="text-danger"></div>
        <div class="form-group">
            <label asp-for="Input.FirstName"></label>
            <input asp-for="Input.FirstName" class="form-control" />
            <span asp-validation-for="Input.FirstName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Input.LastName"></label>
            <input asp-for="Input.LastName" class="form-control" />
            <span asp-validation-for="Input.LastName" class="text-danger"></span>
        </div>
        <div class="form-group">
            <label asp-for="Username"></label>
            <input asp-for="Username" class="form-control" disabled />
        </div>
    
  2. En Areas/Identity/Pages/Account/Manage/Index.cshtml.cs, realice los cambios siguientes para admitir los cuadros de texto de nombre.

    1. Agregue las propiedades FirstName y LastName a la clase anidada 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; }
      
          [Phone]
          [Display(Name = "Phone number")]
          public string PhoneNumber { get; set; }
      }
      
    2. Incorpore los cambios resaltados en el método LoadAsync:

      private async Task LoadAsync(ContosoPetsUser 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,
          };
      }
      

      El código anterior admite la recuperación del nombre de pila y los apellidos que se muestran en los cuadros de texto correspondientes del formulario de administración de perfiles.

    3. Incorpore los cambios resaltados en el método OnPostAsync. Guarde los cambios.

      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)
              {
                  var userId = await _userManager.GetUserIdAsync(user);
                  throw new InvalidOperationException($"Unexpected error occurred setting phone number for user with ID '{userId}'.");
              }
          }
      
          await _signInManager.RefreshSignInAsync(user);
          StatusMessage = "Your profile has been updated";
          return RedirectToPage();
      }
      

      El código anterior admite la actualización del nombre de pila y los apellidos de la tabla AspNetUsers de la base de datos.

Compilación, implementación y prueba

  1. Ejecute el siguiente comando para compilar la aplicación:

    dotnet build --no-restore
    

    La opción --no-restore se incluye porque no se han agregado paquetes NuGet desde la última compilación. El proceso de compilación omite la restauración de paquetes NuGet y se realiza correctamente sin ninguna advertencia. Si se produce un error en la compilación, compruebe la salida con el fin de obtener información para solucionar problemas.

  2. Implemente la aplicación en Azure App Service ejecutando el comando siguiente:

    az webapp up
    
  3. En el explorador, vaya a la aplicación. Seleccione Cerrar sesión si todavía tiene la sesión iniciada.

    Sugerencia

    Si necesita la dirección URL de la aplicación, puede mostrarla con el comando siguiente:

    echo $webAppUrl
    
  4. Seleccione Registrar y use el formulario actualizado para registrar un nuevo usuario.

    Nota

    Las restricciones de validación en los campos Nombre de pila y Apellidos reflejan las anotaciones de datos en las propiedades FirstName y LastName de InputModel.

    Después del registro, se le redirigirá a la página principal. El encabezado de la aplicación ahora contiene Hola, [Nombre de pila] [Apellidos].

  5. Ejecute el comando siguiente para confirmar que el nombre de pila y los apellidos están almacenados en la base de datos:

    db -c 'SELECT "UserName", "Email", "FirstName", "LastName" FROM "AspNetUsers"'
    

    Se muestra una variación del resultado siguiente:

             UserName          |            Email          | FirstName | LastName
    ---------------------------+---------------------------+-----------+----------
     kai.klein@contoso.com     | kai.klein@contoso.com     |           |
     jana.heinrich@contoso.com | jana.heinrich@contoso.com | Jana      | Heinrich
    (2 rows)
    
    db -Q "SELECT UserName, Email, FirstName, LastName FROM dbo.AspNetUsers" -Y 25
    

    Se muestra una variación del resultado siguiente:

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

    El primer usuario registrado antes de agregar FirstName y LastName al esquema. Por consiguiente, el registro de la tabla AspNetUsers asociada no tiene datos en esas columnas.

Prueba de los cambios en el formulario de administración de perfiles

  1. En la aplicación web, inicie sesión con el primer usuario que se ha creado.

  2. Haga clic en el enlace Hola para navegar al formulario de administración de perfiles.

    Nota

    El vínculo no se muestra correctamente porque la fila de la tabla AspNetUsers para este usuario no contiene valores para FirstName y LastName.

  3. Escriba valores válidos para los campos Nombre de pila y Apellidos. Seleccione Guardar.

    El encabezado de la aplicación se actualiza a Hola, [Nombre de pila] [Apellidos].