File: AddEFMigrationsTests.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 ASPIREDOTNETTOOL
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.EntityFrameworkCore.Tests;
 
public class AddEFMigrationsTests
{
    [Fact]
    public void AddEFMigrationsCreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        Assert.NotNull(migrations);
        Assert.IsAssignableFrom<IResourceBuilder<EFMigrationResource>>(migrations);
        Assert.Equal("mymigrations", migrations.Resource.Name);
        Assert.Equal(project.Resource, migrations.Resource.ProjectResource);
        Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsWithoutContextTypeCreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations");
 
        Assert.NotNull(migrations);
        Assert.IsAssignableFrom<IResourceBuilder<EFMigrationResource>>(migrations);
        Assert.Equal("mymigrations", migrations.Resource.Name);
        Assert.Equal(project.Resource, migrations.Resource.ProjectResource);
        Assert.Null(migrations.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsWithExplicitContextTypeNameCreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        Assert.NotNull(migrations);
        Assert.IsAssignableFrom<IResourceBuilder<EFMigrationResource>>(migrations);
        Assert.Equal("mymigrations", migrations.Resource.Name);
        Assert.Equal(project.Resource, migrations.Resource.ProjectResource);
        Assert.Equal(typeof(TestDbContext).FullName, migrations.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsWithContextTypeNameStringCreatesResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var contextTypeName = "MyApp.Data.ApplicationDbContext";
        var migrations = project.AddEFMigrations("mymigrations", contextTypeName);
 
        Assert.NotNull(migrations);
        Assert.IsAssignableFrom<IResourceBuilder<EFMigrationResource>>(migrations);
        Assert.Equal("mymigrations", migrations.Resource.Name);
        Assert.Equal(project.Resource, migrations.Resource.ProjectResource);
        Assert.Equal(contextTypeName, migrations.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsAddsResourceToAppModel()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        using var app = builder.Build();
 
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var migrationResource = Assert.Single(appModel.Resources.OfType<EFMigrationResource>());
        Assert.Equal("mymigrations", migrationResource.Name);
    }
 
    [Fact]
    public void AddEFMigrationsForMultipleContextsSucceeds()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        var migrations1 = project.AddEFMigrations("migrations1", typeof(TestDbContext).FullName!);
        var migrations2 = project.AddEFMigrations("migrations2", typeof(AnotherDbContext).FullName!);
 
        Assert.NotEqual(migrations1.Resource, migrations2.Resource);
        Assert.Equal(typeof(TestDbContext).FullName, migrations1.Resource.DbContextTypeName);
        Assert.Equal(typeof(AnotherDbContext).FullName, migrations2.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsForMultipleContextsWithStringNamesSucceeds()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        var migrations1 = project.AddEFMigrations("migrations1", "MyApp.Data.AppDbContext");
        var migrations2 = project.AddEFMigrations("migrations2", "MyApp.Data.LoggingDbContext");
 
        Assert.NotEqual(migrations1.Resource, migrations2.Resource);
        Assert.Equal("MyApp.Data.AppDbContext", migrations1.Resource.DbContextTypeName);
        Assert.Equal("MyApp.Data.LoggingDbContext", migrations2.Resource.DbContextTypeName);
    }
 
    [Fact]
    public void AddEFMigrationsDuplicateContextTypeThrows()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        project.AddEFMigrations("migrations1", typeof(TestDbContext).FullName!);
 
        var exception = Assert.Throws<InvalidOperationException>(() =>
        {
            project.AddEFMigrations("migrations2", typeof(TestDbContext).FullName!);
        });
 
        Assert.Contains("TestDbContext", exception.Message);
        Assert.Contains("already been registered", exception.Message);
    }
 
    [Fact]
    public void AddEFMigrationsDuplicateContextTypeNameStringThrows()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        project.AddEFMigrations("migrations1", "MyApp.Data.AppDbContext");
 
        var exception = Assert.Throws<InvalidOperationException>(() =>
        {
            project.AddEFMigrations("migrations2", "MyApp.Data.AppDbContext");
        });
 
        Assert.Contains("AppDbContext", exception.Message);
        Assert.Contains("already been registered", exception.Message);
    }
 
    [Fact]
    public void AddEFMigrationsWithNullNameThrows()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        Assert.Throws<ArgumentNullException>(() =>
        {
            project.AddEFMigrations(null!, typeof(TestDbContext).FullName!);
        });
    }
 
    [Fact]
    public void AddEFMigrationsWithEmptyNameThrows()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        Assert.Throws<ArgumentException>(() =>
        {
            project.AddEFMigrations("", typeof(TestDbContext).FullName!);
        });
    }
 
    [Fact]
    public void AddEFMigrationsWithEmptyContextTypeNameThrows()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
 
        Assert.Throws<ArgumentException>(() =>
        {
            project.AddEFMigrations("mymigrations", "");
        });
    }
 
    [Fact]
    public void AddEFMigrationsHasResourceSnapshotAnnotation()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        var snapshotAnnotation = migrations.Resource.Annotations.OfType<ResourceSnapshotAnnotation>().FirstOrDefault();
        Assert.NotNull(snapshotAnnotation);
        Assert.Equal("EFMigration", snapshotAnnotation.InitialSnapshot.ResourceType);
        Assert.Equal("NotStarted", snapshotAnnotation.InitialSnapshot.State?.Text);
    }
 
    [Fact]
    public void AddEFMigrationsHasDatabaseIcon()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        var iconAnnotation = migrations.Resource.Annotations.OfType<ResourceIconAnnotation>().FirstOrDefault();
        Assert.NotNull(iconAnnotation);
        Assert.Equal("Database", iconAnnotation.IconName);
    }
 
    [Fact]
    public void EFMigrationResourceImplementsIResourceWithWaitSupport()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        Assert.IsAssignableFrom<IResourceWithWaitSupport>(migrations.Resource);
    }
 
    [Fact]
    public void EFMigrationResourceHasOptionsProperty()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        var migrations = project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!);
 
        Assert.False(migrations.Resource.PublishAsMigrationScript);
        Assert.False(migrations.Resource.PublishAsMigrationBundle);
    }
 
    [Fact]
    public async Task ToolResourceInheritsConnectionStringFromMigrationReference()
    {
        // The connection string the user declares via .WithReference(<db>) lands on the migration
        // resource, not on the startup project. The dotnet-ef tool resource must inherit it so the
        // design-time DbContext can open the database. Regression test for the
        // "The ConnectionString property has not been initialized." failure.
        //
        // The forwarding under test is operation-mode agnostic, so this asserts in Publish mode where
        // references resolve to manifest expressions synchronously. Run mode is intentionally avoided
        // because the tool resource also eagerly inherits the startup project's endpoint references,
        // and resolving those in Run mode awaits runtime endpoint allocation (a TaskCompletionSource
        // only completed by DCP when the app is actually running) — which deadlocks a plain unit test.
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var db = builder.AddResource(new TestDatabaseResource("db1"));
        var project = builder.AddProject<Projects.ServiceA>("myproject");
        project.AddEFMigrations("mymigrations", typeof(TestDbContext).FullName!)
            .WithReference(db);
 
        using var app = builder.Build();
        var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var toolResource = Assert.Single(appModel.Resources.OfType<DotnetToolResource>(), r => r.Name == "ef-tool-mymigrations");
 
#pragma warning disable CS0618 // Type or member is obsolete
        var env = await toolResource.GetEnvironmentVariableValuesAsync(DistributedApplicationOperation.Publish);
#pragma warning restore CS0618 // Type or member is obsolete
 
        Assert.Equal("{db1.connectionString}", Assert.Contains("ConnectionStrings__db1", env));
    }
 
    // Test classes for DbContext types
    private sealed class TestDbContext { }
    private sealed class AnotherDbContext { }
 
    /// <summary>
    /// A minimal test resource that implements IResourceWithConnectionString so it can be used as the
    /// target of <c>WithReference</c> on an EF migration resource.
    /// </summary>
    private sealed class TestDatabaseResource(string name) : Resource(name), IResourceWithConnectionString
    {
        public ReferenceExpression ConnectionStringExpression =>
            ReferenceExpression.Create($"Host=localhost;Database={Name}");
    }
}