Easy Schema Migrations in .NET Core
By: Manfred Lange
Tags: Fluent Migrator, Database, Sql Server, Dotnet Core, C Sharp Programming

The First Migration

Directory Structure

We are going to add a few files in this article and there will be more in future articles. I found that it pays to spend a couple of thoughts on an appropriate directory structure. A fit-for-use structure makes it easier to conceptualize and to locate files when needed.

Architecturally, I found that in practice most of the time we can get away with three layers:

Presentation Layer (this is not necessarily the User Interface)

Business Logic or Domain Layer

Data Access Layer

The presentation layer could be the user interface or it could be where API endpoints are handled. For example, if we are implementing a microservice we may choose to use this layer to deal with different representations of incoming or outgoing data, e.g. C# to JSON or JSON to C#.

For the Business Layer, I often use service classes. I keep the code minimal in the presentation layer, and hand off flow to the business logic layer as quickly as possible. Classes in the business layer are relatively easy to test as usually we are dealing with Plain Old C# Objects (POCOs) only.

Typically for each layer I create one directory at the root level. For instance, to represent the data access layer, I typically create a directory “DataAccess” at the root. We’ll introduce the directory structure for other layers in future articles.

Within directory “DataAccess” I typically have another directory called “Migrations”. We’ll use this for all the migration classes we need.

Creating the List Table

As you will recall from the running example, Mahi is an app for managing todo lists. Therefore, it is a reasonable choice to create the table that will hold information about lists first. In future articles we will change from migration-first to a domain class first approach.

Make sure you create the folders “DataAccess” and “Migrations” to get the following structure for the repo:

Directory location for migration classes

In this file, we write the following code:

Available as gist at: https://gist.github.com/ManfredLange/34d42b4210ccb7707ba6c073a10dace8

Let’s have a closer look at this code. In line 5 we use the attribute “Migration” which expects one parameter of type “long”. This is the identifier of the migration. Also, Fluent Migrator sorts all migrations by ascending order of this number.

To make it easier I make this number to represent the current date and time. By grouping the digits as shown in the code, we highlight the date portion and the time portion, even though the result is still a long. The underscore to separate digits, i.e. the “digit separators”, was added in C# 7 (see https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-7#numeric-literal-syntax-improvements) to improve readability.

I used the date and time of when I wrote this piece of code (09 Nov 2020 at 0935 using 24 hours format). This is a very simple mechanism to generate unique ids which also works in team settings. It is extremely unlikely that two different developers working in the same repository at the same time will create a migration at the exact same point in time. So, whenever you write a migration just look at the time of your computer and you know what number to use.

In line 6 we see that we derive from base class Migration. Note that this class is different to the attribute of the same name. The attribute’s full class name is “MigrationAttribute”. It’s just syntactic sugar that you can omit the suffix.

The base class Migration is abstract and requires us to implement two methods: “Up()” and “Down()”. We’ll look into the implementation of “Down()” later. Here we implement “Up()” only.

Lines 15 to 18 create the table named “List” with two columns. In line 16 we say that we want a column of type Guid that is not nullable and that will serve as the primary key on the table. The type “Guid” doesn’t exist in SQL Server under that name. This is where FluentMigrator translates it to “uniqueidentifier” which is the name SQL Server uses for this data type.

In Line 17 we specify a second column to store the name of the list. This particular table probably cannot get much simpler than this.

You can build and run the project again with this change but nothing will have changed in the behavior. We still need to have the FluentMigrator runner execute our migration.

The Database Class

This is where things are getting a more interesting. We’ll add quite a bit more code in this next step. Keep in mind, though, that the code introduced here is more of the infrastructure kind. We’ll write it once and generally won’t have to change it again.

For code that deals with migrations (this article) and object-relational mapping (ORM, future article), I typically create a class named Database. Once in place I rarely need to change it, perhaps once every few months at best. Let’s create a file “Database.cs” in folder “src/CmdLine/DataAccess” with the following content:

Available as gist at: https://gist.github.com/ManfredLange/023f5a4eab3f5dc5a7659e1a7d5fd2f5

Don’t be scared. This may look like a lot, but it’s actually quite simple. Let’s walk through the code line by line.

First, notice that “RunMigrations()” in line 9 is the only public method. All other methods are private. We’ll make use of this public method later.

Fluent NHibernate makes use of the dependency injection infrastructure of .NET Core. In line 11 we call a method that creates all services that we need and that returns a service provider.

In line 15 we set up the scope for the service provider. In line 17 we pass the service provider to the private method “RunMigrations(IServiceProvider)”. And that is all there is to the public parameterless method “RunMigrations()”. Quite simple at this abstraction level. Let’s dig a little deeper.

Lines 24 to 40 create a ServiceCollection object. All services that we want to use need to be registered with this service collection. Line 28 uses Fluent Migrator’s “AddFluentMigratorCore()”to register FluentMigrator itself.

Lines 29 to 35 configure the migration runner. In line 31 we add support for SQL Server. In line 33 we specify what connection string to use. And in line 35 we tell the runner where to look for migrations.

Line 37 adds logging. In this case we add Fluent Migrator’s console, so that we can see if and what migrations are applied when the migration runner executes.

Finally, in line 39 we create the service provider from the ServiceCollection object. Obviously, this new service provider will have access to the service collection. Any code with access to the service provider can ask it for instances of a service.

Let’s have a look at the final piece of code in this class, the private method “RunMigrations(IServiceProvider)” in lines 45 to 52.

In line 48 we use the service provider to obtain an object that implements IMigrationRunner. An object that implements this interface supports running migration. This is what we do in line 51, where we invoke “runner.MigrateUp()”.

And for now that’s all the code in class Database. One further comment on line 54, though. In this example the connection string is hard-coded. For any practical purposes we can’t use a hard-coded string. We will resolve this issue in a future article. The basic idea, though, will be to read it as a configuration value and then pass it as a parameter to class Database.

Using the Database Class

With the Database class in place, we can now make use of it in our console application. We change the content of Program.cs to the following:

Available as gist at https://gist.github.com/ManfredLange/0746322712398a93ceb0073aad7338f2

In line 2 we add the using statement for the namespace that contains the Database class. In line 10 we invoke Database.RunMigrations() to apply the migrations.

As you can see, we were able to encapsulate the more complex code in class Database. In class Program where we make use of it, the code becomes very simple, effectively a single line of code. The details about how to configure the Fluent Migrator runner or how to execute migrations are encapsulated in class Database.

Apply the Migrations

Let’s see if this is actually doing what we expected. In the terminal window make sure you are in directory “/app/src/cmdline”, then execute the command

dotnet run

You should see output similar to the following:

The highlighted output claims that Fluent Migrator applied our migration to the database. Let’s check. Use your preferred tool to connect to the database. The parameters on your development machine are:

Server: localhost,7610

Database: mahi-cmdline

User: sa

Password: PassWord42

With that you should see something similar to the following:

We can see the table List with the two columns, “Id” and “Name”, as well as the entries under “Keys” and “Indexes”. This means the migrations have been applied.

The VersionInfo Table

If you take a closer look, you will notice that the database also contains a table named “VersionInfo”. This is where Fluent Migrator keeps a record of which migrations have already been applied. You should see something like this when you look into that table:

This confirms that all worked as we wanted and that Fluent Migrator has recorded the migration, i.e. the schema change.

If we run “dotnet run” again, then we’ll notice that all output regarding migrations is absent. Fluent Migrator won’t apply our migration a second time since it is already in place.

From here, the workflow is essentially pretty simple. If you need a schema change, write a new migration. Run your program and the schema change will be applied.

If you follow this approach from day one, you can always delete the entire schema and Fluent Migrator will be happy to reapply all changes, thus recreating the full schema. This makes it quite convenient to experiment with changes, in particular if you keep changes small and apply them incrementally.

One word of caution, though: Once a migration has been committed and pushed, it becomes immutable. Do not change an existing migration. If you don’t like what a migration does, just write a new one that reverts what the older migration did. There is no problem to have dozens or even hundreds of migrations.