diff --git a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs index b2386597920..4bd09c4fbb6 100644 --- a/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting.DevTunnels/DevTunnelResourceBuilderExtensions.cs @@ -154,7 +154,9 @@ public static IResourceBuilder AddDevTunnel( var exception = new DistributedApplicationException($"Error trying to create the dev tunnel resource '{tunnelResource.TunnelId}' this port belongs to: {ex.Message}", ex); foreach (var portResource in tunnelResource.Ports) { +#pragma warning disable CS0618 // Type or member is obsolete portResource.TunnelEndpointAnnotation.AllocatedEndpointSnapshot.SetException(exception); +#pragma warning restore CS0618 // Type or member is obsolete } throw; } @@ -209,7 +211,9 @@ await notifications.PublishUpdateAsync(portResource, snapshot => snapshot with catch (Exception ex) { portLogger.LogError(ex, "Error trying to create dev tunnel port '{Port}' on tunnel '{Tunnel}': {Error}", portResource.TargetEndpoint.Port, portResource.DevTunnel.TunnelId, ex.Message); +#pragma warning disable CS0618 // Type or member is obsolete portResource.TunnelEndpointAnnotation.AllocatedEndpointSnapshot.SetException(ex); +#pragma warning restore CS0618 // Type or member is obsolete throw; } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs index 8ee659e3040..cccc6438924 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs @@ -101,7 +101,9 @@ public EndpointAnnotation( IsExternal = isExternal ?? false; IsProxied = isProxied; _networkID = networkID ?? KnownNetworkIdentifiers.LocalhostNetwork; +#pragma warning disable CS0618 // Type or member is obsolete AllAllocatedEndpoints.TryAdd(_networkID, AllocatedEndpointSnapshot); +#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -202,8 +204,10 @@ public string Transport /// public AllocatedEndpoint? AllocatedEndpoint { +#pragma warning disable CS0618 // Type or member is obsolete (AllocatedEndpointSnapshot) get { + if (!AllocatedEndpointSnapshot.IsValueSet) { return null; @@ -223,14 +227,20 @@ public AllocatedEndpoint? AllocatedEndpoint } else { + if (_networkID != value.NetworkID) + { + throw new InvalidOperationException($"The default AllocatedEndpoint's network ID must match the EndpointAnnotation network ID ('{_networkID}'). The attempted AllocatedEndpoint belongs to '{value.NetworkID}'."); + } AllocatedEndpointSnapshot.SetValue(value); } } +#pragma warning restore CS0618 // Type or member is obsolete } /// /// Gets the for the default . /// + [Obsolete("This property will be marked as internal in future Aspire release. Use AllocatedEndpoint and AllAllocatedEndpoints properties to access and change allocated endpoints associated with an EndpointAnnotation.")] public ValueSnapshot AllocatedEndpointSnapshot { get; } = new(); /// @@ -271,6 +281,7 @@ IEnumerator IEnumerable.GetEnumerator() /// /// Adds an AllocatedEndpoint snapshot for a specific network if one does not already exist. /// + [Obsolete("This method is for internal use only and will be marked internal in a future Aspire release. Use AddOrUpdateAllocatedEndpoint instead.")] public bool TryAdd(NetworkIdentifier networkID, ValueSnapshot snapshot) { lock (_snapshots) @@ -283,4 +294,41 @@ public bool TryAdd(NetworkIdentifier networkID, ValueSnapshot return true; } } + + /// + /// Adds or updates an AllocatedEndpoint value associated with a specific network in the snapshot list. + /// + public void AddOrUpdateAllocatedEndpoint(NetworkIdentifier networkID, AllocatedEndpoint endpoint) + { + if (endpoint.NetworkID != networkID) + { + throw new ArgumentException($"AllocatedEndpoint must use the same network as the {nameof(networkID)} parameter", nameof(endpoint)); + } + var nes = GetSnapshotFor(networkID); + nes.Snapshot.SetValue(endpoint); + } + + /// + /// Gets an AllocatedEndpoint for a given network ID, waiting for it to appear if it is not already present. + /// + public Task GetAllocatedEndpointAsync(NetworkIdentifier networkID, CancellationToken cancellationToken = default) + { + var nes = GetSnapshotFor(networkID); + return nes.Snapshot.GetValueAsync(cancellationToken); + } + + private NetworkEndpointSnapshot GetSnapshotFor(NetworkIdentifier networkID) + { + lock (_snapshots) + { + var nes = _snapshots.FirstOrDefault(s => s.NetworkID.Equals(networkID)); + if (nes is null) + { + nes = new NetworkEndpointSnapshot(new ValueSnapshot(), networkID); + _snapshots.Add(nes); + } + return nes; + } + } + } diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 35e758cb74c..08f3fb8b104 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -141,8 +141,10 @@ public EndpointReferenceExpression Property(EndpointProperty property) /// public string Url => AllocatedEndpoint.UriString; +#pragma warning disable CS0618 // Type or member is obsolete internal ValueSnapshot AllocatedEndpointSnapshot => EndpointAnnotation.AllocatedEndpointSnapshot; +#pragma warning restore CS0618 // Type or member is obsolete internal AllocatedEndpoint AllocatedEndpoint => GetAllocatedEndpoint() @@ -307,21 +309,8 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En async ValueTask ResolveValueWithAllocatedAddress() { - // 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 endpointSnapshots = Endpoint.EndpointAnnotation.AllAllocatedEndpoints; - var nes = endpointSnapshots.Where(nes => nes.NetworkID == networkContext).FirstOrDefault(); - if (nes is 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); + var allocatedEndpoint = await endpointSnapshots.GetAllocatedEndpointAsync(networkContext, cancellationToken).ConfigureAwait(false); return Property switch { diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index b2298e56954..4edf0ce335d 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -994,9 +994,7 @@ private void AddAllocatedEndpointInfo(IEnumerable resourc targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""", KnownNetworkIdentifiers.DefaultAspireContainerNetwork ); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(allocatedEndpoint); - sp.EndpointAnnotation.AllAllocatedEndpoints.TryAdd(allocatedEndpoint.NetworkID, snapshot); + sp.EndpointAnnotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(allocatedEndpoint.NetworkID, allocatedEndpoint); } } } @@ -1056,9 +1054,7 @@ ts.Service is not null && targetPortExpression: $$$"""{{- portForServing "{{{ts.Service.Name}}}" -}}""", networkID ); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(tunnelAllocatedEndpoint); - endpoint.AllAllocatedEndpoints.TryAdd(networkID, snapshot); + endpoint.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(networkID, tunnelAllocatedEndpoint); } } } diff --git a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs index a7f55cc7a7a..f0b79fa665d 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzurePostgresExtensionsTests.cs @@ -371,9 +371,7 @@ public async Task WithPostgresMcpOnAzureDatabaseRunAsContainerAddsMcpResource() c.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); }); diff --git a/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs b/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs index 06495623fc2..18791a7fa5c 100644 --- a/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs +++ b/tests/Aspire.Hosting.Containers.Tests/ContainerResourceTests.cs @@ -102,10 +102,7 @@ public async Task AddContainerWithArgs() e.AllocatedEndpoint = new(e, "localhost", 1234, targetPortExpression: "1234"); // For container-container lookup we need to add an AllocatedEndpoint on the container network side - var ccae = new AllocatedEndpoint(e, "c1.dev.internal", 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ccae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "c1.dev.internal", 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var c2 = appBuilder.AddContainer("container", "none") @@ -113,10 +110,7 @@ public async Task AddContainerWithArgs() { e.UriScheme = "http"; // We only care about the container-side endpoint for this test - var snapshot = new ValueSnapshot(); - var ae = new AllocatedEndpoint(e, "container.dev.internal", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - snapshot.SetValue(ae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "container.dev.internal", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithArgs(context => { diff --git a/tests/Aspire.Hosting.Maui.Tests/MauiPlatformExtensionsTests.cs b/tests/Aspire.Hosting.Maui.Tests/MauiPlatformExtensionsTests.cs index 521adacb4ac..3813adb0a2e 100644 --- a/tests/Aspire.Hosting.Maui.Tests/MauiPlatformExtensionsTests.cs +++ b/tests/Aspire.Hosting.Maui.Tests/MauiPlatformExtensionsTests.cs @@ -636,7 +636,7 @@ public async Task WithOtlpDevTunnel_CleansUpIntermediateEnvironmentVariables(Pla foreach (var endpointAnnotation in endpointAnnotations) { - endpointAnnotation.AllocatedEndpointSnapshot.SetValue(new AllocatedEndpoint(endpointAnnotation, "localhost", 1234)); + endpointAnnotation.AllocatedEndpoint = new AllocatedEndpoint(endpointAnnotation, "localhost", 1234); } var envVars = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync( diff --git a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs index 433b15b1a46..6eb39f1c5c0 100644 --- a/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs +++ b/tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs @@ -99,9 +99,7 @@ public async Task MilvusClientAppWithReferenceContainsConnectionStrings() .WithEndpoint("grpc", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) diff --git a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs index e2eb3b0de6d..987bc894af0 100644 --- a/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs +++ b/tests/Aspire.Hosting.PostgreSQL.Tests/PostgresMcpBuilderTests.cs @@ -78,9 +78,7 @@ public async Task WithPostgresMcpOnDatabaseSetsDatabaseUriEnvironmentVariable() .WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .AddDatabase("db") .WithPostgresMcp(); diff --git a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs index 4d50b6207c1..85e46194301 100644 --- a/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs +++ b/tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs @@ -172,16 +172,12 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings() .WithEndpoint("grpc", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithEndpoint("http", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var projectA = appBuilder.AddProject("projecta", o => o.ExcludeLaunchProfile = true) diff --git a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs index cace9589d26..2d54dc90ef9 100644 --- a/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs @@ -304,23 +304,17 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables() redis1.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); redis2.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); redis3.WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5003); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var redisInsight = Assert.Single(builder.Resources.OfType()); @@ -734,9 +728,7 @@ public async Task RedisInsightEnvironmentCallbackIsIdempotent() .WithEndpoint("tcp", e => { e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }) .WithRedisInsight(); diff --git a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs index 11e3e3d7de6..c5f8367d193 100644 --- a/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/EndpointReferenceTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using System.Runtime.CompilerServices; namespace Aspire.Hosting.Tests; @@ -285,6 +286,21 @@ public void TargetPort_ReturnsNullWhenNotDefined() Assert.Null(targetPort); } + [Fact] + public void AllocatedEndpoint_ThrowsWhenNetworkIdDoesNotMatch() + { + var annotation = new EndpointAnnotation(ProtocolType.Tcp, KnownNetworkIdentifiers.LocalhostNetwork, uriScheme: "http", name: "http"); + + // Create an AllocatedEndpoint with a different network ID. + var mismatchedEndpoint = new AllocatedEndpoint( + annotation, "localhost", 8080, + EndpointBindingMode.SingleAddress, + targetPortExpression: null, + networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork); + + var ex = Assert.Throws(() => annotation.AllocatedEndpoint = mismatchedEndpoint); + } + [Theory] [InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Host, "blah://localhost:1234")] [InlineData(EndpointProperty.Url, ResourceKind.Host, ResourceKind.Container, "blah://localhost:1234")] @@ -330,9 +346,7 @@ public async Task PropertyResolutionTest(EndpointProperty property, ResourceKind : ("host.docker.internal", port); var containerEndpoint = new AllocatedEndpoint(annotation, containerHost, containerPort, EndpointBindingMode.SingleAddress, targetPortExpression: targetPort.ToString(), KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(containerEndpoint); - annotation.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + annotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, containerEndpoint); var expression = destination.GetEndpoint(annotation.Name).Property(property); @@ -363,6 +377,36 @@ static IResourceWithEndpoints CreateResource(string name, ResourceKind kind) } } + [Fact] + public async Task WaitingForAllocatedEndpointWorks() + { + var resource = new TestResource("test"); + var annotation = new EndpointAnnotation(ProtocolType.Tcp, uriScheme: "http", name: "http"); + resource.Annotations.Add(annotation); + var endpointRef = new EndpointReference(resource, annotation); + + var waitStarted = new SemaphoreSlim(0, 1); + +#pragma warning disable CA2012 // Use ValueTasks correctly + var consumer = new WithWaitStartedNotification(waitStarted, endpointRef.GetValueAsync(CancellationToken.None).GetAwaiter()); +#pragma warning restore CA2012 // Use ValueTasks correctly + + await Task.WhenAll + ( + Task.Run(async() => + { + await waitStarted.WaitAsync(); + var allocatedEndpoint = new AllocatedEndpoint(annotation, "localhost", 5000); + annotation.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.LocalhostNetwork, allocatedEndpoint); + }), + Task.Run(async () => + { + var url = await consumer; + Assert.Equal("http://localhost:5000", url); + }) + ).WaitAsync(TimeSpan.FromSeconds(10)); + } + public enum ResourceKind { Host, @@ -372,4 +416,37 @@ public enum ResourceKind private sealed class TestResource(string name) : Resource(name), IResourceWithEndpoints { } + + private struct WithWaitStartedNotification + { + private readonly WaitStartedNotificationAwaiter _awaiter; + + public WithWaitStartedNotification(SemaphoreSlim waitStarted, ValueTaskAwaiter inner) + { + _awaiter = new WaitStartedNotificationAwaiter(waitStarted, inner); + } + public WaitStartedNotificationAwaiter GetAwaiter() => _awaiter; + } + + private struct WaitStartedNotificationAwaiter: INotifyCompletion + { + private readonly ValueTaskAwaiter _inner; + private readonly SemaphoreSlim _waitStarted; + + public WaitStartedNotificationAwaiter(SemaphoreSlim waitStarted, ValueTaskAwaiter inner) + { + _waitStarted = waitStarted; + _inner = inner; + } + + public bool IsCompleted => false; // Force continuation + + public void OnCompleted(Action continuation) + { + _waitStarted.Release(); + _inner.OnCompleted(continuation); + } + + public T GetResult() => _inner.GetResult(); + } } diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index 429757d98a8..af16db77edc 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -96,10 +96,7 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN if (sourceIsContainer) { // Note: on the container network side the port and target port are always the same for AllocatedEndpoint. - var ae = new AllocatedEndpoint(e, containerHost, 22345, EndpointBindingMode.SingleAddress, targetPortExpression: "22345", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, containerHost, 22345, EndpointBindingMode.SingleAddress, targetPortExpression: "22345", KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); } }) .WithEndpoint("endpoint2", e => @@ -108,10 +105,7 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN e.AllocatedEndpoint = new(e, "localhost", 12346, targetPortExpression: "10001"); if (sourceIsContainer) { - var ae = new AllocatedEndpoint(e, containerHost, 22346, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, containerHost, 22346, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); } }) .WithEndpoint("endpoint3", e => @@ -120,10 +114,7 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN e.AllocatedEndpoint = new(e, "host with space", 12347); if (sourceIsContainer) { - var ae = new AllocatedEndpoint(e, containerHost, 22347, EndpointBindingMode.SingleAddress, targetPortExpression: "22346", KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ae); - e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + e.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(e, containerHost, 22347, EndpointBindingMode.SingleAddress, targetPortExpression: "22347", KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); } }); diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index c6b9b0a1070..18b57208c57 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -229,10 +229,7 @@ public async Task EnvironmentVariableExpressions() { ep.AllocatedEndpoint = new AllocatedEndpoint(ep, "localhost", 17454); - var ae = new AllocatedEndpoint(ep, "container1.dev.internal", 10005, EndpointBindingMode.SingleAddress, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork); - var snapshot = new ValueSnapshot(); - snapshot.SetValue(ae); - ep.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot); + ep.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint(ep, "container1.dev.internal", 10005, EndpointBindingMode.SingleAddress, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork)); }); var endpoint = container.GetEndpoint("primary"); @@ -307,8 +304,7 @@ public async Task EnvironmentVariableWithDynamicTargetPort() .WithHttpEndpoint(name: "primary") .WithEndpoint("primary", ep => { - var endpointSnapshot = new ValueSnapshot(); - endpointSnapshot.SetValue(new AllocatedEndpoint( + ep.AllAllocatedEndpoints.AddOrUpdateAllocatedEndpoint(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, new AllocatedEndpoint( ep, "localhost", 90, @@ -316,7 +312,6 @@ public async Task EnvironmentVariableWithDynamicTargetPort() """{{- portForServing "container1_primary" -}}""", KnownNetworkIdentifiers.DefaultAspireContainerNetwork )); - ep.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, endpointSnapshot); }); var endpoint = container.GetEndpoint("primary");