EF Core migration features that you might not know about

Published: Monday 26 August 2024

There are some invaluable features in Entity Framework that will help you with database migrations.

We'll reveal four features that you might not know about and how they can be used.

Entity type configuration

One way of configurating entities in Entity Framework Core is to override the OnModelCreating method in your DbContext and use the ModelBuilder type parameter within it to select the entity and add the configuration.

C# coding challenges

C# coding challenges

Our online code editor allows you to compile the answer.

However, there is another way you can do it which is arguably cleaner.

For each entity, you can set up the configuration in its own class.

We have created a SocialMedia entity that we want to add to our DbContext. The entity has an Id and Name property.

// SocialMedia.cs
namespace RoundTheCode.HelpfulEfCore.Entities;

public class SocialMedia
{
	public int Id { get; set; }

	public string? Name { get; set; }
}

We want to configure the Name property so it has a maximum length of 50 characters when we migrate it in the database.

To do that, we add a SocialMediaConfiguration class.

For this to work, this class needs to implement the IEntityTypeConfiguration interface, passing in the entity that we are configuring as its generic type.

The IEntityTypeConfiguration interface has a Configure method which has an EntityTypeBuilder type as the parameter. This is where we can add the configuration.

// SocialMediaConfiguration.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RoundTheCode.HelpfulEfCore.Entities;

namespace RoundTheCode.HelpfulEfCore.Configuration;

public class SocialMediaConfiguration : IEntityTypeConfiguration<SocialMedia>
{
	public void Configure(EntityTypeBuilder<SocialMedia> builder)
	{
		builder.Property(s => s.Name)
			.HasMaxLength(50);
	}
}

In-order to add this configuration class to the DbContext, we need to override the OnModelCreating method within it.

This method has a ModelBuilder instance as the parameter. We use that and call the ApplyConfigurationsFromAssembly method, passing in the assembly location of the configuration class for each entity.

As the SocialMediaConfiguration class has a namespace of RoundTheCode.HelpfulEfCore.Configuration, we can load in the RoundTheCode.HelpfulEfCore assembly, and pass that in as the parameter to the ApplyConfigurationsFromAssembly method.

As a result, any other configuration classes that implement the IEntityTypeConfiguration interface and are inside the RoundTheCode.HelpfulEfCore assembly will automatically be configured to that DbContext when adding a new migration.

// MyDbContext.cs
using System.Reflection;

public class MyDbContext : DbContext
{	
	...

	protected override void OnModelCreating(ModelBuilder modelBuilder)
	{
		modelBuilder.ApplyConfigurationsFromAssembly(Assembly.Load("RoundTheCode.HelpfulEfCore"));

		base.OnModelCreating(modelBuilder);
	}
}

With this in place, we can open up the Package Manager Console in Visual Studio by going to Tools, NuGet Package Manager and Package Manager Console. 

Then to add a migration to the DbContext, call Add-Migration and give it a name. For example:

Add-Migration SocialMediaTable

Calling the Update-Database command in Package Manager Console will apply that migration to the database.

Seed data

Seed data is another tool that can be used with EF Core migrations. This populates the database with an initial set of data for an entity.

Going back to the SocialMediaConfiguration class, we can call the HasData method in the EntityTypeBuilder type parameter to add this data to the database.

To demonstrate this, we are going to add Facebook, Instagram and LinkedIn into the SocialMedia table, explicitly specifying the Id for each of them.

// SocialMediaConfiguration.cs
public class SocialMediaConfiguration : IEntityTypeConfiguration<SocialMedia>
{
	public void Configure(EntityTypeBuilder<SocialMedia> builder)
	{
		...

		builder.HasData(new SocialMedia
		{
			Id = 1,
			Name = "Facebook"
		}, new SocialMedia
		{
			Id = 2,
			Name = "Instagram"
		}, new SocialMedia
		{
			Id = 3,
			Name = "LinkedIn"
		});
	}
}

We can then add another migration script by opening the Package Manager Console and running the Add-Migration command.

Add-Migration SocialMediaSeedData

To update the database, we then call Update-Database in Package Manager Console.

Value converters

Value converters allows you to convert data into a specific format in the database, and convert it back when its used in the application.

To demonstrate this, we have created a Product entity.

// Product.cs
public class Product
{
	public int Id { get; set; }

	public string? Name { get; set; }

	public DateTime Created { get; set; }
}

This entity contains a Created property which has a DateTime type.

We'll create a value converter that will store the date and time in the database using the UTC timezone. When it's used in the application, we convert it back to the local timezone on the server.

To do this, we create a new DateTimeUtcConverter class, inheriting the ValueConverter class with two generic types.

As the format for the database record and the application are both DateTime types, we use DateTime for both generic types.

Then it's a case of creating a constructor for the DateTimeUtcConverter class, calling a base constructor in ValueConverter.

When saving the record to the database, we convert it to UTC. When we convert it back, we specify that the date and time is in UTC format and convert it to the local time on the server.

// DateTimeUtcConverter.cs
public class DateTimeUtcConverter : ValueConverter<DateTime, DateTime>
{
	public DateTimeUtcConverter() : base(
		date => date.ToUniversalTime(), 
		date => DateTime.SpecifyKind(date, DateTimeKind.Utc).ToLocalTime()
		)
	{

	}
}

To configure the DateTimeUtcConverter class on a property in an entity, we call it in the configuration class using the EntityTypeBuilder type and calling the HasConversion method.

// ProductConfiguration.cs
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
	public void Configure(EntityTypeBuilder<Product> builder)
	{
		builder.Property(s => s.Name)
			.HasMaxLength(50);

		builder.Property(s => s.Created)
			.HasConversion(new DateTimeUtcConverter());
	}
}

Run a SQL command

SQL commands can also be run when applying a migration.

First of all, we need to create an empty migration by running the Add-Migration command in Package Manager Console.

Add-Migration ProductSql

From there, we can add the migration scripts to the Up method by calling the Sql method in the MigrationBuilder parameter type.

In this example, we are inserting a couple of records to the Product table.

// ProductSql.cs
public partial class ProductSql : Migration
{
	/// <inheritdoc />
	protected override void Up(MigrationBuilder migrationBuilder)
	{
		migrationBuilder.Sql("INSERT INTO dbo.Product (Name, Created) VALUES ('Book', GETUTCDATE())");

		migrationBuilder.Sql("INSERT INTO dbo.Product (Name, Created) VALUES ('Television', GETUTCDATE())");
	}

	/// <inheritdoc />
	protected override void Down(MigrationBuilder migrationBuilder)
	{

	}
}

To run these SQL commands as part of the migration, you just call the Update-Database command in Package Manager Console and it will apply the changes.

Watch the video

Watch our video where we go through each of these EF Core migration features and how they work.

As well as showing you how to set up a value converter, we'll also demonstrate it working in an ASP.NET Core Web API:

If you want to know more about Entity Framework Core migrations, you can watch our six-part series on how to Get Started with EF Core.

Not only does it show you how to add a migration, it also shows you how to create a DbContext and how to insert, update and delete a record from the database.