[12/19/2017 -- This has changed in EF Core 2.0. Here's the updated details.]
In my last post, I showed you how to set up an ASP.NET Core and Entity Framework Core solution so that you can use EF Core Migrations for database updates. That code sample used a hard-coded database connection string in the OnConfiguring() method of my DbContext class...and that stinks. It's terrible for deployment and maintenance and it limits your flexibility and it's just not a good idea (Circle slash hard-coded connection strings.)
[TL;DR -- here's the sample code]
[caption id="attachment_9336" align="alignnone" width="891"] Don't hard-code your DbContext connection strings![/caption]
The problem is that if you get rid of the OnConfiguring() method, you'll get an error when you try to run your EF Core database migration. Try it. Remove that method, and run "dotnet ef database update". Boom!
No parameterless constructor was found on 'MyDbContext'. Either add a parameterless constructor to 'MyDbContext' or add an implementation of 'IDbContextFactory
What is IDbContextFactory?
Ok. Well, you can't remove the constructor and still have this work with ASP.NET Core. So what's this IDbContextFactory thing?
IDbContextFactory
In my implementation of IDbContextFactory
var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json", true)
.AddEnvironmentVariables();
var config = builder.Build();
var connstr = config.GetConnectionString("(default)");
To implement this in my Benday.EfCore.Api project, I added a class called MyDbContextFactory that implemented IDbContextFactory
[caption id="attachment_9337" align="alignnone" width="1072"] Move the database connection string to appsettings.json.[/caption]
Trouble with "dotnet ef", appsettings.json & Unit Tests
I ran into issues with "dotnet ef" not being able to find the appsettings.json file. It turns out that when you run your dotnet ef commands against this project from the command line, it sets the working directory to be the ./bin/debug/netappcore1.* directory and attempts to find the appsettings.json in that directory.
Ok. Not a problem. I'll just mark appsettings.json so that it gets copied to the OutputDir. I recompiled and re-ran my dotnet ef database migration update commands and it worked.
[caption id="attachment_9339" align="alignnone" width="524"] To get appsettings.json into your bin\Debug directory, set Copy to Output Directory to Copy always[/caption]
Fast-forward to a few hours later and I'm trying to write a unit test (technically an integration test) that runs against MyDbContextFactory and MyDbContext. My unit test project *did not* have an appsettings.json in the folder and the unit tests were still passing. That might sound like a good thing but at this point in development, these tests were supposed to be failing. (Remember, when writing unit tests, always start with a failing test.)
The "Copy always" on the appsettings.json in the Benday.EfCore.Api project was causing that appsettings.json to get copied into the run directory for the unit tests. That was ok for right now but this was going to cause problems because my Benday.EfCore.Tests project would forever be fighting the Benday.EfCore.Api project for configuration supremacy. Not good.
Clearly the "Copy always" on the API project couldn't stay. I needed to have separate configs for the API, Web, and Test projects.
The Solution: A Smarter IDbContextFactory
The essence of the problem is that IDbContextFactory was going to be called from multiple environments with multiple configurations and my basic implementation didn't know how to handle that. (NOTE: by default, your ASP.NET Core project isn't going to use this IDbContextFactory and that's fine.)
I ended up with two public methods on my IDbContextFactory implementation.
[caption id="attachment_9341" align="alignnone" width="855"] A smarter implementation of IDbContextFactory[/caption]
Create() would let me easily create instances of MyDbContext by reading from appsettings.json files in the same directory as the DLLs. This is helpful for unit tests.
public MyDbContext Create()
{
var environmentName =
Environment.GetEnvironmentVariable(
"Hosting:Environment");
var basePath = AppContext.BaseDirectory;
return Create(basePath, environmentName);
}
Create(DbContextFactoryOptions options) is the method that gets called by the "dotnet ef database update" command when you deploy your migrations. It assumes that your appsettings.json file will be near the code and the csproj file. The important thing here is that this allows me to AVOID marking appsettings.json as Copy Always in the Benday.EfCore.Api project. This is the key item that keeps this API project from messing up the runtime appsettings.json in all the other projects.
public MyDbContext Create(DbContextFactoryOptions options)
{
return Create(
options.ContentRootPath,
options.EnvironmentName);
}
The two private methods understand 1) how to create instances of ConfigurationBuilder() with the correct references to appsettings.json and environment variables and 2) how to grab a connection string and create an instance of your DbContext class.
private MyDbContext Create(string basePath, string environmentName)
{
var builder = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json", true)
.AddEnvironmentVariables();
var config = builder.Build();
var connstr = config.GetConnectionString("(default)");
if (String.IsNullOrWhiteSpace(connstr) == true)
{
throw new InvalidOperationException(
"Could not find a connection string named '(default)'.");
}
else
{
return Create(connstr);
}
}
private MyDbContext Create(string connectionString)
{
if (string.IsNullOrEmpty(connectionString))
throw new ArgumentException(
$"{nameof(connectionString)} is null or empty.",
nameof(connectionString));
var optionsBuilder =
new DbContextOptionsBuilder<MyDbContext>();
optionsBuilder.UseSqlServer(connectionString);
return new MyDbContext(optionsBuilder.Options);
}
Summary
Adding a class to your project that implements IDbContextFactory
Click here to download the source code.
-Ben
-- Need help wrapping your head around ASP.NET Core and EF Core? Scrum process problems got you down? Not sure what this "DevOps" thing is? We can help. Drop us a line at info@benday.com.