Situation
I'm using Identity ASP.NET Core 3.1 with Angular 8 Template. I want to extend the ASPNETUserRoles table and add another custom key column CompanyId in it.
By Default Identity provides:
public virtual TKey UserId { get; set; }
public virtual TKey RoleId { get; set; }
As I modified my DbContext from UserId (string) to UserId (long), DbContext looks like:
public class CompanyDBContext : KeyApiAuthorizationDbContext<User, Role, UserRole, long>
{
public CompanyDBContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options, operationalStoreOptions)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
public DbSet<Company> Companies { get; set; }
}
KeyApiAuthorizationDbContext
public class KeyApiAuthorizationDbContext<TUser, TRole, IdentityUserRole, TKey> : IdentityDbContext<TUser, TRole, TKey, IdentityUserClaim<TKey>, IdentityUserRole<TKey>, IdentityUserLogin<TKey>, IdentityRoleClaim<TKey>, IdentityUserToken<TKey>>, IPersistedGrantDbContext
where TUser : IdentityUser<TKey>
where TRole : IdentityRole<TKey>
where IdentityUserRole : IdentityUserRole<TKey>
where TKey : IEquatable<TKey>
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
/// <summary>
/// Initializes a new instance of <see cref="ApiAuthorizationDbContext{TUser, TRole, TKey}"/>.
/// </summary>
/// <param name="options">The <see cref="DbContextOptions"/>.</param>
/// <param name="operationalStoreOptions">The <see cref="IOptions{OperationalStoreOptions}"/>.</param>
public KeyApiAuthorizationDbContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions)
: base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
/// <summary>
/// Gets or sets the <see cref="DbSet{PersistedGrant}"/>.
/// </summary>
public DbSet<PersistedGrant> PersistedGrants { get; set; }
/// <summary>
/// Gets or sets the <see cref="DbSet{DeviceFlowCodes}"/>.
/// </summary>
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
}
}
Entities
public class User : IdentityUser<long> {}
public class Role : IdentityRole<long> {}
public class UserRole : IdentityUserRole<long>
{
public long CompanyId { get; set; }
}
Problem Occur
When I registered my user and it returns true then I'll add a current user in UserRole table like below but when my debugger reached await _context.SaveChangesAsync();
method its shows me an exception
if (result.Succeeded)
{
foreach (var role in model.Roles.Where(x => x.IsChecked = true))
{
var entity = new Core.Entities.Identity.UserRole()
{
UserId = model.User.Id,
RoleId = role.Id,
CompanyId = companycode
};
_context.UserRoles.Add(entity);
}
await _context.SaveChangesAsync();
}
I don't know where I'm making my mistake? If above steps of overriding userrole are wrong then assist me on that.
I'm also sharing my migration details for your information may be something I'm doing wrong there.
Migrations
migrationBuilder.CreateTable(
name: "Companies",
columns: table => new
{
Id = table.Column<long>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(nullable: false),
Code = table.Column<string>(nullable: false),
Logo = table.Column<string>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Companies", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AspNetUserRoles",
columns: table => new
{
UserId = table.Column<long>(nullable: false),
RoleId = table.Column<long>(nullable: false),
CompanyId = table.Column<long>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId, x.CompanyId });
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetRoles_RoleId",
column: x => x.RoleId,
principalTable: "AspNetRoles",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
column: x => x.UserId,
principalTable: "AspNetUsers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AspNetUserRoles_Companies_CompanyId",
column: x => x.CompanyId,
principalTable: "Companies",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
Migration Snapshot
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<long>", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn);
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<long>("RoleId")
.HasColumnType("bigint");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("AspNetRoleClaims");
});
Now I came up with an answer to my own question.
Recall Question
I want to extend the User Role (IdentityUserRole) and adding a few foreign keys, but faced two errors:
Error 1
Invalid column name 'discriminator'
Error 2
A key cannot be configured on ‘UserRole’ because it is a derived type. The key must be configured on the root type ‘IdentityUserRole’. If you did not intend for ‘IdentityUserRole’ to be included in the model, ensure that it is not included in a DbSet property on your context, referenced in a configuration call to ModelBuilder, or referenced from a navigation property on a type that is included in the model.
This is because if you look into the definition of IdentityUserRole, you’ll find out that it already has primary keys:
public virtual TKey UserId { get; set; }
public virtual TKey RoleId { get; set; }
So, what's the solution?
To fix this, we have to override the user role implementation.
Now if you see the code:
public class CompanyDBContext : IdentityDbContext<User, Role, long, UserClaim, UserRole, UserLogin, RoleClaim, UserToken>, IPersistedGrantDbContext
{
private readonly IOptions<OperationalStoreOptions> _operationalStoreOptions;
public CompanyDBContext(
DbContextOptions options,
IOptions<OperationalStoreOptions> operationalStoreOptions) : base(options)
{
_operationalStoreOptions = operationalStoreOptions;
}
Task<int> IPersistedGrantDbContext.SaveChangesAsync() => base.SaveChangesAsync();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ConfigurePersistedGrantContext(_operationalStoreOptions.Value);
modelBuilder.Entity<UserRole>(b =>
{
b.HasKey(ur => new { ur.UserId, ur.RoleId, ur.CompanyId });
});
public DbSet<DeviceFlowCodes> DeviceFlowCodes { get; set; }
public DbSet<PersistedGrant> PersistedGrants { get; set; }
}
That's it. As @EduCielo suggested I re-consider my code and do the proper mapping and configuration and override the modelbuilder to map the proper relationship for user role.
For new developers, I'm sharing entity classes which I used above to make it fully worked with your code.
public class User : IdentityUser<long> { //Add new props to modify ASP.NETUsers Table }
public class Role : IdentityRole<long> { }
public class UserClaim: IdentityUserClaim<long> { }
public class UserRole : IdentityUserRole<long>
{
[Key, Column(Order = 2)]
public long CompanyId { get; set; }
public Company Company { get; set; }
}
public class UserLogin: IdentityUserLogin<long> { }
public class RoleClaim : IdentityRoleClaim<long> { }
public class UserToken : IdentityUserToken<long> { }
Conclusion
add-migration FirstMigration -context CompanyDbContext
update-database -context CompanyDbContext
Happy Coding :)