Separating EF Core Migrations in to an independent project

Joe Blogs

Purpose

The purpose of this project is to show how you can separate Entity Framework migrations out in to an independent project. This is useful for many reasons, including running migrations in a separate project when automating releases which will be shown in a future blog post. The example code for this solution can be found here.

Process

First start by creating a class library project that will contain the classes that represented the domain entities called EFCoreSeparateProject.Core, an example can be found in the src folder – This will have no references to Entity Framework. Inside this create a Domain folder to house an abstract class called Entity with an Id property that our other entities inherit from, this could be a generic to allow us to set the Id type, this is outside the scope of this blog. Next create a Person class that inherits from Entity, and added a few other classes to represent out value objects for Address Lines and Postcodes. This helps to encapsulate business/domain rules inside the relevant classes, moving from POCO/anaemic objects, to those that contain behaviour.

Next, create the project EFCoreSeparateProject.Infrastructure to hold all infrastructure related code, in this case the ExampleDbContext.cs class which inherits from DbContext. Install the Microsoft.EntityFrameworkCore nuget package to resolve the DbContext references.

Once the references have been added, the next thing step is to add a constructor to the ExampleDbContext, this will take in the options that we want to configure our Entity Framework DbContext to use, including configuring in to use SQL Server, and the connection string  to connect with. This will later be used by our IDesignTimeDbContextFactory<T> to generate migrations:

public ExampleDbContext(DbContextOptions options) : base(options)
{
}

Now we need to add our DbSets. In this psrticular example specify the below:

public ExampleDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Person> Persons { get; set; }

The next step is to configure our models. Entity Framework provides a method that we can override in our implementation of DbContext – override void OnModelCreating(ModelBuilder builder). We could add all configuration directly inside this method, but with projects that have a large number of domain entities, this class can become huge and hard to maintain.

This is where  creating individual configuration classes can be used that inherit from IEntityTypeConfiguration<T>. Using IEntityTypeConfiguration<T> you can implement the Configure method to add your custom configurations.

Add a new folder in the EFCoreSeparateProject.Infrastructure project called EntityConfigurations. Within this folder add a new class called PersonConfigurations.cs with the below code:

using EFCoreSeparateProject.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EFCoreSeparateProject.Infrastructure.EntityConfigurations
{
public class PersonConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.OwnsOne(e => e.Address, t =>
{
//t.OwnsOne(a => a.Line1).Property(e => e.Line).HasColumnName("AddressLine1");
t.OwnsOne(a => a.Line1);
t.OwnsOne(a => a.Line2);
t.OwnsOne(a => a.Postcode);
});
}
}
}

Owned Types as shown above, were introduced in EF Core 2.1, and can be read about in more depth using the Microsoft documentation here. This is perfect for modelling value objects when practising Domain Driven Design.

The complete ExampleDbContext should look like the below:

using EFCoreSeparateProject.Core.Domain;
using EFCoreSeparateProject.Infrastructure.EntityConfigurations;
using Microsoft.EntityFrameworkCore;
namespace EFCoreSeparateProject.Infrastructure
{
public class ExampleDbContext : DbContext
{
public ExampleDbContext(DbContextOptions options) : base(options)
{
}
public DbSet<Person> Persons { get; set; }
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfiguration(new PersonConfiguration());
}
}
}

With all the above in place, create a new dotnet core console application called EFCoreSeparateProject.Migrations. Next delete the Program.cs file, this is no longer needed. Now open the EFCoreSeparateProject.cs file and remove the following:

<OutputType>Exe</OutputType>

Add a new file, appsettings.json to the project, and add a new setting called SqlConnectionString. Set this to the connection string required for your SQL database (This is an example, remember not to store clear text passwords anywhere including git in a real-world situation – an alternative might be Azure Key Vault), I am using Docker to host SQL Server, an example of my appsettings.json file is shown below:

{
"SqlConnectionString": "Server=127.0.0.1,5433;Database=EFCoreSeparateProjectDb;User Id=sa;Password=yourStrong(!)Password;"
}

The next step is important to ensure the appsettings.json file gets copied when we build, we need to ensure the Build Action is set to “Content”, and Copy to Output Directory is set to “Copy always”. This is so the settings will be copied to the output folder if we decide to publish in the future.

appsettings.json properties

Create a new class called ExampleDbContextFactory.cs and inherit from IDesignTimeDbContextFactory<ExampleDbContext>, you will need to import references to EFCoreSeparateProject.Infrastructure and Microsoft.EntityFrameworkCore. Microsoft.Configuration.* nuget packages will need to be installed to read the configuration file, and if we wish to use environment variables or command line args in the future. Please see lines 13-18 below for an example. We will also need to add Microsoft.EntityFrameworkCore.SqlServer and Microsoft.EntityFrameworkCore.Relational as we will be using SQL Server, as well as Microsoft.EntityFrameworkCore.Design to be able to generate the migrations using the dotnet ef tools. Now we can go ahead and implement the CreateDbContext method as shown below:

using EFCoreSeparateProject.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.Extensions.Configuration;
using System.IO;
namespace EFCoreSeparateProject.Migrations
{
public class ExampleDbContextFactory : IDesignTimeDbContextFactory<ExampleDbContext>
{
public ExampleDbContext CreateDbContext(string[] args)
{
var configuration = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true)
.AddEnvironmentVariables()
.AddCommandLine(args)
.Build();
var optionsBuilder = new DbContextOptionsBuilder<ExampleDbContext>();
optionsBuilder.UseSqlServer(configuration.GetValue<string>("SqlConnectionString"), x => x.MigrationsAssembly("EFCoreSeparateProject.Migrations"));
return new ExampleDbContext(optionsBuilder.Options);
}
}
}

There are a couple of important lines to highlight. Line 15 loads our configuration file which includes the connection string to use. 20-21 builds the options for our entity framework DbContext – MigrationsAssembly is important as it identifies the project where migrations should be generated from the dotnet ef tools.

Now we are ready to add our initial migration. This article assumes you have already installed to dotnet ef tools globally. Switch to a command prompt and navigate to the directory of your EFCoreSeperateProject.Migrations project and type the following command:

dotnet ef migrations add InitialMigration

You should see the below output:

Output from adding initial migration

You can check the migrations have been generated by viewing the newly added Migrations folder under EFCoreSeparateProject.Migrations. It should include the generating migration and the current snapshot.

Now run the following command to apply the migrations:

dotnet ef database update

You should see the output below:

Migrations applied to database successfully

If we open up SSMS, connect to our SQL Server instance using the details we specified in our appsettings.json, we should see our database with the both the migrations table and persons table.

Generated database and tables

If we expand the dbo.Persons table, you will see that some of the column names might have a naming convention we are not happy with, for example Address_Line1_Line:

Default naming convention for Owned Types

We can modify this by opening our PersonConfigurations.cs file and uncommenting line 13, commenting line 14 as shown below, and then generating and applying a new migration, you will see the names have now changed to the desired name. This hasn’t been performed, but is commented out for reference.

using EFCoreSeparateProject.Core.Domain;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace EFCoreSeparateProject.Infrastructure.EntityConfigurations
{
public class PersonConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
builder.OwnsOne(e => e.Address, t =>
{
//t.OwnsOne(a => a.Line1).Property(e => e.Line).HasColumnName("AddressLine1");
t.OwnsOne(a => a.Line1);
t.OwnsOne(a => a.Line2);
t.OwnsOne(a => a.Postcode);
});
}
}
}

I hope this has been a useful guide on how to separate your migrations for Entity Framework Core in to an independent project.