Triet Nguyen
Triet Nguyen

Reputation: 821

AspNet Identity - Custom IdentityRoleClaim & IdentityUserRole & IdentityUserClaim that duplicated foreign key

currently, I am using AspNetCore and the configuration dbcontext as below:

public class AppDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, IdentityUserLogin<string>, ApplicationRoleClaim, IdentityUserToken<string>>
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
        : base(options)
    { }

So I would like to customize AspNetIdentity, but for simplicity I have just configured foreign key between tables:


public class ApplicationUser : IdentityUser
{
    public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
    public virtual ICollection<ApplicationUserClaim> UserClaims { get; set; }
}

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

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

public class ApplicationUserRole : IdentityUserRole<string>
{
     public int Id { get; set; }

     public virtual ApplicationRole Role { get; set; }
     public virtual ApplicationUser User { get; set; }
}

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

And follows as are derived class implement IEntityTypeConfiguration for particular custom identity model.

public class ApplicationUserEntityBuilder : IEntityTypeConfiguration<ApplicationUser>
{
    public void Configure(EntityTypeBuilder<ApplicationUser> builder)
    {
        // still use default table name
        builder.ToTable("AspNetUsers");

        builder.Property(p => p.Id)
               .HasColumnType("CHAR(36)");
    }
}

public class ApplicationRoleEntityBuilder : IEntityTypeConfiguration<ApplicationRole>
 {
     public void Configure(EntityTypeBuilder<ApplicationRole> builder)
     {
         // still use default table name
         builder.ToTable("AspNetRoles");
     }

 }

 public class ApplicationUserClaimEntityBuilder : IEntityTypeConfiguration<ApplicationUserClaim>
  {
        public void Configure(EntityTypeBuilder<ApplicationUserClaim> builder)
        {
            builder.Property(p => p.UserId)
                   .HasColumnName(nameof(ApplicationUserClaim.UserId))
                   .HasColumnType("CHAR(36)");

            builder.HasOne(p => p.User)
                   .WithMany(u => u.UserClaims)
                   .HasForeignKey(p => p.UserId)
                   .IsRequired();
        }

    }

public class ApplicationUserRoleEntityBuilder : IEntityTypeConfiguration<ApplicationUserRole>
{
    public void Configure(EntityTypeBuilder<ApplicationUserRole> builder)
    {

        // still use default table name
        builder.ToTable("AspNetUserRoles");

        builder.HasKey(p => p.Id);
        builder.Property(p => p.Id)
               .ValueGeneratedOnAdd();

        builder.HasOne(p => p.Role)
               .WithMany(r => r.UserRoles)
               .HasForeignKey(p => p.RoleId)
               .IsRequired();

        builder.HasOne(p => p.User)
               .WithMany(r => r.UserRoles)
               .HasForeignKey(p => p.UserId)
               .IsRequired();
    }
}

public class ApplicationRoleClaimEntityBuilder : IEntityTypeConfiguration<ApplicationRoleClaim>
{
    public void Configure(EntityTypeBuilder<ApplicationRoleClaim> builder)
    {
        // still use default table name
        builder.ToTable("AspNetRoleClaims");

        builder.HasKey(p => p.Id);

        builder.Property(p => p.Id)
               .ValueGeneratedOnAdd();

        builder.Property(p => p.RoleId)
               .HasColumnName(nameof(ApplicationRoleClaim.RoleId))
               .HasColumnType("CHAR(36)");

        builder.HasOne(p => p.Role)
               .WithMany(r => r.RoleClaims)
               .HasForeignKey(p => p.RoleId)
               .IsRequired();
    }
}

After these all fluent API configuration, EF Core generated duplicate Foreign Key in table AspNetRoleClaims, AspNetUserClaims, AspNetUserRoles

migrationBuilder.CreateTable(
                name: "AspNetRoleClaims",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    RoleId = table.Column<string>(type: "CHAR(36)", nullable: false),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    RoleId1 = table.Column<string>(nullable: true) // DUPLICATE
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id);
                    table.ForeignKey(
                        name: "FK_AspNetRoleClaims_AspNetRoles_RoleId",
                        column: x => x.RoleId,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_AspNetRoleClaims_AspNetRoles_RoleId1",
                        column: x => x.RoleId1,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });


migrationBuilder.CreateTable(
                name: "AspNetUserClaims",
                columns: table => new
                {
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    UserId = table.Column<string>(type: "CHAR(36)", nullable: false),
                    ClaimType = table.Column<string>(nullable: true),
                    ClaimValue = table.Column<string>(nullable: true),
                    UserId1 = table.Column<string>(nullable: true) //DUPLICATE
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserClaims", x => x.Id);
                    table.ForeignKey(
                        name: "FK_AspNetUserClaims_AspNetUsers_UserId",
                        column: x => x.UserId,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_AspNetUserClaims_AspNetUsers_UserId1",
                        column: x => x.UserId1,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });


migrationBuilder.CreateTable(
                name: "AspNetUserRoles",
                columns: table => new
                {
                    UserId = table.Column<string>(nullable: false),
                    RoleId = table.Column<string>(nullable: false),
                    Id = table.Column<int>(nullable: false)
                        .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
                    RoleId1 = table.Column<string>(nullable: true), //DUPLICATE
                    UserId1 = table.Column<string>(nullable: true)  //DUPLICATE
                },
                constraints: table =>
                {
                    table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId });
                    table.UniqueConstraint("AK_AspNetUserRoles_Id", x => x.Id);
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
                        column: x => x.RoleId,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetRoles_RoleId1",
                        column: x => x.RoleId1,
                        principalTable: "AspNetRoles",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetUsers_UserId",
                        column: x => x.UserId,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Cascade);
                    table.ForeignKey(
                        name: "FK_AspNetUserRoles_AspNetUsers_UserId1",
                        column: x => x.UserId1,
                        principalTable: "AspNetUsers",
                        principalColumn: "Id",
                        onDelete: ReferentialAction.Restrict);
                });

Please review it for me if anythings I did wrong. Thank you guys in advance.


UPDATE 1

Because of Navigation Property that causes duplicate foreign key. If I remove these properties in custom identity model that EFCore will not generate "DuplicateProperty1". So what I have to do now?


FINAL UPDATE

Root cause that I put base.OnModelCreating(builder); at the end of method protected override void OnModelCreating(ModelBuilder builder). Therefore, anythings I have configured inner derived classes implemented IEntityTypeConfiguration that will be override by base class. And final version as below:

public class AppDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, IdentityUserLogin<string>, ApplicationRoleClaim, IdentityUserToken<string>>
{
    public AppDbContext(DbContextOptions<AppDbContext> options)
            : base(options)
    { }

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

        var entitiesBuilder = Assembly.GetExecutingAssembly()
                                      .GetTypes()
                                      .Where(type => type.ContainsGenericParameters == false && type.GetInterface(nameof(IEntityBuilder)) != null)
                                      .ToList();

        foreach (var entityBuilder in entitiesBuilder)
        {
            var instance = Activator.CreateInstance(entityBuilder) as IEntityBuilder;
            instance.RunConfiguration(builder);
        }

        // base.OnModelCreating(builder); move this line to the beginning of this method
    }
}

Upvotes: 3

Views: 8268

Answers (3)

Edward
Edward

Reputation: 30046

Make sure you configure the IEntityTypeConfiguration like below in AppDbContext.

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string, ApplicationUserClaim, ApplicationUserRole, IdentityUserLogin<string>, ApplicationRoleClaim, IdentityUserToken<string>>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options)
    { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.ApplyConfiguration(new ApplicationUserEntityBuilder());
        builder.ApplyConfiguration(new ApplicationRoleEntityBuilder());
        builder.ApplyConfiguration(new ApplicationUserClaimEntityBuilder());
        builder.ApplyConfiguration(new ApplicationUserRoleEntityBuilder());
        builder.ApplyConfiguration(new ApplicationRoleClaimEntityBuilder());
    }
}

Upvotes: 0

Darjan Bogdan
Darjan Bogdan

Reputation: 3900

Relationships between tables are already set inside Identity's IdentityDbContext default configuration. That means you don't have to specify them explicitly (using navigational props / fluent api configuration).

However, it looks like you want to have Guid/Uuid instead of string as a column type for your Primary/Foreign keys. If that's the case, the proper way to achieve that is to use generic variants of classes you've already inherited.

For example, you can define only classes you need to expand and add additional properties to them - which will be included into DB Migration:

public class ApplicationUser : IdentityUser<Guid>
{
}

public class ApplicationRole : IdentityRole<Guid>
{
}

In addition to that, your DbContext must inherit proper variant of IdentityDbContext:

public class ApplicationUserDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid>
{
}

After that, your migration will generate a script to change PK/FKs into proper column type. If you need to expand other types from Identity, you can use most generic variant of IdentityDbContext which takes a bunch of type parameters.

Upvotes: 3

Krystian Sitek
Krystian Sitek

Reputation: 588

This is happening because when you inherit from identity models it already has this relations and other properties specified. Your code just added additional relations between entities. You can read more about identity customization in documentation: https://learn.microsoft.com/en-us/aspnet/core/security/authentication/customize-identity-model?view=aspnetcore-2.1

Upvotes: 0

Related Questions