EFCore: Implementing a multi-environment DesignTimeDbContextFactory
Intro
For those who already worked with Entity Framework Core using Code-First approach, knows that Migrations was a really good way to “version” the database structure of your project or service. Using CLI commands like: “dotnet ef migrations add application_v1” or “dotnet ef database update” , we can reflect the changes created from the class models to database, and deploy automatically through development, tests and production environments. However, when executing these commands in different environments, Entity Framework does not know in which database to connect to perform the updates unless you tell it explicitly. To solve this, the IDesignTimeDbContextFactory<YourDbContext> was created, and through it, it’s possible to configure it to search for different connection strings of your environments.
In this guide I’ll show an effective implementation of a generic DesignTimeDbContextFactory that supports development and production environments, and reads connection strings using the files appsettings.Development.json and appsettings.json. To simplify, I’ll use EF Core with SQLite, the whole code is available in this github repository.
Creating the project
Create a WebApi project named DesignTimeExample in the IDE/editor of your choice. I recommend you to use the following project structure:
Add a directory at project's root named: Data. We'll store in this folder all classes responsible for data access such as DbContext and our implementation of DesignTimeDbContextFactory. Also create a directory inside Data named: Migrations. This one will hold all snapshots of our database state. It's important to notice that despite these files are auto-generated, they define the state of the application, so it is necessary to version them.
Also add a folder named: Models. It will keep all classes related to business model or POCOs, in this example I'll use a really simple model of Plane and Flight.
Creating the Models
The following classes will be used as models to interact with the database tables:
Data Access
In this example we won't configure the names of the output tables for the sake of simplicity, so all tables will be named after the classes by default.
DbContext
To interact with the database we'll have to create a DbContext for the application, so create a file named: ApplicationDbContext.cs inside Data directory such as:
Connection String
Now we will configure a connection string in our development environment project configuration file. Edit appsettings.Development.json as it follows:
We'll pretend that this will be our development database, and the production will be in appsettings.json, like this:
Naturally in real scenario, the connection strings will point to different machines, so development and production are in separated infrastructures. But, in order to pretend this separation, the databases files generated for SQLite will be named: application_dev.db for development and application_prod.db for production.
DesignTimeDbContextFactory
To correctly identify the different connection strings when running EFCore's CLI commands, we'll need to implement IDesignTimeDbContextFactory interface. To do so, create a file named: DesignTimeDbContextFactory.cs in Data directory as it follows:
Explaining a bit of the code, we receive as parameters in constructor: connectionStringName and migrationAssemblyName. We also read the ASPNETCORE_ENVIRONMENT system's environment variable, which is the standard .NET Core variable for us to check in which environment we are in. We'll assume that if it contains with anything other than "Development", our implementation will assume the current environment is production. The files appsettings.Development.json and appsettings.json are added so it can read the input parameters and generate the migrations.
With this done, we'll only need to create a concrete implementation of this Factory, using the DbContext above. To do so, add the following class in ApplicationDbContext.cs:
Basically we only extended our DesignTimeDbContextFactory and passed the connection string name that is in the configuration files.
Creating and updating the migrations
With all this done, we can already test the migrations creation. Execute the following CLI command to create the initial version of our database structure:
dotnet ef migrations add application_v1 -o Data/Migrations
With the initial migration created, let's execute it in development environment. But before, check the content of the ASPNETCORE_ENVIRONMENT of your machine using:
Linux/OSx
echo $ASPNETCORE_ENVIRONMENT
Windows
echo %ASPNETCORE_ENVIRONMENT%
To set the variable content, use:
Linux/OSx
export ASPNETCORE_ENVIRONMENT=Development
Windows
set ASPNETCORE_ENVIRONMENT=Development
Run the CLI Command with your environment set to development:
dotnet ef database update
Notice that the file application_dev.db was generated at the root directory of your project. After that, set your environment to production and run again the command:
dotnet ef database update
Now the file application_prod.db was generated:
Conclusion
With this implementation it's easy to segregate environments in Design time (CLI commands executions), and it helps us to gain more control over the commands that are being executed by us or by a CI/CD software if you are running DevOps model.
See you soon!
Originally published at https://blog.danielpadua.dev on April 7, 2019.