diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 08e372bb453..9f70b815f42 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -305,10 +305,16 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En { // We are going to take the first snapshot that matches the context network ID. In general there might be multiple endpoints for a single service, // and in future we might need some sort of policy to choose between them, but for now we just take the first one. - var nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == networkContext).FirstOrDefault(); + var endpointSnapshots = Endpoint.EndpointAnnotation.AllAllocatedEndpoints; + var nes = endpointSnapshots.Where(nes => nes.NetworkID == networkContext).FirstOrDefault(); if (nes is null) { - return null; + nes = new NetworkEndpointSnapshot(new ValueSnapshot(), networkContext); + if (!endpointSnapshots.TryAdd(networkContext, nes.Snapshot)) + { + // Someone else added it first, use theirs. + nes = endpointSnapshots.Where(nes => nes.NetworkID == networkContext).First(); + } } var allocatedEndpoint = await nes.Snapshot.GetValueAsync(cancellationToken).ConfigureAwait(false); diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index de43bb1028d..5166c764101 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Dcp; +using Aspire.Hosting.Testing; using Aspire.Hosting.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; using Microsoft.Extensions.Options; @@ -232,6 +233,28 @@ public async Task ContainerToContainerEndpointShouldResolve() Assert.Equal("http://myContainer.dev.internal:8080", config["ConnectionStrings__myContainer"]); } + + [Fact] + public async Task ExpressionResolutionShouldWaitOnMissingAllocatedEndpoint() + { + var builder = DistributedApplicationTestingBuilder.Create(); + + var dependency = builder + .AddResource(new TestHostResource("hostResource")) + .WithHttpEndpoint(name: "hostEndpoint"); + + var consumer = builder.AddResource(new MyContainerResource("containerResource")) + .WithImage("redis") + .WithReference(dependency.GetEndpoint("hostEndpoint")); + + var endpointAnnotation = dependency.Resource.Annotations.OfType().Single(); + endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 1234); + + await Assert.ThrowsAsync(async () => + { + _ = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(consumer.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).AsTask().TimeoutAfter(TimeSpan.FromSeconds(2)); + }); + } } sealed class MyContainerResource : ContainerResource, IResourceWithConnectionString @@ -286,3 +309,8 @@ public TestExpressionResolverResource(string exprName) : base("testresource") public ReferenceExpression ConnectionStringExpression => Expressions[_exprName]; } + +public sealed class TestHostResource : Resource, IResourceWithEndpoints +{ + public TestHostResource(string name) : base(name) { } +}