Seed admin user to database OnModelCreating
I have been browsing internet the whole day and tried whatever I could already. I have no errors and user is created into database but there is no UserRole assigned. Also roles are created just fine and can be visible in database. Why so? Can somebody see what's the problem in here?
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
Guid ADMIN_ID = Guid.NewGuid();
Guid ROLE_ID = ADMIN_ID;
builder.Entity<ApplicationRole>().HasData(new ApplicationRole { Name = "User", NormalizedName = "USER", Id = Guid.NewGuid(), ConcurrencyStamp = Guid.NewGuid().ToString() });
builder.Entity<ApplicationRole>().HasData(new ApplicationRole { Name = "Admin", NormalizedName = "ADMIN", Id = ROLE_ID, ConcurrencyStamp = ROLE_ID.ToString() });
PasswordHasher<ApplicationUser> hasher = new PasswordHasher<ApplicationUser>();
builder.Entity<ApplicationUser>().HasData(new ApplicationUser
{
Id = ADMIN_ID,
FirstName = "MrJack",
LastName = "Jackson",
UserName = "Administrator",
NormalizedUserName = "Administrator",
Email = "[email protected]",
IsActive = true,
ProfilePicture = System.IO.File.ReadAllBytes(string.Concat(Path.GetFullPath("Resources\\"), "avatar.png")),
NormalizedEmail = "[email protected]",
EmailConfirmed = true,
PasswordHash = hasher.HashPassword(null, "QWERTY"),
SecurityStamp = string.Empty,
ConcurrencyStamp = Guid.NewGuid().ToString(),
});
builder.Entity<IdentityUserRole<Guid>>().HasData(new IdentityUserRole<Guid>
{
RoleId = ROLE_ID,
UserId = ADMIN_ID
});
builder.Entity<IdentityUserRole<Guid>>().HasKey(p => new { p.UserId, p.RoleId });
}
I think problem is in this part, as user is not assigned in DB to user. This table is empty for some reason, but what is actually the problem?
builder.Entity<IdentityUserRole<Guid>>().HasData(new IdentityUserRole<Guid>
{
RoleId = ROLE_ID,
UserId = ADMIN_ID
});
builder.Entity<IdentityUserRole<Guid>>().HasKey(p => new { p.UserId, p.RoleId });
ApplicationRole.cs:
public class ApplicationRole : IdentityRole<Guid>
{
public virtual ICollection<ApplicationUserRole> UserRoles { get; set; }
}
ApplicationDbContext:
public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, Guid, IdentityUserClaim<Guid>,
ApplicationUserRole, IdentityUserLogin<Guid>,
IdentityRoleClaim<Guid>, IdentityUserToken<Guid>>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
...
}
The problem is of course NewGuid()
call here ADMIN_ID = Guid.NewGuid();
.
All keys (primary, alternate, foreign) used in Model seed data must be predefined values, that's why they are required even for normally auto generated columns. See Data Seeding section of the documentation, and especially Limitations of model seed data:
- The primary key value needs to be specified even if it's usually generated by the database. It will be used to detect data changes between migrations.
- Previously seeded data will be removed if the primary key is changed in any way.
And then
Therefore this feature is most useful for static data that's not expected to change outside of migrations and does not depend on anything else in the database, for example ZIP codes.
So the first thing you need to decide is whether your data (default user, role etc.) is appropriate for model data seeding (can these be modified/deleted by the application/end user). If the data is not static, then you shouldn't use model data seeding (HasData
) but some of the other methods mentioned there.
And if it is, then simply pregenerate the needed Guids (with some tool or with Guid.NewGuid()
), and then, since C# does not support Guid
constants, take their string representation and use Guid
constructor with string argument to populate them. e.g.
Guid ADMIN_ID = new Guid("22ffc532-008e-492d-92b1-e867501f2d54"); // from Guid.NewGuid().ToString() beforehand
Guid ROLE_ID = ADMIN_ID;
or use static readonly fields/properties outside the method in case you need them in other places
static Guid ADMIN_ID { get; } = new Guid("22ffc532-008e-492d-92b1-e867501f2d54");
static Guid ROLE_ID => ADMIN_ID;
I can't really say where the issue is without running some code but I can show you my implementation and maybe it will ring a bell. This is my version:
public class SessionDbContext : IdentityDbContext<MachineUserIdentity, IdentityRole<Guid>, Guid>
{
...
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Default users
modelBuilder.ApplyConfiguration(new IdentityUserModelConfig());
// Default roles
modelBuilder.ApplyConfiguration(new IdentityRoleModelConfig());
// User claims
modelBuilder.ApplyConfiguration(new IdentityUserClaimsModelConfig());
// Role claims
modelBuilder.ApplyConfiguration(new IdentityRoleClaimsModelConfig());
// User roles assignment
modelBuilder.ApplyConfiguration(new IdentityUserRoleModelConfig());
}
}
Foreach data table I've created a separate IEntityTypeConfiguration
but it shouldn't change anything.
Here are the implementations for Users, Roles and UserRoles:
IEntityTypeConfiguration<MachineUserIdentity>
with MachineUserIdentity
inheriting IdentityUser<Guid>
:
public void Configure(EntityTypeBuilder<MachineUserIdentity> builder)
{
// Seed default roles
builder.HasData(SeedHelpers.DefaultUsers);
}
IEntityTypeConfiguration<IdentityRole<Guid>>
:
public void Configure(EntityTypeBuilder<IdentityRole<Guid>> builder)
{
// Seed default roles
builder.HasData(SeedHelpers.GenerateDefaultRoles());
}
IEntityTypeConfiguration<IdentityUserRole<Guid>>
:
public void Configure(EntityTypeBuilder<IdentityUserRole<Guid>> builder)
{
// Seed default roles
builder.HasData(SeedHelpers.GenerateUserRolesAssignment());
}
Very similar to what you are doing. Finally, I have a seeding helper class where I define data like this:
internal const string GUID_USER_OPERATOR = "491a282c-3af1-4d45-b6a1-6014b8195744";
internal const string GUID_USER_SERVICE = "337acfd6-fde7-4d5c-9c0b-08fed843a3ed";
internal const string GUID_ROLE_OPERATOR = "d80a2a14-78a7-4e9d-b228-1cd259bd8cd3";
internal const string GUID_ROLE_SERVICE = "69605900-4fb1-4558-a9d4-6bdf3f184819";
// Password do not belong here, move them to a key vault
// This is just for demo purposes
internal const string ID_DEFAULT_ACCOUNT = "ADMIN";
internal const string ID_DEFAULT_PASSWORD = "ADMIN";
internal const string ID_SERVICE_ACCOUNT = "SERVICE";
internal const string ID_SERVICE_PASSWORD = "SERVICE1234";
internal static List<MachineUserIdentity> DefaultUsers = new()
{
new()
{
Id = Guid.Parse(GUID_USER_OPERATOR),
UserName = ID_DEFAULT_ACCOUNT,
NormalizedUserName = ID_DEFAULT_ACCOUNT,
Role = MachineRoleId.Operator.ToString(),
PasswordHash = hasher.HashPassword(null, ID_DEFAULT_PASSWORD),
},
new()
{
Id = Guid.Parse(GUID_USER_SERVICE),
UserName = ID_SERVICE_ACCOUNT,
NormalizedUserName = ID_SERVICE_ACCOUNT,
Role = MachineRoleId.Service.ToString(),
PasswordHash = hasher.HashPassword(null, ID_SERVICE_PASSWORD),
}
};
internal static IdentityRole<Guid>[] GenerateDefaultRoles()
{
return new[]
{
new(MachineRoleId.Operator.ToString())
{
Id = Guid.Parse(GUID_ROLE_OPERATOR),
Name = MachineRoleId.Operator.ToString(),
NormalizedName = MachineRoleId.Operator.ToString().ToUpper(),
},
new IdentityRole<Guid>
{
Id = Guid.Parse(GUID_ROLE_SERVICE),
Name = MachineRoleId.Service.ToString(),
NormalizedName = MachineRoleId.Service.ToString().ToUpper(),
}
};
}
internal static IdentityUserRole<Guid>[] GenerateUserRolesAssignment()
{
return new[]
{
new()
{
RoleId = Guid.Parse(GUID_ROLE_OPERATOR),
UserId = Guid.Parse(GUID_USER_OPERATOR),
},
new IdentityUserRole<Guid>
{
RoleId = Guid.Parse(GUID_ROLE_SERVICE),
UserId = Guid.Parse(GUID_USER_SERVICE),
}
};
}
I guess there isn't much difference except that I'm using hard-coded GUIDs (my own choice to have same identifiers on all target machines) and that I do not explicitly declare the key of the UserRole table like you.
Hope this helps ;)