// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using Aspire.Hosting.Tests.Utils;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
namespace Aspire.Hosting.Tests;
[Trait("Partition", "5")]
public class WithReferenceTests
{
[Theory]
[InlineData("mybinding")]
[InlineData("MYbinding")]
public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariables(string endpointName)
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a binding and its matching annotation (simulating DCP behavior)
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
// Get the service provider.
var projectB = builder.AddProject<ProjectB>("b").WithReference(projectA.GetEndpoint(endpointName));
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]);
Assert.Equal("https://localhost:2000", config["PROJECTA_MYBINDING"]);
Assert.True(projectB.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
var r = Assert.Single(relationships);
Assert.Equal("Reference", r.Type);
Assert.Same(projectA.Resource, r.Resource);
}
[Fact]
public async Task ResourceNamesWithDashesAreEncodedInEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("project-a")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
var projectB = builder.AddProject<ProjectB>("consumer")
.WithReference(projectA);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__project-a__mybinding__0"]);
Assert.Equal("https://localhost:2000", config["PROJECT_A_MYBINDING"]);
Assert.DoesNotContain("services__project_a__mybinding__0", config.Keys);
Assert.DoesNotContain("PROJECT-A_MYBINDING", config.Keys);
}
[Fact]
public async Task OverriddenServiceNamesAreEncodedInEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("project-a")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
var projectB = builder.AddProject<ProjectB>("consumer")
.WithReference(projectA, "custom-name");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__custom-name__mybinding__0"]);
Assert.Equal("https://localhost:2000", config["custom_name_MYBINDING"]);
Assert.DoesNotContain("services__custom_name__mybinding__0", config.Keys);
Assert.DoesNotContain("custom-name_MYBINDING", config.Keys);
}
[Theory]
[InlineData(ReferenceEnvironmentInjectionFlags.All)]
[InlineData(ReferenceEnvironmentInjectionFlags.ConnectionProperties)]
[InlineData(ReferenceEnvironmentInjectionFlags.ConnectionString)]
[InlineData(ReferenceEnvironmentInjectionFlags.ServiceDiscovery)]
[InlineData(ReferenceEnvironmentInjectionFlags.Endpoints)]
[InlineData(ReferenceEnvironmentInjectionFlags.None)]
public async Task ResourceWithEndpointRespectsCustomEnvironmentVariableNaming(ReferenceEnvironmentInjectionFlags flags)
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a binding and its matching annotation (simulating DCP behavior)
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
// Get the service provider.
var projectB = builder.AddProject<ProjectB>("b")
.WithReference(projectA, "custom")
.WithReferenceEnvironment(flags);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
switch (flags)
{
case ReferenceEnvironmentInjectionFlags.All:
Assert.Equal("https://localhost:2000", config["services__custom__mybinding__0"]);
Assert.Equal("https://localhost:2000", config["custom_MYBINDING"]);
break;
case ReferenceEnvironmentInjectionFlags.ConnectionProperties:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__mybinding__0"));
break;
case ReferenceEnvironmentInjectionFlags.ConnectionString:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__mybinding__0"));
break;
case ReferenceEnvironmentInjectionFlags.ServiceDiscovery:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.True(config.ContainsKey("services__custom__mybinding__0"));
break;
case ReferenceEnvironmentInjectionFlags.Endpoints:
Assert.True(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__mybinding__0"));
break;
case ReferenceEnvironmentInjectionFlags.None:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__mybinding__0"));
break;
}
}
[Fact]
public async Task ResourceWithConflictingEndpointsProducesFullyScopedEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpsEndpoint(1000, 3000, "myconflictingbinding")
// Create a binding and its matching annotation (simulating DCP behavior) - HOWEVER
// this binding conflicts with the earlier because they have the same scheme.
.WithEndpoint("myconflictingbinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000));
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA.GetEndpoint("mybinding"))
.WithReference(projectA.GetEndpoint("myconflictingbinding"));
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]);
Assert.Equal("https://localhost:3000", config["services__projecta__myconflictingbinding__0"]);
Assert.Equal("https://localhost:2000", config["PROJECTA_MYBINDING"]);
Assert.Equal("https://localhost:3000", config["PROJECTA_MYCONFLICTINGBINDING"]);
}
[Fact]
public async Task ResourceWithNonConflictingEndpointsProducesAllVariantsOfEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a binding and its matching annotation (simulating DCP behavior)
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
// Create a binding and its matching annotation (simulating DCP behavior) - not
// conflicting because the scheme is different to the first binding.
.WithHttpEndpoint(1000, 3000, "mynonconflictingbinding")
.WithEndpoint("mynonconflictingbinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000));
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA.GetEndpoint("mybinding"))
.WithReference(projectA.GetEndpoint("mynonconflictingbinding"));
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]);
Assert.Equal("http://localhost:3000", config["services__projecta__mynonconflictingbinding__0"]);
Assert.Equal("https://localhost:2000", config["PROJECTA_MYBINDING"]);
Assert.Equal("http://localhost:3000", config["PROJECTA_MYNONCONFLICTINGBINDING"]);
}
[Fact]
public async Task ResourceWithConflictingEndpointsProducesAllEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a binding and its matching annotation (simulating DCP behavior)
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpsEndpoint(1000, 3000, "mybinding2")
.WithEndpoint("mybinding2", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000));
// Get the service provider.
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]);
Assert.Equal("https://localhost:3000", config["services__projecta__mybinding2__0"]);
Assert.Equal("https://localhost:2000", config["PROJECTA_MYBINDING"]);
Assert.Equal("https://localhost:3000", config["PROJECTA_MYBINDING2"]);
Assert.True(projectB.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
var r = Assert.Single(relationships);
Assert.Equal("Reference", r.Type);
Assert.Same(projectA.Resource, r.Resource);
}
[Fact]
public async Task ResourceWithEndpointsProducesAllEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpEndpoint(1000, 3000, "mybinding2")
.WithEndpoint("mybinding2", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000));
// Get the service provider.
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("https://localhost:2000", config["services__projecta__mybinding__0"]);
Assert.Equal("http://localhost:3000", config["services__projecta__mybinding2__0"]);
Assert.Equal("https://localhost:2000", config["PROJECTA_MYBINDING"]);
Assert.Equal("http://localhost:3000", config["PROJECTA_MYBINDING2"]);
Assert.True(projectB.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
var r = Assert.Single(relationships);
Assert.Equal("Reference", r.Type);
Assert.Same(projectA.Resource, r.Resource);
}
[Fact]
public async Task InternalWithReferenceAddsConnectionStringAndServiceDiscoveryWhenResourceSupportsBoth()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResourceWithConnectionStringAndServiceDiscovery("resource")
{
ConnectionString = "123"
})
.WithHttpEndpoint(1000, 2000, "http")
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
var projectB = builder.AddProject<ProjectB>("projectb");
ResourceBuilderExtensions.WithReference(projectB, (IResourceBuilder<IResource>)resource, connectionName: "db", name: "api");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("123", config["ConnectionStrings__db"]);
Assert.Equal("http://localhost:2000", config["services__api__http__0"]);
}
[Fact]
public async Task InternalWithReferenceKeepsDefaultServiceDiscoveryNameWhenOnlyConnectionNameIsProvided()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResourceWithConnectionStringAndServiceDiscovery("resource")
{
ConnectionString = "123"
})
.WithHttpEndpoint(1000, 2000, "http")
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));
var projectB = builder.AddProject<ProjectB>("projectb");
ResourceBuilderExtensions.WithReference(projectB, (IResourceBuilder<IResource>)resource, connectionName: "db");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("123", config["ConnectionStrings__db"]);
Assert.Equal("http://localhost:2000", config["services__resource__http__0"]);
Assert.DoesNotContain("services__db__http__0", config.Keys);
}
[Fact]
public async Task ConnectionStringResourceThrowsWhenMissingConnectionString()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddResource(new TestResource("resource"));
var projectB = builder.AddProject<ProjectB>("projectb").WithReference(resource, optional: false);
// Call environment variable callbacks.
var aggregate = await Assert.ThrowsAsync<AggregateException>(async () =>
{
await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);
}).DefaultTimeout();
var inner = Assert.IsType<DistributedApplicationException>(aggregate.InnerException);
Assert.Equal("The connection string for the resource 'resource' is not available.", inner.Message);
}
[Fact]
public async Task ConnectionStringResourceOptionalWithMissingConnectionString()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddResource(new TestResource("resource"));
var projectB = builder.AddProject<ProjectB>("projectB")
.WithReference(resource, optional: true);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
Assert.Equal(0, servicesKeysCount);
}
[Fact]
public async Task ParameterAsConnectionStringResourceThrowsWhenConnectionStringSectionMissing()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var missingResource = builder.AddConnectionString("missingresource");
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(missingResource);
// Call environment variable callbacks.
var aggregate = await Assert.ThrowsAsync<AggregateException>(async () =>
{
await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);
}).DefaultTimeout();
var inner = Assert.IsType<MissingParameterValueException>(aggregate.InnerException);
Assert.Equal("Connection string parameter resource could not be used because connection string 'missingresource' is missing.", inner.Message);
}
[Fact]
public async Task ParameterAsConnectionStringResourceInjectsConnectionStringWhenPresent()
{
using var builder = TestDistributedApplicationBuilder.Create();
builder.Configuration["ConnectionStrings:resource"] = "test connection string";
// Get the service provider.
var resource = builder.AddConnectionString("resource");
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("test connection string", config["ConnectionStrings__resource"]);
}
[Fact]
public async Task ParameterAsConnectionStringResourceInjectsExpressionWhenPublishingManifest()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddConnectionString("resource");
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout();
Assert.Equal("{resource.connectionString}", config["ConnectionStrings__resource"]);
}
[Fact]
public async Task ParameterAsConnectionStringResourceInjectsCorrectEnvWhenPublishingManifest()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddConnectionString("resource", "MY_ENV");
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout();
Assert.Equal("{resource.connectionString}", config["MY_ENV"]);
}
[Fact]
public async Task ConnectionStringResourceWithConnectionString()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddResource(new TestResource("resource")
{
ConnectionString = "123"
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
Assert.Equal(1, servicesKeysCount);
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "123");
}
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task ConnectionStringResourceWithExpressionConnectionString(bool useFormat)
{
using var builder = TestDistributedApplicationBuilder.Create();
var endpoint = builder.AddParameter("endpoint", "http://localhost:3452");
var key = builder.AddParameter("key", "secretKey", secret: true);
// Ensure that parameters wrapped in IValueEncoder are added in ResourceRelationshipAnnotation.
var cs = useFormat
? ReferenceExpression.Create($"Endpoint={endpoint};Key={key:uri}")
: ReferenceExpression.Create($"Endpoint={endpoint};Key={key}")
;
// Get the service provider.
var resource = builder.AddConnectionString("cs", cs);
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
Assert.Equal(1, servicesKeysCount);
Assert.Equal("Endpoint=http://localhost:3452;Key=secretKey", config["ConnectionStrings__cs"]);
Assert.True(projectB.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationships));
var r = Assert.Single(relationships);
Assert.Equal("Reference", r.Type);
Assert.Same(resource.Resource, r.Resource);
Assert.True(resource.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var csRelationships));
Assert.Collection(csRelationships,
r =>
{
Assert.Equal("WaitFor", r.Type);
Assert.Same(endpoint.Resource, r.Resource);
},
r =>
{
Assert.Equal("WaitFor", r.Type);
Assert.Same(key.Resource, r.Resource);
},
r =>
{
Assert.Equal("Reference", r.Type);
Assert.Same(endpoint.Resource, r.Resource);
},
r =>
{
Assert.Equal("Reference", r.Type);
Assert.Same(key.Resource, r.Resource);
});
}
[Fact]
public async Task ConnectionStringResourceWithExpressionConnectionStringBuilder()
{
using var builder = TestDistributedApplicationBuilder.Create();
var endpoint = builder.AddParameter("endpoint", "http://localhost:3452");
var key = builder.AddParameter("key", "secretKey", secret: true);
// Get the service provider.
var resource = builder.AddConnectionString("cs", b =>
{
b.Append($"Endpoint={endpoint};Key={key}");
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
Assert.Equal(1, servicesKeysCount);
Assert.Equal("Endpoint=http://localhost:3452;Key=secretKey", config["ConnectionStrings__cs"]);
}
[Fact]
public async Task ConnectionStringResourceWithConnectionStringOverwriteName()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Get the service provider.
var resource = builder.AddResource(new TestResource("resource")
{
ConnectionString = "123"
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource, connectionName: "bob");
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__"));
Assert.Equal(1, servicesKeysCount);
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__bob" && kvp.Value == "123");
}
[Fact]
public void WithReferenceHttpRelativeUriThrowsException()
{
using var builder = TestDistributedApplicationBuilder.Create();
Assert.Throws<InvalidOperationException>(() => builder.AddProject<ProjectA>("projecta").WithReference("petstore", new Uri("petstore.swagger.io", UriKind.Relative)));
}
[Fact]
public async Task WithReferenceHttpUriWithPathProducesEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithReference("petstore", new Uri("https://petstore.swagger.io/v2"));
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__"));
Assert.Equal(1, servicesKeysCount);
Assert.Contains(config, kvp => kvp.Key == "services__petstore__default__0" && kvp.Value == "https://petstore.swagger.io/v2");
Assert.Contains(config, kvp => kvp.Key == "petstore" && kvp.Value == "https://petstore.swagger.io/v2");
}
[Fact]
public async Task WithReferenceHttpProduceEnvironmentVariables()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithReference("petstore", new Uri("https://petstore.swagger.io/"));
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectA.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__"));
Assert.Equal(1, servicesKeysCount);
Assert.Contains(config, kvp => kvp.Key == "services__petstore__default__0" && kvp.Value == "https://petstore.swagger.io/");
Assert.Contains(config, kvp => kvp.Key == "petstore" && kvp.Value == "https://petstore.swagger.io/");
}
[Fact]
public async Task ProjectResourceWithReferenceGetsConnectionStringAndProperties()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are present (from the annotation - ProjectResource has All flag)
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT" && kvp.Value == "5432");
}
[Fact]
public async Task ExecutableResourceWithReferenceGetsConnectionStringAndProperties()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var executable = builder.AddExecutable("myexe", "cmd", ".", args: [])
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(executable.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are present (from the annotation - ExecutableResource has All flag)
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT" && kvp.Value == "5432");
}
[Fact]
public async Task JavaScriptAppAppResourceWithReferenceGetsConnectionStringAndProperties()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var executable = builder.AddJavaScriptApp("NpmApp", ".\\app")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(executable.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are present (from the annotation - ExecutableResource has All flag)
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT" && kvp.Value == "5432");
}
[Fact]
public async Task PythonAppResourceWithReferenceGetsConnectionStringAndProperties()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
#pragma warning disable CS0618
var executable = builder.AddPythonApp("PythonApp", ".\\app", "app.py")
.WithReference(resource);
#pragma warning restore CS0618
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(executable.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are present (from the annotation - ExecutableResource has All flag)
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT" && kvp.Value == "5432");
}
[Fact]
public async Task ContainerResourceWithReferenceGetsOnlyConnectionString()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var container = builder.AddContainer("mycontainer", "myimage")
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(container.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are present (no annotation - defaults to has All flag)
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT");
}
[Fact]
public async Task ResourceWithConnectionPropertiesExtensionRespectsFlags()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
// Create a container and explicitly configure it to emit only connection properties
var container = builder.AddContainer("mycontainer", "myimage")
.WithReferenceEnvironment(ReferenceEnvironmentInjectionFlags.ConnectionProperties)
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(container.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is NOT present
Assert.DoesNotContain(config, kvp => kvp.Key == "ConnectionStrings__resource");
// Verify connection properties ARE present
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_PORT" && kvp.Value == "5432");
}
[Fact]
public async Task ResourceWithConnectionPropertiesExtensionOverridesDefault()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Create a test resource with connection properties
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
// Create a project resource and override the default injection flags
// ProjectResource defaults to ReferenceEnvironmentInjectionFlags.All
// Here we configure it to only inject ConnectionString (not ConnectionProperties)
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReferenceEnvironment(ReferenceEnvironmentInjectionFlags.ConnectionString)
.WithReference(resource);
// Call environment variable callbacks.
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Verify connection string is present (included in ConnectionString flag)
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__resource" && kvp.Value == "Server=localhost;Database=mydb");
// Verify connection properties are NOT present (excluded by our custom annotation)
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE_HOST");
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE_PORT");
}
[Fact]
public async Task ConnectionStringResourceWithConnectionPropertiesOverwriteName()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResourceWithProperties("resource")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource, connectionName: "bob");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Contains(config, kvp => kvp.Key == "ConnectionStrings__bob" && kvp.Value == "Server=localhost;Database=mydb");
Assert.Contains(config, kvp => kvp.Key == "BOB_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "BOB_PORT" && kvp.Value == "5432");
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE_HOST");
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE_PORT");
}
[Fact]
public async Task ConnectionPropertiesWithDashedNamesAreEncoded()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResourceWithProperties("resource-with-dash")
{
ConnectionString = "Server=localhost;Database=mydb"
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(resource);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("Server=localhost;Database=mydb", config["ConnectionStrings__resource-with-dash"]);
Assert.Equal("Server=localhost;Database=mydb", config["ConnectionStrings__resource_with_dash"]);
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_WITH_DASH_HOST" && kvp.Value == "localhost");
Assert.Contains(config, kvp => kvp.Key == "RESOURCE_WITH_DASH_PORT" && kvp.Value == "5432");
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE-WITH-DASH_HOST");
Assert.DoesNotContain(config, kvp => kvp.Key == "RESOURCE-WITH-DASH_PORT");
}
[Fact]
public async Task ConnectionStringWithDashedLogicalNameEmitsOriginalAndPortableAliases()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResource("my-db")
{
ConnectionString = "Host=localhost"
});
var project = builder.AddProject<ProjectB>("project")
.WithReference(resource, connectionName: "my--db");
var context = new EnvironmentCallbackContext(builder.ExecutionContext, project.Resource);
foreach (var callback in project.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>())
{
await callback.Callback(context);
}
var reference = Assert.IsType<ConnectionStringReference>(context.EnvironmentVariables["ConnectionStrings__my--db"]);
Assert.Same(resource.Resource, reference.Resource);
Assert.Same(reference, context.EnvironmentVariables["ConnectionStrings__my_db"]);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource,
DistributedApplicationOperation.Run,
TestServiceProvider.Instance).DefaultTimeout();
var connectionStrings = config
.Where(static entry => entry.Key.StartsWith("ConnectionStrings__", StringComparison.Ordinal))
.OrderBy(static entry => entry.Key, StringComparer.Ordinal)
.ToArray();
Assert.Collection(
connectionStrings,
entry =>
{
Assert.Equal("ConnectionStrings__my--db", entry.Key);
Assert.Equal("Host=localhost", entry.Value);
},
entry =>
{
Assert.Equal("ConnectionStrings__my_db", entry.Key);
Assert.Equal("Host=localhost", entry.Value);
});
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task StandaloneConnectionStringReferencePreservesValueBehavior(bool optional)
{
var resource = new TestResource("my-db");
var reference = new ConnectionStringReference(resource, optional);
var provider = (IValueProvider)reference;
#pragma warning disable ASPIRECONNECTIONSTRINGS001
Assert.Null(reference.EnvironmentVariableNames);
#pragma warning restore ASPIRECONNECTIONSTRINGS001
Assert.Same(resource, reference.Resource);
Assert.Same(resource, Assert.Single(((IValueWithReferences)reference).References));
Assert.Equal(optional, reference.Optional);
Assert.Equal("{my-db.connectionString}", ((IManifestExpressionProvider)reference).ValueExpression);
// The original context-free overload delegates to the resource, even for required references.
Assert.Null(await provider.GetValueAsync(CancellationToken.None));
if (optional)
{
Assert.Null(await provider.GetValueAsync(new ValueProviderContext(), CancellationToken.None));
}
else
{
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
await provider.GetValueAsync(new ValueProviderContext(), CancellationToken.None));
Assert.Equal("The connection string for the resource 'my-db' is not available.", exception.Message);
}
resource.ConnectionString = "Host=after";
Assert.Equal("Host=after", await provider.GetValueAsync(CancellationToken.None));
Assert.Equal("Host=after", await provider.GetValueAsync(new ValueProviderContext(), CancellationToken.None));
Assert.Equal("Host=after", await reference.ConnectionStringExpression.GetValueAsync(CancellationToken.None));
}
[Fact]
public async Task StandaloneConnectionStringValueDoesNotReserveGeneratedAliases()
{
using var builder = TestDistributedApplicationBuilder.Create();
var standalone = builder.AddResource(new TestResource("my-db") { ConnectionString = "Host=standalone" });
var generated = builder.AddResource(new TestResource("generated") { ConnectionString = "Host=generated" });
var project = builder.AddProject<ProjectB>("project")
.WithEnvironment("ConnectionStrings__my-db", new ConnectionStringReference(standalone.Resource, optional: false))
.WithReference(generated, connectionName: "my_db");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource,
DistributedApplicationOperation.Run,
TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal(
[
new KeyValuePair<string, string>("ConnectionStrings__my-db", "Host=standalone"),
new KeyValuePair<string, string>("ConnectionStrings__my_db", "Host=generated")
],
config.Where(static entry => entry.Key.StartsWith("ConnectionStrings__", StringComparison.Ordinal)));
}
[Theory]
[InlineData(DistributedApplicationOperation.Run, null)]
[InlineData(DistributedApplicationOperation.Run, "ConnectionStrings__my-db")]
[InlineData(DistributedApplicationOperation.Run, "ConnectionStrings__my_db")]
[InlineData(DistributedApplicationOperation.Publish, null)]
[InlineData(DistributedApplicationOperation.Publish, "ConnectionStrings__my-db")]
[InlineData(DistributedApplicationOperation.Publish, "ConnectionStrings__my_db")]
public async Task ConnectionStringPortableAliasCollisionsFailDuringEnvironmentEvaluation(DistributedApplicationOperation operation, string? overriddenName)
{
using var builder = TestDistributedApplicationBuilder.Create(operation);
var dashed = builder.AddResource(new TestResource("dashed")
{
ConnectionString = "Host=dashed"
});
var underscored = builder.AddResource(new TestResource("underscored")
{
ConnectionString = "Host=underscored"
});
var project = builder.AddProject<ProjectB>("project")
.WithReference(dashed, connectionName: "my-db");
if (overriddenName is not null)
{
project.WithEnvironment(overriddenName, "Host=override");
}
project.WithReference(underscored, connectionName: "my_db");
var context = new EnvironmentCallbackContext(builder.ExecutionContext, project.Resource);
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
{
foreach (var callback in project.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>())
{
await callback.Callback(context);
}
});
Assert.Equal(
"Connection-string references 'my-db' and 'my_db' on resource 'project' both use the environment variable " +
"'ConnectionStrings__my_db'. Use unique connectionName values when calling WithReference.",
exception.Message);
}
[Fact]
public async Task ConnectionStringPortableAliasCollisionsAreCaseInsensitive()
{
using var builder = TestDistributedApplicationBuilder.Create();
var first = builder.AddResource(new TestResource("first"));
var second = builder.AddResource(new TestResource("second"));
var project = builder.AddProject<ProjectB>("project")
.WithReference(first, connectionName: "My-Db")
.WithReference(second, connectionName: "my_db");
var context = new EnvironmentCallbackContext(builder.ExecutionContext, project.Resource);
var exception = await Assert.ThrowsAsync<DistributedApplicationException>(async () =>
{
foreach (var callback in project.Resource.Annotations.OfType<EnvironmentCallbackAnnotation>())
{
await callback.Callback(context);
}
});
Assert.Equal(
"Connection-string references 'My-Db' and 'my_db' on resource 'project' both use the environment variable " +
"'ConnectionStrings__My_Db'. Use unique connectionName values when calling WithReference.",
exception.Message);
}
[Fact]
public async Task ConnectionStringPortableAliasCollisionsAreIgnoredWhenConnectionStringsAreNotInjected()
{
using var builder = TestDistributedApplicationBuilder.Create();
var dashed = builder.AddResource(new TestResourceWithProperties("dashed"));
var underscored = builder.AddResource(new TestResourceWithProperties("underscored"));
var project = builder.AddContainer("project", "fake")
.WithReferenceEnvironment(ReferenceEnvironmentInjectionFlags.ConnectionProperties)
.WithReference(dashed, connectionName: "my-db");
var result = project.WithReference(underscored, connectionName: "my_db");
Assert.Same(project, result);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);
Assert.Equal(
new Dictionary<string, string> { ["MY_DB_HOST"] = "localhost", ["MY_DB_PORT"] = "5432" },
config);
}
[Fact]
public async Task RemovedConnectionStringValuesDoNotReserveAliases()
{
using var builder = TestDistributedApplicationBuilder.Create();
var first = builder.AddResource(new TestResource("first") { ConnectionString = "Host=first" });
var second = builder.AddResource(new TestResource("second") { ConnectionString = "Host=second" });
var consumer = builder.AddContainer("consumer", "fake")
.WithReference(first, connectionName: "my-db")
.WithEnvironment(context =>
{
context.EnvironmentVariables.Remove("ConnectionStrings__my-db");
context.EnvironmentVariables.Remove("ConnectionStrings__my_db");
})
.WithReference(second, connectionName: "my_db");
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
consumer.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance);
Assert.Equal(
[new KeyValuePair<string, string>("ConnectionStrings__my_db", "Host=second")],
config);
}
[Fact]
public async Task RepeatedEquivalentConnectionStringReferencesRemainValid()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResource("my-db")
{
ConnectionString = "Host=localhost"
});
var project = builder.AddProject<ProjectB>("project")
.WithReference(resource)
.WithReference(resource);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource,
DistributedApplicationOperation.Run,
TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("Host=localhost", config["ConnectionStrings__my-db"]);
Assert.Equal("Host=localhost", config["ConnectionStrings__my_db"]);
}
[Theory]
[InlineData(false, true)]
[InlineData(true, false)]
public async Task RepeatedConnectionStringReferencesWithDifferentOptionalValuesUseLastValue(bool firstOptional, bool secondOptional)
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResource("resource"));
var project = builder.AddProject<ProjectB>("project")
.WithReference(resource, optional: firstOptional)
.WithReference(resource, optional: secondOptional);
var exception = await Record.ExceptionAsync(async () =>
{
await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource,
DistributedApplicationOperation.Run,
TestServiceProvider.Instance);
}).DefaultTimeout();
if (secondOptional)
{
Assert.Null(exception);
}
else
{
var aggregate = Assert.IsType<AggregateException>(exception);
var inner = Assert.IsType<DistributedApplicationException>(aggregate.InnerException);
Assert.Equal("The connection string for the resource 'resource' is not available.", inner.Message);
}
}
[Fact]
public async Task RepeatedConnectionStringReferencesRemainValidWhenExpressionChanges()
{
using var builder = TestDistributedApplicationBuilder.Create();
var resource = builder.AddResource(new TestResource("my-db")
{
ConnectionString = "Host=before"
});
var project = builder.AddProject<ProjectB>("project")
.WithReference(resource);
resource.Resource.ConnectionString = "Host=after";
project.WithReference(resource);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(
project.Resource,
DistributedApplicationOperation.Run,
TestServiceProvider.Instance).DefaultTimeout();
Assert.Equal("Host=after", config["ConnectionStrings__my-db"]);
Assert.Equal("Host=after", config["ConnectionStrings__my_db"]);
}
private sealed class TestResourceWithProperties(string name) : Resource(name), IResourceWithConnectionString
{
public string? ConnectionString { get; set; }
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{ConnectionString}");
public IEnumerable<KeyValuePair<string, ReferenceExpression>> GetConnectionProperties()
{
yield return new KeyValuePair<string, ReferenceExpression>("Host", ReferenceExpression.Create($"localhost"));
yield return new KeyValuePair<string, ReferenceExpression>("Port", ReferenceExpression.Create($"5432"));
}
}
private sealed class TestResource(string name) : Resource(name), IResourceWithConnectionString
{
public string? ConnectionString { get; set; }
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{ConnectionString}");
}
[Fact]
public async Task ExcludedReferenceEndpointExcludedFromUseAllEndpoints()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpEndpoint(1000, 2000, "api")
.WithEndpoint("api", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpEndpoint(1000, 3000, "management")
.WithEndpoint("management", e =>
{
e.ExcludeReferenceEndpoint = true;
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000);
});
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// The "api" endpoint should be included (it's not excluded)
Assert.Equal("http://localhost:2000", config["services__projecta__api__0"]);
Assert.Equal("http://localhost:2000", config["PROJECTA_API"]);
// The "management" endpoint should NOT be included (ExcludeReferenceEndpoint = true)
Assert.False(config.ContainsKey("services__projecta__management__0"));
Assert.False(config.ContainsKey("PROJECTA_MANAGEMENT"));
}
[Fact]
public async Task ExcludedReferenceEndpointCanBeReferencedExplicitly()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpEndpoint(1000, 2000, "api")
.WithEndpoint("api", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpEndpoint(1000, 3000, "management")
.WithEndpoint("management", e =>
{
e.ExcludeReferenceEndpoint = true;
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000);
});
// Explicitly reference the excluded endpoint
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA.GetEndpoint("management"));
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// The "management" endpoint should be included because it was explicitly referenced
Assert.Equal("http://localhost:3000", config["services__projecta__management__0"]);
Assert.Equal("http://localhost:3000", config["PROJECTA_MANAGEMENT"]);
// The "api" endpoint should NOT be included (wasn't referenced)
Assert.False(config.ContainsKey("PROJECTA_API"));
}
[Fact]
public async Task CombinedWithReferenceAndExplicitEndpointIncludesBoth()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpEndpoint(1000, 2000, "api")
.WithEndpoint("api", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000))
.WithHttpEndpoint(1000, 3000, "management")
.WithEndpoint("management", e =>
{
e.ExcludeReferenceEndpoint = true;
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 3000);
});
// Reference all default endpoints AND the excluded management endpoint explicitly
var projectB = builder.AddProject<ProjectB>("projectb")
.WithReference(projectA)
.WithReference(projectA.GetEndpoint("management"));
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Both endpoints should be included
Assert.Equal("http://localhost:2000", config["services__projecta__api__0"]);
Assert.Equal("http://localhost:3000", config["services__projecta__management__0"]);
Assert.Equal("http://localhost:2000", config["PROJECTA_API"]);
Assert.Equal("http://localhost:3000", config["PROJECTA_MANAGEMENT"]);
}
[Fact]
public void ExcludeReferenceEndpointDefaultsToFalse()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(targetPort: 8080);
var endpoint = container.Resource.Annotations.OfType<EndpointAnnotation>().Single();
Assert.False(endpoint.ExcludeReferenceEndpoint);
}
[Fact]
public void WithEndpointCallbackCanSetExcludeReferenceEndpoint()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(targetPort: 8080, name: "api")
.WithEndpoint("api", e => e.ExcludeReferenceEndpoint = true);
var endpoint = container.Resource.Annotations.OfType<EndpointAnnotation>().Single();
Assert.True(endpoint.ExcludeReferenceEndpoint);
}
[Fact]
public void EndpointReferenceReflectsExcludeReferenceEndpoint()
{
using var builder = TestDistributedApplicationBuilder.Create();
var container = builder.AddContainer("mycontainer", "myimage")
.WithHttpEndpoint(targetPort: 8080, name: "primary")
.WithHttpEndpoint(targetPort: 9000, name: "management")
.WithEndpoint("management", e => e.ExcludeReferenceEndpoint = true);
var primaryRef = container.GetEndpoint("primary");
var managementRef = container.GetEndpoint("management");
Assert.False(primaryRef.ExcludeReferenceEndpoint);
Assert.True(managementRef.ExcludeReferenceEndpoint);
}
[Fact]
public async Task HttpEndpointWithTlsUsesActualSchemeAsKey()
{
using var builder = TestDistributedApplicationBuilder.Create();
// Endpoint named "http" but with https scheme (TLS upgrade)
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint(name: "http", scheme: "https", targetPort: 443)
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 443));
var projectB = builder.AddProject<ProjectB>("b").WithReference(projectA);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Endpoint named "http" but scheme is "https" — key should use actual scheme
Assert.Equal("https://localhost:443", config["services__projecta__https__0"]);
Assert.DoesNotContain("services__projecta__http__0", config.Keys);
}
[Fact]
public async Task MultipleEndpointsWithDifferentNamesProduceDistinctKeys()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithHttpEndpoint(targetPort: 8080)
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 8080))
.WithEndpoint(name: "prometheus", scheme: "http", targetPort: 9090)
.WithEndpoint("prometheus", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 9090));
var projectB = builder.AddProject<ProjectB>("b").WithReference(projectA);
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// "http" endpoint uses scheme key "http"; "prometheus" endpoint uses endpoint name
Assert.Equal("http://localhost:8080", config["services__projecta__http__0"]);
Assert.Equal("http://localhost:9090", config["services__projecta__prometheus__0"]);
}
[Fact]
public async Task PerEndpointNameOverridesServiceName()
{
using var builder = TestDistributedApplicationBuilder.Create();
var projectA = builder.AddProject<ProjectA>("projecta")
.WithEndpoint(name: "data", scheme: "http", targetPort: 8080)
.WithEndpoint("data", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 8080));
var projectB = builder.AddProject<ProjectB>("b")
.WithReference(projectA, "projecta-data")
.WithReference(projectA.GetEndpoint("data"));
var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();
// Service name overridden via WithReference(resource, name), endpoint name as scheme key
Assert.Equal("http://localhost:8080", config["services__projecta-data__data__0"]);
}
private sealed class TestResourceWithConnectionStringAndServiceDiscovery(string name) : ContainerResource(name), IResourceWithConnectionString, IResourceWithServiceDiscovery
{
public string? ConnectionString { get; set; }
public ReferenceExpression ConnectionStringExpression =>
ReferenceExpression.Create($"{ConnectionString}");
}
private sealed class ProjectA : IProjectMetadata
{
public string ProjectPath => "projectA";
public LaunchSettings LaunchSettings { get; } = new();
}
private sealed class ProjectB : IProjectMetadata
{
public string ProjectPath => "projectB";
public LaunchSettings LaunchSettings { get; } = new();
}
}