Validación de modelos de Identity en ASP.NET Core

Por Arthur Vickers

ASP.NET Core Identity proporciona un marco para administrar y almacenar cuentas de usuario en aplicaciones ASP.NET Core. Identity se agrega al proyecto cuando se seleccionan cuentas de usuario individuales como mecanismo de autenticación. De forma predeterminada, Identity usa un modelo de datos de Entity Framework (EF) Core. En este artículo se describe cómo personalizar el modelo de Identity.

Identity y Migraciones de EF Core

Antes de examinar el modelo, resulta útil comprender cómo funciona Identity con Migraciones de EF Core para crear y actualizar una base de datos. En el nivel superior, el proceso es:

  1. Defina o actualice un modelo de datos en el código.
  2. Agregue una migración para traducir este modelo a los cambios que se pueden aplicar a la base de datos.
  3. Compruebe que la migración representa correctamente sus intenciones.
  4. Aplique la migración para actualizar la base de datos para que esté sincronizada con el modelo.
  5. Repita los pasos del 1 al 4 para refinar aún más el modelo y mantener la base de datos sincronizada.

Use uno de los métodos siguientes para agregar y aplicar migraciones:

ASP.NET Core tiene un controlador de páginas de error en tiempo de desarrollo. El controlador puede aplicar migraciones cuando se ejecuta la aplicación. Normalmente, las aplicaciones de producción generan scripts SQL a partir de las migraciones e implementan cambios en la base de datos como parte de una implementación controlada de aplicaciones y bases de datos.

Cuando se crea una nueva aplicación mediante Identity, ya se han completado los pasos 1 y 2 anteriores. Es decir, el modelo de datos inicial ya existe y la migración inicial se agregó al proyecto. La migración inicial todavía debe aplicarse a la base de datos. La migración inicial se puede aplicar a través de uno de los métodos siguientes:

  • Ejecute Update-Database en PMC.
  • Ejecute dotnet ef database update en un shell de comandos.
  • Haga clic en el botón Aplicar migraciones de la página de error cuando se ejecute la aplicación.

Repita los pasos anteriores a medida que se hacen cambios en el modelo.

El modelo de Identity

Tipos de entidades

El modelo de Identity consta de los siguientes tipos de entidad.

Tipo de entidad Descripción
User Representa al usuario.
Role Representa un rol.
UserClaim Representa una notificación que el usuario posee.
UserToken Representa un token de autenticación de un usuario.
UserLogin Asocia un usuario a un inicio de sesión.
RoleClaim Representa una notificación que se concede a todos los usuarios dentro de un rol.
UserRole Una entidad de combinación que asocia usuarios y roles.

Relaciones de tipo de entidad

Los tipos de entidad se relacionan entre sí de las maneras siguientes:

  • Cada User tiene varios elementos UserClaims.
  • Cada User tiene varios elementos UserLogins.
  • Cada User tiene varios elementos UserTokens.
  • Cada Role puede tener varias RoleClaims asociadas.
  • Cada elemento User puede tener muchos elementos Roles asociados, y cada elemento Role puede estar asociado a varios elementos Users. Se trata de una relación de varios a varios que requiere una tabla de combinación en la base de datos. La tabla de combinación se representa mediante la entidad UserRole.

Configuración predeterminada del modelo

Identity define muchas clases de contexto que heredan de DbContext para configurar y usar el modelo. Esta configuración se realiza mediante Code EF Core First Fluent API en el método OnModelCreating de la clase de contexto. La configuración predeterminada es la siguiente:

builder.Entity<TUser>(b =>
{
    // Primary key
    b.HasKey(u => u.Id);

    // Indexes for "normalized" username and email, to allow efficient lookups
    b.HasIndex(u => u.NormalizedUserName).HasName("UserNameIndex").IsUnique();
    b.HasIndex(u => u.NormalizedEmail).HasName("EmailIndex");

    // Maps to the AspNetUsers table
    b.ToTable("AspNetUsers");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(u => u.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.UserName).HasMaxLength(256);
    b.Property(u => u.NormalizedUserName).HasMaxLength(256);
    b.Property(u => u.Email).HasMaxLength(256);
    b.Property(u => u.NormalizedEmail).HasMaxLength(256);

    // The relationships between User and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each User can have many UserClaims
    b.HasMany<TUserClaim>().WithOne().HasForeignKey(uc => uc.UserId).IsRequired();

    // Each User can have many UserLogins
    b.HasMany<TUserLogin>().WithOne().HasForeignKey(ul => ul.UserId).IsRequired();

    // Each User can have many UserTokens
    b.HasMany<TUserToken>().WithOne().HasForeignKey(ut => ut.UserId).IsRequired();

    // Each User can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.UserId).IsRequired();
});

builder.Entity<TUserClaim>(b =>
{
    // Primary key
    b.HasKey(uc => uc.Id);

    // Maps to the AspNetUserClaims table
    b.ToTable("AspNetUserClaims");
});

builder.Entity<TUserLogin>(b =>
{
    // Composite primary key consisting of the LoginProvider and the key to use
    // with that provider
    b.HasKey(l => new { l.LoginProvider, l.ProviderKey });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(l => l.LoginProvider).HasMaxLength(128);
    b.Property(l => l.ProviderKey).HasMaxLength(128);

    // Maps to the AspNetUserLogins table
    b.ToTable("AspNetUserLogins");
});

builder.Entity<TUserToken>(b =>
{
    // Composite primary key consisting of the UserId, LoginProvider and Name
    b.HasKey(t => new { t.UserId, t.LoginProvider, t.Name });

    // Limit the size of the composite key columns due to common DB restrictions
    b.Property(t => t.LoginProvider).HasMaxLength(maxKeyLength);
    b.Property(t => t.Name).HasMaxLength(maxKeyLength);

    // Maps to the AspNetUserTokens table
    b.ToTable("AspNetUserTokens");
});

builder.Entity<TRole>(b =>
{
    // Primary key
    b.HasKey(r => r.Id);

    // Index for "normalized" role name to allow efficient lookups
    b.HasIndex(r => r.NormalizedName).HasName("RoleNameIndex").IsUnique();

    // Maps to the AspNetRoles table
    b.ToTable("AspNetRoles");

    // A concurrency token for use with the optimistic concurrency checking
    b.Property(r => r.ConcurrencyStamp).IsConcurrencyToken();

    // Limit the size of columns to use efficient database types
    b.Property(u => u.Name).HasMaxLength(256);
    b.Property(u => u.NormalizedName).HasMaxLength(256);

    // The relationships between Role and other entity types
    // Note that these relationships are configured with no navigation properties

    // Each Role can have many entries in the UserRole join table
    b.HasMany<TUserRole>().WithOne().HasForeignKey(ur => ur.RoleId).IsRequired();

    // Each Role can have many associated RoleClaims
    b.HasMany<TRoleClaim>().WithOne().HasForeignKey(rc => rc.RoleId).IsRequired();
});

builder.Entity<TRoleClaim>(b =>
{
    // Primary key
    b.HasKey(rc => rc.Id);

    // Maps to the AspNetRoleClaims table
    b.ToTable("AspNetRoleClaims");
});

builder.Entity<TUserRole>(b =>
{
    // Primary key
    b.HasKey(r => new { r.UserId, r.RoleId });

    // Maps to the AspNetUserRoles table
    b.ToTable("AspNetUserRoles");
});

Tipos genéricos de modelos

Identity define los tipos predeterminados de Common Language Runtime (CLR) para cada uno de los tipos de entidad enumerados anteriormente. Estos tipos tienen el prefijo Identity:

  • IdentityUser
  • IdentityRole
  • IdentityUserClaim
  • IdentityUserToken
  • IdentityUserLogin
  • IdentityRoleClaim
  • IdentityUserRole

En lugar de usar estos tipos directamente, los tipos se pueden usar como clases base para los propios tipos de la aplicación. Las clases DbContext definidas por Identity son genéricas, de modo que se pueden usar diferentes tipos de CLR para uno o varios de los tipos de entidad del modelo. Estos tipos genéricos también permiten cambiar el tipo de datos de la clave principal (PK) del User.

Cuando se usa Identity con compatibilidad con roles, se debe usar una clase IdentityDbContext. Por ejemplo:

// Uses all the built-in Identity types
// Uses `string` as the key type
public class IdentityDbContext
    : IdentityDbContext<IdentityUser, IdentityRole, string>
{
}

// Uses the built-in Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityDbContext<TUser>
    : IdentityDbContext<TUser, IdentityRole, string>
        where TUser : IdentityUser
{
}

// Uses the built-in Identity types except with custom User and Role types
// The key type is defined by TKey
public class IdentityDbContext<TUser, TRole, TKey> : IdentityDbContext<
    TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>,
    IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TRole : IdentityRole<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments
// The key type is defined by TKey
public abstract class IdentityDbContext<
    TUser, TRole, TKey, TUserClaim, TUserRole, TUserLogin, TRoleClaim, TUserToken>
    : IdentityUserContext<TUser, TKey, TUserClaim, TUserLogin, TUserToken>
         where TUser : IdentityUser<TKey>
         where TRole : IdentityRole<TKey>
         where TKey : IEquatable<TKey>
         where TUserClaim : IdentityUserClaim<TKey>
         where TUserRole : IdentityUserRole<TKey>
         where TUserLogin : IdentityUserLogin<TKey>
         where TRoleClaim : IdentityRoleClaim<TKey>
         where TUserToken : IdentityUserToken<TKey>

También es posible usar Identity sin roles (solo notificaciones), en cuyo caso se debe usar una clase IdentityUserContext<TUser>:

// Uses the built-in non-role Identity types except with a custom User type
// Uses `string` as the key type
public class IdentityUserContext<TUser>
    : IdentityUserContext<TUser, string>
        where TUser : IdentityUser
{
}

// Uses the built-in non-role Identity types except with a custom User type
// The key type is defined by TKey
public class IdentityUserContext<TUser, TKey> : IdentityUserContext<
    TUser, TKey, IdentityUserClaim<TKey>, IdentityUserLogin<TKey>,
    IdentityUserToken<TKey>>
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
{
}

// No built-in Identity types are used; all are specified by generic arguments, with no roles
// The key type is defined by TKey
public abstract class IdentityUserContext<
    TUser, TKey, TUserClaim, TUserLogin, TUserToken> : DbContext
        where TUser : IdentityUser<TKey>
        where TKey : IEquatable<TKey>
        where TUserClaim : IdentityUserClaim<TKey>
        where TUserLogin : IdentityUserLogin<TKey>
        where TUserToken : IdentityUserToken<TKey>
{
}

Personaliza el modelo

El punto de partida para la personalización del modelo es derivar del tipo de contexto adecuado. Consulta la sección Tipos genéricos de modelos. Normalmente, se llama a ApplicationDbContext a este tipo de contexto y se crea mediante las plantillas de ASP.NET Core.

El contexto se usa para configurar el modelo de dos maneras:

  • Proporcionar tipos de entidad y clave para los parámetros de tipo genérico.
  • Invalidación de OnModelCreating para modificar la asignación de estos tipos.

Cuando OnModelCreating se invalida, se debe llamar primero a base.OnModelCreating; a continuación, se debe llamar a la configuración de invalidación. Por lo general, EF Core tiene una directiva de "el último gana" para la configuración. Por ejemplo, si primero se llama al método ToTable de un tipo de entidad con un nombre de tabla y, más tarde, se vuelve a llamar con otro nombre de tabla diferente, se usa el de la segunda llamada.

NOTA: Si DbContext no se deriva de IdentityDbContext, es posible que AddEntityFrameworkStores no infiera los tipos POCO correctos de TUserClaim, TUserLoginy TUserToken. Si AddEntityFrameworkStores no deduce los tipos POCO correctos, una solución alternativa consiste en agregarlos directamente a través de services.AddScoped<IUser/RoleStore<TUser> y UserStore<...>>.

Datos de usuario personalizados

Los datos de usuario personalizados se admiten al heredarlos de IdentityUser. Es habitual asignar un nombre a este tipo de ApplicationUser:

public class ApplicationUser : IdentityUser
{
    public string CustomTag { get; set; }
}

Usa el tipo ApplicationUser como argumento genérico para el contexto:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
    }
}

No es necesario invalidar OnModelCreating en la clase ApplicationDbContext. EF Core asigna la propiedad CustomTag por convención. Sin embargo, la base de datos debe actualizarse para crear una nueva columna CustomTag. Para crear la columna, agrega una migración y actualiza la base de datos como se describe en Identity y Migraciones de EF Core.

Actualiza Pages/Shared/_LoginPartial.cshtml y reemplaza IdentityUser con ApplicationUser:

@using Microsoft.AspNetCore.Identity
@using WebApp1.Areas.Identity.Data
@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

Actualiza Areas/Identity/IdentityHostingStartup.cs o Startup.ConfigureServices y reemplaza IdentityUser con ApplicationUser.

services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
        .AddEntityFrameworkStores<ApplicationDbContext>();                                    

La llamada a AddDefaultIdentity es equivalente al siguiente código:

services.AddAuthentication(o =>
{
    o.DefaultScheme = IdentityConstants.ApplicationScheme;
    o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
})
.AddIdentityCookies(o => { });

services.AddIdentityCore<TUser>(o =>
{
    o.Stores.MaxLengthForKeys = 128;
    o.SignIn.RequireConfirmedAccount = true;
})
.AddDefaultUI()
.AddDefaultTokenProviders();

Identity se proporciona como biblioteca de clases Razor. Para obtener más información, consulta Scaffolding Identity en proyectos de ASP.NET Core. Por lo tanto, el código anterior requiere una llamada a AddDefaultUI. Si el proveedor de scaffodling de Identity se usó para agregar archivos de Identity al proyecto, quite la llamada a AddDefaultUI. Para obtener más información, consulta:

Cambio del tipo de clave principal

Un cambio en el tipo de datos de la columna de PK una vez creada la base de datos es problemático en muchos sistemas de base de datos. Por lo general, el cambio de la clave de acceso implica anular y volver a crear la tabla. Por lo tanto, los tipos de clave se deben especificar en la migración inicial cuando se crea la base de datos.

Siga los siguientes pasos para cambiar el tipo de PK:

  1. Si la base de datos se creó antes del cambio de PK, ejecute Drop-Database (PMC) o dotnet ef database drop (CLI de .NET) para eliminarla.

  2. Después de confirmar la eliminación de la base de datos, quite la migración inicial con Remove-Migration (PMC) o dotnet ef migrations remove (CLI de .NET).

  3. Actualice la clase ApplicationDbContext para que se derive de IdentityDbContext<TUser,TRole,TKey>. Especifique el nuevo tipo de clave de TKey. Por ejemplo, para usar un tipo de clave Guid :

    public class ApplicationDbContext
        : IdentityDbContext<IdentityUser<Guid>, IdentityRole<Guid>, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    En el código anterior, se deben especificar las clases genéricas IdentityUser<TKey> y se debe especificar IdentityRole<TKey> para usar el nuevo tipo de clave.

    Se debe actualizar Startup.ConfigureServices para usar el usuario genérico:

    services.AddDefaultIdentity<IdentityUser<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    
  4. Si se usa una clase personalizada ApplicationUser, actualice la clase para heredar de IdentityUser. Por ejemplo:

    using System;
    using Microsoft.AspNetCore.Identity;
    
    public class ApplicationUser : IdentityUser<Guid>
    {
        public string CustomTag { get; set; }
    }
    

    Actualice ApplicationDbContext para hacer referencia a la clase personalizada ApplicationUser:

    public class ApplicationDbContext
        : IdentityDbContext<ApplicationUser, IdentityRole<Guid>, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    Registre la clase de contexto de base de datos personalizada al agregar el servicio de Identity en Startup.ConfigureServices:

    services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    

    El tipo de datos de la clave principal se deduce mediante el análisis del objeto DbContext.

    Identity se proporciona como biblioteca de clases Razor. Para obtener más información, consulta Scaffolding Identity en proyectos de ASP.NET Core. Por lo tanto, el código anterior requiere una llamada a AddDefaultUI. Si el proveedor de scaffodling de Identity se usó para agregar archivos de Identity al proyecto, quite la llamada a AddDefaultUI.

  5. Si se usa una clase personalizada ApplicationRole, actualice la clase para heredar de IdentityRole<TKey>. Por ejemplo:

    using System;
    using Microsoft.AspNetCore.Identity;
    
    public class ApplicationRole : IdentityRole<Guid>
    {
        public string Description { get; set; }
    }
    

    Actualice ApplicationDbContext para hacer referencia a la clase personalizada ApplicationRole. Por ejemplo, la siguiente clase hace referencia a un ApplicationUser personalizado y a un ApplicationRole personalizado:

    using System;
    using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore;
    
    public class ApplicationDbContext :
        IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
    }
    

    Registre la clase de contexto de base de datos personalizada al agregar el servicio de Identity en Startup.ConfigureServices:

    public void ConfigureServices(IServiceCollection services)
    {
        services.Configure<CookiePolicyOptions>(options =>
        {
            options.CheckConsentNeeded = context => true;
            options.MinimumSameSitePolicy = SameSiteMode.None;
        });
    
        services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(
                Configuration.GetConnectionString("DefaultConnection")));
    
        services.AddIdentity<ApplicationUser, ApplicationRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultUI()
                .AddDefaultTokenProviders();
    
        services.AddMvc()
                .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }
    

    El tipo de datos de la clave principal se deduce mediante el análisis del objeto DbContext.

    Identity se proporciona como biblioteca de clases Razor. Para obtener más información, consulta Scaffolding Identity en proyectos de ASP.NET Core. Por lo tanto, el código anterior requiere una llamada a AddDefaultUI. Si el proveedor de scaffodling de Identity se usó para agregar archivos de Identity al proyecto, quite la llamada a AddDefaultUI.

Agregar propiedades de navegación

Hacer cambios en la configuración del modelo de las relaciones puede ser más difícil que hacer otros cambios. Se debe tener cuidado a la hora de reemplazar las relaciones existentes en lugar de crear relaciones nuevas y adicionales. En concreto, la relación modificada debe especificar la misma propiedad de clave externa (FK) que la relación existente. Por ejemplo, la relación entre Users y UserClaims se especifica de la siguiente manera de forma predeterminada:

builder.Entity<TUser>(b =>
{
    // Each User can have many UserClaims
    b.HasMany<TUserClaim>()
     .WithOne()
     .HasForeignKey(uc => uc.UserId)
     .IsRequired();
});

La FK de esta relación se especifica como la propiedad UserClaim.UserId. Se llama a HasMany y WithOne sin argumentos para crear la relación sin propiedades de navegación.

Agregue una propiedad de navegación a ApplicationUser que permita hacer referencia a las UserClaims del usuario:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
}

El elemento TKey de IdentityUserClaim<TKey> es el tipo especificado para la PK de los usuarios. En este caso, TKey es string porque se usan los valores predeterminados. No es el tipo de PK del tipo de entidad UserClaim.

Ahora que la propiedad de navegación existe, debe configurarse en OnModelCreating:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();
        });
    }
}

Observe que la relación se configura exactamente como era antes, solo con una propiedad de navegación especificada en la llamada a HasMany.

Las propiedades de navegación solo existen en el modelo de EF, no en la base de datos. Dado que la FK de la relación no ha cambiado, este tipo de cambio de modelo no necesita que se actualice la base de datos. Esto se puede comprobar al agregar una migración después de hacer el cambio. Los métodos Up y Down están vacíos.

Agregar todas las propiedades de navegación del usuario

Con la sección anterior como guía, en el ejemplo siguiente se configuran las propiedades de navegación unidireccionales de todas las relaciones del usuario:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<IdentityUserRole<string>> UserRoles { get; set; }
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne()
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });
    }
}

Agregar propiedades de navegación del usuario y rol

Con la sección anterior como guía, en el ejemplo siguiente se configuran las propiedades de navegación de todas las relaciones en Usuario y Rol:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<IdentityUserClaim<string>> Claims { get; set; }
    public virtual ICollection<IdentityUserLogin<string>> Logins { get; set; }
    public virtual ICollection<IdentityUserToken<string>> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        IdentityUserClaim<string>, ApplicationUserRole, IdentityUserLogin<string>,
        IdentityRoleClaim<string>, IdentityUserToken<string>>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne()
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne()
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne()
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();
        });

    }
}

Notas:

  • En este ejemplo también se incluye la entidad e combinación UserRole, que es necesaria para navegar por la relación de varios a varios entre Usuarios y Roles.
  • Recuerde cambiar los tipos de las propiedades de navegación para reflejar ahora se usan que los tipos de Application{...} en lugar de tipos de Identity{...}.
  • Recuerde usar Application{...} en la definición genérica de ApplicationContext.

Agregar todas las propiedades de navegación

Con la sección anterior como guía, en el ejemplo siguiente se configuran las propiedades de navegación de todos los tipos de entidad:

public class ApplicationUser : IdentityUser
{
    public virtual ICollection<ApplicationUserClaim> Claims { get; set; }
    public virtual ICollection<ApplicationUserLogin> Logins { get; set; }
    public virtual ICollection<ApplicationUserToken> Tokens { get; set; }
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}

public class ApplicationRole : IdentityRole
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
    public virtual ICollection<ApplicationRoleClaim> RoleClaims { get; set; }
}

public class ApplicationUserRole : IdentityUserRole<string>
{
    public virtual ApplicationUser User { get; set; }
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserClaim : IdentityUserClaim<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationUserLogin : IdentityUserLogin<string>
{
    public virtual ApplicationUser User { get; set; }
}

public class ApplicationRoleClaim : IdentityRoleClaim<string>
{
    public virtual ApplicationRole Role { get; set; }
}

public class ApplicationUserToken : IdentityUserToken<string>
{
    public virtual ApplicationUser User { get; set; }
}
public class ApplicationDbContext
    : IdentityDbContext<
        ApplicationUser, ApplicationRole, string,
        ApplicationUserClaim, ApplicationUserRole, ApplicationUserLogin,
        ApplicationRoleClaim, ApplicationUserToken>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    {
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<ApplicationUser>(b =>
        {
            // Each User can have many UserClaims
            b.HasMany(e => e.Claims)
                .WithOne(e => e.User)
                .HasForeignKey(uc => uc.UserId)
                .IsRequired();

            // Each User can have many UserLogins
            b.HasMany(e => e.Logins)
                .WithOne(e => e.User)
                .HasForeignKey(ul => ul.UserId)
                .IsRequired();

            // Each User can have many UserTokens
            b.HasMany(e => e.Tokens)
                .WithOne(e => e.User)
                .HasForeignKey(ut => ut.UserId)
                .IsRequired();

            // Each User can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.User)
                .HasForeignKey(ur => ur.UserId)
                .IsRequired();
        });

        modelBuilder.Entity<ApplicationRole>(b =>
        {
            // Each Role can have many entries in the UserRole join table
            b.HasMany(e => e.UserRoles)
                .WithOne(e => e.Role)
                .HasForeignKey(ur => ur.RoleId)
                .IsRequired();

            // Each Role can have many associated RoleClaims
            b.HasMany(e => e.RoleClaims)
                .WithOne(e => e.Role)
                .HasForeignKey(rc => rc.RoleId)
                .IsRequired();
        });
    }
}

Uso de claves compuestas

En las secciones anteriores se mostró cómo cambiar el tipo de clave usada en el modelo de Identity. No se admite ni recomienda cambiar el modelo de la clave de Identity para usar claves compuestas. El uso de una clave compuesta con Identity implica cambiar cómo el código de Identity interactúa con el modelo. Esta personalización está fuera del ámbito de este documento.

Cambiar los nombres de tabla o columna y las facetas

Para cambiar los nombres de las tablas y columnas, llame a base.OnModelCreating. A continuación, agregue la configuración para invalidar cualquiera de los valores predeterminados. Por ejemplo, para cambiar el nombre de todas las tablas de Identity:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.ToTable("MyUsers");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.ToTable("MyUserClaims");
    });

    modelBuilder.Entity<IdentityUserLogin<string>>(b =>
    {
        b.ToTable("MyUserLogins");
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.ToTable("MyUserTokens");
    });

    modelBuilder.Entity<IdentityRole>(b =>
    {
        b.ToTable("MyRoles");
    });

    modelBuilder.Entity<IdentityRoleClaim<string>>(b =>
    {
        b.ToTable("MyRoleClaims");
    });

    modelBuilder.Entity<IdentityUserRole<string>>(b =>
    {
        b.ToTable("MyUserRoles");
    });
}

En estos ejemplos se usan los tipos predeterminados de Identity. Si usa un tipo de aplicación como ApplicationUser, configúrelo en lugar del tipo predeterminado.

En el ejemplo siguiente se cambian algunos nombres de columnas:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(e => e.Email).HasColumnName("EMail");
    });

    modelBuilder.Entity<IdentityUserClaim<string>>(b =>
    {
        b.Property(e => e.ClaimType).HasColumnName("CType");
        b.Property(e => e.ClaimValue).HasColumnName("CValue");
    });
}

Algunos tipos de columnas de base de datos se pueden configurar con determinadas facetas (por ejemplo, la longitud máxima de string permitida). En el ejemplo siguiente se establecen las longitudes máximas de columna para varias propiedades string del modelo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.Entity<IdentityUser>(b =>
    {
        b.Property(u => u.UserName).HasMaxLength(128);
        b.Property(u => u.NormalizedUserName).HasMaxLength(128);
        b.Property(u => u.Email).HasMaxLength(128);
        b.Property(u => u.NormalizedEmail).HasMaxLength(128);
    });

    modelBuilder.Entity<IdentityUserToken<string>>(b =>
    {
        b.Property(t => t.LoginProvider).HasMaxLength(128);
        b.Property(t => t.Name).HasMaxLength(128);
    });
}

Asignar a un esquema diferente

Los esquemas se pueden comportar de forma diferente entre los proveedores de bases de datos. Para SQL Server, el valor predeterminado es crear todas las tablas en el esquema dbo. Las tablas se pueden crear en un esquema diferente. Por ejemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);

    modelBuilder.HasDefaultSchema("notdbo");
}

Carga diferida

En esta sección, se agrega compatibilidad con servidores proxy de carga diferida en el modelo de Identity. La carga diferida es útil, ya que permite usar las propiedades de navegación sin asegurarse de que se carguen primero.

Hay varias maneras en que los tipos de entidad se pueden hacer adecuados para la carga diferida, como se describe en la documentación de EF Core. Para simplificar, use servidores proxy de carga diferida. Para ello es necesario lo siguiente:

En el ejemplo siguiente se muestra la forma de llamar a UseLazyLoadingProxies en Startup.ConfigureServices:

services
    .AddDbContext<ApplicationDbContext>(
        b => b.UseSqlServer(connectionString)
              .UseLazyLoadingProxies())
    .AddDefaultIdentity<ApplicationUser>()
    .AddEntityFrameworkStores<ApplicationDbContext>();

Consulte los ejemplos anteriores para obtener una guía sobre cómo agregar propiedades de navegación a los tipos de entidad.

Advertencia

En este artículo se muestra el uso de cadena de conexión. Con una base de datos local, el usuario no tiene que autenticarse, pero en producción, cadena de conexión a veces incluye una contraseña para autenticarse. Una credencial de contraseña de propietario de recursos (ROPC) es un riesgo de seguridad que se debe evitar en las bases de datos de producción. Las aplicaciones de producción deben usar el flujo de autenticación más seguro disponible. Para obtener más información sobre la autenticación de aplicaciones implementadas para probar o entornos de producción, consulte Flujos de autenticación seguros.

Recursos adicionales