Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -713,7 +713,7 @@ private static Action<EnvironmentCallbackContext> CreateEndpointReferenceEnviron
context.EnvironmentVariables[$"{EnvironmentVariableNameEncoder.Encode(serviceKey)}_{encodedEndpointName.ToUpperInvariant()}"] = endpoint;
}

if (flags.HasFlag(ReferenceEnvironmentInjectionFlags.ServiceDiscovery))
if (flags.HasFlag(ReferenceEnvironmentInjectionFlags.ServiceDiscovery) && annotation.Resource is IResourceWithServiceDiscovery)
Comment thread
Falco20019 marked this conversation as resolved.
{
// Use the endpoint's scheme for "http" and "https" endpoint names to handle
// TLS upgrades correctly. For all other endpoint names, use the endpoint name
Expand Down Expand Up @@ -1064,6 +1064,59 @@ public static IResourceBuilder<TDestination> WithReference<TDestination>(this IR
return builder;
}

/// <summary>
/// Injects endpoint information as environment variables from the source resource into the destination resource, using the source resource's name as the service name.
/// Each endpoint defined on the source resource will be injected using the format defined by the <see cref="ReferenceEnvironmentInjectionAnnotation"/> on the destination resource, i.e.
/// either "services__{sourceResourceName}__{endpointScheme}__{endpointIndex}={uriString}" for .NET service discovery, or "{RESOURCE_ENDPOINT}={uri}" for endpoint injection.
/// </summary>
Comment thread
Falco20019 marked this conversation as resolved.
/// <typeparam name="TDestination">The destination resource.</typeparam>
/// <param name="builder">The resource where the endpoint information will be injected.</param>
/// <param name="source">The resource from which to extract endpoint information.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
/// <remarks>
/// <para>
/// Service discovery environment variables (format "services__{name}__{scheme}__{index}") are only emitted when the source resource implements <see cref="IResourceWithServiceDiscovery"/>.
/// For resources that only implement <see cref="IResourceWithEndpoints"/>, only endpoint reference variables (format "{NAME}_{ENDPOINT}") are injected.
/// </para>
/// </remarks>
[AspireExport(Description = "Adds all endpoint references to another resource")]
public static IResourceBuilder<TDestination> WithEndpoints<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<IResourceWithEndpoints> source)
where TDestination : IResourceWithEnvironment
Comment thread
Falco20019 marked this conversation as resolved.
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);

ApplyEndpoints(builder, source.Resource);
return builder;
}

/// <summary>
/// Injects endpoint information as environment variables from the source resource into the destination resource, using the source resource's name as the service name.
/// Each endpoint defined on the source resource will be injected using the format defined by the <see cref="ReferenceEnvironmentInjectionAnnotation"/> on the destination resource, i.e.
/// either "services__{sourceResourceName}__{endpointScheme}__{endpointIndex}={uriString}" for .NET service discovery, or "{RESOURCE_ENDPOINT}={uri}" for endpoint injection.
/// </summary>
/// <typeparam name="TDestination">The destination resource.</typeparam>
/// <param name="builder">The resource where the endpoint information will be injected.</param>
/// <param name="source">The resource from which to extract endpoint information.</param>
/// <param name="name">The name of the resource for the environment variable.</param>
/// <returns>The <see cref="IResourceBuilder{T}"/>.</returns>
Comment thread
Falco20019 marked this conversation as resolved.
/// <remarks>
/// <para>
/// Service discovery environment variables (format "services__{name}__{scheme}__{index}") are only emitted when the source resource implements <see cref="IResourceWithServiceDiscovery"/>.
/// For resources that only implement <see cref="IResourceWithEndpoints"/>, only endpoint reference variables (format "{NAME}_{ENDPOINT}") are injected.
/// </para>
/// </remarks>
[AspireExportIgnore(Reason = "Polyglot app hosts use the generic withEndpoints export.")]
public static IResourceBuilder<TDestination> WithEndpoints<TDestination>(this IResourceBuilder<TDestination> builder, IResourceBuilder<IResourceWithEndpoints> source, string name)
where TDestination : IResourceWithEnvironment
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(source);

ApplyEndpoints(builder, source.Resource, endpointName: null, name);
return builder;
}

/// <summary>
/// Injects service discovery and endpoint information as environment variables from the uri into the destination resource, using the name as the service name.
/// The uri will be injected using the format defined by the <see cref="ReferenceEnvironmentInjectionAnnotation"/> on the destination resource, i.e.
Expand Down
160 changes: 160 additions & 0 deletions tests/Aspire.Hosting.Tests/WithEndpointsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// 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<ProjectA>("project-a")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));

var projectB = builder.AddProject<ProjectB>("consumer")
.WithEndpoints(projectA);

var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(projectB.Resource, DistributedApplicationOperation.Run, TestServiceProvider.Instance).DefaultTimeout();

Assert.Equal("https://localhost:2000", config["services__project-a__https__0"]);
Assert.Equal("https://localhost:2000", config["PROJECT_A_MYBINDING"]);
Comment thread
Falco20019 marked this conversation as resolved.
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<ProjectA>("project-a")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));

var projectB = builder.AddProject<ProjectB>("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__https__0"]);
Assert.Equal("https://localhost:2000", config["custom_name_MYBINDING"]);
Assert.DoesNotContain("services__custom_name__https__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>("projecta")
.WithHttpsEndpoint(1000, 2000, "mybinding")
.WithEndpoint("mybinding", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));

// Get the service provider.
var projectB = builder.AddProject<ProjectB>("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["custom_MYBINDING"]);
Assert.Equal("https://localhost:2000", config["services__custom__https__0"]);
break;

case ReferenceEnvironmentInjectionFlags.ConnectionProperties:
case ReferenceEnvironmentInjectionFlags.ConnectionString:
case ReferenceEnvironmentInjectionFlags.None:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__https__0"));
break;

case ReferenceEnvironmentInjectionFlags.ServiceDiscovery:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.True(config.ContainsKey("services__custom__https__0"));
break;

case ReferenceEnvironmentInjectionFlags.Endpoints:
Assert.True(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey("services__custom__https__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<ProjectB>("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["custom_MYBINDING"]);
Assert.False(config.ContainsKey["services__custom__https__0"]);
break;

case ReferenceEnvironmentInjectionFlags.ConnectionProperties:
case ReferenceEnvironmentInjectionFlags.ConnectionString:
case ReferenceEnvironmentInjectionFlags.ServiceDiscovery:
case ReferenceEnvironmentInjectionFlags.None:
Assert.False(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey["services__custom__https__0"]);
break;

case ReferenceEnvironmentInjectionFlags.Endpoints:
Assert.True(config.ContainsKey("custom_MYBINDING"));
Assert.False(config.ContainsKey["services__custom__https__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();
}
}
Loading