Re-thinking Running Migrations and Seeding in ASP.NET Core 2.0


SeedBack in ASP.NET 4, I really liked the way that it supported running migrations and seeding of the database for you. But in ASP.NET Core and EF Core, that hasn't come to the table yet.

I doubt it actually needs to happen because since ASP.NET Core gives you much more control over the life cycle of the web project. In Entity Framework Core, I've been using an approach to run migrations and seed the database that I kind of crufted together in the Betas. I don't think it's working.

Early Thoughts

Back when I realized that you couldn't just use the seeding and initialization from EF6 in Entity Framework Core, I looked at a few solutions and pick and choose what made the most sense to me. This is what I had come up with:

I created a class to initialize the database somewhat like this:

  public class StoreDbInitializer
  {
    private readonly StoreContext _ctx;

    public StoreDbInitializer(StoreContext ctx)
    {
      _ctx = ctx;
    }

    public void Seed()
    {
      // Run Migrations
      _ctx.Database.Migrate();

      if (!_ctx.Products.Any())
      {
        // Seeding the Database
        // ...
        
        _ctx.SaveChanges();
      }
    }
  }

Then I registered the initializer:

    public void ConfigureServices(IServiceCollection services)
    {
      services.AddScoped<StoreDbInitializer>();

      // ...

      services.AddMvc();
    }

And created it so I could seed the database:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
      // ...
      using (var scope = app.ApplicationServices.CreateScope())
      {
        var init = scope.ServiceProvider.GetService<StoreDbInitializer>();
        init.Seed();
      }
      
    }

All good, right? Not exactly.

Wrong Place

I'd been showing them this solution for quite a while now, but recently in a discussion of EF Tooling, one of the ASP.NET Core team presented a better solution that matches with ASP.NET Core 2.0 better:

https://github.com/aspnet/EntityFrameworkCore/issues/9033#issuecomment-317063370

Essentially the change is

  public class Program
  {
    public static void Main(string[] args)
    {
      BuildWebHost(args).Run();
    }

    public static IWebHost BuildWebHost(string[] args)
    {
      var host = WebHost.CreateDefaultBuilder(args)
          .UseStartup<Startup>()
          .ConfigureAppConfiguration(ConfigureConfig)
          .Build();

      using (var scope = host.Services.CreateScope())
      {
        var initializer = scope.ServiceProvider.GetService<StoreDbInitializer>();
        initializer.Seed();
      }

      return host;
    }
  }

The big change here is to move this to the Program.cs. This is allowing you to control when this happens (especially doing the database work after the host is created but before the server starts). I also find it clearer to see when the seeder is firing. But we have a little problem.

Entity Framework Core Tooling

Of course there is a wrinkle. If you're like me, you are using the EF Core tooling to create the database and generating migrations. But that's where things get weird.

When I start to use the tooling to create migrations or generate a database, it didn't work. Why? Because the initializer runs and attempts to query the database that might not exist (or have any schema). It throws an error and all work stops (sure I could put a try...catch in, but that's not elegant).

To solve this I decide to follow the advice of the error and implement the design time factory:

  public class StoreContextFactory : IDesignTimeDbContextFactory<StoreContext>
  {
    public StoreContext CreateDbContext(string[] args)
    {
      // Create a configuration 
      var builder = new WebHostBuilder();
      var config = new ConfigurationBuilder()
        .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile("config.json")
        .Build();

      return new StoreContext(new DbContextOptionsBuilder<StoreContext>().Options, config);
    }
  }

But when I ran it after this, still didn't work. In fact my factory was never reached. What is going on.

When the tooling runs, it loads your project and tries to setup the environment (except for starting the web server). But how does this happen?

** This may change as Entity Framework Core matures, but as of now this is how it works:

  1. It loads the Project
  2. It looks for a method on Program class called "BuildWebHost".
    1. If that method exists, it executes it to setup the environment then use the ServiceCollection to find DbContext derived classes.
    2. If it doesn't exist, it searches for a class that implements IDesignTimeDbContextFactory and calls it to get a DbContext class
  3. It then does the work with the DbContext.

Ah, it looks for the method called "BuildWebHost" before it searches for the design-time factory. I do wish they'd change this behavior, but the fix is to actually change the name of the method (the project generators use the BuildWebHost name by default). For example:

    public static IWebHost BuildHost(string[] args)
    {
      var host = WebHost.CreateDefaultBuilder(args)
          .UseStartup<Startup>()
          .ConfigureAppConfiguration(ConfigureConfig)
          .Build();
      // ...
    }

By changing it to BuildHost (or whatever you want to call it), it just works now. Sometimes knowing the internals is important. Luckily all the code is in GitHub so you can see what is happening (though I learned it via a GitHub issue).

What do you think?



Shawn
Shawn Wildermuth
Author, Teacher, and Coach



My Courses

pluralsight
Building a Web App with ASP.NET Core, MVC6, EF Core, Bootstrap and Angular (updated for 2.0)
Using Visual Studio Code for ASP.NET Core Projects (new)
Implementing and Securing an API with ASP.NET Core
Building a Web App with ASP.NET Core, MVC6, EF Core and AngularJS
Building a Web App with ASP.NET5, MVC6, EF7, and AngularJS (Retired)
Best Practices in ASP.NET: Entities, Validation, and View Models
Webstorm Fundamentals
Front-End Web Development Quick Start
Lessons from Real World .NET Code Reviews
Node.js for .NET Developers

Application Name WilderBlog Environment Name Production
Application Ver 2.0.0.0 Runtime Framework .NETCoreApp,Version=v2.0
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.26020.03
Operating System Microsoft Windows 10.0.14393 Runtime Arch X86