Skip to content
2 changes: 2 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/EndpointAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,13 +244,15 @@ public AllocatedEndpoint? AllocatedEndpoint
/// </summary>
/// <param name="Snapshot">AllocatedEndpoint snapshot</param>
/// <param name="NetworkID">The ID of the network that is associated with the AllocatedEndpoint snapshot.</param>
[DebuggerDisplay("NetworkID = {NetworkID}, Endpoint = {Snapshot}")]
public record class NetworkEndpointSnapshot(ValueSnapshot<AllocatedEndpoint> Snapshot, NetworkIdentifier NetworkID);

/// <summary>
/// Holds a list of <see cref="NetworkEndpointSnapshot"/> for an Endpoint, providing thread-safe enumeration and addition.
/// </summary>
public class NetworkEndpointSnapshotList : IEnumerable<NetworkEndpointSnapshot>
{
[DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
private readonly ConcurrentBag<NetworkEndpointSnapshot> _snapshots = new();

/// <summary>
Expand Down
29 changes: 0 additions & 29 deletions src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,6 @@ namespace Aspire.Hosting.ApplicationModel;

internal class ExpressionResolver(CancellationToken cancellationToken)
{

async Task<string?> ResolveInContainerContextAsync(EndpointReference endpointReference, EndpointProperty property, ValueProviderContext context)
{
// We need to use the root resource, e.g. AzureStorageResource instead of AzureBlobResource
// Otherwise, we get the wrong values for IsContainer and Name
var target = endpointReference.Resource.GetRootResource();

return (property, target.IsContainer()) switch
{
// If Container -> Container, we use <container name>.dev.internal as host, and target port as port
// This assumes both containers are on the same container network.
// Different networks will require addtional routing/tunneling that we do not support today.
(EndpointProperty.Host or EndpointProperty.IPV4Host, true) => $"{target.Name}.dev.internal",
(EndpointProperty.Port, true) => await endpointReference.Property(EndpointProperty.TargetPort).GetValueAsync(context, cancellationToken).ConfigureAwait(false),

(EndpointProperty.Url, _) => string.Format(CultureInfo.InvariantCulture, "{0}://{1}:{2}",
endpointReference.Scheme,
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
(EndpointProperty.HostAndPort, _) => string.Format(CultureInfo.InvariantCulture, "{0}:{1}",
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Host, context).ConfigureAwait(false),
await ResolveInContainerContextAsync(endpointReference, EndpointProperty.Port, context).ConfigureAwait(false)),
_ => await endpointReference.Property(property).GetValueAsync(context, cancellationToken).ConfigureAwait(false)
};
}

async Task<ResolvedValue> EvalExpressionAsync(ReferenceExpression expr, ValueProviderContext context)
{
// This logic is similar to ReferenceExpression.GetValueAsync, except that we recurse on
Expand Down Expand Up @@ -95,14 +69,11 @@ async Task<ResolvedValue> ResolveConnectionStringReferenceAsync(ConnectionString
/// </summary>
async ValueTask<ResolvedValue> ResolveInternalAsync(object? value, ValueProviderContext context)
{
var networkContext = context.GetNetworkIdentifier();
return value switch
{
ConnectionStringReference cs => await ResolveConnectionStringReferenceAsync(cs, context).ConfigureAwait(false),
IResourceWithConnectionString cs and not ConnectionStringParameterResource => await ResolveInternalAsync(cs.ConnectionStringExpression, context).ConfigureAwait(false),
ReferenceExpression ex => await EvalExpressionAsync(ex, context).ConfigureAwait(false),
EndpointReference er when er.ContextNetworkID == KnownNetworkIdentifiers.DefaultAspireContainerNetwork || (er.ContextNetworkID == null && networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork) => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false),
EndpointReferenceExpression ep when ep.Endpoint.ContextNetworkID == KnownNetworkIdentifiers.DefaultAspireContainerNetwork || (ep.Endpoint.ContextNetworkID == null && networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork) => new ResolvedValue(await ResolveInContainerContextAsync(ep.Endpoint, ep.Property, context).ConfigureAwait(false), false),
IValueProvider vp => await EvalValueProvider(vp, context).ConfigureAwait(false),
_ => throw new NotImplementedException()
};
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ValueSnapshot.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;

namespace Aspire.Hosting.ApplicationModel;

/// <summary>
Expand All @@ -12,6 +14,7 @@ namespace Aspire.Hosting.ApplicationModel;
///
/// Thread-safe for concurrent SetValue / SetException / GetValueAsync calls.
/// </summary>
[DebuggerDisplay("{Value= {DebuggerValue()}")]
public sealed class ValueSnapshot<T> where T : notnull
{
private readonly TaskCompletionSource<T> _firstValueTcs =
Expand Down Expand Up @@ -73,4 +76,5 @@ public void SetException(Exception exception)
}
}
}
private T? DebuggerValue() => IsValueSet ? _firstValueTcs.Task.Result : default;
}
24 changes: 24 additions & 0 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,29 @@ private void AddAllocatedEndpointInfo(IEnumerable<RenderedModelResource> resourc
bindingMode,
targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""",
KnownNetworkIdentifiers.LocalhostNetwork);

if (appResource.DcpResource is Container ctr && ctr.Spec.Networks is not null)
{
// Once container networks are fully supported, this should allocate endpoints on those networks
var containerNetwork = ctr.Spec.Networks.FirstOrDefault(n => n.Name == KnownNetworkIdentifiers.DefaultAspireContainerNetwork.Value);

if (containerNetwork is not null)
{
var port = sp.EndpointAnnotation.TargetPort!;

var allocatedEndpoint = new AllocatedEndpoint(
sp.EndpointAnnotation,
$"{sp.ModelResource.Name}.dev.internal",
(int)port,
EndpointBindingMode.SingleAddress,
targetPortExpression: $$$"""{{- portForServing "{{{svc.Metadata.Name}}}" -}}""",
KnownNetworkIdentifiers.DefaultAspireContainerNetwork
);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(allocatedEndpoint);
sp.EndpointAnnotation.AllAllocatedEndpoints.TryAdd(allocatedEndpoint.NetworkID, snapshot);
}
}
}
}

Expand Down Expand Up @@ -1040,6 +1063,7 @@ ts.Service is not null &&
}
}
}

}

private void PrepareContainerNetworks()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,13 @@ public async Task WithPostgresMcpOnAzureDatabaseRunAsContainerAddsMcpResource()
.WithPasswordAuthentication(userName: user, password: pass)
.RunAsContainer(c =>
{
c.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432));
c.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});
});

var db = postgres.AddDatabase("db")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +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, KnownHostNames.DefaultContainerTunnelHostName, 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var ccae = new AllocatedEndpoint(e, "c1.dev.internal", 2234, EndpointBindingMode.SingleAddress, targetPortExpression: "2234", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(ccae);
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
Expand All @@ -114,7 +114,7 @@ public async Task AddContainerWithArgs()
e.UriScheme = "http";
// We only care about the container-side endpoint for this test
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
var ae = new AllocatedEndpoint(e, "localhost", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
var ae = new AllocatedEndpoint(e, "container.dev.internal", 5678, EndpointBindingMode.SingleAddress, targetPortExpression: "5678", KnownNetworkIdentifiers.DefaultAspireContainerNetwork);
snapshot.SetValue(ae);
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
})
Expand Down
8 changes: 7 additions & 1 deletion tests/Aspire.Hosting.Milvus.Tests/AddMilvusTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,13 @@ public async Task MilvusClientAppWithReferenceContainsConnectionStrings()
var pass = appBuilder.AddParameter("apikey", "pass");

var milvus = appBuilder.AddMilvus("my-milvus", pass)
.WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc));
.WithEndpoint("grpc", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", MilvusPortGrpc);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "my-milvus.dev.internal", MilvusPortGrpc, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});

var projectA = appBuilder.AddProject<ProjectA>("projecta", o => o.ExcludeLaunchProfile = true)
.WithReference(milvus);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ public async Task WithPostgresMcpOnDatabaseSetsDatabaseUriEnvironmentVariable()
var pass = appBuilder.AddParameter("pass", "p@ssw0rd1");

appBuilder.AddPostgres("postgres", password: pass)
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432))
.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5432);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "postgres.dev.internal", 5432, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
})
.AddDatabase("db")
.WithPostgresMcp();

Expand Down
16 changes: 14 additions & 2 deletions tests/Aspire.Hosting.Qdrant.Tests/AddQdrantTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,20 @@ public async Task QdrantClientAppWithReferenceContainsConnectionStrings()
var pass = appBuilder.AddParameter("pass", "pass");

var qdrant = appBuilder.AddQdrant("my-qdrant", pass)
.WithEndpoint("grpc", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334))
.WithEndpoint("http", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333));
.WithEndpoint("grpc", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6334);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6334, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
})
.WithEndpoint("http", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6333);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "my-qdrant.dev.internal", 6333, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});

var projectA = appBuilder.AddProject<ProjectA>("projecta", o => o.ExcludeLaunchProfile = true)
.WithReference(qdrant);
Expand Down
32 changes: 28 additions & 4 deletions tests/Aspire.Hosting.Redis.Tests/AddRedisTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,27 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables()
using var app = builder.Build();

// Add fake allocated endpoints.
redis1.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001));
redis2.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002));
redis1.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5001);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "myredis1.dev.internal", 5001, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});
redis2.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5002);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "myredis2.dev.internal", 5002, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});
redis3.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 5003);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "myredis3.dev.internal", 5003, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
});

var redisInsight = Assert.Single(builder.Resources.OfType<RedisInsightResource>());
var envs = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(redisInsight);
Expand Down Expand Up @@ -367,7 +386,6 @@ public async Task WithRedisInsightProducesCorrectEnvironmentVariables()
Assert.Equal("RI_REDIS_ALIAS3", item.Key);
Assert.Equal(redis3.Resource.Name, item.Value);
});

}

[Fact]
Expand Down Expand Up @@ -713,7 +731,13 @@ public async Task RedisInsightEnvironmentCallbackIsIdempotent()
using var appBuilder = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);

var redis = appBuilder.AddRedis("redis")
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379))
.WithEndpoint("tcp", e =>
{
e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 6379);
var snapshot = new ValueSnapshot<AllocatedEndpoint>();
snapshot.SetValue(new AllocatedEndpoint(e, "redis.dev.internal", 6379, EndpointBindingMode.SingleAddress, targetPortExpression: null, networkID: KnownNetworkIdentifiers.DefaultAspireContainerNetwork));
e.AllAllocatedEndpoints.TryAdd(KnownNetworkIdentifiers.DefaultAspireContainerNetwork, snapshot);
})
.WithRedisInsight();

using var app = appBuilder.Build();
Expand Down
Loading
Loading