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

enter image description here

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