VOOZH about

URL: https://aspire.dev/integrations/databases/efcore/migrations/

⇱ Apply EF Core migrations in Aspire | Aspire


Skip to content
👁 Entity Framework Core logo

Since Aspire projects use a containerized architecture, databases are ephemeral and can be recreated at any time. Entity Framework Core (EF Core) uses a feature called migrations to create and update database schemas. Since databases are recreated when the app starts, you need to apply migrations to initialize the database schema each time your app starts. This is accomplished by registering a migration service project in your app that runs migrations during startup.

In this article, you learn how to configure Aspire projects to run EF Core migrations during app startup. Before continuing, ensure your development environment is set up — see Prerequisites.

This tutorial uses a sample app that demonstrates how to apply EF Core migrations in Aspire. Use Visual Studio to clone the sample app from GitHub or use the following command:

Terminal window
gitclonehttps://github.com/MicrosoftDocs/aspire-docs-samples/

The sample app is in the SupportTicketApi folder. Open the solution in Visual Studio or VS Code and take a moment to review the sample app and make sure it runs before proceeding. The sample app is a rudimentary support ticket API, and it contains the following projects:

  • SupportTicketApi.Api: The ASP.NET Core project that hosts the API.
  • SupportTicketApi.AppHost: Contains the Aspire AppHost and configuration.
  • SupportTicketApi.Data: Contains the EF Core contexts and models.
  • SupportTicketApi.ServiceDefaults: Contains the default service configurations.

Run the app to ensure it works as expected. In the Aspire dashboard, wait until all resources are running and healthy. Then select the https Swagger endpoint and test the API’s:

HTTP
GET /api/SupportTickets/1 HTTP/1.1
Host:example.com
Accept:application/json

To do this, expand the GET /api/SupportTickets/1 endpoint by expanding the operation and selecting Try it out. Select Execute to send the request and view the response:

JSON — example response
[
{
"id":1,
"title":"Initial Ticket",
"description":"Test ticket, please ignore."
}
]

Close the browser tabs that display the Swagger endpoint and the Aspire dashboard and then stop debugging.

Start by creating some migrations to apply.

  1. Modify the model so that it includes a new property. Open SupportTicketApi.Data/Models/SupportTicket.cs and add a new property to the SupportTicket class:

    C# — SupportTicket.cs
    usingSystem.ComponentModel.DataAnnotations;
    namespaceSupportTicketApi.Data.Models;
    publicsealedclassSupportTicket
    {
    publicint Id {get;set;}
    [Required]
    publicstring Title {get;set;}=string.Empty;
    [Required]
    publicstring Description {get;set;}=string.Empty;
    publicbool Completed {get;set;}
    }
  2. Create another new migration to capture the changes to the model:

Now you’ve got some migrations to apply. Next, you’ll create a migration service that applies these migrations during app startup.

When working with EF Core migrations in Aspire projects, you might encounter some common issues. Here are solutions to the most frequent problems:

“No database provider has been configured” error

Section titled ““No database provider has been configured” error”

If you get an error like “No database provider has been configured for this DbContext” when running migration commands, it’s because the EF tools can’t find a connection string or database provider configuration. This happens because Aspire projects use service discovery and orchestration that’s only available at runtime.

Solution: Temporarily add a connection string to your project’s appsettings.json file:

  1. In your API project (where the DbContext is registered), open or create an appsettings.json file.

  2. Add a connection string with the same name used in your Aspire AppHost:

    JSON — appsettings.json
    {
    "ConnectionStrings":{
    "ticketdb":"Server=(localdb)\\mssqllocaldb;Database=TicketDb;Trusted_Connection=true"
    }
    }
  3. Run your migration commands as normal.

  4. Remove the connection string from appsettings.json when you’re done, as Aspire will provide it at runtime.

When your Aspire solution has multiple services with different databases, create migrations for each database separately:

  1. Navigate to each service project directory that has a DbContext.

  2. Run migration commands with the appropriate project reference:

    .NET CLI
    # For the first service/database
    dotnetefmigrationsaddInitialCreate--project../FirstService.Data/FirstService.Data.csproj
    # For the second service/database
    dotnetefmigrationsaddInitialCreate--project../SecondService.Data/SecondService.Data.csproj
  3. Create separate migration services for each database, or handle multiple DbContexts in a single migration service.

Ensure you’re running migration commands from the correct project:

  • CLI: Navigate to the project directory that contains the DbContext registration (usually your API project)
  • Package Manager Console: Set the startup project to the one that configures the DbContext, and the default project to where migrations should be created

To execute migrations, call the EF Core Microsoft.EntityFrameworkCore.Migrations.IMigrator.Migrate method or the IMigrator.MigrateAsync method. In this tutorial, you’ll create a separate worker service to apply migrations. This approach separates migration concerns into a dedicated project, which is easier to maintain and allows migrations to run before other services start.

To create a service that applies the migrations:

  1. Add a new Worker Service project to the solution. If using Visual Studio, right-click the solution in Solution Explorer and select Add > New Project. Select Worker Service, name the project SupportTicketApi.MigrationService and target .NET 10.0. If using the command line, use the following commands from the solution directory:

    .NET CLI
    dotnetnewworker-nSupportTicketApi.MigrationService-f"net10.0"
    dotnetslnaddSupportTicketApi.MigrationService
  2. Add the SupportTicketApi.DataandSupportTicketApi.ServiceDefaultsproject references to theSupportTicketApi.MigrationService` project using Visual Studio or the command line:

    .NET CLI
    dotnetaddSupportTicketApi.MigrationServicereferenceSupportTicketApi.Data
    dotnetaddSupportTicketApi.MigrationServicereferenceSupportTicketApi.ServiceDefaults
  3. Add the 📦 Aspire.Microsoft.EntityFrameworkCore.SqlServer NuGet package reference to the SupportTicketApi.MigrationService project using Visual Studio or the command line:

    .NET CLI
    cdSupportTicketApi.MigrationService
    dotnetaddpackageAspire.Microsoft.EntityFrameworkCore.SqlServer-v"13.1.0"
  4. Add the highlighted lines to the Program.cs file in the SupportTicketApi.MigrationService project:

    C# — Program.cs
    usingSupportTicketApi.Data.Contexts;
    usingSupportTicketApi.MigrationService;
    var builder =Host.CreateApplicationBuilder(args);
    builder.AddServiceDefaults();
    builder.Services.AddHostedService<Worker>();
    builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>tracing.AddSource(Worker.ActivitySourceName));
    builder.AddSqlServerDbContext<TicketContext>("sqldata");
    var host =builder.Build();
    host.Run();

    In the preceding code:

  5. Replace the contents of the Worker.cs file in the SupportTicketApi.MigrationService project with the following code:

    C# — Worker.cs
    usingSystem.Diagnostics;
    usingMicrosoft.EntityFrameworkCore;
    usingMicrosoft.EntityFrameworkCore.Infrastructure;
    usingMicrosoft.EntityFrameworkCore.Storage;
    usingOpenTelemetry.Trace;
    usingSupportTicketApi.Data.Contexts;
    usingSupportTicketApi.Data.Models;
    namespaceSupportTicketApi.MigrationService;
    publicclassWorker(
    IServiceProvider serviceProvider,
    IHostApplicationLifetime hostApplicationLifetime):BackgroundService
    {
    publicconststring ActivitySourceName ="Migrations";
    privatestaticreadonlyActivitySource s_activitySource =new(ActivitySourceName);
    protectedoverrideasyncTaskExecuteAsync(
    CancellationToken cancellationToken)
    {
    usingvar activity =s_activitySource.StartActivity(
    "Migrating database",ActivityKind.Client);
    try
    {
    usingvar scope =serviceProvider.CreateScope();
    var dbContext =scope.ServiceProvider.GetRequiredService<TicketContext>();
    awaitRunMigrationAsync(dbContext,cancellationToken);
    awaitSeedDataAsync(dbContext,cancellationToken);
    }
    catch(Exception ex)
    {
    activity?.AddException(ex);
    throw;
    }
    hostApplicationLifetime.StopApplication();
    }
    privatestaticasyncTaskRunMigrationAsync(
    TicketContext dbContext,CancellationToken cancellationToken)
    {
    var strategy =dbContext.Database.CreateExecutionStrategy();
    awaitstrategy.ExecuteAsync(async()=>
    {
    // Run migration in a transaction to avoid partial migration if it fails.
    awaitdbContext.Database.MigrateAsync(cancellationToken);
    });
    }
    privatestaticasyncTaskSeedDataAsync(
    TicketContext dbContext,CancellationToken cancellationToken)
    {
    SupportTicket firstTicket =new()
    {
    Title="Test Ticket",
    Description="Default ticket, please ignore!",
    Completed=true
    };
    var strategy =dbContext.Database.CreateExecutionStrategy();
    awaitstrategy.ExecuteAsync(async()=>
    {
    // Seed the database
    awaitusingvar transaction =awaitdbContext.Database
    .BeginTransactionAsync(cancellationToken);
    awaitdbContext.Tickets.AddAsync(firstTicket,cancellationToken);
    awaitdbContext.SaveChangesAsync(cancellationToken);
    awaittransaction.CommitAsync(cancellationToken);
    });
    }
    }

    In the preceding code:

    • The ExecuteAsync method is called when the worker starts. It in turn performs the following steps:
      1. Gets a reference to the TicketContext service from the service provider.
      2. Calls RunMigrationAsync to apply any pending migrations.
      3. Calls SeedDataAsync to seed the database with initial data.
      4. Stops the worker with StopApplication.
    • The RunMigrationAsync and SeedDataAsync methods both encapsulate their respective database operations using execution strategies to handle transient errors that may occur when interacting with the database. To learn more about execution strategies, see Connection Resiliency.

Add the migration service to the orchestrator

Section titled “Add the migration service to the orchestrator”

The migration service is created, but it needs to be added to the Aspire AppHost so that it runs when the app starts.

  1. In the SupportTicketApi.AppHost project, open the AppHost.cs file.

  2. Add the following highlighted code:

    C# — AppHost.cs
    var builder =DistributedApplication.CreateBuilder(args);
    var sql =builder.AddSqlServer("sql")
    .AddDatabase("sqldata");
    var migrations =builder.AddProject<Projects.SupportTicketApi_MigrationService>("migrations")
    .WithReference(sql)
    .WaitFor(sql);
    builder.AddProject<Projects.SupportTicketApi_Api>("api")
    .WithReference(sql)
    .WithReference(migrations)
    .WaitForCompletion(migrations);
    builder.Build().Run();

    This code enlists the SupportTicketApi.MigrationService project as a service in the Aspire AppHost. It also ensures that the API resource doesn’t run until the migrations are complete.

  3. If the code cannot resolve the migration service project, add a reference to the migration service project in the AppHost project:

    .NET CLI
    dotnetaddSupportTicketApi.AppHostreferenceSupportTicketApi.MigrationService

If your Aspire solution uses multiple databases, you have two options for managing migrations:

Option 1: Separate migration services (Recommended)

Section titled “Option 1: Separate migration services (Recommended)”

Create a dedicated migration service for each database. This approach provides better isolation and makes it easier to manage different database schemas independently.

  1. Create separate migration service projects for each database:

    .NET CLI
    dotnetnewworker-nFirstService.MigrationService-f"net8.0"
    dotnetnewworker-nSecondService.MigrationService-f"net8.0"
  2. Configure each migration service to handle its specific database context.

  3. Add both migration services to your AppHost:

    C# — AppHost.cs
    var firstDb =sqlServer.AddDatabase("firstdb");
    var secondDb =postgres.AddDatabase("seconddb");
    var firstMigrations =builder.AddProject<Projects.FirstService_MigrationService>()
    .WithReference(firstDb);
    var secondMigrations =builder.AddProject<Projects.SecondService_MigrationService>()
    .WithReference(secondDb);
    // Ensure services wait for their respective migrations
    builder.AddProject<Projects.FirstService_Api>()
    .WithReference(firstDb)
    .WaitFor(firstMigrations);
    builder.AddProject<Projects.SecondService_Api>()
    .WithReference(secondDb)
    .WaitFor(secondMigrations);

Option 2: Single migration service with multiple contexts

Section titled “Option 2: Single migration service with multiple contexts”

Alternatively, you can create one migration service that handles multiple database contexts:

  1. Add references to all data projects in the migration service.

  2. Register all DbContexts in the migration service’s Program.cs.

  3. Modify the Worker.cs to apply migrations for each context:

    C# — Worker.cs
    publicasyncTask<bool>RunMigrationAsync(IServiceProvider serviceProvider)
    {
    awaitusingvar scope =serviceProvider.CreateAsyncScope();
    var firstContext =scope.ServiceProvider.GetRequiredService<FirstDbContext>();
    var secondContext =scope.ServiceProvider.GetRequiredService<SecondDbContext>();
    awaitfirstContext.Database.MigrateAsync();
    awaitsecondContext.Database.MigrateAsync();
    returntrue;
    }

Since the migration service seeds the database, you should remove the existing data seeding code from the API project.

  1. In the SupportTicketApi.Api project, open the Program.cs file.

  2. Delete the highlighted lines.

    C# — Program.cs
    if(app.Environment.IsDevelopment())
    {
    app.UseSwagger();
    app.UseSwaggerUI();
    using(var scope =app.Services.CreateScope())
    {
    var context =scope.ServiceProvider.GetRequiredService<TicketContext>();
    context.Database.EnsureCreated();
    if(!context.Tickets.Any())
    {
    context.Tickets.Add(newSupportTicket
    {
    Title="Initial Ticket",
    Description="Test ticket, please ignore."
    });
    context.SaveChanges();
    }
    }
    }

Now that the migration service is configured, run the app to test the migrations.

  1. Run the app and observe the SupportTicketApi dashboard.

  2. After a short wait, the migrations service state will display Finished.

  3. Select the Console logs icon on the migration service to investigate the logs showing the SQL commands that were executed.

You can find the completed sample app on GitHub.

The Aspire Shop sample app uses this approach to apply migrations. See the AspireShop.CatalogDbManager project for the migration service implementation.

Automated EF migrations with AddEFMigrations

Section titled “Automated EF migrations with AddEFMigrations”

In addition to creating a dedicated worker service as described above, Aspire provides a first-class AppHost API for managing EF Core migrations: AddEFMigrations. This API integrates migration execution directly into the AppHost orchestration and the Aspire publishing pipeline, without requiring a separate migration service project.

Add the 📦 Aspire.Hosting.EntityFrameworkCore NuGet package to your AppHost project:

.NET CLI
dotnetaddpackageAspire.Hosting.EntityFrameworkCore

Call AddEFMigrations on any project resource that hosts an EF Core DbContext:

C# — AppHost.cs
var db =builder.AddPostgres("pg").AddDatabase("appdb");
var api =builder.AddProject<Projects.Api>("api")
.WithReference(db);
var apiMigrations =api.AddEFMigrations("api-migrations");

The first argument is a resource name for the migration resource. The optional second argument is the fully qualified name of the DbContext type; it’s only required when the target assembly contains more than one DbContext.

If your migrations live in a separate project (for example, Projects.Data), reference that project from your AppHost and point to it using .WithMigrationsProject<Projects.Data>():

C# — AppHost.cs
var apiMigrations =api.AddEFMigrations("api-migrations")
.WithMigrationsProject<Projects.Data>();

To apply migrations automatically when running locally with aspire run, chain RunDatabaseUpdateOnStart():

C# — AppHost.cs
var apiMigrations =api.AddEFMigrations("api-migrations")
.RunDatabaseUpdateOnStart();
// The api project waits for migrations to complete before starting
api.WaitForCompletion(apiMigrations);

When RunDatabaseUpdateOnStart() is called, a health check is registered for the migration resource. The resource transitions through the following states:

StateDescription
PendingWaiting for database resource to become healthy
RunningExecuting the dotnet ef database update command
FinishedMigrations applied successfully
FailedToStartAn error occurred during migration

To generate migration artifacts during aspire publish, use PublishAsMigrationScript or PublishAsMigrationBundle:

C# — AppHost.cs
// Generate both a SQL script and a self-contained bundle during publish
var apiMigrations =api.AddEFMigrations("api-migrations")
.PublishAsMigrationScript()
.PublishAsMigrationBundle();

Both methods add pipeline steps that run during aspire publish and write their artifacts into the publish output directory under efmigrations/.

Publish the migration bundle as a container image

Section titled “Publish the migration bundle as a container image”

Pass publishContainer: true to PublishAsMigrationBundle to wrap the generated bundle in a container image. The migration resource becomes a compute resource that each compute environment (Docker Compose, Azure Container Apps, Kubernetes, and so on) deploys like any other container:

C# — AppHost.cs
var db =builder.AddPostgres("pg").AddDatabase("appdb");
var api =builder.AddProject<Projects.Api>("api").WithReference(db);
var apiMigrations =api.AddEFMigrations("api-migrations")
.WithReference(db)
.WaitFor(db)
.PublishAsMigrationBundle(publishContainer:true);

Key behaviors:

  • Use the baseImage argument to specify a custom base image for the generated container.
  • The targetRuntime argument defaults to linux-x64 when publishContainer: true; set it explicitly for other architectures (for example, linux-arm64).
  • Local run mode (aspire run) is unaffected — no container image is built, and the migration resource still appears in the dashboard with its tool commands.

Preventing container restarts per environment

Section titled “Preventing container restarts per environment”

A migration bundle is idempotent (running it twice is safe), but different compute environments need explicit configuration to avoid restarting the container after it exits.

Connect with us
SHA — 505a971 © Microsoft