Personnalisation de modèle Identity dans ASP.NET Core

Par Arthur Vickers

ASP.NET Core Identity fournit une infrastructure pour la gestion et le stockage des comptes d’utilisateur dans les applications ASP.NET Core. Identity est ajouté à votre projet lorsque Comptes d’utilisateur individuels sont sélectionnés comme mécanisme d’authentification. Par défaut, Identity utilise un modèle de données Entity Framework (EF) Core. Cet article explique comment personnaliser le modèle Identity.

Migrations Identity et EF Core

Avant d’examiner le modèle, il est utile de comprendre comment Identity fonctionne avec EF Core Migrations pour créer et mettre à jour une base de données. Au niveau supérieur, le processus est le suivant :

  1. Définissez ou mettez à jour un modèle de données dans le code.
  2. Ajoutez une Migration pour traduire ce modèle en modifications qui peuvent être appliquées à la base de données.
  3. Vérifiez que la Migration représente correctement vos intentions.
  4. Appliquez la Migration pour mettre à jour la base de données afin qu’elle soit synchronisée avec le modèle.
  5. Répétez les étapes 1 à 4 pour affiner davantage le modèle et maintenir la base de données synchronisée.

Utilisez l’une des approches suivantes pour ajouter et appliquer des Migrations :

  • Fenêtre Console du gestionnaire de package (PMC) si vous utilisez Visual Studio. Pour plus d’informations, consultez EF CoreOutils .NET.
  • CLI .NET Core si vous utilisez la ligne de commande. Pour plus d'informations, consultez EF CoreOutils en ligne de commande.
  • Cliquez sur le bouton Appliquer les Migrations dans la page d’erreur lors de l’exécution de l’application.

ASP.NET Core dispose d’un gestionnaire de page d’erreurs au moment du développement. Le gestionnaire peut appliquer des migrations lorsque l’application est exécutée. Les applications de production génèrent généralement des scripts SQL à partir des migrations et déploient les modifications de base de données dans le cadre d’un déploiement d’application et de base de données contrôlé.

Lorsqu’une application utilisant Identity est créée, les étapes 1 et 2 ci-dessus ont déjà été effectuées. Autrement dit, le modèle de données initial existe déjà et la migration initiale a été ajoutée au projet. La migration initiale doit toujours être appliquée à la base de données. La migration initiale peut être appliquée via l’une des approches suivantes :

  • Exécuter Update-Database dans PMC.
  • Exécutez dotnet ef database update dans un interpréteur de commandes.
  • Cliquer sur le bouton Appliquer les Migrations dans la page d’erreur lors de l’exécution de l’application.

Répéter les étapes précédentes lorsque des modifications sont apportées au modèle.

Modèle Identity

Types d’entités

Le modèle Identity se compose des types d’entité suivants.

Type d'entité Description
User Représente l’utilisateur.
Role Représente un rôle.
UserClaim Représente une revendication qu’un utilisateur possède.
UserToken Représente un jeton d’authentification pour un utilisateur.
UserLogin Associe un utilisateur à une connexion.
RoleClaim Représente une revendication octroyée à tous les utilisateurs au sein d’un rôle.
UserRole Entité de jointure qui associe des utilisateurs et des rôles.

Relations de type d’entité

Les types d’entité sont liés les uns aux autres des manières suivantes :

  • Chaque User peut avoir plusieurs UserClaims.
  • Chaque User peut avoir plusieurs UserLogins.
  • Chaque User peut avoir plusieurs UserTokens.
  • Chaque Role peut avoir plusieurs RoleClaims associées.
  • Chaque User peut avoir plusieurs Roles associés , et chaque Role peut être associé à plusieurs Users. Il s’agit d’une relation plusieurs-à-plusieurs qui nécessite une table de jointure dans la base de données. La table de jointure est représentée par l’entité UserRole.

Configuration du modèle par défaut

Identity définit de nombreuses classes de contexte qui héritent de DbContext pour configurer et utiliser le modèle. Cette configuration est effectuée à l’aide de l’EF CoreAPI Code First Fluent dans la méthode OnModelCreating de la classe de contexte. La configuration par défaut est :

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

Types génériques de modèles

Identity définit les types Common Language Runtime (CLR) par défaut pour chacun des types d’entité répertoriés ci-dessus. Ces types sont tous précédés de Identity :

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

Au lieu d’utiliser ces types directement, nous pouvons les utiliser comme classes de base pour les propres types de l’application. Les classes DbContext définies par Identity sont génériques, de sorte que différents types CLR peuvent être utilisés pour un ou plusieurs des types d’entité dans le modèle. Ces types génériques permettent également de modifier le type de données clé primaire (PK) User.

Lorsque vous utilisez Identity avec la prise en charge des rôles, une classe IdentityDbContext doit être utilisée. Par exemple :

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

Il est également possible d’utiliser Identity sans rôles (revendications uniquement), auquel cas une classe IdentityUserContext<TUser> doit être utilisée :

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

Personnaliser le modèle

Le point de départ de la personnalisation du modèle consiste à dériver du type de contexte approprié. Consultez la section Types génériques de modèle. Ce type de contexte est généralement appelé ApplicationDbContext et est créé par les modèles ASP.NET Core.

Le contexte est utilisé pour configurer le modèle de deux manières :

  • Fourniture de types d’entité et de clé pour les paramètres de type générique.
  • Substitution de OnModelCreating pour modifier le mappage de ces types.

Lors de la substitution de OnModelCreating, base.OnModelCreating doit être appelé en premier ; la configuration de substitution doit être appelée ensuite. EF Core a généralement une stratégie last-one-wins pour la configuration. Par exemple, si la méthode ToTable d’un type d’entité est appelée d’abord avec un nom de table, puis plus tard avec un autre nom de table, le nom de la table dans le deuxième appel est utilisé.

REMARQUE : si le DbContext ne dérive pas de IdentityDbContext, AddEntityFrameworkStores peut ne pas déduire les types OCT corrects pour TUserClaim, TUserLoginet TUserToken. Si AddEntityFrameworkStores ne déduit pas les types OCT corrects, une solution de contournement consiste à ajouter directement les types appropriés via services.AddScoped<IUser/RoleStore<TUser> et UserStore<...>>.

Données utilisateur personnalisées

Les données utilisateur personnalisées sont prises en charge en héritant de IdentityUser. Il est d’usage de nommer ce type ApplicationUser :

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

Utilisez le type ApplicationUser comme argument générique pour le contexte :

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

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

Il n’est pas nécessaire de remplacer OnModelCreating dans la classe ApplicationDbContext. EF Core mappe la propriété CustomTag par convention. Toutefois, la base de données doit être mise à jour pour créer une nouvelle colonne CustomTag. Pour créer la colonne, ajoutez une migration, puis mettez à jour la base de données comme décrit dans Identity et EF Core Migrations.

Mettez à jour le fichier Pages/Shared/_LoginPartial.cshtml, et remplacez IdentityUser par ApplicationUser :

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

Mettez à jour Areas/Identity/IdentityHostingStartup.cs ou Startup.ConfigureServices et remplacez IdentityUser par ApplicationUser.

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

Appeler AddDefaultIdentity équivaut au code suivant :

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 est fourni en tant que bibliothèque de classes Razor. Pour plus d’informations, consultez Génération de modèles automatiqueIdentity dans les projets ASP.NET Core. Par conséquent, le code précédent nécessite un appel à AddDefaultUI. Si le générateur de modèles Identity a été utilisé pour ajouter des fichiers Identity au projet, supprimez l’appel à AddDefaultUI. Pour en savoir plus, consultez :

Modifier le type de clé primaire

Une modification du type de données de la colonne PK après la création de la base de données pose des problèmes sur de nombreux systèmes de base de données. La modification de la PK implique généralement la suppression et la recréation de la table. Par conséquent, les types de clés doivent être spécifiés lors de la migration initiale, quand la base de données est créée.

Procédez comme suit pour modifier le type de la PK :

  1. Si la base de données a été créée avant la modification de la PK, exécutez Drop-Database (PMC) ou dotnet ef database drop (CLI.NET Core) pour la supprimer.

  2. Après avoir confirmé la suppression de la base de données, supprimez la migration initiale avec Remove-Migration (PMC) ou dotnet ef migrations remove (CLI.NET Core).

  3. Mettez à jour la classe ApplicationDbContext à dériver de IdentityDbContext<TUser,TRole,TKey>. Spécifiez le nouveau type de clé pour TKey. Par exemple, pour utiliser un type de clé Guid :

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

    Dans le code précédent, les classes génériques IdentityUser<TKey> et IdentityRole<TKey> doivent être spécifiées pour utiliser le nouveau type de clé.

    Startup.ConfigureServices doit être mis à jour pour utiliser l’utilisateur générique :

    services.AddDefaultIdentity<IdentityUser<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>();
    
  4. Si une classe personnalisée ApplicationUser est utilisée, mettez à jour la classe pour qu’elle hérite de IdentityUser. Par exemple :

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

    Mettez à jour ApplicationDbContext pour référencer la classe personnalisée ApplicationUser :

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

    Inscrivez la classe de contexte de base de données personnalisée lors de l’ajout du service Identity dans Startup.ConfigureServices :

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

    Le type de données de la clé primaire est déduit en analysant l’objet DbContext.

    Identity est fourni en tant que bibliothèque de classes Razor. Pour plus d’informations, consultez Génération de modèles automatiqueIdentity dans les projets ASP.NET Core. Par conséquent, le code précédent nécessite un appel à AddDefaultUI. Si le générateur de modèles Identity a été utilisé pour ajouter des fichiers Identity au projet, supprimez l’appel à AddDefaultUI.

  5. Si une classe personnalisée ApplicationRole est utilisée, mettez à jour la classe pour qu’elle hérite de IdentityRole<TKey>. Par exemple :

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

    Mettez à jour ApplicationDbContext pour référencer la classe personnalisée ApplicationRole. Par exemple, la classe suivante référence un ApplicationUser personnalisé et un ApplicationRole personnalisé :

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

    Inscrivez la classe de contexte de base de données personnalisée lors de l’ajout du service Identity dans 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);
    }
    

    Le type de données de la clé primaire est déduit en analysant l’objet DbContext.

    Identity est fourni en tant que bibliothèque de classes Razor. Pour plus d’informations, consultez Génération de modèles automatiqueIdentity dans les projets ASP.NET Core. Par conséquent, le code précédent nécessite un appel à AddDefaultUI. Si le générateur de modèles Identity a été utilisé pour ajouter des fichiers Identity au projet, supprimez l’appel à AddDefaultUI.

Ajouter des propriétés de navigation

La modification de la configuration du modèle pour les relations peut être plus difficile que d’apporter d’autres modifications. Vous devez veiller à remplacer les relations existantes plutôt que de créer de nouvelles relations supplémentaires. En particulier, la relation modifiée doit spécifier la même propriété de clé étrangère (FK) que la relation existante. Par exemple, la relation entre Users et UserClaims est, par défaut, spécifiée comme suit :

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

La FK pour cette relation est spécifiée en tant que propriété UserClaim.UserId. HasMany et WithOne sont appelés sans arguments pour créer la relation sans propriétés de navigation.

Ajoutez une propriété de navigation à ApplicationUser qui permet à la UserClaims associée d’être référencée à partir de l’utilisateur :

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

Le TKey pour IdentityUserClaim<TKey> est le type spécifié pour la PK des utilisateurs. Dans ce cas, TKey est string car les valeurs par défaut sont utilisées. Il ne s’agit pas du type de la PK pour le type d’entité UserClaim.

Maintenant que la propriété de navigation existe, elle doit être configurée dans 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();
        });
    }
}

Notez que la relation est configurée exactement comme avant, avec uniquement une propriété de navigation spécifiée dans l’appel à HasMany.

Les propriétés de navigation existent uniquement dans le modèle EF, pas dans la base de données. Étant donné que la FK de la relation n’a pas changé, ce type de changement de modèle ne nécessite pas la mise à jour de la base de données. Cela peut être vérifié en ajoutant une migration après avoir apporté la modification. Les méthodes Up et Down sont vides.

Ajouter toutes les propriétés de navigation Utilisateur

Appliquant la section ci-dessus, l’exemple suivant configure les propriétés de navigation unidirectionnelles pour toutes les relations sur Utilisateur :

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

Ajouter des propriétés de navigation Utilisateur et Rôle

Appliquant la section ci-dessus, l’exemple suivant configure les propriétés de navigation pour toutes les relations sur Utilisateur et Rôle :

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

    }
}

Remarques :

  • Cet exemple inclut également l’entité de jointure UserRole, qui est nécessaire pour parcourir la relation plusieurs-à-plusieurs d’Utilisateurs à Rôles.
  • N’oubliez pas de modifier les types des propriétés de navigation pour refléter que les types Application{...} sont maintenant utilisés à la place des types Identity{...}.
  • N’oubliez pas d’utiliser Application{...} dans la définition ApplicationContext générique.

Ajouter toutes les propriétés de navigation

Appliquant la section ci-dessus, l’exemple suivant configure les propriétés de navigation pour toutes les relations sur tous les types d’entité :

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

Utiliser des clés composites

Les sections précédentes ont montré la modification du type de clé utilisé dans le modèle Identity. La modification du modèle de clé Identity pour utiliser des clés composites n’est pas prise en charge ni recommandée. L’utilisation d’une clé composite avec Identity implique de modifier la façon dont le code du gestionnaire d’Identity interagit avec le modèle. Ce document n’abordera pas la personnalisation.

Modifier les noms et les facettes des tables/colonnes

Pour modifier les noms des tables et des colonnes, appelez base.OnModelCreating. Ensuite, ajoutez une configuration pour remplacer les valeurs par défaut. Par exemple, pour modifier le nom de toutes les tables 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");
    });
}

Ces exemples utilisent les types Identity par défaut. Si vous utilisez un type d’application tel que ApplicationUser, configurez ce type au lieu du type par défaut.

L’exemple suivant modifie certains noms de colonnes :

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

Certains types de colonnes de base de données peuvent être configurés avec certaines facettes (par exemple, la longueur maximale de string autorisée). L’exemple suivant définit les longueurs maximales des colonnes pour plusieurs propriétés string dans le modèle :

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

Mapper à un autre schéma

Les schémas peuvent se comporter différemment d’un fournisseur de base de données à l’autre. Pour SQL Server, toutes les tables sont créées dans le schéma dbo par défaut. Les tables peuvent être créées dans un autre schéma. Par exemple :

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

    modelBuilder.HasDefaultSchema("notdbo");
}

Chargement différé

Dans cette section, la prise en charge des proxys à chargement différé dans le modèle Identity est ajoutée. Le chargement différé est utile, car il permet d’utiliser les propriétés de navigation sans devoir s’assurer au préalable qu’elles sont chargées.

Les types d’entité peuvent être adaptés au chargement différé de plusieurs façons, comme décrit dans la documentationEF Core. Par souci de simplicité, utilisez des proxys à chargement différé, ce qui nécessite :

L'exemple suivant montre l'appel de UseLazyLoadingProxies dans Startup.ConfigureServices :

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

Reportez-vous aux exemples précédents pour obtenir des conseils sur l’ajout de propriétés de navigation aux types d’entité.

Ressources supplémentaires