In this post, I will demonstrate strategies for database initialisation during early and late development stages. The focus is on productively and effectively leveraging EF Core and its built-in capabilities for keeping your data model and database schema in sync.
Automatic Initialisation
First, the application should be configured to automatically initialise the database on startup.
Create DbInitialiser.cs with the following code:
public class DbInitialiser
{
private readonly ApplicationDbContext _context;
public DbInitialiser(ApplicationDbContext context)
{
_context = context;
}
public void Run()
{
// TODO: Add initialisation logic.
}
}
Add the new initialiser to dependency injection. Update Program.cs with the following code:
builder.Services.AddTransient<DbInitialiser>();
Automatically run the initiliser on application startup. Update Program.cs with the following code:
using var scope = app.Services.CreateScope();
var services = scope.ServiceProvider;
var initialiser = services.GetRequiredService<DbInitialiser>();
initialiser.Run();
Early Development
During early development, the data model will change frequently. Entities and properties will be created, updated, and removed at a staggering pace. A lightweight strategy is to recreate the database on application startup, and optionally, seed the database with sample data.
In Data/DbInitialiser.cs, update the Run
method with the following code:
public void Run()
{
_context.Database.EnsureDeleted();
_context.Database.EnsureCreated();
// TODO: Seed with sample data
}
The preceding code results in the following database initialisation:
EnsureDeleted
will drop the database if it exists, andEnsureCreated
will create the database and initialise the database schema, and- The database will be seeded with sample data.
Seeding the database with sample data is important as existing data will not be preserved when the database is deleted. This strategy requires minimal effort to maintain data model changes and includes the benefit of ensuring the database will be in a clean state after application startup.
Late Development
Once initial development has been completed the data model will change less frequently. During this stage, a good strategy for database initialisation is to use EF Core migrations to update the database schema on application startup, and optionally, seed the database with sample data.
In Data/DbInitialiser.cs, update the Run
method with the following code:
public void Run()
{
_context.Database.Migrate();
// TODO: Seed with sample data
}
The preceding code results in the following database initialisation:
Migrate
will create the database if it doesn’t already exist and apply any pending migrations, and- The database will be seeded with sample data.
Note that EnsureDeleted
and EnsureCreated
have been removed. Databases created with EnsureCreated
are not supported by EF Core migrations.
Since this strategy upgrades, rather than recreating the database, all existing data will be preserved. This strategy will require some effort to maintain data model changes as you will be adding, updating, and (in some cases) removing migrations. You may also need to merge your data model changes with those from another developer.
Deployment
The previous strategy initialises the database on application startup while preserving existing data. This is a productive and simple strategy for local development environments. However, it is not appropriate for other environments such as staging and production. For these environments database initialisation should occur during deployment.
Using the EF Core CLI tools there are numerous approaches, here are two popular choices:
- Generate idempotent SQL Scripts on build, and then execute those scripts during deployment:
dotnet ef migrations script --idempotent --output migrations.sql
- (Recommended) Generate a migration bundle, and then execute the bundle on deployment:
dotnet ef migrations bundle
The simplest approach is migration bundles (requires EF Core 6). However, both will create the database if it doesn’t exist and then apply any pending migrations.
Recommended Resources
For additional information, review the following resources:
- Create and Drop APIs | Learn lightweight strategies for managing your database schema
- Migrations Overview | Learn how to use migrations to manage your database schema
- Applying Migrations | Learn numerous strategies for generating and applying migratrations
Hi Jason, I love this! Thanks for sharing.
I was wondering whether you think even for early development, there is value in wrapping the EnsureCreated/Deleted calls in a check for a flag? You could put something in appsettings.Development.json, or even secrets. Or make it dependent on a debug or development environment check.
Thanks, Matt. You could certainly take that approach and this means that, in other environments, the database will not be recreated and sample data will still be seeded.
Another approach would be to create a separate initialiser per environment. For example, you could create a
StagingDbInitialiser
for whenEnvironment.IsStaging()
. This would allow you to use an appropriate strategy for each environment.