Thanks for visiting my blog!
Back 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();
}
}
}
```csharp
Then I registered the initializer:
```csharp
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);
}
}
```csharp
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:
```csharp
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?