From 101c9e8c265e0f67d215e16a283ce9003241807b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Kr=C3=A4mer?= Date: Fri, 30 Jan 2026 16:52:00 +0100 Subject: [PATCH 1/6] Fix WithReference to only use IResourceWithEndpoints --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 3695ec12ca9..e38dc19bd2d 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -600,16 +600,16 @@ public static ReferenceExpression GetConnectionProperty(this IResourceWithConnec } /// - /// Injects service discovery and endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. + /// Injects endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. /// Each endpoint defined on the project resource will be injected using the format defined by the on the destination resource, i.e. /// either "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}" for .NET service discovery, or "{RESOURCE_ENDPOINT}={uri}" for endpoint injection. /// /// The destination resource. - /// The resource where the service discovery information will be injected. - /// The resource from which to extract service discovery information. + /// The resource where the endpoint information will be injected. + /// The resource from which to extract endpoint information. /// The . - [AspireExport("withServiceReference", Description = "Adds a service discovery reference to another resource")] - public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) + [AspireExport("withEndpoints", Description = "Adds a endpoint references to another resource")] + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) where TDestination : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); From 190a08b4e1f7e100a1350d34a130077337bcb03c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Kr=C3=A4mer?= Date: Fri, 30 Jan 2026 16:56:01 +0100 Subject: [PATCH 2/6] Also ally to other variant --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index e38dc19bd2d..c6f6f200980 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -620,16 +620,16 @@ public static IResourceBuilder WithReference(this IR } /// - /// Injects service discovery and endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. + /// Injects endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. /// Each endpoint defined on the project resource will be injected using the format defined by the on the destination resource, i.e. /// either "services__{name}__{endpointName}__{endpointIndex}={uriString}" for .NET service discovery, or "{name}_{ENDPOINT}={uri}" for endpoint injection. /// /// The destination resource. - /// The resource where the service discovery information will be injected. - /// The resource from which to extract service discovery information. + /// The resource where the endpoint information will be injected. + /// The resource from which to extract endpoint information. /// The name of the resource for the environment variable. /// The . - public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source, string name) + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source, string name) where TDestination : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); From 00e8642d46c8538fbd2cd18ac0dc6d42cc3ea063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Kr=C3=A4mer?= Date: Fri, 30 Jan 2026 17:00:12 +0100 Subject: [PATCH 3/6] Keep original ones --- .../ResourceBuilderExtensions.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index c6f6f200980..b2ba1ff8e06 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -599,6 +599,46 @@ public static ReferenceExpression GetConnectionProperty(this IResourceWithConnec return ReferenceExpression.Empty; } + /// + /// Injects service discovery and endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. + /// Each endpoint defined on the project resource will be injected using the format defined by the on the destination resource, i.e. + /// either "services__{sourceResourceName}__{endpointName}__{endpointIndex}={uriString}" for .NET service discovery, or "{RESOURCE_ENDPOINT}={uri}" for endpoint injection. + /// + /// The destination resource. + /// The resource where the service discovery information will be injected. + /// The resource from which to extract service discovery information. + /// The . + [AspireExport("withServiceReference", Description = "Adds a service discovery reference to another resource")] + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) + where TDestination : IResourceWithEnvironment + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + ApplyEndpoints(builder, source.Resource); + return builder; + } + + /// + /// Injects service discovery and endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. + /// Each endpoint defined on the project resource will be injected using the format defined by the on the destination resource, i.e. + /// either "services__{name}__{endpointName}__{endpointIndex}={uriString}" for .NET service discovery, or "{name}_{ENDPOINT}={uri}" for endpoint injection. + /// + /// The destination resource. + /// The resource where the service discovery information will be injected. + /// The resource from which to extract service discovery information. + /// The name of the resource for the environment variable. + /// The . + public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source, string name) + where TDestination : IResourceWithEnvironment + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(source); + + ApplyEndpoints(builder, source.Resource, endpointName: null, name); + return builder; + } + /// /// Injects endpoint information as environment variables from the project resource into the destination resource, using the source resource's name as the service name. /// Each endpoint defined on the project resource will be injected using the format defined by the on the destination resource, i.e. From ef67ec812f3645ee829d2abfc29f80e697490924 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Kr=C3=A4mer?= Date: Fri, 30 Jan 2026 17:09:40 +0100 Subject: [PATCH 4/6] Fix typo --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index b2ba1ff8e06..3da157d1829 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -648,7 +648,7 @@ public static IResourceBuilder WithReference(this IR /// The resource where the endpoint information will be injected. /// The resource from which to extract endpoint information. /// The . - [AspireExport("withEndpoints", Description = "Adds a endpoint references to another resource")] + [AspireExport("withEndpoints", Description = "Adds all endpoint references to another resource")] public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) where TDestination : IResourceWithEnvironment { From 7e90218e5d403065808d5c6c77018a7534c520a0 Mon Sep 17 00:00:00 2001 From: "Kraemer, Benjamin" Date: Mon, 2 Feb 2026 11:17:47 +0100 Subject: [PATCH 5/6] Rename to avoid issues and add tests --- .../ResourceBuilderExtensions.cs | 4 +- .../WithEndpointsTests.cs | 170 ++++++++++++++++++ 2 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 tests/Aspire.Hosting.Tests/WithEndpointsTests.cs diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 3da157d1829..7915cc30a9f 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -649,7 +649,7 @@ public static IResourceBuilder WithReference(this IR /// The resource from which to extract endpoint information. /// The . [AspireExport("withEndpoints", Description = "Adds all endpoint references to another resource")] - public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source) + public static IResourceBuilder WithEndpoints(this IResourceBuilder builder, IResourceBuilder source) where TDestination : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); @@ -669,7 +669,7 @@ public static IResourceBuilder WithReference(this IR /// The resource from which to extract endpoint information. /// The name of the resource for the environment variable. /// The . - public static IResourceBuilder WithReference(this IResourceBuilder builder, IResourceBuilder source, string name) + public static IResourceBuilder WithEndpoints(this IResourceBuilder builder, IResourceBuilder source, string name) where TDestination : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); diff --git a/tests/Aspire.Hosting.Tests/WithEndpointsTests.cs b/tests/Aspire.Hosting.Tests/WithEndpointsTests.cs new file mode 100644 index 00000000000..080eb83272d --- /dev/null +++ b/tests/Aspire.Hosting.Tests/WithEndpointsTests.cs @@ -0,0 +1,170 @@ +// 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.Tests.Utils; +using Aspire.Hosting.Utils; +using Microsoft.AspNetCore.InternalTesting; + +namespace Aspire.Hosting.Tests; + +public class WithEndpointsTests +{ + [Fact] + public async Task ResourceNamesWithDashesAreEncodedInEnvironmentVariables() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var projectA = builder.AddProject("project-a") + .WithHttpsEndpoint(1000, 2000, "mybinding") + .WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var projectB = builder.AddProject("consumer") + .WithEndpoints(projectA); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + + Assert.Equal("https://localhost:2000", config["services__project-a__mybinding__0"]); + Assert.Equal("https://localhost:2000", config["PROJECT_A_MYBINDING"]); + Assert.DoesNotContain("services__project_a__mybinding__0", config.Keys); + Assert.DoesNotContain("PROJECT-A_MYBINDING", config.Keys); + } + + [Fact] + public async Task OverriddenServiceNamesAreEncodedInEnvironmentVariables() + { + using var builder = TestDistributedApplicationBuilder.Create(); + + var projectA = builder.AddProject("project-a") + .WithHttpsEndpoint(1000, 2000, "mybinding") + .WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + var projectB = builder.AddProject("consumer") + .WithEndpoints(projectA, "custom-name"); + + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + + Assert.Equal("https://localhost:2000", config["services__custom-name__mybinding__0"]); + Assert.Equal("https://localhost:2000", config["custom_name_MYBINDING"]); + Assert.DoesNotContain("services__custom_name__mybinding__0", config.Keys); + Assert.DoesNotContain("custom-name_MYBINDING", config.Keys); + } + + [Theory] + [InlineData(ReferenceEnvironmentInjectionFlags.All)] + [InlineData(ReferenceEnvironmentInjectionFlags.ConnectionProperties)] + [InlineData(ReferenceEnvironmentInjectionFlags.ConnectionString)] + [InlineData(ReferenceEnvironmentInjectionFlags.ServiceDiscovery)] + [InlineData(ReferenceEnvironmentInjectionFlags.Endpoints)] + [InlineData(ReferenceEnvironmentInjectionFlags.None)] + public async Task ProjectWithEndpointRespectsCustomEnvironmentVariableNaming(ReferenceEnvironmentInjectionFlags flags) + { + using var builder = TestDistributedApplicationBuilder.Create(); + + // Create a binding and its matching annotation (simulating DCP behavior) + var projectA = builder.AddProject("projecta") + .WithHttpsEndpoint(1000, 2000, "mybinding") + .WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + // Get the service provider. + var projectB = builder.AddProject("b") + .WithEndpoints(projectA, "custom") + .WithReferenceEnvironment(flags); + + // Call environment variable callbacks. + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + + switch (flags) + { + case ReferenceEnvironmentInjectionFlags.All: + Assert.Equal("https://localhost:2000", config["services__custom__mybinding__0"]); + Assert.Equal("https://localhost:2000", config["custom_MYBINDING"]); + break; + case ReferenceEnvironmentInjectionFlags.ConnectionProperties: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.ConnectionString: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.ServiceDiscovery: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.True(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.Endpoints: + Assert.True(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.None: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + } + } + + [Theory] + [InlineData(ReferenceEnvironmentInjectionFlags.All)] + [InlineData(ReferenceEnvironmentInjectionFlags.ConnectionProperties)] + [InlineData(ReferenceEnvironmentInjectionFlags.ConnectionString)] + [InlineData(ReferenceEnvironmentInjectionFlags.ServiceDiscovery)] + [InlineData(ReferenceEnvironmentInjectionFlags.Endpoints)] + [InlineData(ReferenceEnvironmentInjectionFlags.None)] + public async Task ContainerResourceWithEndpointRespectsCustomEnvironmentVariableNaming(ReferenceEnvironmentInjectionFlags flags) + { + using var builder = TestDistributedApplicationBuilder.Create(); + + // Create a binding and its matching annotation (simulating DCP behavior) + var container = builder.AddContainer("mycontainer", "myimage") + .WithHttpsEndpoint(1000, 2000, "mybinding") + .WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000)); + + // Get the service provider. + var project = builder.AddProject("b") + .WithEndpoints(container, "custom") + .WithReferenceEnvironment(flags); + + // Call environment variable callbacks. + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(project.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout(); + + switch (flags) + { + case ReferenceEnvironmentInjectionFlags.All: + Assert.Equal("https://localhost:2000", config["services__custom__mybinding__0"]); + Assert.Equal("https://localhost:2000", config["custom_MYBINDING"]); + break; + case ReferenceEnvironmentInjectionFlags.ConnectionProperties: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.ConnectionString: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.ServiceDiscovery: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.True(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.Endpoints: + Assert.True(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + case ReferenceEnvironmentInjectionFlags.None: + Assert.False(config.ContainsKey("custom_MYBINDING")); + Assert.False(config.ContainsKey("services__custom__mybinding__0")); + break; + } + } + + private sealed class ProjectA : IProjectMetadata + { + public string ProjectPath => "projectA"; + + public LaunchSettings LaunchSettings { get; } = new(); + } + + private sealed class ProjectB : IProjectMetadata + { + public string ProjectPath => "projectB"; + public LaunchSettings LaunchSettings { get; } = new(); + } +} From 3f8e3701f00e7d18e8c6f35c271767fe0606efa9 Mon Sep 17 00:00:00 2001 From: "Kraemer, Benjamin" Date: Mon, 2 Feb 2026 11:27:41 +0100 Subject: [PATCH 6/6] Only add ReferenceEnvironmentInjectionFlags.ServiceDiscovery on IResourceWithServiceDiscovery --- src/Aspire.Hosting/ResourceBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 7915cc30a9f..9f423e1fadb 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -481,7 +481,7 @@ private static Action CreateEndpointReferenceEnviron context.EnvironmentVariables[$"{EnvironmentVariableNameEncoder.Encode(serviceKey)}_{encodedEndpointName.ToUpperInvariant()}"] = endpoint; } - if (flags.HasFlag(ReferenceEnvironmentInjectionFlags.ServiceDiscovery)) + if (flags.HasFlag(ReferenceEnvironmentInjectionFlags.ServiceDiscovery) && annotation.Resource is IResourceWithServiceDiscovery) { context.EnvironmentVariables[$"services__{serviceName}__{endpointName}__0"] = endpoint; }