Skip to content

Add Aspire.Hosting.EntityFrameworkCore hosting integration for EF Core migration management#13481

Open
Copilot wants to merge 4 commits intomainfrom
copilot/add-hosting-integration-ef-migrations
Open

Add Aspire.Hosting.EntityFrameworkCore hosting integration for EF Core migration management#13481
Copilot wants to merge 4 commits intomainfrom
copilot/add-hosting-integration-ef-migrations

Conversation

Copy link
Contributor

Copilot AI commented Dec 10, 2025

Description

New hosting integration package Aspire.Hosting.EntityFrameworkCore that provides EF Core migration management commands for Aspire AppHost projects.

Fixes #10306

Key Features

  • AddEFMigrations extension methods - Add migration resources to project resources with optional DbContext type specification. Supports both generic AddEFMigrations<TContext>() and string-based AddEFMigrations(name, contextTypeName) overloads.
  • EFMigrationResourceBuilder - Custom builder class with fluent configuration methods: RunDatabaseUpdateOnStart(), PublishAsMigrationScript(), PublishAsMigrationBundle(), WithMigrationOutputDirectory(), WithMigrationNamespace(), WithMigrationsProject()
  • 6 resource commands on project resource - Update Database, Drop Database, Reset Database, Add Migration (with interactive prompt), Remove Migration, Get Database Status - commands are added to the original project resource with context name suffix to avoid confusion when multiple DbContexts are registered
  • WaitFor support with health checks - EFMigrationResource implements IResourceWithWaitSupport. When RunDatabaseUpdateOnStart() is called, a health check is registered that reports healthy only when migrations complete. Migrations wait for the project resource to be healthy before executing.
  • Separate migration project support - Use WithMigrationsProject() when migrations are in a different project than the startup project. Both startup and target assemblies are loaded in the same AssemblyLoadContext.
  • Interactive migration naming - Uses IInteractionService.PromptInputAsync for Add Migration command with recompilation notification via PromptNotificationAsync. Remove Migration also shows recompilation notification.
  • Database status display - Uses IInteractionService.PromptMessageBoxAsync to show current migration status
  • Event-driven startup and publishing - EFMigrationEventSubscriber handles AfterResourcesCreatedEvent for startup migrations (after project resource is healthy) and BeforePublishEvent for generating migration scripts/bundles during publish

Checklist

  • Create new Aspire.Hosting.EntityFrameworkCore project structure
  • Create EFMigrationResource with IResourceWithWaitSupport
  • Create extension methods and EFMigrationResourceBuilder
  • Implement resource commands (on project resource with context name suffix)
  • Implement RunDatabaseUpdateOnStart with event subscriber (waits for project resource to be healthy)
  • Create EFCoreOperationExecutor using collectible AssemblyLoadContext
  • Integrate with resource health checks
  • Add migration configuration options (output directory, namespace, migrations project)
  • Integrate with publish pipeline for script/bundle generation
  • Use MSBuild with configuration/runtime heuristics for property resolution
  • All 50 tests pass
Original prompt

Create a new hosting integration that provides EF migration management commands. Start by creating a detailed step-by-step checklist as an .md file that should be updated as you finish tasks. Add tests for all the new functionality, including tests that use other features that could interact with this, like explicit start and wait for.

Package name: Aspire.Hosting.EFCoreCommands
Assembly name: Aspire.Hosting.EFCoreCommands

High-level feature outline

  • Support for producing migration scripts and/or migration bundles as part of publishing the AppHost, e.g. .PublishAsMigrationScript()
  • Support for running migrations on AppHost start and having other resources wait on the migrations to complete, e.g.:
    var api = builder.AddProject<Api>("api");
    var apiMigrations = api.AddEFMigrations("api-migrations").RunDatabaseUpdateOnStart();
  • Adds resource commands to resources that AddEfMigrations was called on:
    • "Update database": Runs the equivalent of ef database update command on the resource
    • "Drop database": Runs the equivalent of ef database drop command on the resource
    • "Reset database": Runs equivalent of ef database drop followed by dotnet ef database update on the resource
    • "Add migration...": Prompts for a migration name (and some options from dotnet ef migrations add) before running the equivalent of dotnet ef migrations add <NAME> on the resource. Add some notification to indicate that in order to apply the new migration the target project needs to be recompiled.
    • "Remove migration": Runs the equivalent of ef migrations remove on the resource to remove the last migration
    • "Get database status": Shows the current migration of the database or whether it hasn't been created. Whether the current migration is also the latest and whether there are any pending model changes.
  • AddEFMigrations should allow a context type to be specified. Multiple calls on the same resource with different context types are allowed and will add commands for each context used. Using the same context type results in an exception.
  • The implementation will check that the target references Microsoft.EntityFrameworkcore.Design and show an error message if it doesn't. Then it will use reflection to invoke commands on it with appropriate arguments, see https://github.com/dotnet/efcore/blob/main/src/ef/ReflectionOperationExecutor.cs
  • IOperationReporter should be used to capture the output of the operations

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

Copilot AI changed the title [WIP] Add hosting integration for EF migration management commands Add Aspire.Hosting.EFCoreCommands hosting integration for EF Core migration management Dec 10, 2025
Copilot AI requested a review from AndriySvyryd December 10, 2025 23:34
Copy link
Member

@AndriySvyryd AndriySvyryd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Continue implementation according to the checklist

Copy link
Contributor Author

Copilot AI commented Dec 11, 2025

@copilot Continue implementation according to the checklist

All review comments have been addressed in 5c8b132. Added EFCoreCommands demo to SqlServerEndToEnd playground and addressed all documentation feedback.

@github-actions
Copy link
Contributor

The transient CI rerun workflow requested reruns for the following jobs after analyzing the failed attempt.
GitHub's job rerun API also reruns dependent jobs, so the retry is being tracked in the rerun attempt.
The job links below point to the failed attempt that matched the retry-safe transient failure rules.

@joperezr
Copy link
Member

@AndriySvyryd where are we with this? is the last remaining thread the one above about exceptions not getting catched potentially? If we want to include this in 13.2, it should get merged today. If there are still things to fix, let's instead re-target to main and land it in 13.3

@github-actions
Copy link
Contributor

The transient CI rerun workflow requested reruns for the following jobs after analyzing the failed attempt.
GitHub's job rerun API also reruns dependent jobs, so the retry is being tracked in the rerun attempt.
The job links below point to the failed attempt that matched the retry-safe transient failure rules.

@joperezr
Copy link
Member

@joperezr Addressed the potential unobservable exceptions. This is ready to merge. Future improvements tracked in:

Awesome, thanks so much for working hard on getting this through the finish line @AndriySvyryd, I'm excited to try this one out. Unfortunately, we are too late to try to squish this in for 13.2 now as we are in escrow mode, but can I ask that you retarget this to main and we can merge immediately there and start trying it out?

…anagement

- Create EFMigrationResource with IResourceWithWaitSupport
- Add AddEFMigrations extension methods for project resources
- Implement 6 resource commands: Update, Drop, Reset, Add, Remove, Status
- Add RunDatabaseUpdateOnStart and PublishAs* configuration methods
- Add EFCoreOperationExecutor for command execution out-of-proc

Fixes #10306

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@DamianEdwards
Copy link
Member

@joperezr seems this is ready to merge to main?

@eerhardt
Copy link
Member

Code Review - Aspire.Hosting.EntityFrameworkCore

I've reviewed this PR for consistency with Aspire codebase conventions. Here are my findings:


🐛 Bug: Fire-and-forget in RunDatabaseUpdateOnStart silently swallows failures

File: EFMigrationResourceBuilderExtensions.csRunDatabaseUpdateOnStart()

builder.ApplicationBuilder.Eventing.Subscribe<BeforeStartEvent>((@event, ct) =>
{
    // Schedule the migration command to run asynchronously after startup completes to avoid deadlocks.
    // See #15234
    var _ = ExecuteMigrationsAsync(@event.Services, migrationResource, ct);
    return Task.CompletedTask;
});

The fire-and-forget pattern (var _ = ...) means if ExecuteMigrationsAsync throws an exception before reaching the try/catch inside that method (e.g., GetRequiredService throws), the exception will be swallowed entirely and go unobserved. This could result in a TaskScheduler.UnobservedTaskException event and silent migration failures.

Consider wrapping in a top-level try/catch or using _ = Task.Run(async () => { try { await ... } catch { ... } }) to ensure no unobserved task exceptions occur. Alternatively, capture the task and register a continuation to log failures.


🐛 Bug: IsExecutingCommand is not thread-safe

File: EFResourceBuilderExtensions.csExecuteWithStateManagementAsync()

migrationResource.IsExecutingCommand is a plain bool property that serves as a concurrency guard, but it's accessed from multiple threads without synchronization. Two rapid command invocations could both read false, both set it to true, and execute concurrently. Consider using Interlocked.CompareExchange with an int field, or a SemaphoreSlim(1,1) for a proper mutual exclusion guard.


🐛 Bug: RequiresRebuild is set but never cleared

File: EFResourceBuilderExtensions.cs

After AddMigration or RemoveMigration, migrationResource.RequiresRebuild = true is set, which disables all commands. However, there is no mechanism to clear this flag. The user would need to restart the AppHost to re-enable commands. This should be documented more explicitly, or a mechanism to detect a rebuild (e.g., watching project output timestamps) and clear the flag should be implemented.


⚠️ Suggestion: EFCoreOperationExecutor modifies DotnetToolResource.Annotations collection directly

File: EFCoreOperationExecutor.csExecuteEfCommandAsync() and EnsurePathsInitialized()

The executor mutates the shared _toolResource.Annotations collection by adding/removing CommandLineArgsCallbackAnnotation and ExecutableAnnotation objects. If two commands were ever executed concurrently (despite the IsExecutingCommand guard, which as noted above isn't thread-safe), this would corrupt the annotations collection. Consider using a lock or ensuring the tool resource is truly single-use.


⚠️ Suggestion: EFCoreOperationExecutor constructor does work in the constructor

File: EFCoreOperationExecutor.cs — Constructor

The constructor calls Assembly.GetEntryAssembly(), reads custom attributes, and calls ParseBuildSettingsFromPath(). This is a lot of logic for a constructor. Per Aspire conventions, consider moving initialization to a factory method or the EnsurePathsInitialized() method so the constructor remains lightweight.


⚠️ Suggestion: Missing Aspire-Core.slnf inclusion

The new project src/Aspire.Hosting.EntityFrameworkCore and test project tests/Aspire.Hosting.EntityFrameworkCore.Tests are added to Aspire.slnx but not to Aspire-Core.slnf. Other hosting packages and their tests (e.g., Aspire.Hosting.Containers.Tests, Aspire.Hosting.Testing.Tests) are included in this filter. Consider adding both projects to Aspire-Core.slnf for consistency.


⚠️ Suggestion: csproj uses $(AllTargetFrameworks) instead of $(DefaultTargetFramework)

File: Aspire.Hosting.EntityFrameworkCore.csproj

<TargetFrameworks>$(AllTargetFrameworks)</TargetFrameworks>

Most other hosting integration packages (e.g., Aspire.Hosting.PostgreSQL, Aspire.Hosting.Redis) use multi-targeting. Verify this is intentional and that the package needs to target all frameworks. If it only depends on Aspire.Hosting, it should match whatever that project targets.


⚠️ Suggestion: Task.Delay(200) for log flushing is fragile

File: EFCoreOperationExecutor.csExecuteEfCommandAsync()

// Give a moment for logs to flush, then cancel log capture
await Task.Delay(200, _cancellationToken).ConfigureAwait(false);

A hardcoded 200ms delay for log flushing is unreliable, especially under CI load. Consider using a more deterministic approach — e.g., waiting until the log channel completes naturally after the resource exits, or using a timeout-based drain.


📝 Nit: EFOperationResult could be a record or use required properties

File: EFCoreOperationExecutor.cs

internal sealed class EFOperationResult
{
    public bool Success { get; init; }
    public string? ErrorMessage { get; init; }
    public string? Output { get; init; }
}

Consider making this a record for immutability and value equality semantics, which is more consistent with Aspire's use of records for snapshot/result types (e.g., ResourceStateSnapshot, CustomResourceSnapshot).


📝 Nit: README.md has a TODO comment

File: README.md

<!-- TODO: Update this to the EntityFrameworkCore-specific page once published, https://github.com/microsoft/aspire.dev/issues/536 -->

This is fine for now, but ensure this is tracked and resolved before the package ships. The link to the aspire.dev issue is good.


Summary

Overall, this is a well-structured integration that follows Aspire patterns. The main concerns are:

  1. Thread safety of IsExecutingCommand and annotation mutation
  2. Fire-and-forget task in RunDatabaseUpdateOnStart needs better unobserved-exception handling
  3. RequiresRebuild has no reset mechanism, permanently disabling commands after add/remove migration until restart

…ameworkCore.csproj

Co-authored-by: Eric Erhardt <eric.erhardt@microsoft.com>
@github-actions
Copy link
Contributor

Re-running the failed jobs in the CI workflow for this pull request because 2 jobs were identified as retry-safe transient failures in the CI run attempt.
GitHub was asked to rerun all failed jobs for that attempt, and the rerun is being tracked in the rerun attempt.
The job links below point to the failed attempt jobs that matched the retry-safe transient failure rules.

@github-actions
Copy link
Contributor

🎬 CLI E2E Test Recordings — 52 recordings uploaded (commit 0e18c86)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndDeployToDockerCompose ▶️ View Recording
CreateAndDeployToDockerComposeInteractive ▶️ View Recording
CreateAndPublishToKubernetes ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23324581652

Copy link
Member

@eerhardt eerhardt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry these weren't posted.

/// <param name="targetRuntime">The target runtime identifier for the bundle (e.g., "linux-x64", "win-x64"). If null, uses the current runtime.</param>
/// <param name="selfContained">If <c>true</c>, creates a self-contained bundle that includes the .NET runtime.</param>
/// <returns>The resource builder for chaining.</returns>
[AspireExport]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these have names on them? To make them camelCase.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AspireExportAttribute.MethodName XML docs state Each language generator will apply its own formatting convention (camelCase for TypeScript, snake_case for Python, etc.).

{
yield return new PipelineStep
{
Name = $"{migrationResource.Name}-generate-migration-bundle",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these steps be hooked into an existing step? Like hook it on "publish" or something? So when someone does aspire publish these scripts/bundles get generated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a hosting integration for the dotnet ef global tool

8 participants