I recently hosted a live stream with badass-as-a-service Chris Klug, titled “Stop using Entity Framework Core as a DTO Provider!”. It’s worth a watch, and it gave me, a long-time Entity Framework user, a lot to think about and reevaluate in my workflows. That said, regardless of whether you agree with Chris’ style, he showed a masterclass of tool usage and an understanding that’s easy to admire. In our live stream, one new trick (to me, at least) stood out as something every Entity Framework Core user should know about.

In this post, we’ll explore one strategy to appease the dotnet ef CLI tools regarding design-time configuration and how it opens up a world of possibilities when dealing with database migrations.

Dependencies and ceremony

Entity Framework Core is heavily built around conventions and flexibility. It’s a multi-provider object-relational mapper (ORM), so it needs to operate under many unknown factors, with you, the developer, filling in the gaps. What database are you using? How many databases are you targeting? What migration strategies are you applying? The flexibility is excellent for solving complex problems but also leads developers into the woods of opaque exceptions.

One such problem you’ve likely encountered is the following error output when using the dotnet ef migrations add command.

Unable to create a 'DbContext' of type ''. The exception 
'Unable to resolve service for type 
'Microsoft.EntityFrameworkCore.DbContextOptions`1[MigrationLibrary.Database]'
while attempting to activate 'MigrationLibrary.Database'.

The DbContextOptions class is used to configure a DbContext.

using Microsoft.EntityFrameworkCore;

namespace MigrationLibrary;

public class Database(DbContextOptions<Database> options) 
    : DbContext(options)
{
    public DbSet<Person> People => Set<Person>();
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

Everything looks good, but we need to see the OnConfiguring implementation here, which adds the necessary answers to which database we hope to use. Let’s tweak the code to solve this issue.

using Microsoft.EntityFrameworkCore;

namespace MigrationLibrary;

public class Database() : DbContext 
{
    public DbSet<Person> People => Set<Person>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlite();
    }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public int Age { get; set; }
}

The error is gone now, but we’ve locked into options we can no longer configure outside the Database class. We want something else.

We still want to be able to pass in a DbContextOptions instance. This allows us to configure the DbContext to point to different database implementations and connection strings and alter logging options. We want all that, trust me.

Let’s use a little-known feature to fix this: IDesignTimeDbContextFactory.

IDesignTimeDbContextFactory to the rescue

The IDesignTimeDbContextFactory is a tool that can change how you write migrations, and I don’t say this lightly. Let’s look at the documentation.

A factory for creating derived DbContext instances. 
Implement this interface to enable design-time services 
for context types that do not have a public default constructor.
At design-time, derived DbContext instances can be created 
in order to enable specific design-time experiences such 
as Migrations.
 
Design-time services will automatically discover 
implementations of this interface that are in the 
startup assembly or the same assembly as the derived context.

Hmmm, nice. So what does that look like in C#?

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

namespace MigrationLibrary.Configuration;

// ReSharper disable once UnusedType.Global
public class DatabaseDesignTimeDbContextFactory 
    : IDesignTimeDbContextFactory<Database>
{
    public Database CreateDbContext(string[] args)
    {
        var builder = new DbContextOptionsBuilder<Database>();
        builder.UseSqlite();
        return new Database(builder.Options);
    }
}

Adding this class definition to your project lets Entity Framework’s design-time services answer some of the questions needed to generate migrations and other configuration options.

Running the dotnet ef migrations add command now results in the following output.

(base) ~/RiderProjects/MigrationLibrary
dotnet ef migrations add --project MigrationLibrary AddPerson
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'

Now, you can define all your migrations and the database configuration in one project while still allowing consuming projects to modify and change options within reason (i.e., you can’t apply SQLite migrations to a SQL Server database).

Conclusion

I love doing live streams because I get to learn from some of the best .NET professionals in the industry, and Chris Klug didn’t disappoint. Again, I highly recommend you watch the stream. This tip, amongst others, is sprinkled throughout the presentation. As always, thanks for reading and sharing these posts with friends and colleagues. Cheers.