From 61b9d9c611a5ad05ea6805248a5cb88d23c13880 Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Tue, 18 Nov 2025 17:23:31 -0800 Subject: [PATCH 1/3] Expression resolver should wait on missing AllocatedEndpoint(s) --- .../ExpressionResolverTests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index de43bb1028d..52a1bd2ff02 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,32 @@ 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(20000)); + }); + + + // Assert.That(envVars["services__fake__http__0"], Is.EqualTo("http://localhost:1234")); + // Actual: `http://:` + } } sealed class MyContainerResource : ContainerResource, IResourceWithConnectionString @@ -286,3 +313,8 @@ public TestExpressionResolverResource(string exprName) : base("testresource") public ReferenceExpression ConnectionStringExpression => Expressions[_exprName]; } + +public sealed class TestHostResource : Resource, IResourceWithEndpoints +{ + public TestHostResource(string name) : base(name) { } +} From 36a6e1f7f7a76f73fe43b2d7bf700f0f82a26b9c Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Wed, 19 Nov 2025 10:33:00 -0800 Subject: [PATCH 2/3] Target test now passing --- .../ApplicationModel/EndpointReference.cs | 10 ++++++++-- tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) 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 52a1bd2ff02..c7fbb60890b 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -252,7 +252,7 @@ public async Task ExpressionResolutionShouldWaitOnMissingAllocatedEndpoint() await Assert.ThrowsAsync(async () => { - _ = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(consumer.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).AsTask().TimeoutAfter(TimeSpan.FromSeconds(20000)); + _ = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(consumer.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).AsTask().TimeoutAfter(TimeSpan.FromSeconds(2)); }); From faec76cb0f490cfb6fe106a58576b23ad271788a Mon Sep 17 00:00:00 2001 From: Karol Zadora-Przylecki Date: Wed, 19 Nov 2025 10:34:52 -0800 Subject: [PATCH 3/3] ExpressionResolverTests passing --- tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index c7fbb60890b..5166c764101 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -254,10 +254,6 @@ await Assert.ThrowsAsync(async () => { _ = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(consumer.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).AsTask().TimeoutAfter(TimeSpan.FromSeconds(2)); }); - - - // Assert.That(envVars["services__fake__http__0"], Is.EqualTo("http://localhost:1234")); - // Actual: `http://:` } }