Recently I've been playing around with Entity Framework Core, and it's been a great positive experience overall, however, as I started to port one of my projects over, I fell foul of the lack of Seeding support.
For those that haven't used the Seed functionality in EF 6, it's basically a method to populate the database with data that is invoked when migrations finish. There is an open GitHub issue regarding seeding support in EF Core.
My requirements are straightforward I'd like to use the dotnet CLI to run environment-specific Migrations and Seeding from my continuous deployment pipeline.
In this post, I'm going to show you how I got environment-specific Migrations and Seeding working. There are a couple of things that work differently that we need to work around, but that's fine.
The first problem to solve is the lack of seeding support, this is easily solved and not an issue at all. You can use an empty migration and seed the data in the migration, this is a great because the seed is run once if I need to update the data I create a new migration. We also benefit from being able to roll back any seed migrations.
The second problem I came across is that targeting class library projects for the update-database command is not supported. No problem I created a console app and problem solved.
The third problem to solve is how to get the environment switch from ef database update. This is what took a little bit of work - it turns out that currently EF Core tools are geared around a ASP.NET project hence not supporting targeting class library projects, running migrations on web start isn't something I wanted to do for my production apps.
Reviewing the EF Core environment feature commit (which is one of the benefits of open source) confirmed that EF tools are looking for a Startup class and the switch is set via the injected IHostingEnvironment.
This is now a problem because DI isn't supported anymore for console apps.My first attempt was to pull the EnvironmentName directly from the environment variable turns out this won't work as it is only set for the current cmd session. It was feeling like I was hitting friction, so I changed a direction and tried using WebHostBuilder instead, and bingo it worked. It turns out that you can have a Startup class and call Build() but you don't have to call Run() so you don't end up running Kestrel by mistake.
Below is the working solution with some commentary:
Here I'm just bootstrapping our Startup class but notice I'm not calling Run().
// Program.cs
using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
namespace DalSoft.Data
{
public class Program
{
public static void Main(string[] args)
{
Console.Write("This is the workaround for:" +
"Could not invoke this command on the startup project. " +
"This preview of Entity Framework tools does not support commands on class library projects in ASP.NET Core and " +
".NET Core applications. See http://go.microsoft.com/fwlink/?LinkId=798221 for details and workarounds.");
new WebHostBuilder()
.UseKestrel()
.UseContentRoot(Directory.GetCurrentDirectory())
.UseStartup<Startup>()
.Build();
}
}
}
In the Startup ctor I take IHostingEnvironment and set it to a private variable, then when ConfigureServices is called, I set an Environment Variable for future use. Note that if "Development" is passed as the environment we default to "local", this is because "Development" is the default and I wanted to avoid using the default in CD.
// Startup.cs
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace DalSoft.Data
{
/// <summary>EF core mirgations at the moment expects a Web startup and if we don't bootstrap it like this the -environmentName switch never gets populated.
/// I had to read the source to see why it wasn't working https://github.com/aspnet/EntityFramework/commit/7f48d0c0fca054ed70bebe0e8d2c58ee3cc3df9b</summary>
public class Startup
{
private readonly IConfigurationRoot _configuration;
private readonly IHostingEnvironment _hostingEnvironment;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: false, reloadOnChange: false);
builder.AddEnvironmentVariables();
_configuration = builder.Build();
_hostingEnvironment = env;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DalSoftDbContext>(options =>
{
Environment.SetEnvironmentVariable("EnvironmentName", _hostingEnvironment.EnvironmentName.ToLower()== "development" ? "local" : _hostingEnvironment.EnvironmentName);
options.UseSqlServer(_configuration.GetConnectionString("DalSoftDbContext"));
});
}
public void Configure(IApplicationBuilder app)
{
}
}
}
DbContext changes are quite simple if a database connectionstring is passed via OnConfiguring then we return. Otherwise, if no connectionstring is passed we set up the config using the environment variable we set in startup, it's this code that will be called during a migration.
// DalSoftDbContext.cs
using System;
using System.IO;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Configuration;
namespace DalSoft.Data
{
public class DalSoftDbContext : DbContext
{
public DalSoftDbContext(DbContextOptions options) : base(options) { }
public DalSoftDbContext() /* Required for migrations */{ }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (optionsBuilder.IsConfigured) return;
//Called by parameterless ctor Usually Migrations
var environmentName = Environment.GetEnvironmentVariable("EnvironmentName") ?? "local";
optionsBuilder.UseSqlServer(
new ConfigurationBuilder()
.SetBasePath(Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location))
.AddJsonFile($"appsettings.{environmentName}.json", optional: false, reloadOnChange: false)
.Build()
.GetConnectionString("DalSoftDbContext")
);
}
}
}
Here is an example migration notice that DalSoftDbContext parameterless ctor is called allowing OnConfiguring (as described above) to use the environment config, which was set using the environment variable.
// ExampleSeedMigration.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.EntityFrameworkCore.Migrations;
namespace DalSoft.Data.Migrations
{
/// <summary>Workaound for EF core not having Seed support https://github.com/aspnet/EntityFramework/issues/629 </summary>
public partial class ExampleSeedMigration : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
using (var db = new DalSoftDbContext())
{
db.Apps.AddRange(new MyEntities[] {...});
db.SaveChanges();
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
using (var db = new DalSoftDbContext())
{
db.RemoveRange(db.MyEntities)
db.SaveChanges();
}
}
}
}
Here is my project.json for reference, but nothing special is going on here.
{
"version": "1.0.0-*",
"buildOptions": {
"emitEntryPoint": true
},
"dependencies": {
"Microsoft.NETCore.App": {
"type": "platform",
"version": "1.0.1"
},
"Microsoft.Extensions.DependencyInjection": "1.0.0-*",
"Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0",
"Microsoft.Extensions.PlatformAbstractions": "1.0.0",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
"Microsoft.Extensions.Configuration.FileExtensions": "1.0.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.Extensions.Options.ConfigurationExtensions": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.1",
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final",
"Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
"Microsoft.AspNetCore.Server.Kestrel": "1.0.0"
},
"tools": {
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
},
"runtimes": {
"win10-x64": {}
},
"frameworks": {
"netcoreapp1.0": {
"imports": [
"dotnet5.6",
"portable-net45+win8"
]
}
}
}
Now all my migrations using the dotnet CLI use the correct appsettings for example for appsettings.production.json:
dotnet ef --project ../DalSoft.Data --startup-project ../DalSoft.Data database update --environment production