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:
Project Structure Example
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System.Collections.Generic;
namespace DesignTimeExample.Models
{
public class Plane
{
public int Id { get; set; }
public int SeatsNumber { get; set; }
public PlaneModel Model { get; set; }
public ICollection<Flight> Flights { get; set; }
}
public enum PlaneModel
{
AIRBUS_A380,
BOEING_707,
AIRBUS_A320,
BOEING_727,
BOEING_767,
BOEING_757,
BOEING_787,
BOEING_737,
BOEING_777,
BOEING_747
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
using System.ComponentModel.DataAnnotations;
namespace DesignTimeExample.Models
{
public class Flight
{
public int Id { get; set; }
public string Code { get; set; }
[Required]
public Plane Plane { get; set; }
public int PassengerNumber { get; set; }
}
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System.Reflection;
using DesignTimeExample.Models;
using Microsoft.EntityFrameworkCore;
namespace DesignTimeExample.Data
{
public class ApplicationDbContext : DbContext
{
public DbSet<Plane> Planes { get; set; }
public DbSet<Flight> Flights { get; set; }
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
}
}
}
Connection String
Now we will configure a connection string in our development environment project configuration file. Edit appsettings.Development.json
as it follows:
1
2
3
4
5
6
7
8
9
10
11
12
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=application_dev.db"
},
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}
We’ll pretend that this will be our development database, and the production will be in appsettings.json
, like this:
1
2
3
4
5
6
7
8
9
10
11
{
"ConnectionStrings": {
"DefaultConnection": "Data Source=application_prod.db"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
namespace DesignTimeExample.Data
{
public abstract class DesignTimeDbContextFactoryBase<TContext> :
IDesignTimeDbContextFactory<TContext> where TContext : DbContext
{
protected string ConnectionStringName { get; }
protected string MigrationsAssemblyName { get; }
public DesignTimeDbContextFactoryBase(string connectionStringName, string migrationsAssemblyName)
{
ConnectionStringName = connectionStringName;
MigrationsAssemblyName = migrationsAssemblyName;
}
public TContext CreateDbContext(string[] args)
{
return Create(
Directory.GetCurrentDirectory(),
Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"),
ConnectionStringName, MigrationsAssemblyName);
}
protected abstract TContext CreateNewInstance(
DbContextOptions<TContext> options);
public TContext CreateWithConnectionStringName(string connectionStringName, string migrationsAssemblyName)
{
var environmentName =
Environment.GetEnvironmentVariable(
"ASPNETCORE_ENVIRONMENT");
var basePath = AppContext.BaseDirectory;
return Create(basePath, environmentName, connectionStringName, migrationsAssemblyName);
}
private TContext Create(string basePath, string environmentName, string connectionStringName, string migrationsAssemblyName)
{
var builder = new ConfigurationBuilder()
.SetBasePath(basePath)
.AddJsonFile("appsettings.json")
.AddJsonFile($"appsettings.{environmentName}.json", true)
.AddEnvironmentVariables();
var config = builder.Build();
var connstr = config.GetConnectionString(connectionStringName);
Console.WriteLine($"Environment: {environmentName ?? "PRODUCTION"}");
if (string.IsNullOrWhiteSpace(connstr))
{
throw new InvalidOperationException(
$"Could not find a connection string named '{connectionStringName}'.");
}
else
{
return CreateWithConnectionString(connectionStringName, connstr, migrationsAssemblyName);
}
}
private TContext CreateWithConnectionString(string connectionStringName, string connectionString, string migrationsAssembly)
{
if (string.IsNullOrEmpty(connectionString))
throw new ArgumentException(
$"{nameof(connectionString)} is null or empty.",
nameof(connectionString));
var optionsBuilder =
new DbContextOptionsBuilder<TContext>();
Console.WriteLine(
"DesignTimeDbContextFactory.Create(string): Connection string: {0}",
connectionStringName);
optionsBuilder.UseSqlite(connectionString, db => db.MigrationsAssembly(migrationsAssembly));
DbContextOptions<TContext> options = optionsBuilder.Options;
return CreateNewInstance(options);
}
}
}
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
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System.Reflection;
using DesignTimeExample.Models;
using Microsoft.EntityFrameworkCore;
namespace DesignTimeExample.Data
{
public class ApplicationContextDesignFactory : DesignTimeDbContextFactoryBase<ApplicationDbContext>
{
public ApplicationContextDesignFactory() : base("DefaultConnection", typeof(Startup).GetTypeInfo().Assembly.GetName().Name)
{ }
protected override ApplicationDbContext CreateNewInstance(DbContextOptions<ApplicationDbContext> options)
{
return new ApplicationDbContext(options);
}
}
}
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:
1
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
1
echo $ASPNETCORE_ENVIRONMENT
Windows
1
echo %ASPNETCORE_ENVIRONMENT%
To set the variable content, use: Linux/OSx
1
export ASPNETCORE_ENVIRONMENT=Development
Windows
1
set ASPNETCORE_ENVIRONMENT=Development
Run the CLI Command with your environment set to development:
1
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:
1
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!