Azure DevOps, Scrum, & .NET Software Leadership and Consulting Services

Free course! Predicting the Future, Estimating, and Running Your Projects with Flow Metrics

EF Core Migrations without Hard-coding a Connection String using IDbContextFactory


[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]

Don’t hard-code your DbContext connection strings!

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<MyDbContext>’ in the same assembly as ‘MyDbContext’.

What is IDbContextFactory<T>?

Ok.  Well, you can’t remove the constructor and still have this work with ASP.NET Core.  So what’s this IDbContextFactory thing?

IDbContextFactory<T> allows you to put the logic of creating instances of your DbContext into a type-safe class that follows a pattern that is known and usable by your code and the tooling of “dotnet ef”, Visual Studio, and Visual Studio Code.  It gives you a nice, clean way to separate the persistence details of your DbContext from the construction and configuration details for your DbContext.  This pattern fits nicely with the SOLID principles and Separation of Concerns and keeps your code organized.

In my implementation of IDbContextFactory<T>, I wanted to be able to read my connection strings from appsettings.json and from environment variables.  This would mirror the same kind of configuration and initialization that happens in ASP.NET Core’s Startup.cs.

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<MyDbContext> and then I added an appsettings.json to the project and moved the connection string out of MyDbContext and into appsettings.json.

Move the database connection string to appsettings.json.

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.

To get appsettings.json into your bin\Debug directory, set Copy to Output Directory to Copy always

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.

A smarter implementation of IDbContextFactory

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<T> helps you to handle the initialization details of your DbContext class but be sure to keep in mind that it will be called from various environments.

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

SUBSCRIBE TO THE BLOG


7 responses to “EF Core Migrations without Hard-coding a Connection String using IDbContextFactory

  1. […] this year, I blogged about how to do EF Core migrations without hard-coding your connection string.  The short answer was to use IDbContextFactory.  Well, that was Entity Framework Core 1.0 and […]

  2. Leo Avatar
    Leo

    Hi Ben,

    Thanks for the article.
    I wonder if you have an updated version of it since the launch of Net Core 2.1 where IDbContextFactory is marked as obsolete with suggestions to use IDesignTimeDbContextFactory instead.

    Your comments about the changes are much appreciated,

    Thanks
    Leo

  3. Leo Avatar
    Leo

    Sorry, I should have mentioned, based on the “rename” announcement: https://docs.microsoft.com/en-us/ef/core/miscellaneous/1x-2x-upgrade

    Also, in terms of using the Configuration, I’m thinking not only about not only the Environment and not hardcoding the connection string, but also the fact that the built-in DI will do a good job in managing that Configuration instance, thus I’m looking into using it inside the IDesingTimeDbContextFactory in the same way. I’ve put a sample on Github that shows what I mean:

    https://github.com/ljcorreia/.NetConfigurationModel/tree/master/DetectingConfigChanges/NetCore.DetectConfigChange/NetCoreDetectConfigChangesWithMonitor

    Thanks,
    Leo

    1. Ben Day Avatar

      Hey Leo –

      I wrote a sample app that I haven’t published yet on my blog that has an implementation of IDesignTimeDbContexetFactory in it.

      https://github.com/benday/asp-mvc-invoice-sample

      Lemme know if this is what you’re looking for.

      -Ben

  4. Leo Avatar
    Leo

    Hey Ben,

    Thanks a lot for the quick reply. I’ll have a look in detail and see what you’ve got there.

    Leo

  5. Leo Avatar
    Leo

    Hey Ben,

    Ok, I see… it may be my confusion around the way a DbContext is used and the “Design” side of things.

    I am not sure how that Design class is hooked behind the scenes. What I meant in my first comment and I was wondering if the Configuration part of it and including Environment could be coming from a unique place leaving the DI to resolve that dependency. Something like this:

    https://github.com/ljcorreia/asp-mvc-invoice-sample/blob/master/src/Benday.InvoiceApp.Api/NewInvoiceDesignTimeDbContextFactory.cs

    So, I put the IConfiguration in the ctor of that class. But as I mentioned, I don’t know how the CreateDbContext(string[] args) and the purpose of args there as I would need to understand it better how it works when it is called from the Update-Database, etc.

    What I was really after is the understanding around the DbContext, and I’m sorry if this is all too basic questions. I am not too sure if a Factory would apply for that context as I can see in your example that it gets resolved by the DI Container when you use the Repository. Is that right?

    Thanks
    Leo

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.