Folks working with EF Core are likely very fond of the library’s migration features, one of the most vital selling points for adopting an ORM. If you’re building a .NET solution, your database schema will evolve, and adding, removing, and updating are everyday actions you must perform.

.NET Aspire can ease the development of distributed solutions, but you still need to bridge the gap between development time actions and runtime execution. With EF Core, a development time action is managing migrations, while at runtime, you’ll need to execute those migrations against a database. The original tutorial by the Microsoft documentation explains how to run a .NET Aspire application with migrations but leaves out how to do development time tasks.

In this post, we’ll explore how to manage migrations during the development process so you can get the most out of your .NET Aspire and Entity Framework Core adoption.

The Solution Structure

We’ll first need to understand the solution structure of our Aspire distributed application. This is what my solution looks like, but you can change the approach depending on your preferences.

AspireSandbox
|- AspireSandbox.AppHost
|- AspireSandbox.Data
|- AspireSandbox.ServiceDefaults
|- AspireSandbox.Web

The AspireSanbox.Data project contains my DbContext implementation, which is nothing remarkable.

using Microsoft.EntityFrameworkCore;

namespace AspireSandbox.Data;

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

public class Count
{
    public int Id { get; set; }
    public DateTimeOffset CountedAt { get; set; } = DateTimeOffset.UtcNow;
}

The dependencies for this project include the following Entity Framework Core packages.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
      <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
      <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
        <PrivateAssets>all</PrivateAssets>
        <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      </PackageReference>
      <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.8" />
    </ItemGroup>

</Project>

Next, we must add this project as a reference to our AspireSandbox.AppHost project, paying attention to adding the attribute of IsAspireProjectResource and setting its value to false. This opts this project out of the Aspire source generators.

I have a few other package references, but pay attention to the Entity Framework Core dependencies.

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net8.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <IsAspireHost>true</IsAspireHost>
        <UserSecretsId>ba2648f9-6953-4e8b-9918-c241b1d99b09</UserSecretsId>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Aspire.Hosting.AppHost" Version="8.2.1"/>
        <PackageReference Include="Aspire.Hosting.PostgreSQL" Version="8.2.1" />
        <PackageReference Include="Aspire.Hosting.Redis" Version="8.2.1" />
        <PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8">
          <PrivateAssets>all</PrivateAssets>
          <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
        </PackageReference>
    </ItemGroup>

    <ItemGroup>
      <ProjectReference Include="..\AspireSandbox.Data\AspireSandbox.Data.csproj" 
                        IsAspireProjectResource="false" />
      <ProjectReference Include="..\AspireSandbox.Web\AspireSandbox.Web.csproj" />
    </ItemGroup>

</Project>

OK, we’re ready to write some code.

Entity Framework Core Design Time Factory

Entity Framework Core provides a mechanism for the tooling to connect and work with a database. Since our database is in the scope of our distributed application, we need to run our tooling when Aspire has built our dependency. Don’t worry; it will make sense in a second.

In the AspireSandbox.AppHost, create a new DataContextDesignTimeFactory class.

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

namespace AspireSandbox.AppHost;

public sealed class DataContextDesignTimeFactory :
	IDesignTimeDbContextFactory<Data.Database>
{
    public Data.Database CreateDbContext(string[] args)
    {
        var builder = DistributedApplication.CreateBuilder(args);
    
        var postgres = builder
            .AddPostgres("postgres")
            .AddDatabase("migrations", databaseName: "migrations");

        var optionsBuilder = new DbContextOptionsBuilder<Data.Database>();
        optionsBuilder.UseNpgsql("migrations");
        return new Data.Database(optionsBuilder.Options);
    }
}

This implementation will spin up our database dependency and allow us to create an instance of our DbContext implementation. At least long enough to create and add our migrations to our project. Remember that EF Core creates a model snapshot as a C# file, so there is no need to persist the database across migration runs.

EF Core CLI Command

Now, we can run the following command to spin up Aspire long enough to create a migration.

dotnet ef migrations --project ./AspireSandbox.Data --startup-project ./AspireSandbox.AppHost add Initial

Change the --project and --startup-project to match your solution structure.

If all goes well, you should now have a new database migration.

Conclusion

There you have it; you can now work seamlessly between your development environment and Aspire solution with just a new IDesignTimeDbContextFactory implementation and tweaking your CLI command.

Thanks to James Hancock for leaving this comment on the Aspire GitHub issues and inspiring this blog post. His solution is pretty +$%! good.