File: EFBuildReadinessTests.cs
Web Access
Project: src\tests\Aspire.Hosting.EntityFrameworkCore.Tests\Aspire.Hosting.EntityFrameworkCore.Tests.csproj (Aspire.Hosting.EntityFrameworkCore.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREDOTNETPROJECT001, ASPIREPROJECTS001, ASPIREDOTNETTOOL
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.EntityFrameworkCore.Tests.TestServices;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Aspire.Hosting.EntityFrameworkCore.Tests;
 
public class EFBuildReadinessTests
{
    [Theory]
    [InlineData("ef-database-update")]
    [InlineData("ef-migrations-remove")]
    [InlineData("ef-database-status")]
    public async Task CommandsWaitForBuildWithoutWaitingForStartupApplication(string command)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var project = builder.AddProject<Projects.ServiceA>("api")
            .WithAnnotation(new DotnetProgramBuildCompletionAnnotation((_, cancellationToken) =>
            {
                entered.TrySetResult();
                return completion.Task.WaitAsync(cancellationToken);
            }));
        var migrations = project.AddEFMigrations("migrations");
        project.WaitFor(migrations);
        var tool = new TestEfTool();
        migrations.Resource.ToolResource = tool.Resource;
        using var app = builder.Build();
 
        var execution = app.Services.GetRequiredService<ResourceCommandService>().ExecuteCommandAsync(
            migrations.Resource, command, TestContext.Current.CancellationToken);
        await entered.Task.WaitAsync(TestContext.Current.CancellationToken);
        Assert.Empty(tool.Invocations);
        Assert.False(execution.IsCompleted);
 
        completion.SetResult();
        var result = await execution.WaitAsync(TestContext.Current.CancellationToken);
 
        Assert.True(result.Success, result.Message);
        Assert.NotEmpty(tool.Invocations);
        Assert.True(app.ResourceNotifications.TryGetCurrentState(migrations.Resource.Name, out var state));
        Assert.Equal(KnownResourceStates.Finished, state.Snapshot.State?.Text);
    }
 
    [Fact]
    public async Task AutomaticMigrationDoesNotBlockBeforeStartAndPreservesBuildFailure()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var project = builder.AddProject<Projects.ServiceA>("api")
            .WithAnnotation(new DotnetProgramBuildCompletionAnnotation((_, cancellationToken) =>
            {
                entered.TrySetResult();
                return completion.Task.WaitAsync(cancellationToken);
            }));
        var migrations = project.AddEFMigrations("migrations").RunDatabaseUpdateOnStart();
        project.WaitFor(migrations);
        var tool = new TestEfTool();
        migrations.Resource.ToolResource = tool.Resource;
        using var app = builder.Build();
        var token = TestContext.Current.CancellationToken;
 
        await builder.Eventing.PublishAsync(
            new BeforeStartEvent(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()), token);
        await entered.Task.WaitAsync(token);
        Assert.Empty(tool.Invocations);
 
        completion.SetException(new DistributedApplicationException("coordinated build failed"));
        await app.ResourceNotifications.WaitForResourceAsync(
            migrations.Resource.Name, KnownResourceStates.FailedToStart, token);
        Assert.Empty(tool.Invocations);
        await Assert.ThrowsAsync<DistributedApplicationException>(
            () => app.ResourceNotifications.WaitForDependenciesAsync(project.Resource, token));
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CustomBuildWarningDoesNotBypassAutomaticMigrationFailure(bool succeed)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        builder.Configuration["DEBUG_SESSION_PORT"] = "localhost:12345";
        var project = builder.AddDotnetProject("api", new Projects.ServiceA().ProjectPath,
            options => options.ExcludeLaunchProfile = true).WithBuildEnvironment("BUILD_FLAVOR", "custom");
        var migrations = project.AddEFMigrations("migrations").RunDatabaseUpdateOnStart();
        project.WaitFor(migrations);
        var tool = new TestEfTool
        {
            Result = succeed ? CommandResults.Success() : CommandResults.Failure("EF output is missing")
        };
        migrations.Resource.ToolResource = tool.Resource;
        using var app = builder.Build();
        var token = TestContext.Current.CancellationToken;
 
        await builder.Eventing.PublishAsync(
            new BeforeStartEvent(app.Services, app.Services.GetRequiredService<DistributedApplicationModel>()), token);
        await app.ResourceNotifications.WaitForResourceAsync(
            migrations.Resource.Name, succeed ? KnownResourceStates.Finished : KnownResourceStates.FailedToStart, token);
 
        Assert.Single(tool.Invocations);
        Assert.False(migrations.Resource.IsExecutingCommand);
        if (!succeed)
        {
            await Assert.ThrowsAsync<DistributedApplicationException>(
                () => app.ResourceNotifications.WaitForDependenciesAsync(project.Resource, token));
        }
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task OperationCanRetryAfterBuildFailureOrCancellation(bool cancel)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var first = true;
        var project = builder.AddProject<Projects.ServiceA>("api")
            .WithAnnotation(new DotnetProgramBuildCompletionAnnotation((_, _) =>
            {
                if (!first)
                {
                    return Task.CompletedTask;
                }
 
                first = false;
                return cancel
                    ? Task.FromCanceled(new CancellationToken(canceled: true))
                    : Task.FromException(new DistributedApplicationException("build failed"));
            }));
        var migrations = project.AddEFMigrations("migrations");
        var tool = new TestEfTool();
        migrations.Resource.ToolResource = tool.Resource;
        using var app = builder.Build();
        var commands = app.Services.GetRequiredService<ResourceCommandService>();
 
        var failed = await commands.ExecuteCommandAsync(
            migrations.Resource, "ef-database-update", TestContext.Current.CancellationToken);
        Assert.False(failed.Success);
        Assert.Equal(cancel, failed.Canceled);
        Assert.Empty(tool.Invocations);
        Assert.False(migrations.Resource.IsExecutingCommand);
 
        var retried = await commands.ExecuteCommandAsync(
            migrations.Resource, "ef-database-update", TestContext.Current.CancellationToken);
        Assert.True(retried.Success, retried.Message);
        Assert.Single(tool.Invocations);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task OperationWaitsForSeparateMigrationsProject(bool useResource)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("api");
        var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        var target = builder.AddProject<Projects.ServiceB>("migration-project")
            .WithAnnotation(new DotnetProgramBuildCompletionAnnotation((_, token) => completion.Task.WaitAsync(token)));
        var migrations = project.AddEFMigrations("migrations");
        if (useResource)
        {
            migrations.WithMigrationsProjectForPolyglot(target);
            Assert.Same(target.Resource, migrations.Resource.MigrationsProjectResource);
        }
        else
        {
            migrations.WithMigrationsProject<Projects.ServiceB>();
        }
        var tool = new TestEfTool();
        using var app = builder.Build();
        using var executor = new EFCoreOperationExecutor(
            migrations.Resource, NullLogger.Instance, TestContext.Current.CancellationToken, app.Services, tool.Resource);
 
        var operation = executor.UpdateDatabaseAsync();
        Assert.False(operation.IsCompleted);
        Assert.Empty(tool.Invocations);
        completion.SetResult();
 
        Assert.True((await operation).Success);
        var arguments = Assert.Single(tool.Invocations);
        Assert.Equal(new Projects.ServiceB().ProjectPath, arguments[Array.IndexOf(arguments, "--project") + 1]);
        Assert.Equal(new Projects.ServiceA().ProjectPath, arguments[Array.IndexOf(arguments, "--startup-project") + 1]);
    }
 
    [Fact]
    public async Task NewOperationWaitsForNewBuildGeneration()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var completion = Task.CompletedTask;
        var project = builder.AddProject<Projects.ServiceA>("api")
            .WithAnnotation(new DotnetProgramBuildCompletionAnnotation((_, token) => completion.WaitAsync(token)));
        var migrations = project.AddEFMigrations("migrations");
        var tool = new TestEfTool();
        using var app = builder.Build();
        using var executor = new EFCoreOperationExecutor(
            migrations.Resource, NullLogger.Instance, TestContext.Current.CancellationToken, app.Services, tool.Resource);
 
        Assert.True((await executor.UpdateDatabaseAsync()).Success);
        var nextBuild = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
        completion = nextBuild.Task;
        var nextOperation = executor.UpdateDatabaseAsync();
        Assert.False(nextOperation.IsCompleted);
        Assert.Single(tool.Invocations);
        nextBuild.SetResult();
 
        Assert.True((await nextOperation).Success);
        Assert.Equal(2, tool.Invocations.Count);
    }
}