File: ManifestExpressionTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.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.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting.Tests;
 
[Trait("Partition", "5")]
public class ManifestExpressionTests(ITestOutputHelper outputHelper)
{
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task CustomSecretProviderIsNeverMaterialized(bool dependencyOnly)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var calls = 0;
        var secret = new TestManifestValue("{vault.custom.key}", (_, _) =>
        {
            calls++;
            return ValueTask.FromResult<string?>("must-not-be-published");
        });
        var consumer = builder.AddContainer("consumer", "image");
        if (dependencyOnly)
        {
            consumer.WithManifestPublishingCallback(context =>
            {
                context.Writer.WriteString("type", "custom.v0");
                context.TryAddDependentResources(secret);
            });
        }
        else
        {
            consumer.WithEnvironment("SECRET", secret);
        }
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        Assert.Equal(0, calls);
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json").UseParameters(dependencyOnly);
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task EndpointOriginInputIdentityDoesNotDependOnReferenceOrder(bool reverseOrder)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var first = builder.AddContainer("a-b", "image").WithHttpEndpoint(name: "c", targetPort: 80);
        var second = builder.AddContainer("a", "image").WithHttpEndpoint(name: "b-c", targetPort: 80);
        var consumer = builder.AddContainer("consumer", "image");
        if (reverseOrder)
        {
            consumer.WithEnvironment("SECOND", second.GetEndpoint("b-c").BrowserOrigins);
            consumer.WithEnvironment("FIRST", first.GetEndpoint("c").BrowserOrigins);
        }
        else
        {
            consumer.WithEnvironment("FIRST", first.GetEndpoint("c").BrowserOrigins);
            consumer.WithEnvironment("SECOND", second.GetEndpoint("b-c").BrowserOrigins);
        }
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
        var variables = manifest["resources"]!["consumer"]!["env"]!.AsObject();
        Assert.Equal("{a-b-c-BrowserOrigins.value}", variables["FIRST"]!.GetValue<string>());
        var secondName = ResourceNameHelpers.GetEndpointPropertyInputName("a", "b-c", nameof(EndpointProperty.BrowserOrigins), ModelName.DefaultMaxLength);
        Assert.Equal($"{{{secondName}.value}}", variables["SECOND"]!.GetValue<string>());
        Assert.Equal(
            ["a-b-c-BrowserOrigins", secondName],
            manifest["resources"]!.AsObject().Where(resource => resource.Value!["type"]!.GetValue<string>() == "parameter.v0")
                .Select(resource => resource.Key).Order(StringComparer.Ordinal));
    }
 
    [Fact]
    public async Task SymbolicOnlyProvidersPreserveOpaqueTokensAlongsideLoweredOrigins()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var origin = frontend.GetEndpoint("http").BrowserOrigins;
        var opaque = new TestManifestExpression("{customValue}");
        var mixed = new TestManifestExpression($"{opaque.ValueExpression}|{origin.ValueExpression}", opaque, origin);
        var module = builder.AddResource(new AzureBicepResource("module", templateString: "param custom string\nparam mixed string"));
        module.Resource.Parameters["custom"] = opaque;
        module.Resource.Parameters["mixed"] = mixed;
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>(), workspace.Path)
            .DefaultTimeout();
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData("{resource.connectionString}", true)]
    [InlineData("{parameter.value}", true)]
    [InlineData("{parameter.inputs.value}", true)]
    [InlineData("{infra.outputs.host}", true)]
    [InlineData("{infra.secretOutputs.password}", true)]
    [InlineData("{infra.secretOutputs}", true)]
    [InlineData("{infra.secrets.password}", true)]
    [InlineData("{.outputs.host}", true)]
    [InlineData("{frontend.bindings.http.url}", true)]
    [InlineData("{frontend.bindings.http.host}", true)]
    [InlineData("{frontend.bindings.http.port}", true)]
    [InlineData("{frontend.bindings.http.targetPort}", true)]
    [InlineData("{frontend.bindings.http.scheme}", true)]
    [InlineData("{frontend.bindings.http.protocol}", true)]
    [InlineData("{frontend.bindings.http.transport}", true)]
    [InlineData("{frontend.bindings.http.external}", true)]
    [InlineData("{frontend.containerImage}", true)]
    [InlineData("{frontend.containerPort}", true)]
    [InlineData("{frontend.volumes.0.storage}", true)]
    [InlineData("{frontend.bindMounts.0.storage}", true)]
    [InlineData("{frontend.bindings.http.tlsEnabled}", false)]
    [InlineData("{frontend.customProperty}", false)]
    [InlineData("{frontend.bindings.http.customProperty}", false)]
    [InlineData("{frontend.bindings.http}", false)]
    [InlineData("{frontend.volumes.bad.storage}", false)]
    [InlineData("{frontend.volumes.-1.storage}", false)]
    [InlineData("{parameter.inputs.value.extra}", false)]
    [InlineData("{.unknown.host}", false)]
    [InlineData("{unknown}", false)]
    public void ClassifiesManifestVocabularyIndependentlyOfProviderType(string expression, bool supported)
    {
        Assert.Equal(supported, ManifestExpression.IsSupportedReference(expression));
    }
 
    [Fact]
    public void TokenizationPreservesEscapedReferences()
    {
        var expression = "{{ignored.value}}/{real.value}/{{{other.value}}}";
 
        Assert.Collection(ManifestExpression.GetTokens(expression),
            token => Assert.Equal("{real.value}", expression.Substring(token.Start, token.Length)),
            token => Assert.Equal("{other.value}", expression.Substring(token.Start, token.Length)));
    }
 
    [Theory]
    [InlineData("{unclosed")]
    [InlineData("unexpected}")]
    public void MalformedExpressionsFailExplicitly(string expression)
    {
        Assert.Throws<FormatException>(() => ManifestExpression.GetTokens(expression).ToArray());
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task UnsupportedEndpointPropertyBecomesConcreteInDirectAndSelectedValues(bool tlsEnabled)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var publicHost = builder.AddParameter("public-host");
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var endpoint = frontend.GetEndpoint("http");
        endpoint.EndpointAnnotation.TlsEnabled = tlsEnabled;
        var tls = endpoint.Property(EndpointProperty.TlsEnabled);
        var selected = ReferenceExpression.CreateConditional(tls, tlsEnabled.ToString(),
            ReferenceExpression.Create($"https://{publicHost}?tls={tls}"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image")
            .WithEnvironment("TLS", tls)
            .WithEnvironment("URL", selected);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseParameters(tlsEnabled);
    }
 
    [Fact]
    public async Task UnsupportedCustomPropertyRemainsSymbolicAlongsideParameters()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var publicHost = new ParameterResource("public-host", _ =>
            throw new InvalidOperationException("A supported deployment parameter must stay symbolic."));
        var calls = 0;
        var custom = new TestManifestValue("{frontend.customProperty}", (context, _) =>
        {
            Assert.True(context.ExecutionContext!.IsPublishMode);
            calls++;
            return ValueTask.FromResult<string?>("path with spaces/{literal.value}");
        });
        builder.AddContainer("backend", "image")
            .WithEnvironment("CUSTOM", custom)
            .WithEnvironment("URL", ReferenceExpression.Create($"https://{publicHost}/{custom}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        Assert.Equal(0, calls);
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task DistinctProviderInstancesRetainValuesAndDeterministicNames()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var firstCalls = 0;
        var secondCalls = 0;
        var first = new TestManifestValue("{opaque}", (_, _) =>
        {
            firstCalls++;
            return ValueTask.FromResult<string?>("first");
        });
        var second = new TestManifestValue("{opaque}", (_, _) =>
        {
            secondCalls++;
            return ValueTask.FromResult<string?>("second");
        });
        var selected = ReferenceExpression.CreateConditional(new ParameterResource("enabled", _ => bool.TrueString),
            bool.TrueString, ReferenceExpression.Create($"{first}|{second}|{first}"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image")
            .WithEnvironment("FIRST", first)
            .WithEnvironment("VALUES", selected);
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        var manifest = await ManifestUtils.GetManifestForModel(model);
 
        Assert.Equal(0, firstCalls);
        Assert.Equal(0, secondCalls);
        var repeated = await ManifestUtils.GetManifestForModel(model);
        Assert.Equal(0, firstCalls);
        Assert.Equal(0, secondCalls);
        Assert.True(JsonNode.DeepEquals(manifest, repeated));
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task WrappedStructuredExpressionsPreserveProviderOccurrencesAndFormats(bool reverseOrder)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var first = builder.AddParameter("first");
        var second = builder.AddParameter("second");
        var inner = reverseOrder
            ? ReferenceExpression.Create($"{second:uri}|{first}|{second}")
            : ReferenceExpression.Create($"{first}|{second:uri}|{first}");
        var wrapper = new TestManifestValue(inner.ValueExpression, (_, _) =>
            throw new InvalidOperationException("A structured wrapper must not be resolved."), inner);
        builder.AddContainer("backend", "image").WithEnvironment("VALUES", wrapper);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseParameters(reverseOrder);
    }
 
    [Fact]
    public async Task WrappedRepeatedProviderIsNotAmbiguous()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var calls = 0;
        var provider = new TestManifestValue("{opaque}", (_, _) =>
        {
            calls++;
            return ValueTask.FromResult<string?>("value");
        });
        var wrapper = new TestManifestValue("prefix/{opaque}/{opaque}", (_, _) =>
            throw new InvalidOperationException("An explainable wrapper must not be resolved."), provider, provider);
        builder.AddContainer("backend", "image").WithEnvironment("VALUES", wrapper);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        Assert.Equal(0, calls);
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task AmbiguousWrapperTokensFailWithoutSelectingAProvider()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var first = new TestManifestValue("{opaque}", (_, _) => throw new InvalidOperationException("Do not resolve first."));
        var second = new TestManifestValue("{opaque}", (_, _) => throw new InvalidOperationException("Do not resolve second."));
        var wrapper = new TestManifestValue("prefix/{opaque}/{opaque}", (_, _) =>
            throw new InvalidOperationException("Do not resolve the ambiguous wrapper."), first, second);
        builder.AddContainer("backend", "image").WithEnvironment("VALUES", wrapper);
        using var app = builder.Build();
 
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() => ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.Equal(
            "Manifest expression '{opaque}' matches multiple distinct providers without unambiguous expression structure.",
            error.Message);
    }
 
    [Fact]
    public async Task ConcreteNamesAvoidExistingModelResourceNames()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var backend = builder.AddContainer("backend", "image").WithHttpEndpoint(targetPort: 8080);
        backend.WithEnvironment("TLS", backend.GetEndpoint("http").Property(EndpointProperty.TlsEnabled));
        using var app = builder.Build();
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
        var initial = await ManifestUtils.GetManifestForModel(model);
        var name = Assert.Single(initial["resources"]!.AsObject(), resource =>
            resource.Value!["type"]!.GetValue<string>() == "value.v0").Key;
        model.Resources.Add(new ParameterResource(name, _ => "reserved"));
 
        var manifest = await ManifestUtils.GetManifestForModel(model);
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task SupportedCustomProviderRetainsSymbolicValueWithoutResolution()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var host = builder.AddParameter("public-host");
        var symbolic = new TestManifestValue("{public-host.value}", (_, _) =>
            throw new InvalidOperationException("A supported symbolic provider must not be resolved."), host.Resource);
        builder.AddContainer("backend", "image").WithEnvironment("HOST", symbolic);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task LiteralManifestValuesAreFormattedWithoutResolvingProviders()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var literal = new TestManifestValue("path with spaces/{{literal}}", (_, _) =>
            throw new InvalidOperationException("A literal manifest value must not be resolved."));
        builder.AddContainer("backend", "image")
            .WithEnvironment("URL", ReferenceExpression.Create($"https://public.example/{literal:uri}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task CompositeUriFormattingEncodesLiteralsAndDefersParameters()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var host = new ParameterResource("host", _ =>
            throw new InvalidOperationException("A deferred parameter must not be resolved."));
        var query = new ParameterResource("query", _ =>
            throw new InvalidOperationException("A deferred parameter must not be resolved."));
        var wrapper = new TestManifestValue("prefix/{host.value}/{{literal}}?q={query.value}&again={host.value}",
            (_, _) => throw new InvalidOperationException("A symbolic wrapper must not be resolved."), host, query);
        var selected = ReferenceExpression.CreateConditional(new ParameterResource("enabled", _ => bool.TrueString),
            bool.TrueString, ReferenceExpression.Create($"{wrapper:uri}"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", selected);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task UriFormattingExpandsOnlySelectedConditionalAliases()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var host = builder.AddParameter("public-host");
        var conditionCalls = 0;
        var condition = new ParameterResource("selected", _ =>
        {
            conditionCalls++;
            return bool.TrueString;
        });
        var custom = new TestManifestValue("path with spaces/{{literal}}", (_, _) =>
            throw new InvalidOperationException("A literal expression must not be resolved."));
        var inactive = ReferenceExpression.CreateConditional(new ParameterResource("inactive", _ =>
                throw new InvalidOperationException("An unselected condition must not be resolved.")),
            bool.TrueString, ReferenceExpression.Empty, ReferenceExpression.Empty);
        var conditional = ReferenceExpression.CreateConditional(condition,
            bool.TrueString, ReferenceExpression.Create($"https://{host}/{custom}"), inactive);
        var wrapper = new TestManifestValue(conditional.ValueExpression, (_, _) =>
            throw new InvalidOperationException("The selected expression must remain symbolic."), conditional);
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", ReferenceExpression.Create($"{wrapper:uri}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        Assert.Equal(1, conditionCalls);
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData(EndpointProperty.BrowserOrigins)]
    [InlineData(EndpointProperty.PrimaryBrowserOrigin)]
    public async Task EndpointOriginPropertiesBecomeRequiredInputs(EndpointProperty property)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var endpoint = frontend.GetEndpoint("http");
        var origin = endpoint.Property(property);
        var wrapped = new TestManifestValue(origin.ValueExpression, (_, _) =>
            throw new InvalidOperationException("A manifest input must not read runtime values."), origin);
        var selected = ReferenceExpression.CreateConditional(endpoint.Property(EndpointProperty.TlsEnabled), bool.FalseString,
            ReferenceExpression.Create($"{frontend.GetEndpoint("HTTP").Property(property)}"),
            ReferenceExpression.Create($"unused"));
        builder.AddContainer("backend", "image")
            .WithEnvironment("DIRECT", origin)
            .WithEnvironment("COMPOSITE", ReferenceExpression.Create($"prefix/{origin}/suffix"))
            .WithEnvironment("WRAPPED", wrapped)
            .WithEnvironment("SELECTED", selected)
            .WithEnvironment("ENCODED", ReferenceExpression.Create($"{origin:uri}"))
            .WithEnvironment("ENDPOINT", endpoint)
            .WithEnvironment("HOST", endpoint.Property(EndpointProperty.Host));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>())
            .DefaultTimeout();
 
        Assert.False(endpoint.EndpointAnnotation.BrowserUrlsSnapshot.IsValueSet);
        Assert.Collection(manifest["resources"]!.AsObject().Where(resource => resource.Value!["type"]!.GetValue<string>() == "parameter.v0"),
            input => Assert.Equal($"frontend-http-{property}", input.Key));
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json").UseParameters(property);
    }
 
    [Fact]
    public async Task EndpointOriginInputsAreLoweredInResourceManifestCallbacks()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var origin = frontend.GetEndpoint("http").BrowserOrigins;
        var module = builder.AddResource(new AzureBicepResource("module", templateString: "param origin string\nparam redirect string"));
        module.Resource.Parameters["origin"] = origin;
        module.Resource.Parameters["redirect"] = () => ReferenceExpression.Create($"{origin}/signin-oidc");
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>(), workspace.Path)
            .DefaultTimeout();
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task EndpointOriginInputsAvoidExistingResourceNames()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddParameter("frontend-http-BrowserOrigins");
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        builder.AddContainer("backend", "image")
            .WithEnvironment("ORIGIN", frontend.GetEndpoint("http").BrowserOrigins);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>())
            .DefaultTimeout();
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData(true)]
    [InlineData(false)]
    public async Task UnselectedEndpointOriginDoesNotRequireDeploymentInput(bool originInTrueBranch)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var origin = ReferenceExpression.Create($"{frontend.GetEndpoint("http").Property(EndpointProperty.BrowserOrigins)}");
        var deployed = ReferenceExpression.Create($"https://deployed.example");
        var conditional = ReferenceExpression.CreateConditional(
            frontend.GetEndpoint("http").Property(EndpointProperty.TlsEnabled),
            originInTrueBranch ? bool.TrueString : bool.FalseString,
            originInTrueBranch ? origin : deployed,
            originInTrueBranch ? deployed : origin);
        builder.AddContainer("backend", "image").WithEnvironment("ORIGIN", conditional);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseParameters(originInTrueBranch);
        Assert.False(frontend.GetEndpoint("http").EndpointAnnotation.BrowserUrlsSnapshot.IsValueSet);
    }
 
    [Fact]
    public async Task ConditionalDoesNotEvaluateUnselectedNestedCondition()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var unselectedCondition = new ParameterResource("unselected-condition", _ =>
            throw new InvalidOperationException("An unselected condition must not be evaluated."));
        var selectedValue = new ParameterResource("selected-value", _ => "selected");
        var nested = ReferenceExpression.CreateConditional(unselectedCondition, bool.TrueString,
            ReferenceExpression.Create($"nested-true"), ReferenceExpression.Create($"nested-false"));
        var conditional = ReferenceExpression.CreateConditional(
            frontend.GetEndpoint("http").Property(EndpointProperty.TlsEnabled), bool.TrueString,
            nested, ReferenceExpression.Create($"{selectedValue}"));
        builder.AddContainer("backend", "image").WithEnvironment("RESULT", conditional);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task CompositeUriFormattingDefersInfrastructureOutputs()
    {
        using var workspace = TemporaryWorkspace.Create(outputHelper);
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish, workspace.Path);
        var infrastructure = builder.AddResource(new AzureBicepResource("infra",
            templateString: "output host string = 'public.example'"));
        var host = new BicepOutputReference("host", infrastructure.Resource);
        var wrapper = new TestManifestValue("https://{infra.outputs.host}/signin-oidc",
            (_, _) => throw new InvalidOperationException("An infrastructure output must not be resolved."), host);
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", ReferenceExpression.Create($"{wrapper:uri}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>(), workspace.Path);
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData(false)]
    [InlineData(true)]
    public async Task NestedUriFormattingPreservesParameterAndCompositeAliasOperations(bool composite)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var query = new ParameterResource("query", _ =>
            throw new InvalidOperationException("A deployment parameter must remain deferred."));
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var wrapper = new TestManifestValue("segment/{query.value}", (_, _) =>
            throw new InvalidOperationException("A symbolic wrapper must not be resolved."), query);
        var inner = composite
            ? ReferenceExpression.Create($"https://example/?q={wrapper:uri}")
            : ReferenceExpression.Create($"https://example/?q={query:uri}");
        var inactive = new ParameterResource("inactive", _ =>
            throw new InvalidOperationException("An inactive branch must not be resolved."));
        var conditional = ReferenceExpression.CreateConditional(frontend.GetEndpoint("http").Property(EndpointProperty.TlsEnabled),
            bool.FalseString, inner, ReferenceExpression.Create($"{inactive}"));
        var browser = new TestManifestValue(conditional.ValueExpression, (_, _) =>
            throw new InvalidOperationException("The selected expression must remain symbolic."), conditional);
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", ReferenceExpression.Create($"{browser:uri}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json")
            .UseParameters(composite);
    }
 
    [Fact]
    public async Task NestedUriFormattingPreservesConcreteEncodingPasses()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var calls = 0;
        var concrete = new TestManifestValue("one two/%", (_, _) =>
        {
            calls++;
            return ValueTask.FromResult<string?>("one two/%");
        });
        var frontend = builder.AddContainer("frontend", "image").WithHttpEndpoint(targetPort: 8080);
        var inner = ReferenceExpression.Create($"https://example/?q={concrete:uri}");
        var browser = new TestManifestValue(inner.ValueExpression, (_, _) =>
            throw new InvalidOperationException("The composed expression must remain symbolic."), inner);
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", ReferenceExpression.Create($"{browser:uri}"));
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        Assert.Equal(0, calls);
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Theory]
    [InlineData("{resource.connectionString}")]
    [InlineData("{vault.custom.key}")]
    [InlineData("{value}")]
    public async Task OpaqueSymbolicUriFormattingFailsWithoutResolvingTheProvider(string token)
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var opaque = new TestManifestValue(token, (_, _) =>
            throw new InvalidOperationException("An opaque symbolic reference must not be forced to resolve."));
        builder.AddContainer("backend", "image").WithEnvironment("ENCODED", ReferenceExpression.Create($"{opaque:uri}"));
        using var app = builder.Build();
 
        var error = await Assert.ThrowsAsync<NotSupportedException>(() => ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.Equal(
            $"URI formatting cannot be deferred for manifest reference '{token}'. " +
            "Use a parameter/output expression or a publication-time value.", error.Message);
    }
 
    [Fact]
    public async Task EndpointUriFormattingFailsExplicitlyWhenPublishing()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var web = builder.AddContainer("web", "image").WithHttpsEndpoint(targetPort: 8080);
        builder.AddContainer("consumer", "image").WithEnvironment("REDIRECT",
            ReferenceExpression.Create($"https://login.example/?redirect_uri={web.GetEndpoint("https"):uri}"));
        using var app = builder.Build();
 
        var error = await Assert.ThrowsAsync<NotSupportedException>(() => ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.Equal(
            "URI formatting cannot be deferred for manifest reference '{web.bindings.https.url}'. " +
            "Use a parameter/output expression or a publication-time value.", error.Message);
    }
 
    [Fact]
    public async Task WrappersDiscoverSelectedConditionalAndUnsupportedLeafReferences()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var host = builder.AddParameter("public-host");
        var condition = new ParameterResource("enabled", _ => bool.TrueString);
        var custom = new TestManifestValue("{frontend.customProperty}", (_, _) => ValueTask.FromResult<string?>("concrete"));
        var inactive = new TestManifestValue("{frontend.inactiveProperty}", (_, _) =>
            throw new InvalidOperationException("An inactive value must not be resolved."));
        var selected = ReferenceExpression.CreateConditional(condition, bool.TrueString,
            ReferenceExpression.Create($"https://{host}/{custom}"), ReferenceExpression.Create($"{inactive}"));
        var inner = new TestManifestValue(selected.ValueExpression, (_, _) =>
            throw new InvalidOperationException("A wrapper around a conditional must remain symbolic."), selected);
        var wrapper = new TestManifestValue($"redirect={inner.ValueExpression}", (_, _) =>
            throw new InvalidOperationException("A composite wrapper must remain symbolic."), inner);
        builder.AddContainer("backend", "image").WithEnvironment("REDIRECT", wrapper);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task WrapperRewritesOnlyReferencedTokensAndIgnoresEscapedDependencies()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var host = builder.AddParameter("public-host");
        var unusedCondition = new ParameterResource("unused-condition", _ =>
            throw new InvalidOperationException("An escaped reference must not evaluate its condition."));
        var unused = ReferenceExpression.CreateConditional(unusedCondition, bool.TrueString,
            ReferenceExpression.Create($"unused-true"), ReferenceExpression.Create($"unused-false"));
        var custom = new TestManifestValue("{frontend.customProperty}", (_, _) => ValueTask.FromResult<string?>("concrete"));
        var wrapper = new TestManifestValue(
            $"https://{{public-host.value}}/{custom.ValueExpression}?literal={{{unused.ValueExpression}}}",
            (_, _) => throw new InvalidOperationException("A wrapper with explainable references must remain symbolic."),
            host.Resource, custom, unused);
        builder.AddContainer("backend", "image").WithEnvironment("URL", wrapper);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task UnavailableCustomProviderIsNotEvaluated()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var value = new TestManifestValue("{frontend.customProperty}", (_, _) =>
            throw new InvalidOperationException("The publication-time value is unavailable."));
        builder.AddContainer("backend", "image").WithEnvironment("CUSTOM", value);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task NullCustomProviderRetainsSymbolicValue()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var value = new TestManifestValue("{frontend.customProperty}", (_, _) => ValueTask.FromResult<string?>(null));
        builder.AddContainer("backend", "image").WithEnvironment("CUSTOM", value);
        using var app = builder.Build();
 
        var manifest = await ManifestUtils.GetManifestForModel(app.Services.GetRequiredService<DistributedApplicationModel>());
 
        await Verify(manifest.ToJsonString(new JsonSerializerOptions { WriteIndented = true }), "json");
    }
 
    [Fact]
    public async Task CyclicWrappersFailWithoutEvaluatingProviders()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        var references = new object[1];
        var first = new TestManifestValue("{custom.value}", (_, _) =>
            throw new InvalidOperationException("A cyclic wrapper must not be evaluated."), references);
        references[0] = new TestManifestValue("{custom.value}", (_, _) =>
            throw new InvalidOperationException("A cyclic wrapper must not be evaluated."), first);
        builder.AddContainer("backend", "image").WithEnvironment("CUSTOM", first);
        using var app = builder.Build();
 
        var error = await Assert.ThrowsAsync<InvalidOperationException>(() => ManifestUtils.GetManifestForModel(
            app.Services.GetRequiredService<DistributedApplicationModel>()));
 
        Assert.Equal("A cycle was encountered while lowering a manifest expression.", error.Message);
    }
 
    [Fact]
    public async Task ConditionalResolutionReceivesPublicationContextAndCancellation()
    {
        using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        using var cts = new CancellationTokenSource();
        var value = new TestManifestValue("{frontend.customProperty}", (context, token) =>
        {
            Assert.Same(builder.ExecutionContext, context.ExecutionContext);
            Assert.Equal(cts.Token, token);
            return ValueTask.FromException<string?>(new OperationCanceledException(token));
        });
        var conditional = ReferenceExpression.CreateConditional(value, bool.TrueString,
            ReferenceExpression.Create($"selected"), ReferenceExpression.Empty);
        builder.AddContainer("backend", "image").WithEnvironment("CUSTOM", conditional);
        using var app = builder.Build();
        using var buffer = new MemoryStream();
        using var writer = new Utf8JsonWriter(buffer);
        var context = new ManifestPublishingContext(builder.ExecutionContext,
            Path.Combine(builder.AppHostDirectory, "manifest.json"), writer, cts.Token);
 
        await Assert.ThrowsAnyAsync<OperationCanceledException>(() => context.WriteModel(
            app.Services.GetRequiredService<DistributedApplicationModel>(), cts.Token));
    }
}