Seimann's answer is good but I found working with migrations to be a pain. I wanted little or no manual work to get it working. I found the easiest way was to create a separate assembly for each provider and add an implementation of IDesignTimeDbContextFactory
.
Another solution is to create a design time assembly but selecting which provider to use for migrations turned out to be difficult, at least until this feature is implemented here. I tried the suggested method of setting an environment variable before executing the migrations but I found using compiler constants to select the correct provider to be easier.
I organized this by creating a shared project to be used by all providers. Here is an example implementation that pulls your main projects configuration settings. This class will support both methods explained above so it can be simplified depending on your needs.
#if DEBUG
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System;
using System.IO;
namespace Database.DesignTime
{
public class ApplicationDbContextDesignTimeFactory : IDesignTimeDbContextFactory<ApplicationDbContext>
{
public ApplicationDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Path.GetFullPath(@"..\MainProjectDirectory"))
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json")
.Build();
// Determine provider from environment variable or use compiler constants below
var databaseProvider = Environment.GetEnvironmentVariable("DatabaseProvider");
#if SQLSERVER
databaseProvider = "SqlServer";
#endif
#if POSTGRESQL
databaseProvider = "PostgreSql";
#endif
var connectionString = configuration.GetConnectionString($"{databaseProvider}Connection");
var contextBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
switch (databaseProvider)
{
#if SQLSERVER
case "SqlServer":
contextBuilder.UseSqlServer(connectionString, dbOptions =>
{
dbOptions.MigrationsAssembly("Database.SqlServer");
});
break;
#endif
#if POSTGRESQL
case "PostgreSql":
contextBuilder.UseNpgsql(connectionString, dbOptions =>
{
dbOptions.MigrationsAssembly("Database.PostgreSql");
});
break;
#endif
default:
throw new NotSupportedException(databaseProvider);
}
return new ApplicationDbContext(contextBuilder.Options);
}
}
}
#endif
Then in your database migration project add the compiler constant for each provider. For example:
Database.SqlServer.csproj
<DefineConstants>SQLSERVER</DefineConstants>
Database.PostgreSql.csproj
<DefineConstants>POSTGRESQL</DefineConstants>
When you want to add migrations from within VS, open the Package Manager Console and select the migration project as the Default project
. When executing the command, you need to specify the project containing the implementation of IDesignTimeDbContextFactory
you want to use.
Add-Migration Initial -StartupProject "Database.SqlServer"
Now you can switch back to your main project and use it as normal. Just for reference this is my relevant appsettings.json and startup code.
{
"DatabaseProvider": "SqlServer",
"ConnectionStrings": {
"SqlServerConnection": "Server=(localdb)\\mssqllocaldb;Database=DatabaseName;Trusted_Connection=True;MultipleActiveResultSets=true",
"PostgreSqlConnection": "Host=host;Database=DatabaseName;User ID=Test;Password=secrectPass"
}
services.AddDbContext<ApplicationDbContext>(options =>
{
switch (Configuration["DatabaseProvider"])
{
case "SqlServer":
options.UseSqlServer(Configuration.GetConnectionString("SqlServerConnection"), dbOptions =>
{
dbOptions.MigrationsAssembly("Database.SqlServer");
});
break;
case "PostgreSql":
options.UseNpgsql(Configuration.GetConnectionString("PostgreSqlConnection"), dbOptions =>
{
dbOptions.MigrationsAssembly("Database.PostgreSql");
});
break;
}
});
There is another suggested way to accomplish this as explained here but I found creating derived classes means the migration will only work for instances of the derived class and not the base class. So you would need to specify the derived class type in AddDbContext. The other method mentioned requires manual work which I want to avoid.