From 1e87bfd0672b39d6a0f16ef4256942fd87ac12bc Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 02:37:39 -0700 Subject: [PATCH 01/14] Allow HostUrl to remap both address and port --- .../ConnectionStringReference.cs | 16 +---- .../ApplicationModel/EndpointReference.cs | 20 ++---- .../ApplicationModel/HostUrl.cs | 71 ++++++++++++++----- .../IResourceWithConnectionString.cs | 7 +- .../ApplicationModel/IValueProvider.cs | 4 +- .../ApplicationModel/ReferenceExpression.cs | 2 +- .../ApplicationModel/ResourceExtensions.cs | 50 ++++++------- .../ValueProviderExtensions.cs | 12 ++++ src/Aspire.Hosting/Dcp/DcpExecutor.cs | 9 +-- .../OtlpConfigurationExtensions.cs | 2 +- 10 files changed, 102 insertions(+), 91 deletions(-) create mode 100644 src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs diff --git a/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs index a8804c193fa..9d464b9cd6e 100644 --- a/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs @@ -23,22 +23,12 @@ public class ConnectionStringReference(IResourceWithConnectionString resource, b ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) { - return this.GetNetworkValueAsync(null, cancellationToken); + return Resource.GetValueAsync(cancellationToken); } - ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) + async ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) { - return context.Network switch - { - NetworkIdentifier networkContext => GetNetworkValueAsync(networkContext, cancellationToken), - _ => GetNetworkValueAsync(null, cancellationToken) - }; - } - - private async ValueTask GetNetworkValueAsync(NetworkIdentifier? networkContext, CancellationToken cancellationToken) - { - ValueProviderContext vpc = new() { Network = networkContext }; - var value = await Resource.GetValueAsync(vpc, cancellationToken).ConfigureAwait(false); + var value = await Resource.GetValueAsync(context, cancellationToken).ConfigureAwait(false); if (string.IsNullOrEmpty(value) && !Optional) { diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 85b21887452..05d77c4352f 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -265,7 +265,7 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En /// Throws when the selected enumeration is not known. public ValueTask GetValueAsync(CancellationToken cancellationToken = default) { - return GetNetworkValueAsync(null, cancellationToken); + return GetValueAsync(new(), cancellationToken); } /// @@ -275,33 +275,23 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En /// A . /// A containing the selected value. /// Throws when the selected enumeration is not known. - public ValueTask GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default) + public async ValueTask GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default) { - return context.Network switch - { - NetworkIdentifier networkID => GetNetworkValueAsync(networkID, cancellationToken), - _ => GetNetworkValueAsync(null, cancellationToken) - }; - } + var networkContext = context.GetNetworkIdentifier(); - - private async ValueTask GetNetworkValueAsync(NetworkIdentifier? context, CancellationToken cancellationToken = default) - { return Property switch { EndpointProperty.Scheme => new(Endpoint.Scheme), - EndpointProperty.IPV4Host when context is null || context == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1", + EndpointProperty.IPV4Host when context is null || networkContext == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1", EndpointProperty.TargetPort when Endpoint.TargetPort is int port => new(port.ToString(CultureInfo.InvariantCulture)), _ => await ResolveValueWithAllocatedAddress().ConfigureAwait(false) }; async ValueTask ResolveValueWithAllocatedAddress() { - var effectiveContext = context ?? Endpoint.ContextNetworkID; - // 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 == effectiveContext).FirstOrDefault(); + var nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == networkContext).FirstOrDefault(); if (nes is null) { return null; diff --git a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs index 51a732af4ef..21ea2e00cb5 100644 --- a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs +++ b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs @@ -1,6 +1,10 @@ // 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.Dcp; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + namespace Aspire.Hosting.ApplicationModel; /// @@ -13,26 +17,19 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider string IManifestExpressionProvider.ValueExpression => Url; // Returns the url - ValueTask IValueProvider.GetValueAsync(System.Threading.CancellationToken _) => GetNetworkValueAsync(null); + ValueTask IValueProvider.GetValueAsync(System.Threading.CancellationToken cancellationToken) => ((IValueProvider)this).GetValueAsync(new(), cancellationToken); // Returns the url - ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken _) + async ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) { - return context.Network switch - { - NetworkIdentifier networkContext => GetNetworkValueAsync(networkContext), - _ => GetNetworkValueAsync(null) - }; - } + var networkContext = context.GetNetworkIdentifier(); - private ValueTask GetNetworkValueAsync(NetworkIdentifier? context) - { // HostUrl is a bit of a hack that is not modeled as an expression // So in this one case, we need to fix up the container host name 'manually' // Internally, this is only used for OTEL_EXPORTER_OTLP_ENDPOINT, but HostUrl // is public, so we don't control how it is used - if (context is null || context == KnownNetworkIdentifiers.LocalhostNetwork) + if (networkContext == KnownNetworkIdentifiers.LocalhostNetwork) { return new(Url); } @@ -44,14 +41,52 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider var uri = new UriBuilder(Url); if (uri.Host is "localhost" or "127.0.0.1" or "[::1]") { - var hasEndingSlash = Url.EndsWith('/'); - uri.Host = KnownHostNames.DefaultContainerTunnelHostName; - retval = uri.ToString(); - - // Remove trailing slash if we didn't have one before (UriBuilder always adds one) - if (!hasEndingSlash && retval.EndsWith('/')) + if (context.ExecutionContext is { } && context.ExecutionContext.IsRunMode) { - retval = retval[..^1]; + var options = context.ExecutionContext.ServiceProvider.GetRequiredService>(); + + var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService(); + var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + var hasEndingSlash = Url.EndsWith('/'); + uri.Host = options.Value.EnableAspireContainerTunnel == true ? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge; + + if (options.Value.EnableAspireContainerTunnel) + { + // We need to consider that both the host and port may need to be remapped + var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); + var targetResource = model.Resources.FirstOrDefault(r => + { + // Find a non-container resource with an endpoint matching the original localhost:port + return !r.IsContainer() && + r is IResourceWithEndpoints && + r.TryGetEndpoints(out var endpoints) && + endpoints.Any(ep => ep.DefaultNetworkID == KnownNetworkIdentifiers.LocalhostNetwork && ep.Port == uri.Port); + }); + + if (targetResource is IResourceWithEndpoints resourceWithEndpoints) + { + var originalEndpoint = resourceWithEndpoints.GetEndpoints().FirstOrDefault(ep => ep.ContextNetworkID == KnownNetworkIdentifiers.LocalhostNetwork && ep.Port == uri.Port); + if (originalEndpoint is not null) + { + // Find the mapped endpoint for the target network context + var mappedEndpoint = resourceWithEndpoints.GetEndpoint(originalEndpoint.EndpointName, networkContext); + if (mappedEndpoint is not null) + { + // Update the port to the mapped port + uri.Port = mappedEndpoint.Port; + } + } + } + } + + retval = uri.ToString(); + + // Remove trailing slash if we didn't have one before (UriBuilder always adds one) + if (!hasEndingSlash && retval.EndsWith('/')) + { + retval = retval[..^1]; + } } } } diff --git a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs index 4750d40b7a6..c60dfb1f1a3 100644 --- a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs +++ b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs @@ -20,11 +20,8 @@ public interface IResourceWithConnectionString : IResource, IManifestExpressionP ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) => GetConnectionStringAsync(cancellationToken); - ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) => context.Network switch - { - NetworkIdentifier networkContext => ConnectionStringExpression.GetValueAsync(new ValueProviderContext { Network = networkContext }, cancellationToken), - _ => GetConnectionStringAsync(cancellationToken), - }; + ValueTask IValueProvider.GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken) => + ConnectionStringExpression.GetValueAsync(context, cancellationToken); /// /// Describes the connection string format string used for this resource. diff --git a/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs b/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs index 2c5e42f517d..afc80841c28 100644 --- a/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs +++ b/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs @@ -9,9 +9,9 @@ namespace Aspire.Hosting.ApplicationModel; public class ValueProviderContext { /// - /// Additional services that can be used during value resolution. + /// The execution context for the distributed application. /// - public IServiceProvider? Services { get; init; } + public DistributedApplicationExecutionContext? ExecutionContext { get; init; } /// /// The resource that is requesting the value. diff --git a/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs index e96764eff72..169ce1382fd 100644 --- a/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs +++ b/src/Aspire.Hosting/ApplicationModel/ReferenceExpression.cs @@ -98,7 +98,7 @@ private ReferenceExpression(string format, IValueProvider[] valueProviders, stri /// A . public ValueTask GetValueAsync(CancellationToken cancellationToken) { - return this.GetValueAsync(new ValueProviderContext(), cancellationToken); + return this.GetValueAsync(new(), cancellationToken); } internal static ReferenceExpression Create(string format, IValueProvider[] valueProviders, string[] manifestExpressions, string?[] stringFormats) diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index ad66cb87edc..50c56695155 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -271,7 +271,6 @@ await resource.ProcessArgumentValuesAsync( /// /// The logger used for logging information or errors during the argument processing. /// A token for cancelling the operation, if needed. - /// An optional network identifier providing context for resolving network-related values. /// A task representing the asynchronous operation. public static async ValueTask ProcessArgumentValuesAsync( this IResource resource, @@ -279,8 +278,7 @@ public static async ValueTask ProcessArgumentValuesAsync( // (unprocessed, processed, exception, isSensitive) Action processValue, ILogger logger, - CancellationToken cancellationToken = default, - NetworkIdentifier? networkContext = null) + CancellationToken cancellationToken = default) { if (resource.TryGetAnnotationsOfType(out var callbacks)) { @@ -296,13 +294,11 @@ public static async ValueTask ProcessArgumentValuesAsync( await callback.Callback(context).ConfigureAwait(false); } - networkContext ??= resource.GetDefaultResourceNetwork(); - foreach (var a in args) { try { - var resolvedValue = await ResolveValueAsync(executionContext, logger, a, null, networkContext, cancellationToken).ConfigureAwait(false); + var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, a, null, cancellationToken).ConfigureAwait(false); if (resolvedValue?.Value != null) { @@ -325,15 +321,13 @@ public static async ValueTask ProcessArgumentValuesAsync( /// An action delegate invoked for each environment variable, providing the key, the unprocessed value, the processed value (if available), and any exception encountered during processing. /// The logger used to log any information or errors during the environment variables processing. /// A cancellation token to observe during the asynchronous operation. - /// An optional network identifier providing context for resolving network-related values. /// A task that represents the asynchronous operation. public static async ValueTask ProcessEnvironmentVariableValuesAsync( this IResource resource, DistributedApplicationExecutionContext executionContext, Action processValue, ILogger logger, - CancellationToken cancellationToken = default, - NetworkIdentifier? networkContext = null) + CancellationToken cancellationToken = default) { if (resource.TryGetEnvironmentVariables(out var callbacks)) { @@ -348,13 +342,11 @@ public static async ValueTask ProcessEnvironmentVariableValuesAsync( await callback.Callback(context).ConfigureAwait(false); } - networkContext ??= resource.GetDefaultResourceNetwork(); - foreach (var (key, expr) in config) { try { - var resolvedValue = await ResolveValueAsync(executionContext, logger, expr, key, networkContext, cancellationToken).ConfigureAwait(false); + var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, expr, key, cancellationToken).ConfigureAwait(false); if (resolvedValue?.Value is not null) { @@ -378,8 +370,8 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou /// Processes trusted certificates configuration for the specified resource within the given execution context. /// This may produce additional and /// annotations on the resource to configure certificate trust as needed and therefore must be run before - /// - /// and are called. + /// + /// and are called. /// /// The resource for which to process the certificate trust configuration. /// The execution context used during the processing. @@ -388,7 +380,6 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou /// The logger used for logging information during the processing. /// A function that takes the active and returns a representing the path to a custom certificate bundle for the resource. /// A function that takes the active and returns a representing path(s) to a directory containing the custom certificates for the resource. - /// An optional network identifier providing context for resolving network-related values. /// A cancellation token to observe while processing. /// A task that represents the asynchronous operation. public static async ValueTask<(CertificateTrustScope, X509Certificate2Collection?)> ProcessCertificateTrustConfigAsync( @@ -401,7 +392,6 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou ILogger logger, Func bundlePathFactory, Func certificateDirectoryPathsFactory, - NetworkIdentifier? networkContext = null, CancellationToken cancellationToken = default) { var developerCertificateService = executionContext.ServiceProvider.GetRequiredService(); @@ -494,7 +484,7 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou { try { - var resolvedValue = await ResolveValueAsync(executionContext, logger, a, null, networkContext, cancellationToken).ConfigureAwait(false); + var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, a, null, cancellationToken).ConfigureAwait(false); if (resolvedValue?.Value != null) { @@ -511,7 +501,7 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou { try { - var resolvedValue = await ResolveValueAsync(executionContext, logger, expr, key, networkContext, cancellationToken).ConfigureAwait(false); + var resolvedValue = await resource.ResolveValueAsync(executionContext, logger, expr, key, cancellationToken).ConfigureAwait(false); if (resolvedValue?.Value is not null) { @@ -528,18 +518,18 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou } private static async ValueTask ResolveValueAsync( + this IResource resource, DistributedApplicationExecutionContext executionContext, ILogger logger, - object value, + object? value, string? key = null, - NetworkIdentifier? networkContext = null, CancellationToken cancellationToken = default) { return (executionContext.Operation, value) switch { (_, string s) => new(s, false), - (DistributedApplicationOperation.Run, IValueProvider provider) => await GetValue(key, provider, logger, networkContext, cancellationToken).ConfigureAwait(false), - (DistributedApplicationOperation.Run, IResourceBuilder rb) when rb.Resource is IValueProvider provider => await GetValue(key, provider, logger, networkContext, cancellationToken).ConfigureAwait(false), + (DistributedApplicationOperation.Run, IValueProvider provider) => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false), + (DistributedApplicationOperation.Run, IResourceBuilder rb) when rb.Resource is IValueProvider provider => await resource.GetValue(executionContext, key, provider, logger, cancellationToken).ConfigureAwait(false), (DistributedApplicationOperation.Publish, IManifestExpressionProvider provider) => new(provider.ValueExpression, false), (DistributedApplicationOperation.Publish, IResourceBuilder rb) when rb.Resource is IManifestExpressionProvider provider => new(provider.ValueExpression, false), (_, { } o) => new(o.ToString(), false), @@ -556,10 +546,10 @@ public static bool IsExcludedFromPublish(this IResource resource) => internal static async ValueTask ProcessContainerRuntimeArgValues( this IResource resource, + DistributedApplicationExecutionContext executionContext, Action processValue, ILogger logger, - CancellationToken cancellationToken = default, - NetworkIdentifier? context = null) + CancellationToken cancellationToken = default) { // Apply optional extra arguments to the container run command. if (resource.TryGetAnnotationsOfType(out var runArgsCallback)) @@ -580,7 +570,7 @@ internal static async ValueTask ProcessContainerRuntimeArgValues( var value = arg switch { string s => s, - IValueProvider valueProvider => (await GetValue(key: null, valueProvider, logger, context, cancellationToken).ConfigureAwait(false))?.Value, + IValueProvider valueProvider => (await resource.GetValue(executionContext, key: null, valueProvider, logger, cancellationToken).ConfigureAwait(false))?.Value, { } obj => obj.ToString(), null => null }; @@ -598,21 +588,21 @@ internal static async ValueTask ProcessContainerRuntimeArgValues( } } - private static async Task GetValue(string? key, IValueProvider valueProvider, ILogger logger, NetworkIdentifier? networkContext, CancellationToken cancellationToken) + private static async Task GetValue(this IResource resource, DistributedApplicationExecutionContext executionContext, string? key, IValueProvider valueProvider, ILogger logger, CancellationToken cancellationToken) { - var task = ExpressionResolver.ResolveAsync(valueProvider, new ValueProviderContext() { Network = networkContext }, cancellationToken); + var task = ExpressionResolver.ResolveAsync(valueProvider, new ValueProviderContext() { ExecutionContext = executionContext, Caller = resource }, cancellationToken); if (!task.IsCompleted) { - if (valueProvider is IResource resource) + if (valueProvider is IResource providerResource) { if (key is null) { - logger.LogInformation("Waiting for value from resource '{ResourceName}'", resource.Name); + logger.LogInformation("Waiting for value from resource '{ResourceName}'", providerResource.Name); } else { - logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, resource.Name); + logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, providerResource.Name); } } else if (valueProvider is ConnectionStringReference { Resource: var cs }) diff --git a/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs new file mode 100644 index 00000000000..ea89f92a5da --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.ApplicationModel; + +internal static class ValueProviderExtensions +{ + public static NetworkIdentifier GetNetworkIdentifier(this ValueProviderContext context) + { + return context?.Network ?? context?.Caller?.GetDefaultResourceNetwork() ?? KnownNetworkIdentifiers.LocalhostNetwork; + } +} \ No newline at end of file diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index c60216be665..a492dbd3049 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -131,7 +131,7 @@ public DcpExecutor(ILogger logger, } private string ContainerHostName => _configuration["AppHost:ContainerHostname"] ?? - (_options.Value.EnableAspireContainerTunnel ? KnownHostNames.DefaultContainerTunnelHostName : KnownHostNames.DockerDesktopHostBridge); + (_options.Value.EnableAspireContainerTunnel ? KnownHostNames.DefaultContainerTunnelHostName : _dcpInfo?.Containers?.HostName ?? KnownHostNames.DockerDesktopHostBridge); public async Task RunApplicationAsync(CancellationToken cancellationToken = default) { @@ -846,7 +846,7 @@ private async Task EnsureContainerServiceAddressInfo(CancellationToken cancellat } else { - // Container services are services that "mirror" their primary (host) service counterparts, but expose addresses usable from container network. + // Container services are services that "mirror" their primary (host) service counterparts, but expose addresses usable from container network. // We just need to update their ports from primary services, changing the address to container host. var containerServices = _appResources.Where(r => r.DcpResource is Service { }).Select(r => ( Service: r.DcpResource as Service, @@ -1186,7 +1186,6 @@ private void PrepareServices() svc.Annotate(CustomResource.ContainerTunnelInstanceName, tunnelProxy?.Metadata?.Name ?? ""); var svcAppResource = new ServiceAppResource(svc); - _appResources.Add(svcAppResource); if (useTunnel) @@ -2266,6 +2265,7 @@ await modelResource.ProcessEnvironmentVariableValuesAsync( var runArgs = new List(); await modelResource.ProcessContainerRuntimeArgValues( + _executionContext, (a, ex) => { if (ex is not null) @@ -2338,7 +2338,6 @@ await modelResource.ProcessContainerRuntimeArgValues( resourceLogger, (scope) => ReferenceExpression.Create($"{bundleOutputPath}"), (scope) => ReferenceExpression.Create($"{certificatesOutputPath}"), - networkContext: null, cancellationToken).ConfigureAwait(false); if (certificates?.Any() == true) @@ -2390,7 +2389,6 @@ await modelResource.ProcessContainerRuntimeArgValues( var env = new List(); var createFiles = new List(); - var pathsProvider = new CertificateTrustConfigurationPathsProvider(); (var scope, var certificates) = await modelResource.ProcessCertificateTrustConfigAsync( _executionContext, (unprocessed, value, ex, isSensitive) => @@ -2435,7 +2433,6 @@ await modelResource.ProcessContainerRuntimeArgValues( // Build Linux PATH style colon-separated list of directories return ReferenceExpression.Create($"{string.Join(':', dirs)}"); }, - networkContext: null, cancellationToken).ConfigureAwait(false); if (certificates?.Any() == true) diff --git a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs index 644361bf7fb..f5d79d72da6 100644 --- a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs +++ b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs @@ -168,7 +168,7 @@ public static IResourceBuilder WithOtlpExporter(this IResourceBuilder b ArgumentNullException.ThrowIfNull(builder); AddOtlpEnvironment(builder.Resource, builder.ApplicationBuilder.Configuration, builder.ApplicationBuilder.Environment); - + return builder; } From da15e5abfe3272936d97ab596440fc54e6183d20 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 09:40:20 -0700 Subject: [PATCH 02/14] Fix method signature in test --- .../Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs index 0ccb7636505..7b968faebe6 100644 --- a/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs +++ b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs @@ -35,8 +35,7 @@ await resource.ProcessEnvironmentVariableValuesAsync( } }, NullLogger.Instance, - CancellationToken.None, - networkContext); + CancellationToken.None); return environmentVariables; } From c72f38a2e959981bd7cfa104ae7a76960c16605b Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 09:44:21 -0700 Subject: [PATCH 03/14] Check correct context value --- src/Aspire.Hosting/ApplicationModel/EndpointReference.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 05d77c4352f..11ea3c05451 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -282,7 +282,7 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En return Property switch { EndpointProperty.Scheme => new(Endpoint.Scheme), - EndpointProperty.IPV4Host when context is null || networkContext == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1", + EndpointProperty.IPV4Host when networkContext == KnownNetworkIdentifiers.LocalhostNetwork => "127.0.0.1", EndpointProperty.TargetPort when Endpoint.TargetPort is int port => new(port.ToString(CultureInfo.InvariantCulture)), _ => await ResolveValueWithAllocatedAddress().ConfigureAwait(false) }; From d09b84a8b9ba14913b0f08eebf530e8629f62923 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 13:55:10 -0700 Subject: [PATCH 04/14] Fix failing tests --- .../ApplicationModel/EndpointReference.cs | 5 ++ .../ApplicationModel/ExpressionResolver.cs | 5 +- .../ApplicationModel/HostUrl.cs | 69 ++++++++++--------- .../ApplicationModel/ResourceExtensions.cs | 5 ++ .../ValueProviderExtensions.cs | 17 ++++- src/Aspire.Hosting/Aspire.Hosting.csproj | 1 + .../Dashboard/DashboardEventHandlers.cs | 12 ++-- .../Dashboard/DashboardResourceTests.cs | 4 +- .../ExpressionResolverTests.cs | 60 +++++++++++----- .../Utils/EnvironmentVariableEvaluator.cs | 3 +- .../WithEnvironmentTests.cs | 3 + .../Aspire.Hosting.Yarp.Tests/AddYarpTests.cs | 5 ++ 12 files changed, 127 insertions(+), 62 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 11ea3c05451..02da336a480 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -292,6 +292,11 @@ 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(); + if (nes is null) + { + nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == Endpoint.ContextNetworkID).FirstOrDefault(); + } + if (nes is null) { return null; diff --git a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs index 5127f1558e6..1fdea798226 100644 --- a/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs +++ b/src/Aspire.Hosting/ApplicationModel/ExpressionResolver.cs @@ -95,13 +95,14 @@ async Task ResolveConnectionStringReferenceAsync(ConnectionString /// async ValueTask 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 context.Network == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false), - EndpointReferenceExpression ep when context.Network == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(ep.Endpoint, ep.Property, context).ConfigureAwait(false), false), + EndpointReference er when networkContext == KnownNetworkIdentifiers.DefaultAspireContainerNetwork => new ResolvedValue(await ResolveInContainerContextAsync(er, EndpointProperty.Url, context).ConfigureAwait(false), false), + EndpointReferenceExpression ep when 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() }; diff --git a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs index 21ea2e00cb5..192ae18e2d8 100644 --- a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs +++ b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs @@ -41,63 +41,70 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider var uri = new UriBuilder(Url); if (uri.Host is "localhost" or "127.0.0.1" or "[::1]") { - if (context.ExecutionContext is { } && context.ExecutionContext.IsRunMode) + if (context.ExecutionContext?.IsRunMode == true) { var options = context.ExecutionContext.ServiceProvider.GetRequiredService>(); var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService(); var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - var hasEndingSlash = Url.EndsWith('/'); - uri.Host = options.Value.EnableAspireContainerTunnel == true ? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge; + uri.Host = options.Value.EnableAspireContainerTunnel? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge; if (options.Value.EnableAspireContainerTunnel) { // We need to consider that both the host and port may need to be remapped var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); - var targetResource = model.Resources.FirstOrDefault(r => - { - // Find a non-container resource with an endpoint matching the original localhost:port - return !r.IsContainer() && - r is IResourceWithEndpoints && - r.TryGetEndpoints(out var endpoints) && - endpoints.Any(ep => ep.DefaultNetworkID == KnownNetworkIdentifiers.LocalhostNetwork && ep.Port == uri.Port); - }); - - if (targetResource is IResourceWithEndpoints resourceWithEndpoints) - { - var originalEndpoint = resourceWithEndpoints.GetEndpoints().FirstOrDefault(ep => ep.ContextNetworkID == KnownNetworkIdentifiers.LocalhostNetwork && ep.Port == uri.Port); - if (originalEndpoint is not null) + var targetEndpoint = model.Resources.Where(r => !r.IsContainer()) + .OfType() + .Select(r => { - // Find the mapped endpoint for the target network context - var mappedEndpoint = resourceWithEndpoints.GetEndpoint(originalEndpoint.EndpointName, networkContext); - if (mappedEndpoint is not null) + if (r.GetEndpoints(KnownNetworkIdentifiers.LocalhostNetwork).FirstOrDefault(ep => ep.Port == uri.Port) is EndpointReference ep) { - // Update the port to the mapped port - uri.Port = mappedEndpoint.Port; + return r.GetEndpoint(ep.EndpointName, networkContext); } - } + + return null; + }) + .Where(ep => ep is not null) + .FirstOrDefault(); + + if (targetEndpoint is { }) + { + uri.Port = targetEndpoint.Port; } } retval = uri.ToString(); - - // Remove trailing slash if we didn't have one before (UriBuilder always adds one) - if (!hasEndingSlash && retval.EndsWith('/')) - { - retval = retval[..^1]; - } } } + + var hasEndingSlash = Url.EndsWith('/'); + + // Remove trailing slash if we didn't have one before (UriBuilder always adds one) + if (!hasEndingSlash && retval.EndsWith('/')) + { + retval = retval[..^1]; + } } catch (UriFormatException) { + var replacementHost = KnownHostNames.DockerDesktopHostBridge; + if (context.ExecutionContext?.IsRunMode == true) + { + var options = context.ExecutionContext.ServiceProvider.GetRequiredService>(); + + var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService(); + var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + replacementHost = options.Value.EnableAspireContainerTunnel ? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge; + } + // HostUrl was meant to only be used with valid URLs. However, this was not // previously enforced. So we need to handle the case where it's not a valid URL, // by falling back to a simple string replacement. - retval = retval.Replace(KnownHostNames.Localhost, KnownHostNames.DefaultContainerTunnelHostName, StringComparison.OrdinalIgnoreCase) - .Replace("127.0.0.1", KnownHostNames.DefaultContainerTunnelHostName) - .Replace("[::1]", KnownHostNames.DefaultContainerTunnelHostName); + retval = retval.Replace(KnownHostNames.Localhost, replacementHost, StringComparison.OrdinalIgnoreCase) + .Replace("127.0.0.1", replacementHost) + .Replace("[::1]", replacementHost); } return new(retval); diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 50c56695155..ff17b7b0e28 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -366,6 +366,11 @@ internal static NetworkIdentifier GetDefaultResourceNetwork(this IResource resou return resource.IsContainer() ? KnownNetworkIdentifiers.DefaultAspireContainerNetwork : KnownNetworkIdentifiers.LocalhostNetwork; } + internal static IEnumerable GetSupportedNetworks(this IResource resource) + { + return resource.IsContainer() ? [KnownNetworkIdentifiers.DefaultAspireContainerNetwork, KnownNetworkIdentifiers.LocalhostNetwork] : [KnownNetworkIdentifiers.LocalhostNetwork]; + } + /// /// Processes trusted certificates configuration for the specified resource within the given execution context. /// This may produce additional and diff --git a/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs index ea89f92a5da..0cf33ec5cc7 100644 --- a/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs @@ -9,4 +9,19 @@ public static NetworkIdentifier GetNetworkIdentifier(this ValueProviderContext c { return context?.Network ?? context?.Caller?.GetDefaultResourceNetwork() ?? KnownNetworkIdentifiers.LocalhostNetwork; } -} \ No newline at end of file + + public static IEnumerable GetSupportedNetworkIdentifiers(this ValueProviderContext context) + { + if (context?.Network is { } network) + { + return [network]; + } + + if (context?.Caller?.GetSupportedNetworks() is { } networks) + { + return networks; + } + + return [KnownNetworkIdentifiers.LocalhostNetwork]; + } +} diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index 969b4d80999..079083aadfe 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -105,6 +105,7 @@ + diff --git a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs index f4eb4c6d30a..77ae160fcbe 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs @@ -612,26 +612,26 @@ private static void PopulateDashboardUrls(EnvironmentCallbackContext context) static ReferenceExpression GetTargetUrlExpression(EndpointReference e) => ReferenceExpression.Create($"{e.Property(EndpointProperty.Scheme)}://{e.EndpointAnnotation.TargetHost}:{e.Property(EndpointProperty.TargetPort)}"); - var otlpGrpc = dashboardResource.GetEndpoint(OtlpGrpcEndpointName); + var otlpGrpc = dashboardResource.GetEndpoint(OtlpGrpcEndpointName, KnownNetworkIdentifiers.LocalhostNetwork); if (otlpGrpc.Exists) { context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpGrpcUrlName.EnvVarName] = GetTargetUrlExpression(otlpGrpc); } - var otlpHttp = dashboardResource.GetEndpoint(OtlpHttpEndpointName); + var otlpHttp = dashboardResource.GetEndpoint(OtlpHttpEndpointName, KnownNetworkIdentifiers.LocalhostNetwork); if (otlpHttp.Exists) { context.EnvironmentVariables[DashboardConfigNames.DashboardOtlpHttpUrlName.EnvVarName] = GetTargetUrlExpression(otlpHttp); } - var mcp = dashboardResource.GetEndpoint(McpEndpointName); + var mcp = dashboardResource.GetEndpoint(McpEndpointName, KnownNetworkIdentifiers.LocalhostNetwork); if (!mcp.Exists) { // Fallback to frontend https or http endpoint if not configured. - mcp = dashboardResource.GetEndpoint("https"); + mcp = dashboardResource.GetEndpoint("https", KnownNetworkIdentifiers.LocalhostNetwork); if (!mcp.Exists) { - mcp = dashboardResource.GetEndpoint("http"); + mcp = dashboardResource.GetEndpoint("http", KnownNetworkIdentifiers.LocalhostNetwork); } } @@ -644,7 +644,7 @@ static ReferenceExpression GetTargetUrlExpression(EndpointReference e) => context.EnvironmentVariables[DashboardConfigNames.DashboardMcpUrlName.EnvVarName] = GetTargetUrlExpression(mcp); } - var frontendEndpoints = dashboardResource.GetEndpoints().ToList(); + var frontendEndpoints = dashboardResource.GetEndpoints(KnownNetworkIdentifiers.LocalhostNetwork).ToList(); var aspnetCoreUrls = new ReferenceExpressionBuilder(); var first = true; diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs index 42cfeade6b1..91fb2f62d1b 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardResourceTests.cs @@ -112,7 +112,7 @@ public async Task DashboardDoesNotAddResource_ConfiguresExistingDashboard(string Assert.Same(container.Resource, dashboard); - var config = (await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance, KnownNetworkIdentifiers.LocalhostNetwork).DefaultTimeout()) + var config = (await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout()) .OrderBy(c => c.Key) .ToList(); @@ -214,7 +214,7 @@ public async Task DashboardDoesNotAddResource_ConfiguresMcpEndpoint(int expected Assert.Same(container.Resource, dashboard); - var config = (await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance, KnownNetworkIdentifiers.LocalhostNetwork).DefaultTimeout()) + var config = (await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(dashboard, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout()) .OrderBy(c => c.Key) .ToList(); diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index b9b06627a9d..fe9d0936dda 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -1,8 +1,10 @@ // 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.Dcp; using Aspire.Hosting.Tests.Utils; using Microsoft.AspNetCore.InternalTesting; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Tests; @@ -147,19 +149,31 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN } [Theory] - [InlineData(false, "http://localhost:18889", "http://localhost:18889")] - [InlineData(true, "http://localhost:18889", "http://aspire.dev.internal:18889")] - [InlineData(false, "http://127.0.0.1:18889", "http://127.0.0.1:18889")] - [InlineData(true, "http://127.0.0.1:18889", "http://aspire.dev.internal:18889")] - [InlineData(false, "http://[::1]:18889", "http://[::1]:18889")] - [InlineData(true, "http://[::1]:18889", "http://aspire.dev.internal:18889")] - [InlineData(false, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - public async Task HostUrlPropertyGetsResolved(bool targetIsContainer, string hostUrlVal, string expectedValue) + [InlineData(false, true, "http://localhost:18889", "http://localhost:18889")] + [InlineData(true, true, "http://localhost:18889", "http://aspire.dev.internal:18889")] + [InlineData(false, true, "http://127.0.0.1:18889", "http://127.0.0.1:18889")] + [InlineData(true, true, "http://127.0.0.1:18889", "http://aspire.dev.internal:18889")] + [InlineData(false, true, "http://[::1]:18889", "http://[::1]:18889")] + [InlineData(true, true, "http://[::1]:18889", "http://aspire.dev.internal:18889")] + [InlineData(false, true, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, true, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(false, true, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, true, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(false, true, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, true, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(false, false, "http://localhost:18889", "http://localhost:18889")] + [InlineData(true, false, "http://localhost:18889", "http://host.docker.internal:18889")] + [InlineData(false, false, "http://127.0.0.1:18889", "http://127.0.0.1:18889")] + [InlineData(true, false, "http://127.0.0.1:18889", "http://host.docker.internal:18889")] + [InlineData(false, false, "http://[::1]:18889", "http://[::1]:18889")] + [InlineData(true, false, "http://[::1]:18889", "http://host.docker.internal:18889")] + [InlineData(false, false, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, false, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(false, false, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, false, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(false, false, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy")] + [InlineData(true, false, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] + public async Task HostUrlPropertyGetsResolved(bool targetIsContainer, bool withTunnel, string hostUrlVal, string expectedValue) { var builder = DistributedApplication.CreateBuilder(); @@ -176,14 +190,20 @@ public async Task HostUrlPropertyGetsResolved(bool targetIsContainer, string hos test = test.WithImage("someimage"); } - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + var testServiceProvider = new TestServiceProvider(); + testServiceProvider.AddService(Options.Create(new DcpOptions() { EnableAspireContainerTunnel = withTunnel })); + testServiceProvider.AddService(new DistributedApplicationModel(builder.Resources)); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, testServiceProvider).DefaultTimeout(); Assert.Equal(expectedValue, config["envname"]); } [Theory] - [InlineData(false, "http://localhost:18889")] - [InlineData(true, "http://aspire.dev.internal:18889")] - public async Task HostUrlPropertyGetsResolvedInOtlpExporterEndpoint(bool container, string expectedValue) + [InlineData(false, true, "http://localhost:18889")] + [InlineData(true, true, "http://aspire.dev.internal:18889")] + [InlineData(false, false, "http://localhost:18889")] + [InlineData(true, false, "http://host.docker.internal:18889")] + public async Task HostUrlPropertyGetsResolvedInOtlpExporterEndpoint(bool container, bool withTunnel, string expectedValue) { var builder = DistributedApplication.CreateBuilder(); @@ -195,7 +215,11 @@ public async Task HostUrlPropertyGetsResolvedInOtlpExporterEndpoint(bool contain test = test.WithImage("someimage"); } - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + var testServiceProvider = new TestServiceProvider(); + testServiceProvider.AddService(Options.Create(new DcpOptions() { EnableAspireContainerTunnel = withTunnel })); + testServiceProvider.AddService(new DistributedApplicationModel(builder.Resources)); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(test.Resource, DistributedApplicationOperation.Run, testServiceProvider).DefaultTimeout(); Assert.Equal(expectedValue, config["OTEL_EXPORTER_OTLP_ENDPOINT"]); } diff --git a/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs index 7b968faebe6..7eef30da6df 100644 --- a/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs +++ b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs @@ -11,8 +11,7 @@ public static class EnvironmentVariableEvaluator public static async ValueTask> GetEnvironmentVariablesAsync( IResource resource, DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run, - IServiceProvider? serviceProvider = null, - NetworkIdentifier? networkContext = null) + IServiceProvider? serviceProvider = null) { var executionContext = new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(applicationOperation) { diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index af2a30a50f3..a84abcaa0b1 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -237,6 +237,9 @@ public async Task EnvironmentVariableExpressions() .WithEnvironment("TARGET_PORT", $"{endpoint.Property(EndpointProperty.TargetPort)}") .WithEnvironment("HOST", $"{test.Resource};name=1"); + /*var testServiceProvider = new TestServiceProvider(); + testServiceProvider.AddService<>*/ + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource).DefaultTimeout(); var manifestConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); diff --git a/tests/Aspire.Hosting.Yarp.Tests/AddYarpTests.cs b/tests/Aspire.Hosting.Yarp.Tests/AddYarpTests.cs index aade0c97a43..e3b8a3895a2 100644 --- a/tests/Aspire.Hosting.Yarp.Tests/AddYarpTests.cs +++ b/tests/Aspire.Hosting.Yarp.Tests/AddYarpTests.cs @@ -3,8 +3,10 @@ using System.Security.Cryptography.X509Certificates; using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Dcp; using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; +using Microsoft.Extensions.Options; namespace Aspire.Hosting.Yarp.Tests; @@ -36,6 +38,7 @@ public async Task VerifyRunEnvVariablesAreSet(bool containerCertificateSupport) new List(), containerCertificateSupport, trustCertificate: true)); + testProvider.AddService(Options.Create(new DcpOptions())); var yarp = builder.AddYarp("yarp"); @@ -83,6 +86,7 @@ public async Task VerifyWithStaticFilesAddsEnvironmentVariable() new List(), supportsContainerTrust: false, trustCertificate: true)); + testProvider.AddService(Options.Create(new DcpOptions())); var yarp = builder.AddYarp("yarp").WithStaticFiles(); @@ -123,6 +127,7 @@ public async Task VerifyWithStaticFilesBindMountAddsEnvironmentVariable() new List(), supportsContainerTrust: false, trustCertificate: true)); + testProvider.AddService(Options.Create(new DcpOptions())); using var tempDir = new TempDirectory(); From 6bc79dec88a43db03c66db16816ce6169b3916c4 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 14:01:40 -0700 Subject: [PATCH 05/14] Remove compatibility suppression that no longer applies --- .../CompatibilitySuppressions.xml | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index 61c71755871..f8feae33ce0 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -1,4 +1,4 @@ - + @@ -92,20 +92,6 @@ lib/net8.0/Aspire.Hosting.dll true - - CP0002 - M:Aspire.Hosting.ApplicationModel.ResourceExtensions.ProcessArgumentValuesAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.DistributedApplicationExecutionContext,System.Action{System.Object,System.String,System.Exception,System.Boolean},Microsoft.Extensions.Logging.ILogger,System.String,System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - - - CP0002 - M:Aspire.Hosting.ApplicationModel.ResourceExtensions.ProcessEnvironmentVariableValuesAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.DistributedApplicationExecutionContext,System.Action{System.String,System.Object,System.String,System.Exception},Microsoft.Extensions.Logging.ILogger,System.String,System.Threading.CancellationToken) - lib/net8.0/Aspire.Hosting.dll - lib/net8.0/Aspire.Hosting.dll - true - CP0002 M:Aspire.Hosting.IInteractionService.PromptInputsAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{Aspire.Hosting.InteractionInput},Aspire.Hosting.InputsDialogInteractionOptions,System.Threading.CancellationToken) @@ -197,4 +183,4 @@ lib/net8.0/Aspire.Hosting.dll true - \ No newline at end of file + From e88e58f8f3eac0fbd608600edffdf3bc87dd8e3d Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 14:43:34 -0700 Subject: [PATCH 06/14] Fix more tests --- .../ApplicationModel/EndpointReference.cs | 15 +++++---------- .../Dcp/DcpExecutorTests.cs | 1 + .../ExpressionResolverTests.cs | 18 +++--------------- 3 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 02da336a480..437a5b7fe9b 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -15,7 +15,7 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi // A reference to the endpoint annotation if it exists. private EndpointAnnotation? _endpointAnnotation; private bool? _isAllocated; - private readonly NetworkIdentifier _contextNetworkID; + private readonly NetworkIdentifier? _contextNetworkID; /// /// Gets the endpoint annotation associated with the endpoint reference. @@ -71,7 +71,7 @@ public sealed class EndpointReference : IManifestExpressionProvider, IValueProvi /// The reference will be resolved in the context of this network, which may be different /// from the network associated with the default network of the referenced Endpoint. /// - public NetworkIdentifier ContextNetworkID => _contextNetworkID; + public NetworkIdentifier? ContextNetworkID => _contextNetworkID; /// /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified. @@ -190,7 +190,7 @@ public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoi Resource = owner; EndpointName = endpoint.Name; _endpointAnnotation = endpoint; - _contextNetworkID = contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork; + _contextNetworkID = contextNetworkID; } /// @@ -221,7 +221,7 @@ public EndpointReference(IResourceWithEndpoints owner, string endpointName, Netw Resource = owner; EndpointName = endpointName; - _contextNetworkID = contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork; + _contextNetworkID = contextNetworkID; } /// @@ -277,7 +277,7 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En /// Throws when the selected enumeration is not known. public async ValueTask GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default) { - var networkContext = context.GetNetworkIdentifier(); + var networkContext = Endpoint.ContextNetworkID ?? context.GetNetworkIdentifier(); return Property switch { @@ -292,11 +292,6 @@ 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(); - if (nes is null) - { - nes = Endpoint.EndpointAnnotation.AllAllocatedEndpoints.Where(nes => nes.NetworkID == Endpoint.ContextNetworkID).FirstOrDefault(); - } - if (nes is null) { return null; diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 92876a2fb3e..6759aeef90e 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2035,6 +2035,7 @@ private static DcpExecutor CreateAppExecutor( { ServiceProvider = new TestServiceProvider(configuration) .AddService(developerCertificateService) + .AddService(Options.Create(dcpOptions)) }), resourceLoggerService, new TestDcpDependencyCheckService(), diff --git a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs index fe9d0936dda..65d574e1610 100644 --- a/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs +++ b/tests/Aspire.Hosting.Tests/ExpressionResolverTests.cs @@ -155,24 +155,12 @@ public async Task ExpressionResolverGeneratesCorrectEndpointStrings(string exprN [InlineData(true, true, "http://127.0.0.1:18889", "http://aspire.dev.internal:18889")] [InlineData(false, true, "http://[::1]:18889", "http://[::1]:18889")] [InlineData(true, true, "http://[::1]:18889", "http://aspire.dev.internal:18889")] - [InlineData(false, true, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, true, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, true, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, true, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, true, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, true, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=aspire.dev.internal,1433;User ID=sa;Password=xxx;Database=yyy")] [InlineData(false, false, "http://localhost:18889", "http://localhost:18889")] [InlineData(true, false, "http://localhost:18889", "http://host.docker.internal:18889")] [InlineData(false, false, "http://127.0.0.1:18889", "http://127.0.0.1:18889")] [InlineData(true, false, "http://127.0.0.1:18889", "http://host.docker.internal:18889")] [InlineData(false, false, "http://[::1]:18889", "http://[::1]:18889")] [InlineData(true, false, "http://[::1]:18889", "http://host.docker.internal:18889")] - [InlineData(false, false, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, false, "Server=localhost,1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, false, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, false, "Server=127.0.0.1,1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(false, false, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy")] - [InlineData(true, false, "Server=[::1],1433;User ID=sa;Password=xxx;Database=yyy", "Server=host.docker.internal,1433;User ID=sa;Password=xxx;Database=yyy")] public async Task HostUrlPropertyGetsResolved(bool targetIsContainer, bool withTunnel, string hostUrlVal, string expectedValue) { var builder = DistributedApplication.CreateBuilder(); @@ -270,9 +258,9 @@ sealed class TestValueProviderResource(string name) : Resource(name), IValueProv sealed class TestExpressionResolverResource : ContainerResource, IResourceWithEndpoints, IResourceWithConnectionString { readonly string _exprName; - EndpointReference Endpoint1 => new(this, "endpoint1", KnownNetworkIdentifiers.LocalhostNetwork); - EndpointReference Endpoint2 => new(this, "endpoint2", KnownNetworkIdentifiers.LocalhostNetwork); - EndpointReference Endpoint3 => new(this, "endpoint3", KnownNetworkIdentifiers.LocalhostNetwork); + EndpointReference Endpoint1 => new(this, "endpoint1"); + EndpointReference Endpoint2 => new(this, "endpoint2"); + EndpointReference Endpoint3 => new(this, "endpoint3"); Dictionary Expressions { get; } public TestExpressionResolverResource(string exprName) : base("testresource") { From 037a61394727de7a91527b6dd6b9a23e3f63ceaf Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 14:48:11 -0700 Subject: [PATCH 07/14] REmove unused code --- .../ApplicationModel/ValueProviderExtensions.cs | 15 --------------- .../Aspire.Hosting.Tests/WithEnvironmentTests.cs | 3 --- 2 files changed, 18 deletions(-) diff --git a/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs index 0cf33ec5cc7..49914efd985 100644 --- a/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ValueProviderExtensions.cs @@ -9,19 +9,4 @@ public static NetworkIdentifier GetNetworkIdentifier(this ValueProviderContext c { return context?.Network ?? context?.Caller?.GetDefaultResourceNetwork() ?? KnownNetworkIdentifiers.LocalhostNetwork; } - - public static IEnumerable GetSupportedNetworkIdentifiers(this ValueProviderContext context) - { - if (context?.Network is { } network) - { - return [network]; - } - - if (context?.Caller?.GetSupportedNetworks() is { } networks) - { - return networks; - } - - return [KnownNetworkIdentifiers.LocalhostNetwork]; - } } diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index a84abcaa0b1..af2a30a50f3 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -237,9 +237,6 @@ public async Task EnvironmentVariableExpressions() .WithEnvironment("TARGET_PORT", $"{endpoint.Property(EndpointProperty.TargetPort)}") .WithEnvironment("HOST", $"{test.Resource};name=1"); - /*var testServiceProvider = new TestServiceProvider(); - testServiceProvider.AddService<>*/ - var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource).DefaultTimeout(); var manifestConfig = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerB.Resource, DistributedApplicationOperation.Publish).DefaultTimeout(); From 904eecc32d1351e79d5ac2c0299274ce7d0837e7 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 14:59:57 -0700 Subject: [PATCH 08/14] Add comments and enable tunnel in yarp playground --- playground/yarp/Yarp.AppHost/Program.cs | 1 + .../ApplicationModel/EndpointReference.cs | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/playground/yarp/Yarp.AppHost/Program.cs b/playground/yarp/Yarp.AppHost/Program.cs index 52c56f84a36..52a58d222cc 100644 --- a/playground/yarp/Yarp.AppHost/Program.cs +++ b/playground/yarp/Yarp.AppHost/Program.cs @@ -4,6 +4,7 @@ using Aspire.Hosting.Yarp.Transforms; var builder = DistributedApplication.CreateBuilder(args); +builder.Configuration["DcpPublisher:EnableAspireContainerTunnel"] = "true"; var backendService = builder.AddProject("backend"); diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 437a5b7fe9b..0495035d08e 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -177,12 +177,12 @@ public EndpointReferenceExpression Property(EndpointProperty property) /// The endpoint annotation. /// The ID of the network that serves as the context for the EndpointReference. /// - /// Most Aspire resources are accessed in the context of the "localhost" network (host proceses calling other host processes, - /// or host processes calling container via mapped ports). This is why EndpointReference assumes this - /// context unless specified otherwise. However, for container-to-container, or container-to-host communication, - /// you must specify a container network context for the EndpointReference to be resolved correctly. + /// Most Aspire resources are accessed in the context of the "localhost" network (host processes calling other host processes, + /// or host processes calling container via mapped ports). If a is specified, the + /// will always resolve in the context of that network. If the is null, the reference will attempt to resolve itself + /// based on the context of the requesting resource. /// - public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID = null) + public EndpointReference(IResourceWithEndpoints owner, EndpointAnnotation endpoint, NetworkIdentifier? contextNetworkID) { ArgumentNullException.ThrowIfNull(owner); ArgumentNullException.ThrowIfNull(endpoint); @@ -277,6 +277,8 @@ public class EndpointReferenceExpression(EndpointReference endpointReference, En /// Throws when the selected enumeration is not known. public async ValueTask GetValueAsync(ValueProviderContext context, CancellationToken cancellationToken = default) { + // If the EndpointReference was for a specific network context, then use that. Otherwise, use the network context from the ValueProviderContext. + // This allows the EndpointReference to be resolved in the context of the caller's network if it was not explicitly set. var networkContext = Endpoint.ContextNetworkID ?? context.GetNetworkIdentifier(); return Property switch From 107669f2b708dc15aed98e09234386574d084947 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 15:25:19 -0700 Subject: [PATCH 09/14] Update test helper to ask for specific endpoint --- ...tributedApplicationHostingTestingExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs index 8368bf5a7a2..440316ed6d5 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs @@ -60,15 +60,16 @@ public static HttpClient CreateHttpClient(this DistributedApplication app, strin /// The application. /// The resource name. /// The optional endpoint name. If none are specified, the single defined endpoint is returned. + /// The optional network identifier. If none is specified, the default network is used. /// A URI representation of the endpoint. /// The resource was not found, no matching endpoint was found, or multiple endpoints were found. /// The resource has no endpoints. - public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default) + public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier = default) { ArgumentNullException.ThrowIfNull(app); ArgumentException.ThrowIfNullOrEmpty(resourceName); - return new(GetEndpointUriStringCore(app, resourceName, endpointName)); + return new(GetEndpointUriStringCore(app, resourceName, endpointName, networkIdentifier)); } static IResource GetResource(DistributedApplication app, string resourceName) @@ -87,7 +88,7 @@ static IResource GetResource(DistributedApplication app, string resourceName) return resource; } - static string GetEndpointUriStringCore(DistributedApplication app, string resourceName, string? endpointName = default) + static string GetEndpointUriStringCore(DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier = default) { var resource = GetResource(app, resourceName); if (resource is not IResourceWithEndpoints resourceWithEndpoints) @@ -98,11 +99,11 @@ static string GetEndpointUriStringCore(DistributedApplication app, string resour EndpointReference? endpoint; if (!string.IsNullOrEmpty(endpointName)) { - endpoint = GetEndpointOrDefault(resourceWithEndpoints, endpointName); + endpoint = GetEndpointOrDefault(resourceWithEndpoints, endpointName, networkIdentifier); } else { - endpoint = GetEndpointOrDefault(resourceWithEndpoints, "http") ?? GetEndpointOrDefault(resourceWithEndpoints, "https"); + endpoint = GetEndpointOrDefault(resourceWithEndpoints, "http", networkIdentifier) ?? GetEndpointOrDefault(resourceWithEndpoints, "https", networkIdentifier); } if (endpoint is null) @@ -122,9 +123,9 @@ static void ThrowIfNotStarted(DistributedApplication app) } } - static EndpointReference? GetEndpointOrDefault(IResourceWithEndpoints resourceWithEndpoints, string endpointName) + static EndpointReference? GetEndpointOrDefault(IResourceWithEndpoints resourceWithEndpoints, string endpointName, NetworkIdentifier? networkIdentifier = default) { - var reference = resourceWithEndpoints.GetEndpoint(endpointName); + var reference = resourceWithEndpoints.GetEndpoint(endpointName, networkIdentifier ?? KnownNetworkIdentifiers.LocalhostNetwork); return reference.IsAllocated ? reference : null; } From 1a33f05d7dc8e4df1126ac29c39364678e3961c4 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 15:43:29 -0700 Subject: [PATCH 10/14] Ensure allocated endpoint checks still work the same --- src/Aspire.Hosting/ApplicationModel/EndpointReference.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 0495035d08e..92d03361129 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -156,7 +156,7 @@ public EndpointReferenceExpression Property(EndpointProperty property) foreach (var nes in endpointAnnotation.AllAllocatedEndpoints) { - if (StringComparers.NetworkID.Equals(nes.NetworkID, _contextNetworkID)) + if (StringComparers.NetworkID.Equals(nes.NetworkID, _contextNetworkID ?? KnownNetworkIdentifiers.LocalhostNetwork)) { if (!nes.Snapshot.IsValueSet) { From 3d8b2de74bf4e2496fc6f0a5730a037e7cd7552f Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 16:05:05 -0700 Subject: [PATCH 11/14] Updated method compat --- ...utedApplicationHostingTestingExtensions.cs | 19 ++++++++++++++++++- .../CompatibilitySuppressions.xml | 14 ++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs index 440316ed6d5..dee0d4c8d71 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs @@ -54,6 +54,23 @@ public static HttpClient CreateHttpClient(this DistributedApplication app, strin return resourceWithConnectionString.GetConnectionStringAsync(cancellationToken); } + /// + /// Gets the endpoint for the specified resource. + /// + /// The application. + /// The resource name. + /// The optional endpoint name. If none are specified, the single defined endpoint is returned. + /// A URI representation of the endpoint. + /// The resource was not found, no matching endpoint was found, or multiple endpoints were found. + /// The resource has no endpoints. + public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default) + { + ArgumentNullException.ThrowIfNull(app); + ArgumentException.ThrowIfNullOrEmpty(resourceName); + + return GetEndpoint(app, resourceName, endpointName, networkIdentifier: null); + } + /// /// Gets the endpoint for the specified resource. /// @@ -64,7 +81,7 @@ public static HttpClient CreateHttpClient(this DistributedApplication app, strin /// A URI representation of the endpoint. /// The resource was not found, no matching endpoint was found, or multiple endpoints were found. /// The resource has no endpoints. - public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier = default) + public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier) { ArgumentNullException.ThrowIfNull(app); ArgumentException.ThrowIfNullOrEmpty(resourceName); diff --git a/src/Aspire.Hosting/CompatibilitySuppressions.xml b/src/Aspire.Hosting/CompatibilitySuppressions.xml index f8feae33ce0..f6b7dae8b8e 100644 --- a/src/Aspire.Hosting/CompatibilitySuppressions.xml +++ b/src/Aspire.Hosting/CompatibilitySuppressions.xml @@ -92,6 +92,20 @@ lib/net8.0/Aspire.Hosting.dll true + + CP0002 + M:Aspire.Hosting.ApplicationModel.ResourceExtensions.ProcessArgumentValuesAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.DistributedApplicationExecutionContext,System.Action{System.Object,System.String,System.Exception,System.Boolean},Microsoft.Extensions.Logging.ILogger,System.String,System.Threading.CancellationToken) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + + + CP0002 + M:Aspire.Hosting.ApplicationModel.ResourceExtensions.ProcessEnvironmentVariableValuesAsync(Aspire.Hosting.ApplicationModel.IResource,Aspire.Hosting.DistributedApplicationExecutionContext,System.Action{System.String,System.Object,System.String,System.Exception},Microsoft.Extensions.Logging.ILogger,System.String,System.Threading.CancellationToken) + lib/net8.0/Aspire.Hosting.dll + lib/net8.0/Aspire.Hosting.dll + true + CP0002 M:Aspire.Hosting.IInteractionService.PromptInputsAsync(System.String,System.String,System.Collections.Generic.IReadOnlyList{Aspire.Hosting.InteractionInput},Aspire.Hosting.InputsDialogInteractionOptions,System.Threading.CancellationToken) From 83f2abf561956cc5af8086282b54d3fac3e060ec Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 16:13:45 -0700 Subject: [PATCH 12/14] Optional method --- .../DistributedApplicationHostingTestingExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs index dee0d4c8d71..45459eeb272 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs @@ -81,7 +81,7 @@ public static Uri GetEndpoint(this DistributedApplication app, string resourceNa /// A URI representation of the endpoint. /// The resource was not found, no matching endpoint was found, or multiple endpoints were found. /// The resource has no endpoints. - public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier) + public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier = default) { ArgumentNullException.ThrowIfNull(app); ArgumentException.ThrowIfNullOrEmpty(resourceName); From 0a5a17fbe79a2c7b334fdd52378844816b2d1e1a Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 16:24:18 -0700 Subject: [PATCH 13/14] Don't add ambiguous method --- .../DistributedApplicationHostingTestingExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs index 45459eeb272..2f043a7dcf0 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationHostingTestingExtensions.cs @@ -68,7 +68,7 @@ public static Uri GetEndpoint(this DistributedApplication app, string resourceNa ArgumentNullException.ThrowIfNull(app); ArgumentException.ThrowIfNullOrEmpty(resourceName); - return GetEndpoint(app, resourceName, endpointName, networkIdentifier: null); + return GetEndpointForNetwork(app, resourceName, null, endpointName); } /// @@ -76,12 +76,12 @@ public static Uri GetEndpoint(this DistributedApplication app, string resourceNa /// /// The application. /// The resource name. - /// The optional endpoint name. If none are specified, the single defined endpoint is returned. /// The optional network identifier. If none is specified, the default network is used. + /// The optional endpoint name. If none are specified, the single defined endpoint is returned. /// A URI representation of the endpoint. /// The resource was not found, no matching endpoint was found, or multiple endpoints were found. /// The resource has no endpoints. - public static Uri GetEndpoint(this DistributedApplication app, string resourceName, string? endpointName = default, NetworkIdentifier? networkIdentifier = default) + public static Uri GetEndpointForNetwork(this DistributedApplication app, string resourceName, NetworkIdentifier? networkIdentifier, string? endpointName = default) { ArgumentNullException.ThrowIfNull(app); ArgumentException.ThrowIfNullOrEmpty(resourceName); From 8dbc77fde5b0c4cef3ee1126a627d326d77a0157 Mon Sep 17 00:00:00 2001 From: David Negstad Date: Thu, 30 Oct 2025 22:50:07 -0700 Subject: [PATCH 14/14] Add additional comments and move config to appsettings --- playground/yarp/Yarp.AppHost/Program.cs | 1 - playground/yarp/Yarp.AppHost/appsettings.json | 12 ++++++++++++ src/Aspire.Hosting/ApplicationModel/HostUrl.cs | 13 ++++++++++++- 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 playground/yarp/Yarp.AppHost/appsettings.json diff --git a/playground/yarp/Yarp.AppHost/Program.cs b/playground/yarp/Yarp.AppHost/Program.cs index 52a58d222cc..52c56f84a36 100644 --- a/playground/yarp/Yarp.AppHost/Program.cs +++ b/playground/yarp/Yarp.AppHost/Program.cs @@ -4,7 +4,6 @@ using Aspire.Hosting.Yarp.Transforms; var builder = DistributedApplication.CreateBuilder(args); -builder.Configuration["DcpPublisher:EnableAspireContainerTunnel"] = "true"; var backendService = builder.AddProject("backend"); diff --git a/playground/yarp/Yarp.AppHost/appsettings.json b/playground/yarp/Yarp.AppHost/appsettings.json new file mode 100644 index 00000000000..a1ada7711cd --- /dev/null +++ b/playground/yarp/Yarp.AppHost/appsettings.json @@ -0,0 +1,12 @@ +{ + "DcpPublisher": { + "EnableAspireContainerTunnel": true + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs index 192ae18e2d8..ef29422c12c 100644 --- a/src/Aspire.Hosting/ApplicationModel/HostUrl.cs +++ b/src/Aspire.Hosting/ApplicationModel/HostUrl.cs @@ -43,23 +43,32 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider { if (context.ExecutionContext?.IsRunMode == true) { + // HostUrl isn't modeled as an expression, so we have to find the appropriate allocated endpoint to use manually in the case we're running in a container. + // We're given a URL from the point of view of the host, so need to figure how to modify the URL to be correct from the point of view of the container. + // This could simply be replacing the hostname, but if the container tunnel is running, we may need to translate the port as well. + // Without doing this, we wouldn't be able to resolve the OTEL address correctly from a container as it currently depends on HostUrl rather than the dashboard endpoints. var options = context.ExecutionContext.ServiceProvider.GetRequiredService>(); var infoService = context.ExecutionContext.ServiceProvider.GetRequiredService(); var dcpInfo = await infoService.GetDcpInfoAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + // Determine what hostname means that we want to contact the host machine from the container. If using the new tunnel feature, this needs to be the address of the tunnel instance. + // Otherwise we want to try and determine the container runtime appropriate hostname (host.docker.internal or host.containers.internal). uri.Host = options.Value.EnableAspireContainerTunnel? KnownHostNames.DefaultContainerTunnelHostName : dcpInfo?.Containers?.ContainerHostName ?? KnownHostNames.DockerDesktopHostBridge; if (options.Value.EnableAspireContainerTunnel) { - // We need to consider that both the host and port may need to be remapped + // If we're running with the container tunnel enabled, we need to lookup the port on the tunnel that corresponds to the + // target port on the host machine. var model = context.ExecutionContext.ServiceProvider.GetRequiredService(); var targetEndpoint = model.Resources.Where(r => !r.IsContainer()) .OfType() .Select(r => { + // Find if the resource has a host endpoint with a port matching the one from the request if (r.GetEndpoints(KnownNetworkIdentifiers.LocalhostNetwork).FirstOrDefault(ep => ep.Port == uri.Port) is EndpointReference ep) { + // Return the corresponding endpoint for the container network context. This will be used to determine the port to use when connecting from the container to the host machine. return r.GetEndpoint(ep.EndpointName, networkContext); } @@ -70,6 +79,7 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider if (targetEndpoint is { }) { + // If we found a container endpoint, remap the requested port uri.Port = targetEndpoint.Port; } } @@ -88,6 +98,7 @@ public record HostUrl(string Url) : IValueProvider, IManifestExpressionProvider } catch (UriFormatException) { + // This was a connection string style value instead of a URL. In that case we'll do a simple hostname replacement, but can't do anything about ports. var replacementHost = KnownHostNames.DockerDesktopHostBridge; if (context.ExecutionContext?.IsRunMode == true) {