File: DotnetProjectFacadeTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Dotnet.Tests\Aspire.Hosting.Dotnet.Tests.csproj (Aspire.Hosting.Dotnet.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
 
namespace Aspire.Hosting.Dotnet.Tests;
 
public class DotnetProjectFacadeTests(ITestOutputHelper outputHelper)
{
    [Fact]
    public void ConcreteFacadesAreStableAndRetainTheSameBuilder()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddDotnetProject("service", "Service.fsproj", options => options.ExcludeLaunchProfile = true);
 
        Assert.Same(resource, resource.WithReplicas(3));
        Assert.Same(resource, resource.DisableForwardedHeaders());
        Assert.Same(resource, resource.DisableForwardedHeaders());
        Assert.Same(resource, resource.WithEndpointsInEnvironment(endpoint => endpoint.Name == "http"));
        Assert.Equal(3, Assert.Single(resource.Resource.Annotations.OfType<ReplicaAnnotation>()).Replicas);
        Assert.Single(resource.Resource.Annotations.OfType<DisableForwardedHeadersAnnotation>());
        var filter = Assert.Single(resource.Resource.Annotations.OfType<EndpointEnvironmentInjectionFilterAnnotation>()).Filter;
        Assert.True(filter(new EndpointAnnotation(System.Net.Sockets.ProtocolType.Tcp, uriScheme: "http", name: "http")));
        Assert.False(filter(new EndpointAnnotation(System.Net.Sockets.ProtocolType.Tcp, uriScheme: "http", name: "admin")));
        Assert.Same(
            Assert.Single(resource.Resource.Annotations.OfType<IProjectMetadata>()),
            resource.Resource.GetProjectMetadata());
 
        var type = typeof(DotnetProjectHostingExtensions);
        Assert.Null(type.GetCustomAttribute<ExperimentalAttribute>());
        foreach (var name in new[]
        {
            nameof(DotnetProjectHostingExtensions.WithReplicas),
            nameof(DotnetProjectHostingExtensions.DisableForwardedHeaders),
            nameof(DotnetProjectHostingExtensions.WithEndpointsInEnvironment),
            nameof(DotnetProjectHostingExtensions.GetProjectMetadata)
        })
        {
            var method = Assert.Single(type.GetMethods(BindingFlags.Public | BindingFlags.Static), method => method.Name == name);
            Assert.Null(method.GetCustomAttribute<ExperimentalAttribute>());
            Assert.False(method.IsGenericMethod);
        }
    }
 
    [Fact]
    public void FacadesPreserveTheExistingPolyglotCapabilityIdsWithoutDuplicates()
    {
        var exports = typeof(DotnetProjectHostingExtensions).Assembly.GetTypes()
            .SelectMany(type => type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.DeclaredOnly))
            .SelectMany(method => method.GetCustomAttributes<AspireExportAttribute>().Select(export => (method, export)))
            .Where(item => item.export.Id is
                "withDotnetProjectReplicas" or
                "disableDotnetProjectForwardedHeaders" or
                "withDotnetProjectEndpointsInEnvironment")
            .OrderBy(item => item.export.Id, StringComparer.Ordinal)
            .ToArray();
 
        Assert.Collection(exports,
            item =>
            {
                Assert.Equal("disableDotnetProjectForwardedHeaders", item.export.Id);
                Assert.Equal("disableForwardedHeaders", item.export.MethodName);
                Assert.Equal(nameof(DotnetProjectHostingExtensions.DisableForwardedHeaders), item.method.Name);
                Assert.True(item.method.IsPublic);
            },
            item =>
            {
                Assert.Equal("withDotnetProjectEndpointsInEnvironment", item.export.Id);
                Assert.Equal("withEndpointsInEnvironment", item.export.MethodName);
                Assert.Equal(typeof(string[]), item.method.GetParameters()[1].ParameterType);
            },
            item =>
            {
                Assert.Equal("withDotnetProjectReplicas", item.export.Id);
                Assert.Equal("withReplicas", item.export.MethodName);
                Assert.Equal(nameof(DotnetProjectHostingExtensions.WithReplicas), item.method.Name);
                Assert.True(item.method.IsPublic);
            });
        var predicateFacade = typeof(DotnetProjectHostingExtensions).GetMethod(nameof(DotnetProjectHostingExtensions.WithEndpointsInEnvironment))!;
        Assert.Empty(predicateFacade.GetCustomAttributes<AspireExportAttribute>());
        Assert.NotNull(predicateFacade.GetCustomAttribute<AspireExportIgnoreAttribute>());
    }
 
    [Fact]
    public void ConcreteFacadesValidateNullArguments()
    {
        IResourceBuilder<DotnetProjectResource> missingBuilder = null!;
        Assert.Equal("builder", Assert.Throws<ArgumentNullException>(() => missingBuilder.WithReplicas(2)).ParamName);
        Assert.Equal("builder", Assert.Throws<ArgumentNullException>(missingBuilder.DisableForwardedHeaders).ParamName);
        Assert.Equal("builder", Assert.Throws<ArgumentNullException>(() => missingBuilder.WithEndpointsInEnvironment(_ => true)).ParamName);
        DotnetProjectResource missingResource = null!;
        Assert.Equal("projectResource", Assert.Throws<ArgumentNullException>(missingResource.GetProjectMetadata).ParamName);
 
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddDotnetProject("service", "Service.vbproj", options => options.ExcludeLaunchProfile = true);
        Assert.Equal("filter", Assert.Throws<ArgumentNullException>(() => resource.WithEndpointsInEnvironment(null!)).ParamName);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task EndpointFiltersComposeAndPolyglotNamesRemainCaseInsensitive(bool usePolyglotNames)
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddDotnetProject("service", "Service.vbproj", options => options.ExcludeLaunchProfile = true)
            .WithHttpEndpoint(port: 5111, name: "http")
            .WithHttpEndpoint(port: 5222, name: "admin")
            .WithHttpEndpoint(port: 5333, name: "metrics");
        if (usePolyglotNames)
        {
            Assert.Same(resource, resource.WithEndpointsInEnvironmentForPolyglot(["HTTP", "METRICS"]));
        }
        else
        {
            Assert.Same(resource, resource.WithEndpointsInEnvironment(endpoint => endpoint.Name != "admin"));
        }
        resource.WithEndpointsInEnvironment(endpoint => endpoint.Name != "metrics");
        var index = 0;
        foreach (var endpoint in resource.Resource.Annotations.OfType<EndpointAnnotation>())
        {
            endpoint.AllocatedEndpoint = new(endpoint, "localhost", endpoint.Port!.Value, targetPortExpression: $"p{index++}");
        }
        var environment = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
            resource.Resource, serviceProvider: TestServiceProvider.Instance);
 
        Assert.Equal("http://localhost:p0", environment["ASPNETCORE_URLS"]);
    }
 
    [Fact]
    public void ConcreteMetadataFacadeRetainsMissingDuplicateAndReplacedMetadataValidation()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var raw = new DotnetProjectResource("raw", builder.AppHostDirectory);
        Assert.Equal(
            "Resource 'raw' does not carry an IProjectMetadata annotation.",
            Assert.Throws<InvalidOperationException>(raw.GetProjectMetadata).Message);
 
        var resource = builder.AddDotnetProject("service", "Service.vbproj", options => options.ExcludeLaunchProfile = true);
        var original = resource.Resource.GetProjectMetadata();
        var replacement = new DotnetProjectMetadata(original.ProjectPath, buildConfiguration: null, "service");
        resource.Resource.Annotations.Add(replacement);
        Assert.Equal(
            "Resource 'service' carries more than one IProjectMetadata annotation. Project resources must carry exactly one stable project metadata annotation.",
            Assert.Throws<InvalidOperationException>(resource.Resource.GetProjectMetadata).Message);
 
        resource.Resource.Annotations.Remove(original);
        Assert.Equal(
            "Resource 'service' project metadata was replaced after project defaults were applied. Project metadata must remain unchanged after WithProjectDefaults configures the resource.",
            Assert.Throws<InvalidOperationException>(resource.Resource.GetProjectMetadata).Message);
    }
 
    [Fact]
    public async Task ForwardedHeadersFacadeChangesPublishEnvironmentWithoutChangingLegacyOverloadResolution()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        var path = Path.Combine(workspace.Path, "Service.vbproj");
        File.WriteAllText(path, "<Project />");
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var disabled = builder.AddDotnetProject("disabled", path, options => options.ExcludeLaunchProfile = true)
            .WithHttpEndpoint()
            .DisableForwardedHeaders();
        var enabled = builder.AddDotnetProject("enabled", path, options => options.ExcludeLaunchProfile = true)
            .WithHttpEndpoint();
#pragma warning disable CS0618 // Legacy and V2 concrete helper overloads remain independently callable.
        IResourceBuilder<ProjectResource> legacy = builder.AddProject("legacy", path, options => options.ExcludeLaunchProfile = true)
            .WithReplicas(2)
            .DisableForwardedHeaders()
            .WithEndpointsInEnvironment(_ => true);
#pragma warning restore CS0618
        IResourceBuilder<DotnetProjectResource> modern = disabled.WithReplicas(2).WithEndpointsInEnvironment(_ => true);
 
        var disabledEnvironment = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
            modern.Resource, DistributedApplicationOperation.Publish);
        var enabledEnvironment = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
            enabled.Resource, DistributedApplicationOperation.Publish);
        Assert.False(disabledEnvironment.ContainsKey("ASPNETCORE_FORWARDEDHEADERS_ENABLED"));
        Assert.Equal("true", enabledEnvironment["ASPNETCORE_FORWARDEDHEADERS_ENABLED"]);
        Assert.Equal(2, Assert.Single(legacy.Resource.Annotations.OfType<ReplicaAnnotation>()).Replicas);
        Assert.Same(disabled, modern);
    }
}