Seeding Identity 2.0 database

2019-03-15 17:29发布

问题:

I have an ASP.NET MVC 5 project (razor engine) which has Identity 2.0 with Individual User Accounts. I am using Visual Studio Professional 2013

I have not found any clear example (why doesn't it come out of the box?) of HOW I can seed the Identity 2.0 database and all examples I see are half backed because they don't say WHERE exactly you have to implement that.

I used enable-migrations which created a Migrations folder with a Configuration.cs file. It has a Seed method override but when I place a breakpoint it notice it is never executed, in fact the Identity database is not even populated with the schema either.

So where and what I have to do so that the Identity 2.0 schema is created on the database the first time (the connection string is correct and the empty database exists). And how do I rig up the Seeding?

On IdentityModels.cs I have this: public class ApplicationDbContext : IdentityDbContext { ublic ApplicationDbContext(): base("DefaultConnection", throwIfV1Schema: false) { }

    public static ApplicationDbContext Create() {
         return new ApplicationDbContext();
    }

    protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) {
          base.OnModelCreating(modelBuilder);
          // to avoid the "has no keys" errors when running Update-Database on PM
          modelBuilder.Entity<IdentityRole>().HasKey<string>(r => r.Id).ToTable("AspNetRoles");
          modelBuilder.Entity<IdentityUser>().ToTable("AspNetUsers");
          modelBuilder.Entity<IdentityUserLogin>().HasKey(l => new { l.UserId, l.LoginProvider, l.ProviderKey }).ToTable("AspNetUserLogins");
          modelBuilder.Entity<IdentityUserRole>().HasKey(r => new { r.RoleId, r.UserId }).ToTable("AspNetUserRoles");
          modelBuilder.Entity<IdentityUserClaim>().ToTable("AspNetUserClaims");
     }
}    

In Migrations/Configuration.cs (added by PM>Enable-Migrations) I have this:

internal sealed class Configuration : DbMigrationsConfiguration<Models.ApplicationDbContext> {
    public Configuration() {
        AutomaticMigrationsEnabled = false;
    }

    protected override void Seed(Models.ApplicationDbContext context) {
         WriteReferenceData();
    }
}

In my Global.asax.cs file on the Application_Start() method I added this:

System.Data.Entity.Database.SetInitializer<Models.ApplicationDbContext>(new System.Data.Entity.MigrateDatabaseToLatestVersion<Models.ApplicationDbContext, Migrations.Configuration>());

And in IdentityConfig.cs I have this DB Initializer as well though it seems to be orphan because I don't know where to plug this in:

public class ApplicationDbInitializer : System.Data.Entity.DropCreateDatabaseIfModelChanges<Models.ApplicationDbContext> {
    protected override void Seed(ApplicationDbContext context) {
        WriteReferenceData();
        base.Seed(context);
    }
}

And finally the WriteReferenceData method is in some other class that does this more or less:

System.Data.Entity.DbContextTransaction transaction = null;
try {
    System.Data.Entity.DbContext ctx = Models.ApplicationDbContext.Create();
    transaction = ctx.Database.BeginTransaction();
    CreateRoles(ctx);
    CreateUsers(ctx);
    CreateRoleAssociations(ctx);
    ctx.SaveChanges();
    transaction.Commit();
    succeeded = true;
}
catch (Exception ex) {
    if (transaction != null { transaction.Rollback(); transaction.Dispose(); }
    succeeed = false;
}
return succeeded;

回答1:

EF has two different Seed methods. One that is used with database initializers and another that is used with Migrations. Since you've enabled Migrations, I'll describe how to do this with the Migrations Seed method here...

First of all, even though you've enabled Migrations, by default EF still uses the CreateDatabaseIfNotExists database initializer. Which means when you run your app, the first time the ApplicationDbContext is accessed, the initializer is called and it'll create the database tables from your Code First mappings if the tables don't exist already. You're not seeing the schema created because you probably haven't accessed the db context. In a new web app, this is typically triggered the first time you register a user or try to log in.

To seed the ASP.NET Identity tables, there are two things you need to do. The first is to add seed logic to the Seed method in Configuration.cs. The second is to trigger update-database... either by running it in the Package Manager Console or by using the MigrateDatabaseToLatestVersion database initializer.

Here's an example of what you can put into the Seed method to create a role and a user...

public Configuration()
{
    AutomaticMigrationsEnabled = true;

    // ...
}

protected override void Seed(MyProject.Web.Models.ApplicationDbContext context)
{
    if (!context.Roles.Any())
    {
        var roleStore = new RoleStore<IdentityRole>(context);
        var roleManager = new RoleManager<IdentityRole>(roleStore);
        var role = new IdentityRole{
            Name = "Administrator"
        };
        roleManager.Create(role);
    }

    if (!context.Users.Any())
    {
        var userStore = new UserStore<ApplicationUser>(context);
        var userManager = new ApplicationUserManager(userStore);

        var user = new ApplicationUser {
            Email = "foo@bar.com",
            UserName = "SuperUser"
        };
        userManager.Create(user, "MySecretPassword1234");
        userManager.AddToRole(user.Id, "Administrator");
    }
}

After this is done, you can run Update-Database from the Package Manager Console to migrate the database to the latest schema and run the Seed method.

Or you can change EF to use the MigrateDatabaseToLatestVersion initializer by modifying the context in IdentityModels.cs...

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
        Database.SetInitializer<ApplicationDbContext>(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());
    }
}

Now when you run the application, the first time the database context is used it'll run update-database and seed it.



回答2:

I think I can solve the riddle as to WHY the Seed is never triggered: because the Seed is called only when the application is trying to connect to the database, and NOT when the application starts.

I have created examples and I have used your code successfully with and without migrations. But since you want to use it with migrations enabled, below is the sample code.

VERY IMPORTANT: To actually see the breakpoint inside Seed, run the application, press Login and use the credentials from the seed function to get access to the application. In case you get "Invalid username or password", watch out for the manager.PasswordValidator property in IdentityConfig.cs :: Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context).

  1. Create a new ASP.NET MVC 5 project in VS2013

  2. Update all packages using Update-Package command.

  3. Enable-Migrations

  4. In the Configuration.cs created by migrations add the following code:

    internal sealed class Configuration : DbMigrationsConfiguration
        {
            public Configuration()
            {
                AutomaticMigrationsEnabled = false;
            }

    protected override void Seed(ApplicationDbContext context) { bool itWorks = WriteReferenceData(context); base.Seed(context); } private bool WriteReferenceData(ApplicationDbContext ctx) { DbContextTransaction transaction = null; bool succeeded = false; try { transaction = ctx.Database.BeginTransaction(); CreateRoles(ctx); CreateUsers(ctx); ctx.SaveChanges(); transaction.Commit(); succeeded = true; } catch (Exception ex) { if (transaction != null) { transaction.Rollback(); transaction.Dispose(); } succeeded = false; } return succeeded; } private void CreateRoles(ApplicationDbContext ctx) { // Out of the box // ctx.Roles.AddOrUpdate( // new IdentityRole { Name = "Administrator" }, // new IdentityRole { Name = "Guest" } // ); // Another approach var RoleManager = new RoleManager<IdentityRole>(new RoleStore<IdentityRole>(ctx)); var roleName = "Administrator"; //Create role if it does not exist if (!RoleManager.RoleExists(roleName)) { var roleresult = RoleManager.Create(new IdentityRole(roleName)); } } private void CreateUsers(ApplicationDbContext ctx) { // Out of the box approach // ctx.Users.AddOrUpdate( // new ApplicationUser { Email = "foo@xyz.com", UserName = "foo@xyz.com" } // ); // Another approach var UserManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(ctx)); var user = new ApplicationUser() { UserName = "foo@xyz.com", Email="foo@xyz.com"}; var password = "Wh@tever777"; var adminresult = UserManager.Create(user, password); //Add User Admin to Role Administrator if (adminresult.Succeeded) { var result = UserManager.AddToRole(user.Id, "Administrator"); } } }
  5. In Global.asax.cs :: Application_Start(), add the following line:

    Database.SetInitializer<ApplicationDbContext>(new MigrateDatabaseToLatestVersion<ApplicationDbContext, Configuration>());

  6. Run!

If you also need a sample of code with migrations disabled, let me know.



回答3:

So we do something similar in our samples package in the following way (To seed the db with our admin user)

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
    }

    static ApplicationDbContext()
    {
        // Set the database intializer which is run once during application start
        // This seeds the database with admin user credentials and admin role
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }

    public static ApplicationDbContext Create()
    {
        return new ApplicationDbContext();
    }
}

// This is useful if you do not want to tear down the database each time you run the application.
// public class ApplicationDbInitializer : DropCreateDatabaseAlways<ApplicationDbContext>
// This example shows you how to create a new database if the Model changes
public class ApplicationDbInitializer : DropCreateDatabaseIfModelChanges<ApplicationDbContext> 
{
    protected override void Seed(ApplicationDbContext context) {
        DoYourSeedingHere(context);
        base.Seed(context);
    }

}


回答4:

To complete this question.. As Corneliu Serediuc said all you need to do is simply try to connect to database during application startup, for example, like this (IdentityModel.cs):

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection", throwIfV1Schema: false)
    {
        Database.SetInitializer<ApplicationDbContext>(new ApplicationDbInitializer());
    }


    public static ApplicationDbContext Create()
    {
        var ctx = new ApplicationDbContext();
        var runSeed = ctx.Roles.AnyAsync();

        return ctx;
    }

} By the way, Corneliu thank you for your code examples, they help me a lot.



回答5:

If you want to bulk insert new accounts with a CSV, you can use this project: https://github.com/Stonefinch/AspNetUserMaintenanceAzureSiteExtension

That can also be installed as an Azure Site Extension: https://www.siteextensions.net/packages/AspNetUserMaintenanceAzureSiteExtension/