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.