Personalização de modelo Identity no ASP.NET Core

Por Arthur Vickers

O AsP.NET Core Identity fornece uma estrutura para gerenciar e armazenar contas de usuários em aplicativos ASP.NET Core. Identity é adicionado ao seu projeto quando Contas de usuário individuais são selecionadas como o mecanismo de autenticação. Por padrão, Identity usa um modelo de dados do EF (Entity Framework) Core. Este artigo descreve como personalizar o modelo Identity.

Identity e EF Core migrações

Antes de examinar o modelo, é útil entender como Identity funciona com EF CoreMigrações para criar e atualizar um banco de dados. No nível superior, o processo é:

  1. Definir ou atualizar um modelo de dados no código.
  2. Adicione uma migração para converter esse modelo em alterações que podem ser aplicadas ao banco de dados.
  3. Verifique se a migração representa corretamente suas intenções.
  4. Aplique a migração para atualizar o banco de dados a ser sincronizado com o modelo.
  5. Repita as etapas 1 a 4 para refinar ainda mais o modelo e manter o banco de dados sincronizado.

Use uma das seguintes abordagens para adicionar e aplicar migrações:

  • A janela do Console do Gerenciador de Pacotes (PMC) se estiver usando o Visual Studio. Para obter mais informações, consulte EF Core Ferramentas do PMC.
  • A CLI do .NET Core se estiver usando a linha de comando. Para obter mais informações, consulte EF Core Ferramentas de linha de comando do .NET.
  • Clicar no botão Aplicar Migrações na página de erro quando o aplicativo é executado.

O ASP.NET Core tem um manipulador de página de erro em tempo de desenvolvimento. O manipulador pode aplicar migrações quando o aplicativo é executado. Os aplicativos de produção normalmente geram scripts SQL a partir das migrações e implantam alterações de banco de dados como parte de uma implantação controlada de aplicativo e banco de dados.

Quando um novo aplicativo usando Identity é criado, as etapas 1 e 2 acima já foram concluídas. Ou seja, o modelo de dados inicial já existe e a migração inicial foi adicionada ao projeto. A migração inicial ainda precisa ser aplicada ao banco de dados. A migração inicial pode ser aplicada por meio de uma das seguintes abordagens:

  • Execute Update-Database no PMC.
  • Execute dotnet ef database update em um shell de comando.
  • Clique no botão Aplicar Migrações na página de erro quando o aplicativo for executado.

Repita as etapas anteriores à medida que as alterações são feitas no modelo.

O modelo Identity

Tipos de entidade

O modelo Identity consiste nos seguintes tipos de entidade.

Tipo de entidade Descrição
User Representa o usuário.
Role Representa uma função.
UserClaim Representa uma declaração que um usuário possui.
UserToken Representa um token de autenticação para um usuário.
UserLogin Associa um usuário a um logon.
RoleClaim Representa uma declaração concedida a todos os usuários dentro de uma função.
UserRole Uma entidade de junção que associa usuários e funções.

Relações de tipo de entidade

Os tipos de entidade estão relacionados entre si das seguintes maneiras:

  • Cada User pode ter vários UserClaims.
  • Cada User pode ter vários UserLogins.
  • Cada User pode ter vários UserTokens.
  • Cada Role pode ter vários RoleClaims associados.
  • Cada User pode ter vários Roles associados e cada Role pode ser associados a vários Users. Essa é uma relação muitos para muitos que requer uma tabela de junção no banco de dados. A tabela de junção é representada pela entidade UserRole.

Configuração do modelo de hospedagem

Identity definem várias classes de contexto que herdam de DbContext para configurar e usar o modelo. Essa configuração é feita usando a EF Core API fluente do Code First no método OnModelCreating da classe de contexto. A configuração padrão é:

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 modelo

Identity define os tipos Common Language Runtime (CLR) padrão para cada um dos tipos de entidade listados acima. Todos esses tipos são prefixados com Identity:

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

Em vez de usar esses tipos diretamente, os tipos podem ser usados como classes base para os próprios tipos do aplicativo. As classes DbContext definidas por Identity são genéricas, de modo que diferentes tipos CLR podem ser usados para um ou mais dos tipos de entidade no modelo. Esses tipos genéricos também permitem que o tipo de dados de chave primária (PK) User seja alterado.

Ao usar Identity com suporte para funções, uma classe IdentityDbContext deve ser usada. Por exemplo:

// 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>

Também é possível usar Identity sem funções (somente declarações). Nesse caso, uma classe IdentityUserContext<TUser> deve ser usada:

// 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>
{
}

Personalizar o modelo

O ponto de partida para a personalização do modelo é derivar do tipo de contexto apropriado. Consulte a seção Tipos genéricos de modelo. Esse tipo de contexto normalmente é chamado ApplicationDbContext e é criado pelos modelos do ASP.NET Core.

O contexto é usado para configurar o modelo de duas maneiras:

  • Fornecendo tipos de entidade e chave para os parâmetros de tipo genérico.
  • Substituindo OnModelCreating para modificar o mapeamento desses tipos.

Ao substituir OnModelCreating, base.OnModelCreating deve ser chamado primeiro; a configuração de substituição deve ser chamada em seguida. EF Core geralmente tem uma política “o último vence” para configuração. Por exemplo, se o método ToTable para um tipo de entidade for chamado primeiro com um nome de tabela e depois for chamado novamente com um nome de tabela diferente, o nome de tabela na segunda chamada será usado.

OBSERVAÇÃO: se DbContext não derivar de IdentityDbContext, AddEntityFrameworkStores pode não inferir os tipos POCO corretos para TUserClaim, TUserLogin e TUserToken. Se AddEntityFrameworkStores não inferir os tipos POCO corretos, uma solução alternativa será adicionar diretamente os tipos corretos por meio de services.AddScoped<IUser/RoleStore<TUser> e UserStore<...>>.

Dados de usuário personalizados

Há suporte para dados de usuário personalizados ao herdá-los de IdentityUser. É comum nomear esse tipo ApplicationUser:

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

Use o tipo ApplicationUser como um argumento genérico para o contexto:

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

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

Não é necessário substituir OnModelCreating na classe ApplicationDbContext. EF Core mapeia a propriedade CustomTag por convenção. No entanto, o banco de dados precisa ser atualizado para criar uma nova coluna CustomTag. Para criar a coluna, adicione uma migração e atualize o banco de dados conforme descrito em Identity e EF Core Migrações.

Atualize Pages/Shared/_LoginPartial.cshtml e substitua IdentityUser por ApplicationUser:

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

Atualize Areas/Identity/IdentityHostingStartup.cs ou Startup.ConfigureServices e substitua IdentityUser por ApplicationUser.

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

Chamar AddDefaultIdentity é equivalente ao seguinte 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 é fornecido como uma Biblioteca de Classes Razor. Para obter mais informações, veja Scaffold Identity em projetos ASP.NET Core. Portanto, o código anterior requer uma chamada para AddDefaultUI. Se o scaffolder Identity foi usado para adicionar arquivos Identity ao projeto, remova a chamada para AddDefaultUI. Para obter mais informações, consulte:

Alterar o tipo de chave primária

Uma alteração no tipo de dados da coluna da chave primária após a criação do banco de dados é problemática em muitos sistemas de banco de dados. Alterar a chave primária normalmente envolve a remoção e a recriação da tabela. Portanto, os tipos de chave devem ser especificados na migração inicial quando o banco de dados é criado.

Siga as etapas a seguir para alterar o tipo de chave primária:

  1. Se o banco de dados foi criado antes da alteração da chave primária, execute Drop-Database (PMC) ou dotnet ef database drop (CLI do .NET Core) para excluí-lo.

  2. Depois de confirmar a exclusão do banco de dados, remova a migração inicial com Remove-Migration (PMC) ou dotnet ef migrations remove (CLI do .NET Core).

  3. Atualize a classe ApplicationDbContext para que ela seja derivada de IdentityDbContext<TUser,TRole,TKey>. Especifique o novo tipo de chave para TKey. Por exemplo, para usar um tipo de chave Guid:

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

    No código anterior, as classes genéricas IdentityUser<TKey> e IdentityRole<TKey> devem ser especificadas para usar o novo tipo de chave.

    Startup.ConfigureServices deve ser atualizado para usar o usuário genérico:

    services.AddDefaultIdentity<IdentityUser<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    
  4. Se uma classe personalizada ApplicationUser estiver sendo usada, atualize a classe para herdar de IdentityUser. Por exemplo:

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

    Atualize ApplicationDbContext para fazer referência à classe personalizada ApplicationUser:

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

    Registre a classe de contexto de banco de dados personalizada ao adicionar o serviço Identity em Startup.ConfigureServices:

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

    O tipo de dados da chave primária é inferido analisando o objeto DbContext.

    Identity é fornecido como uma Biblioteca de Classes Razor. Para obter mais informações, veja Scaffold Identity em projetos ASP.NET Core. Portanto, o código anterior requer uma chamada para AddDefaultUI. Se o scaffolder Identity foi usado para adicionar arquivos Identity ao projeto, remova a chamada para AddDefaultUI.

  5. Se uma classe personalizada ApplicationRole estiver sendo usada, atualize a classe para herdar de IdentityRole<TKey>. Por exemplo:

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

    Atualize ApplicationDbContext para fazer referência à classe personalizada ApplicationRole. Por exemplo, a classe a seguir faz referência a um ApplicationUser personalizado e um 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 a classe de contexto de banco de dados personalizada ao adicionar o serviço Identity em 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);
    }
    

    O tipo de dados da chave primária é inferido analisando o objeto DbContext.

    Identity é fornecido como uma Biblioteca de Classes Razor. Para obter mais informações, veja Scaffold Identity em projetos ASP.NET Core. Portanto, o código anterior requer uma chamada para AddDefaultUI. Se o scaffolder Identity foi usado para adicionar arquivos Identity ao projeto, remova a chamada para AddDefaultUI.

Adicionar propriedades de navegação

Alterar a configuração do modelo para relações pode ser mais difícil do que fazer outras alterações. É necessário ter cuidado para substituir as relações existentes em vez de criar relações adicionais. Em particular, a relação alterada deve especificar a mesma propriedade de FK (chave estrangeira) que a relação existente. Por exemplo, a relação entre Users e UserClaims é, por padrão, especificada da seguinte maneira:

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

A chave estrangeira dessa relação é especificada como a propriedade UserClaim.UserId. HasMany e WithOne são chamados sem argumentos para criar a relação sem propriedades de navegação.

Adicione uma propriedade de navegação a ApplicationUser que permite que o UserClaims associado seja referenciado pelo usuário:

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

O TKey para IdentityUserClaim<TKey> é o tipo especificado para a chave estrangeira dos usuários. Nesse caso, TKey é string porque os padrões estão sendo usados. Não é o tipo de chave primária do tipo de entidade UserClaim.

Agora que a propriedade de navegação existe, ela deve ser configurada em 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 a relação está configurada exatamente como anteriormente, somente com uma propriedade de navegação especificada na chamada para HasMany.

As propriedades de navegação só existem no modelo do EF, não no banco de dados. Como a chave estrangeira da relação não foi alterada, esse tipo de alteração de modelo não exige que o banco de dados seja atualizado. Isso pode ser verificado adicionando uma migração depois de fazer a alteração. Os métodos Up e Down estão vazios.

Adicionar todas as propriedades de navegação do usuário

Usando a seção acima como orientação, o exemplo a seguir configura as propriedades de navegação unidirecional para todas as relações no usuário:

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();
        });
    }
}

Adicionar propriedades de navegação de usuário e função

Usando a seção acima como orientação, o exemplo a seguir configura as propriedades de navegação para todas as relações no usuário e na função:

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();
        });

    }
}

Observações:

  • Esse exemplo também inclui a entidade de junção UserRole, que é necessária para navegar na relação muitos para muitos de usuários para funções.
  • Lembre-se de alterar os tipos das propriedades de navegação para refletir que os tipos Application{...} agora estão sendo usados em vez de tipos Identity{...}.
  • Lembre-se de usar o Application{...} na definição genérica ApplicationContext.

Adicionar todas as propriedades de navegação

Usando a seção acima como orientação, o exemplo a seguir configura as propriedades de navegação para todas as relações em todos os tipos de entidade:

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();
        });
    }
}

Usar chaves compostas

As seções anteriores demonstraram a alteração do tipo de chave usado no modelo Identity. Não se recomenda ou há suporte para alterar o modelo de chave Identity para usar chaves compostas. Usar uma chave composta com Identity envolve alterar a forma como o código do gerenciador Identity interage com o modelo. Essa personalização está além do escopo deste documento.

Alterar nomes e facetas de tabela/coluna

Para alterar os nomes de tabelas e colunas, chame base.OnModelCreating. Em seguida, adicione a configuração para substituir os padrões. Por exemplo, para alterar o nome de todas as tabelas 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");
    });
}

Esses exemplos usam os tipos padrão Identity. Se estiver usando um tipo de aplicativo como ApplicationUser, configure esse tipo em vez do tipo padrão.

O exemplo a seguir altera alguns nomes de coluna:

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");
    });
}

Alguns tipos de colunas de banco de dados podem ser configurados com determinadas facetas (por exemplo, o comprimento máximo string permitido). O exemplo a seguir define os comprimentos máximos de coluna para várias propriedades string no 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);
    });
}

Mapear para um esquema diferente

Esquemas podem se comportar de forma diferente entre provedores de banco de dados. Para SQL Server, o padrão é criar todas as tabelas no esquema dbo. As tabelas podem ser criadas em um esquema diferente. Por exemplo:

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

    modelBuilder.HasDefaultSchema("notdbo");
}

Carregamento lento

Nessa seção, o suporte para proxies de carregamento lento no modelo Identity é adicionado. O carregamento lento é útil, pois permite que as propriedades de navegação sejam usadas sem primeiro garantir que elas sejam carregadas.

Os tipos de entidade podem ser adequados para carregamento lento de várias maneiras, conforme descrito na EF Core documentação. Para simplificar, use proxies de carregamento lento, o que requer:

O exemplo a seguir demonstra como chamar UseLazyLoadingProxies em Startup.ConfigureServices:

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

Consulte os exemplos anteriores para obter diretrizes sobre como adicionar propriedades de navegação aos tipos de entidade.

Recursos adicionais