diff --git a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs index 7d65cac22d..23fe1c6d90 100644 --- a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs +++ b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs @@ -35,7 +35,7 @@ public override async Task ConfigureResourceAsync(IConfiguration configura return false; } - var currentCheckSum = GetCurrentChecksum(resource, section); + var currentCheckSum = await GetCurrentChecksumAsync(resource, section, cancellationToken).ConfigureAwait(false); var configCheckSum = section["CheckSum"]; if (currentCheckSum != configCheckSum) @@ -203,7 +203,7 @@ await notificationService.PublishUpdateAsync(resource, state => state with // Convert the parameters to a JSON object var parameters = new JsonObject(); - SetParameters(parameters, resource); + await SetParametersAsync(parameters, resource, cancellationToken: cancellationToken).ConfigureAwait(false); var sw = Stopwatch.StartNew(); @@ -408,7 +408,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param return Convert.ToHexString(hashedContents).ToLowerInvariant(); } - internal static string? GetCurrentChecksum(AzureBicepResource resource, IConfiguration section) + internal static async ValueTask GetCurrentChecksumAsync(AzureBicepResource resource, IConfiguration section, CancellationToken cancellationToken = default) { // Fill in parameters from configuration if (section["Parameters"] is not string jsonString) @@ -427,7 +427,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param // Now overwite with live object values skipping known values. // This is important because the provisioner will fill in the known values - SetParameters(parameters, resource, skipKnownValues: true); + await SetParametersAsync(parameters, resource, skipKnownValues: true, cancellationToken: cancellationToken).ConfigureAwait(false); // Get the checksum of the new values return GetChecksum(resource, parameters); @@ -451,7 +451,7 @@ internal static string GetChecksum(AzureBicepResource resource, JsonObject param ]; // Converts the parameters to a JSON object compatible with the ARM template - internal static void SetParameters(JsonObject parameters, AzureBicepResource resource, bool skipKnownValues = false) + internal static async Task SetParametersAsync(JsonObject parameters, AzureBicepResource resource, bool skipKnownValues = false, CancellationToken cancellationToken = default) { // Convert the parameters to a JSON object foreach (var parameter in resource.Parameters) @@ -473,12 +473,12 @@ internal static void SetParameters(JsonObject parameters, AzureBicepResource res int i => i, bool b => b, JsonNode node => node, - IResourceBuilder c => c.Resource.GetConnectionString(), - IResourceBuilder p => p.Resource.Value, // TODO: Support this + AzureBicepResource bicepResource => throw new NotSupportedException("Referencing bicep resources is not supported"), BicepOutputReference reference => throw new NotSupportedException("Referencing bicep outputs is not supported"), - object o => o.ToString()!, + IValueProvider v => await v.GetValueAsync(cancellationToken).ConfigureAwait(false), null => null, + _ => throw new NotSupportedException($"The parameter value type {parameterValue.GetType()} is not supported.") } }; } diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs index 4d9e2cf6ae..73625bc666 100644 --- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs +++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs @@ -162,9 +162,7 @@ public virtual void WriteToManifest(ManifestPublishingContext context) var value = input.Value switch { - IResourceBuilder p => p.Resource.ValueExpression, - IResourceBuilder p => p.Resource.ConnectionStringReferenceExpression, - BicepOutputReference output => output.ValueExpression, + IManifestExpressionProvider output => output.ValueExpression, object obj => obj.ToString(), null => "" }; @@ -241,7 +239,7 @@ public void Dispose() /// /// The name of the secret output. /// The . -public class BicepSecretOutputReference(string name, AzureBicepResource resource) +public class BicepSecretOutputReference(string name, AzureBicepResource resource) : IManifestExpressionProvider, IValueProvider { /// /// Name of the output. @@ -293,7 +291,7 @@ public string? Value /// /// The name of the output /// The . -public class BicepOutputReference(string name, AzureBicepResource resource) +public class BicepOutputReference(string name, AzureBicepResource resource) : IManifestExpressionProvider, IValueProvider { /// /// Name of the output. diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs index 511466af16..163973e231 100644 --- a/src/Aspire.Hosting.Azure/AzureConstructResource.cs +++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs @@ -85,7 +85,7 @@ public static void AssignParameter(this Resource resource, Expression p.Name == parameterName)) { diff --git a/src/Aspire.Hosting.Azure/Extensions/AzureBicepResourceExtensions.cs b/src/Aspire.Hosting.Azure/Extensions/AzureBicepResourceExtensions.cs index 9e69135fbd..209f0396e3 100644 --- a/src/Aspire.Hosting.Azure/Extensions/AzureBicepResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure/Extensions/AzureBicepResourceExtensions.cs @@ -4,7 +4,6 @@ using System.Text.Json.Nodes; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; -using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -75,17 +74,9 @@ public static BicepSecretOutputReference GetSecretOutput(this IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, BicepOutputReference bicepOutputReference) where T : IResourceWithEnvironment { - return builder.WithEnvironment(async ctx => + return builder.WithEnvironment(ctx => { - if (ctx.ExecutionContext.IsPublishMode) - { - ctx.EnvironmentVariables[name] = bicepOutputReference.ValueExpression; - return; - } - - ctx.Logger?.LogInformation("Getting bicep output {Name} from resource {ResourceName}", bicepOutputReference.Name, bicepOutputReference.Resource.Name); - - ctx.EnvironmentVariables[name] = await bicepOutputReference.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false) ?? ""; + ctx.EnvironmentVariables[name] = bicepOutputReference; }); } @@ -100,17 +91,9 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, BicepSecretOutputReference bicepOutputReference) where T : IResourceWithEnvironment { - return builder.WithEnvironment(async ctx => + return builder.WithEnvironment(ctx => { - if (ctx.ExecutionContext.IsPublishMode) - { - ctx.EnvironmentVariables[name] = bicepOutputReference.ValueExpression; - return; - } - - ctx.Logger?.LogInformation("Getting bicep secret output {Name} from resource {ResourceName}", bicepOutputReference.Name, bicepOutputReference.Resource.Name); - - ctx.EnvironmentVariables[name] = await bicepOutputReference.GetValueAsync(ctx.CancellationToken).ConfigureAwait(false) ?? ""; + ctx.EnvironmentVariables[name] = bicepOutputReference; }); } @@ -199,7 +182,7 @@ public static IResourceBuilder WithParameter(this IResourceBuilder buil public static IResourceBuilder WithParameter(this IResourceBuilder builder, string name, IResourceBuilder value) where T : AzureBicepResource { - builder.Resource.Parameters[name] = value; + builder.Resource.Parameters[name] = value.Resource; return builder; } @@ -214,7 +197,7 @@ public static IResourceBuilder WithParameter(this IResourceBuilder buil public static IResourceBuilder WithParameter(this IResourceBuilder builder, string name, IResourceBuilder value) where T : AzureBicepResource { - builder.Resource.Parameters[name] = value; + builder.Resource.Parameters[name] = value.Resource; return builder; } diff --git a/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs new file mode 100644 index 0000000000..0e9963649a --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/ConnectionStringReference.cs @@ -0,0 +1,33 @@ +// 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; + +/// +/// Represents a reference to a connection string. +/// +public class ConnectionStringReference(IResourceWithConnectionString resource, bool optional) : IManifestExpressionProvider, IValueProvider +{ + /// + /// The resource that the connection string is referencing. + /// + public IResourceWithConnectionString Resource { get; } = resource; + + /// + /// A flag indicating whether the connection string is optional. + /// + public bool Optional { get; } = optional; + + string IManifestExpressionProvider.ValueExpression => Resource.ValueExpression; + + async ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) + { + var value = await Resource.GetValueAsync(cancellationToken).ConfigureAwait(false); + + if (value is null && !Optional) + { + throw new DistributedApplicationException($"The connection string for the resource `{Resource.Name}` is not available."); + } + + return value; + } +} diff --git a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs index 37c8cf1466..7261a072dc 100644 --- a/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs +++ b/src/Aspire.Hosting/ApplicationModel/EndpointReference.cs @@ -8,7 +8,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// The resource with endpoints that owns the endpoint reference. /// The name of the endpoint. -public sealed class EndpointReference(IResourceWithEndpoints owner, string endpointName) +public sealed class EndpointReference(IResourceWithEndpoints owner, string endpointName) : IManifestExpressionProvider, IValueProvider { /// /// Gets the owner of the endpoint reference. @@ -21,34 +21,79 @@ public sealed class EndpointReference(IResourceWithEndpoints owner, string endpo public string EndpointName { get; } = endpointName; /// - /// Gets the expression used in the manifest to reference the value of the endpoint. + /// Gets a value indicating whether the endpoint is allocated. /// - public string ValueExpression => $"{{{Owner.Name}.bindings.{EndpointName}.url}}"; + public bool IsAllocated => Owner.Annotations.OfType().Any(a => a.Name == EndpointName); + + string IManifestExpressionProvider.ValueExpression => GetExpression(); + + ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) => new(Url); /// - /// Gets the URI string for the endpoint reference. + /// Gets the specified property expression of the endpoint. Defaults to the URL if no property is specified. /// - public string Value + public string GetExpression(EndpointProperty property = EndpointProperty.Url) { - get + var prop = property switch { - var allocatedEndpoint = Owner.Annotations.OfType().SingleOrDefault(a => a.Name == EndpointName); + EndpointProperty.Url => "url", + EndpointProperty.Host => "host", + EndpointProperty.Port => "port", + EndpointProperty.Scheme => "scheme", + _ => throw new InvalidOperationException($"The property `{property}` is not supported for the endpoint `{EndpointName}`.") + }; - return allocatedEndpoint?.UriString ?? - throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`."); - } + return $"{{{Owner.Name}.bindings.{EndpointName}.{prop}}}"; } /// - /// Gets the URI string for the endpoint reference. + /// Gets the port for this endpoint. + /// + public int Port => GetAllocatedEndpoint().Port; + + /// + /// Gets the host for this endpoint. + /// + public string Host => GetAllocatedEndpoint().Address ?? "localhost"; + + /// + /// Gets the scheme for this endpoint. + /// + public string Scheme => GetAllocatedEndpoint().UriScheme; + + /// + /// Gets the URL for this endpoint. /// - [Obsolete("Use Value instead.")] - public string UriString + public string Url => GetAllocatedEndpoint().UriString; + + private AllocatedEndpointAnnotation GetAllocatedEndpoint() { - get - { - var allocatedEndpoint = Owner.Annotations.OfType().SingleOrDefault(a => a.Name == EndpointName); - return allocatedEndpoint?.UriString ?? $"{{{Owner.Name}.bindings.{EndpointName}.url}}"; - } + var allocatedEndpoint = Owner.Annotations.OfType().SingleOrDefault(a => a.Name == EndpointName) ?? + throw new InvalidOperationException($"The endpoint `{EndpointName}` is not allocated for the resource `{Owner.Name}`."); + + return allocatedEndpoint; } } + +/// +/// Represents the properties of an endpoint that can be referenced. +/// +public enum EndpointProperty +{ + /// + /// The entire URL of the endpoint. + /// + Url, + /// + /// The host of the endpoint. + /// + Host, + /// + /// The port of the endpoint. + /// + Port, + /// + /// The scheme of the endpoint. + /// + Scheme +} diff --git a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackAnnotation.cs b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackAnnotation.cs index af425b0da6..742c6cc06b 100644 --- a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackAnnotation.cs +++ b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackAnnotation.cs @@ -32,7 +32,7 @@ public EnvironmentCallbackAnnotation(string name, Func callback) /// Initializes a new instance of the class with the specified callback action. /// /// The callback action to be executed. - public EnvironmentCallbackAnnotation(Action> callback) + public EnvironmentCallbackAnnotation(Action> callback) { Callback = (c) => { diff --git a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs index b2e11583a2..b7feb3e399 100644 --- a/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs +++ b/src/Aspire.Hosting/ApplicationModel/EnvironmentCallbackContext.cs @@ -11,7 +11,7 @@ namespace Aspire.Hosting.ApplicationModel; /// The execution context for this invocation of the AppHost. /// The environment variables associated with this execution. /// The cancellation token associated with this execution. -public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default) +public class EnvironmentCallbackContext(DistributedApplicationExecutionContext executionContext, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default) { /// /// Obsolete. Use ExecutionContext instead. Will be removed in next preview. @@ -22,7 +22,7 @@ public class EnvironmentCallbackContext(DistributedApplicationExecutionContext e /// /// Gets the environment variables associated with the callback context. /// - public Dictionary EnvironmentVariables { get; } = environmentVariables ?? new(); + public Dictionary EnvironmentVariables { get; } = environmentVariables ?? new(); /// /// Gets the CancellationToken associated with the callback context. diff --git a/src/Aspire.Hosting/ApplicationModel/IManifestExpressionProvider.cs b/src/Aspire.Hosting/ApplicationModel/IManifestExpressionProvider.cs new file mode 100644 index 0000000000..08905ea768 --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/IManifestExpressionProvider.cs @@ -0,0 +1,15 @@ +// 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; + +/// +/// An interface that allows an object to express how it should be represented in a manifest. +/// +public interface IManifestExpressionProvider +{ + /// + /// Gets the expression that represents a value in manifest. + /// + string ValueExpression { get; } +} diff --git a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs index 5597b4cb92..58a45769c6 100644 --- a/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs +++ b/src/Aspire.Hosting/ApplicationModel/IResourceWithConnectionString.cs @@ -6,7 +6,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents a resource that has a connection string associated with it. /// -public interface IResourceWithConnectionString : IResource +public interface IResourceWithConnectionString : IResource, IManifestExpressionProvider, IValueProvider { /// /// Gets the connection string associated with the resource. @@ -21,6 +21,10 @@ public interface IResourceWithConnectionString : IResource /// The connection string associated with the resource, when one is available. public ValueTask GetConnectionStringAsync(CancellationToken cancellationToken = default) => new(GetConnectionString()); + string IManifestExpressionProvider.ValueExpression => ConnectionStringReferenceExpression; + + ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) => GetConnectionStringAsync(cancellationToken); + /// /// Describes the connection string format string used for this resource in the manifest. /// diff --git a/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs b/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs new file mode 100644 index 0000000000..ff7653df3b --- /dev/null +++ b/src/Aspire.Hosting/ApplicationModel/IValueProvider.cs @@ -0,0 +1,17 @@ +// 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; + +/// +/// A interface that allows the value to be provided for an environment variable. +/// +public interface IValueProvider +{ + /// + /// Gets the value for use as an environment variable. + /// + /// + /// + public ValueTask GetValueAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs index 22247b9a40..c5d7cb5f31 100644 --- a/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs +++ b/src/Aspire.Hosting/ApplicationModel/ParameterResource.cs @@ -6,7 +6,7 @@ namespace Aspire.Hosting.ApplicationModel; /// /// Represents a parameter resource. /// -public sealed class ParameterResource : Resource +public sealed class ParameterResource : Resource, IManifestExpressionProvider, IValueProvider { private readonly Func _callback; @@ -38,4 +38,6 @@ public ParameterResource(string name, Func callback, bool secret = false /// Gets the expression used in the manifest to reference the value of the parameter. /// public string ValueExpression => $"{{{Name}.value}}"; + + ValueTask IValueProvider.GetValueAsync(CancellationToken cancellationToken) => new(Value); } diff --git a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs index dc6ad6e150..6aa7749d18 100644 --- a/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs +++ b/src/Aspire.Hosting/Dcp/ApplicationExecutor.cs @@ -587,7 +587,7 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, } } - var config = new Dictionary(); + var config = new Dictionary(); var context = new EnvironmentCallbackContext(_executionContext, config, cancellationToken) { Logger = resourceLogger @@ -630,7 +630,18 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, spec.Env = []; foreach (var c in config) { - spec.Env.Add(new EnvVar { Name = c.Key, Value = c.Value }); + var value = c.Value switch + { + string s => s, + IValueProvider valueProvider => await GetValue(c.Key, valueProvider, resourceLogger, isContainer: false, cancellationToken).ConfigureAwait(false), + null => null, + _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{c.Key}\".") + }; + + if (value is not null) + { + spec.Env.Add(new EnvVar { Name = c.Key, Value = value }); + } } await createResource().ConfigureAwait(false); @@ -651,7 +662,38 @@ private async Task CreateExecutableAsync(AppResource er, ILogger resourceLogger, } } - private static void ApplyLaunchProfile(AppResource executableResource, Dictionary config, string launchProfileName, LaunchSettings launchSettings) + private async Task GetValue(string key, IValueProvider valueProvider, ILogger logger, bool isContainer, CancellationToken cancellationToken) + { + var task = valueProvider.GetValueAsync(cancellationToken); + + if (!task.IsCompleted) + { + if (valueProvider is IResource resource) + { + logger.LogInformation("Waiting for value for environment variable value '{Name}' from resource '{ResourceName}'", key, resource.Name); + } + else if (valueProvider is ConnectionStringReference { Resource: var cs }) + { + logger.LogInformation("Waiting for value for connection string from resource '{ResourceName}'", cs.Name); + } + else + { + logger.LogInformation("Waiting for value for environment variable value '{Name}' from {Name}.", key, valueProvider.ToString()); + } + } + + var value = await task.ConfigureAwait(false); + + if (value is not null && isContainer && valueProvider is ConnectionStringReference or EndpointReference) + { + // If the value is a connection string or endpoint reference, we need to replace localhost with the container host. + return HostNameResolver.ReplaceLocalhostWithContainerHost(value, configuration); + } + + return value; + } + + private static void ApplyLaunchProfile(AppResource executableResource, Dictionary config, string launchProfileName, LaunchSettings launchSettings) { // Populate DOTNET_LAUNCH_PROFILE environment variable for consistency with "dotnet run" and "dotnet watch". config.Add("DOTNET_LAUNCH_PROFILE", launchProfileName); @@ -684,7 +726,7 @@ private static void ApplyLaunchProfile(AppResource executableResource, Dictionar } } - private static void InjectPortEnvVars(AppResource executableResource, Dictionary config) + private static void InjectPortEnvVars(AppResource executableResource, Dictionary config) { ServiceAppResource? httpsServiceAppResource = null; // Inject environment variables for services produced by this executable. @@ -795,7 +837,7 @@ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with try { - await CreateContainerAsync(cr, cancellationToken).ConfigureAwait(false); + await CreateContainerAsync(cr, logger, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -819,12 +861,12 @@ await notificationService.PublishUpdateAsync(cr.ModelResource, s => s with } } - private async Task CreateContainerAsync(AppResource cr, CancellationToken cancellationToken) + private async Task CreateContainerAsync(AppResource cr, ILogger resourceLogger, CancellationToken cancellationToken) { var dcpContainerResource = (Container)cr.DcpResource; var modelContainerResource = cr.ModelResource; - var config = new Dictionary(); + var config = new Dictionary(); dcpContainerResource.Spec.Env = []; @@ -882,7 +924,18 @@ private async Task CreateContainerAsync(AppResource cr, CancellationToken cancel foreach (var kvp in config) { - dcpContainerResource.Spec.Env.Add(new EnvVar { Name = kvp.Key, Value = kvp.Value }); + var value = kvp.Value switch + { + string s => s, + IValueProvider valueProvider => await GetValue(kvp.Key, valueProvider, resourceLogger, isContainer: true, cancellationToken).ConfigureAwait(false), + null => null, + _ => throw new InvalidOperationException($"Unexpected value for environment variable \"{kvp.Key}\".") + }; + + if (value is not null) + { + dcpContainerResource.Spec.Env.Add(new EnvVar { Name = kvp.Key, Value = value }); + } } if (modelContainerResource.TryGetAnnotationsOfType(out var argsCallback)) diff --git a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs index 804bc022dd..2770623119 100644 --- a/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/Extensions/ResourceBuilderExtensions.cs @@ -5,7 +5,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Publishing; using Aspire.Hosting.Utils; -using Microsoft.Extensions.Logging; namespace Aspire.Hosting; @@ -78,17 +77,7 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu { return builder.WithEnvironment(context => { - if (context.ExecutionContext.IsPublishMode) - { - context.EnvironmentVariables[name] = endpointReference.ValueExpression; - return; - } - - var replaceLocalhostWithContainerHost = builder.Resource is ContainerResource; - - context.EnvironmentVariables[name] = replaceLocalhostWithContainerHost - ? HostNameResolver.ReplaceLocalhostWithContainerHost(endpointReference.Value, builder.ApplicationBuilder.Configuration) - : endpointReference.Value; + context.EnvironmentVariables[name] = endpointReference; }); } @@ -104,13 +93,7 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu { return builder.WithEnvironment(context => { - if (context.ExecutionContext.IsPublishMode) - { - context.EnvironmentVariables[name] = parameter.Resource.ValueExpression; - return; - } - - context.EnvironmentVariables[name] = parameter.Resource.Value; + context.EnvironmentVariables[name] = parameter.Resource; }); } @@ -219,37 +202,11 @@ public static IResourceBuilder WithReference(this IR var resource = source.Resource; connectionName ??= resource.Name; - return builder.WithEnvironment(async context => + return builder.WithEnvironment(context => { var connectionStringName = resource.ConnectionStringEnvironmentVariable ?? $"{ConnectionStringEnvironmentName}{connectionName}"; - if (context.ExecutionContext.IsPublishMode) - { - context.EnvironmentVariables[connectionStringName] = resource.ConnectionStringReferenceExpression; - return; - } - - context.Logger?.LogInformation("Retrieving connection string for '{Name}'.", resource.Name); - - var connectionString = await resource.GetConnectionStringAsync(context.CancellationToken).ConfigureAwait(false); - - if (string.IsNullOrEmpty(connectionString)) - { - if (optional) - { - // This is an optional connection string, so we can just return. - return; - } - - throw new DistributedApplicationException($"A connection string for '{resource.Name}' could not be retrieved."); - } - - if (builder.Resource is ContainerResource) - { - connectionString = HostNameResolver.ReplaceLocalhostWithContainerHost(connectionString, builder.ApplicationBuilder.Configuration); - } - - context.EnvironmentVariables[connectionStringName] = connectionString; + context.EnvironmentVariables[connectionStringName] = new ConnectionStringReference(resource, optional); }); } diff --git a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs index f9355a187e..38642f1025 100644 --- a/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs +++ b/src/Aspire.Hosting/Kafka/KafkaBuilderExtensions.cs @@ -23,13 +23,13 @@ public static IResourceBuilder AddKafka(this IDistributedAp { var kafka = new KafkaServerResource(name); return builder.AddResource(kafka) - .WithEndpoint(containerPort: KafkaBrokerPort, hostPort: port) + .WithEndpoint(containerPort: KafkaBrokerPort, hostPort: port, name: KafkaServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "confluentinc/confluent-local", Tag = "7.6.0" }) .WithEnvironment(context => ConfigureKafkaContainer(context, kafka)) .PublishAsContainer(); } - private static void ConfigureKafkaContainer(EnvironmentCallbackContext context, IResource resource) + private static void ConfigureKafkaContainer(EnvironmentCallbackContext context, KafkaServerResource resource) { // confluentinc/confluent-local is a docker image that contains a Kafka broker started with KRaft to avoid pulling a separate image for ZooKeeper. // See https://github.com/confluentinc/kafka-images/blob/master/local/README.md. @@ -38,19 +38,8 @@ private static void ConfigureKafkaContainer(EnvironmentCallbackContext context, var hostPort = context.ExecutionContext.IsPublishMode ? KafkaBrokerPort - : GetResourcePort(resource); + : resource.PrimaryEndpoint.Port; context.EnvironmentVariables.Add("KAFKA_ADVERTISED_LISTENERS", $"PLAINTEXT://localhost:29092,PLAINTEXT_HOST://localhost:{hostPort}"); - - static int GetResourcePort(IResource resource) - { - if (!resource.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException( - $"Kafka resource \"{resource.Name}\" does not have endpoint annotation."); - } - - return allocatedEndpoints.Single().Port; - } } } diff --git a/src/Aspire.Hosting/Kafka/KafkaServerResource.cs b/src/Aspire.Hosting/Kafka/KafkaServerResource.cs index 72216f1d62..80321405d8 100644 --- a/src/Aspire.Hosting/Kafka/KafkaServerResource.cs +++ b/src/Aspire.Hosting/Kafka/KafkaServerResource.cs @@ -11,11 +11,20 @@ namespace Aspire.Hosting; /// The name of the resource. public class KafkaServerResource(string name) : ContainerResource(name), IResourceWithConnectionString, IResourceWithEnvironment { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Kafka broker. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the connection string expression for Kafka broker for the manifest. /// public string ConnectionStringExpression => - $"{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}"; + $"{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; /// /// Gets the connection string for Kafka broker. @@ -23,21 +32,6 @@ public class KafkaServerResource(string name) : ContainerResource(name), IResour /// A connection string for the Kafka in the form "host:port" to be passed as BootstrapServers. public string? GetConnectionString() { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException($"Kafka resource \"{Name}\" does not have endpoint annotation."); - } - - return allocatedEndpoints.SingleOrDefault()?.EndPointString; - } - - internal int GetPort() - { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException($"Kafka resource \"{Name}\" does not have endpoint annotation."); - } - - return allocatedEndpoints.Single().Port; + return $"{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; } } diff --git a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs index 350a6551a9..a7ca6563ec 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.MongoDB; @@ -12,6 +11,7 @@ namespace Aspire.Hosting; /// public static class MongoDBBuilderExtensions { + // Internal port is always 27017. private const int DefaultContainerPort = 27017; /// @@ -27,7 +27,7 @@ public static IResourceBuilder AddMongoDB(this IDistribut return builder .AddResource(mongoDBContainer) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: DefaultContainerPort)) // Internal port is always 27017. + .WithEndpoint(hostPort: port, containerPort: DefaultContainerPort, name: MongoDBServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "mongo", Tag = "7.0.5" }) .PublishAsContainer(); } @@ -73,22 +73,9 @@ public static IResourceBuilder WithMongoExpress(this IResourceBuilder b return builder; } - private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext context, IResource resource) + private static void ConfigureMongoExpressContainer(EnvironmentCallbackContext context, MongoDBServerResource resource) { - var hostPort = GetResourcePort(resource); - - context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://host.docker.internal:{hostPort}/?directConnection=true"); + context.EnvironmentVariables.Add("ME_CONFIG_MONGODB_URL", $"mongodb://host.docker.internal:{resource.PrimaryEndpoint.Port}/?directConnection=true"); context.EnvironmentVariables.Add("ME_CONFIG_BASICAUTH", "false"); - - static int GetResourcePort(IResource resource) - { - if (!resource.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException( - $"MongoDB resource \"{resource.Name}\" does not have endpoint annotation."); - } - - return allocatedEndpoints.Single().Port; - } } } diff --git a/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs b/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs index 3e1dc3c64d..5059366dff 100644 --- a/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs +++ b/src/Aspire.Hosting/MongoDB/MongoDBServerResource.cs @@ -11,11 +11,20 @@ namespace Aspire.Hosting.ApplicationModel; /// The name of the resource. public class MongoDBServerResource(string name) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the MongoDB server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the connection string for the MongoDB server. /// public string ConnectionStringExpression => - $"mongodb://{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}"; + $"mongodb://{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; /// /// Gets the connection string for the MongoDB server. @@ -23,16 +32,9 @@ public class MongoDBServerResource(string name) : ContainerResource(name), IReso /// A connection string for the MongoDB server in the form "mongodb://host:port". public string? GetConnectionString() { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Expected allocated endpoints!"); - } - - var allocatedEndpoint = allocatedEndpoints.Single(); - return new MongoDBConnectionStringBuilder() - .WithServer(allocatedEndpoint.Address) - .WithPort(allocatedEndpoint.Port) + .WithServer(PrimaryEndpoint.Host) + .WithPort(PrimaryEndpoint.Port) .Build(); } diff --git a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs index 21aea3e847..a28a1718c4 100644 --- a/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs +++ b/src/Aspire.Hosting/MySql/MySqlBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.MySql; @@ -30,7 +29,7 @@ public static IResourceBuilder AddMySql(this IDistributedAp var resource = new MySqlServerResource(name, password); return builder.AddResource(resource) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 3306)) // Internal port is always 3306. + .WithEndpoint(hostPort: port, containerPort: 3306, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 3306. .WithAnnotation(new ContainerImageAnnotation { Image = "mysql", Tag = "8.3.0" }) .WithDefaultPassword() .WithEnvironment(context => diff --git a/src/Aspire.Hosting/MySql/MySqlServerResource.cs b/src/Aspire.Hosting/MySql/MySqlServerResource.cs index c894ba1110..44be120d05 100644 --- a/src/Aspire.Hosting/MySql/MySqlServerResource.cs +++ b/src/Aspire.Hosting/MySql/MySqlServerResource.cs @@ -12,6 +12,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The MySQL server root password. public class MySqlServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString { + internal static string PrimaryEndpointName => "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the MySQL server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the MySQL server root password. /// @@ -21,7 +30,7 @@ public class MySqlServerResource(string name, string password) : ContainerResour /// Gets the connection string expression for the MySQL server. /// public string ConnectionStringExpression => - $"Server={{{Name}.bindings.tcp.host}};Port={{{Name}.bindings.tcp.port}};User ID=root;Password={{{Name}.inputs.password}}"; + $"Server={PrimaryEndpoint.GetExpression(EndpointProperty.Host)};Port={PrimaryEndpoint.GetExpression(EndpointProperty.Port)};User ID=root;Password={{{Name}.inputs.password}}"; /// /// Gets the connection string for the MySQL server. @@ -29,15 +38,7 @@ public class MySqlServerResource(string name, string password) : ContainerResour /// A connection string for the MySQL server in the form "Server=host;Port=port;User ID=root;Password=password". public string? GetConnectionString() { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Expected allocated endpoints!"); - } - - var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for MySQL. - - var connectionString = $"Server={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};User ID=root;Password=\"{PasswordUtil.EscapePassword(Password)}\""; - return connectionString; + return $"Server={PrimaryEndpoint.Host};Port={PrimaryEndpoint.Port};User ID=root;Password=\"{PasswordUtil.EscapePassword(Password)}\""; } private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs index 513fd2d21a..7ed0b6f9a0 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; @@ -28,7 +27,7 @@ public static IResourceBuilder AddOracleDatabase(t var oracleDatabaseServer = new OracleDatabaseServerResource(name, password); return builder.AddResource(oracleDatabaseServer) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1521)) + .WithEndpoint(hostPort: port, containerPort: 1521, name: MySqlServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "database/free", Tag = "23.3.0.0", Registry = "container-registry.oracle.com" }) .WithDefaultPassword() .WithEnvironment(context => diff --git a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs index 6bc843a7df..4388d29b1d 100644 --- a/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs +++ b/src/Aspire.Hosting/Oracle/OracleDatabaseServerResource.cs @@ -12,6 +12,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The Oracle Database server password. public class OracleDatabaseServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Redis server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the Oracle Database server password. /// @@ -21,7 +30,7 @@ public class OracleDatabaseServerResource(string name, string password) : Contai /// Gets the connection string expression for the Oracle Database server. /// public string ConnectionStringExpression => - $"user id=system;password={{{Name}.inputs.password}};data source={{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}};"; + $"user id=system;password={{{Name}.inputs.password}};data source={PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)};"; /// /// Gets the connection string for the Oracle Database server. @@ -29,18 +38,10 @@ public class OracleDatabaseServerResource(string name, string password) : Contai /// A connection string for the Oracle Database server in the form "user id=system;password=password;data source=host:port". public string? GetConnectionString() { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Expected allocated endpoints!"); - } - - var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for Oracle. - - var connectionString = $"user id=system;password={PasswordUtil.EscapePassword(Password)};data source={allocatedEndpoint.Address}:{allocatedEndpoint.Port}"; - return connectionString; + return $"user id=system;password={PasswordUtil.EscapePassword(Password)};data source={PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; } - private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); + private readonly Dictionary _databases = new(StringComparers.ResourceName); /// /// A dictionary where the key is the resource name and the value is the database name. diff --git a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs index 5db68a009a..e2d990b956 100644 --- a/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs +++ b/src/Aspire.Hosting/Postgres/PostgresBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Postgres; @@ -30,7 +29,7 @@ public static IResourceBuilder AddPostgres(this IDistrib var postgresServer = new PostgresServerResource(name, password); return builder.AddResource(postgresServer) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5432)) // Internal port is always 5432. + .WithEndpoint(hostPort: port, containerPort: 5432, name: MySqlServerResource.PrimaryEndpointName) // Internal port is always 5432. .WithAnnotation(new ContainerImageAnnotation { Image = "postgres", Tag = "16.2" }) .WithDefaultPassword() .WithEnvironment("POSTGRES_HOST_AUTH_METHOD", "scram-sha-256") diff --git a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs index aec298ee0e..cc7903deba 100644 --- a/src/Aspire.Hosting/Postgres/PostgresServerResource.cs +++ b/src/Aspire.Hosting/Postgres/PostgresServerResource.cs @@ -12,6 +12,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The PostgreSQL server password. public class PostgresServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Redis server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the PostgreSQL server password. /// @@ -29,7 +38,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"Host={{{Name}.bindings.tcp.host}};Port={{{Name}.bindings.tcp.port}};Username=postgres;Password={{{Name}.inputs.password}}"; + return $"Host={PrimaryEndpoint.GetExpression(EndpointProperty.Host)};Port={PrimaryEndpoint.GetExpression(EndpointProperty.Port)};Username=postgres;Password={{{Name}.inputs.password}}"; } } @@ -59,15 +68,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionString(); } - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Expected allocated endpoints!"); - } - - var allocatedEndpoint = allocatedEndpoints.Single(); // We should only have one endpoint for Postgres. - - var connectionString = $"Host={allocatedEndpoint.Address};Port={allocatedEndpoint.Port};Username=postgres;Password={PasswordUtil.EscapePassword(Password)}"; - return connectionString; + return $"Host={PrimaryEndpoint.Host};Port={PrimaryEndpoint.Port};Username=postgres;Password={PasswordUtil.EscapePassword(Password)}"; } private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); diff --git a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs index 04da369535..02194cf894 100644 --- a/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs +++ b/src/Aspire.Hosting/Publishing/ManifestPublishingContext.cs @@ -161,7 +161,7 @@ public void WriteBindings(IResource resource, bool emitContainerPort = false) /// public async Task WriteEnvironmentVariablesAsync(IResource resource) { - var config = new Dictionary(); + var config = new Dictionary(); var envContext = new EnvironmentCallbackContext(ExecutionContext, config, CancellationToken); @@ -175,7 +175,14 @@ public async Task WriteEnvironmentVariablesAsync(IResource resource) foreach (var (key, value) in config) { - Writer.WriteString(key, value); + var valueString = value switch + { + string stringValue => stringValue, + IManifestExpressionProvider manifestExpression => manifestExpression.ValueExpression, + _ => throw new DistributedApplicationException($"The value of the environment variable `{key}` is not supported.") + }; + + Writer.WriteString(key, valueString); } WriteServiceDiscoveryEnvironmentVariables(resource); diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs index 8546db613a..e7c113e9e6 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; @@ -25,7 +24,7 @@ public static IResourceBuilder AddRabbitMQ(this IDistrib var rabbitMq = new RabbitMQServerResource(name, password); return builder.AddResource(rabbitMq) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 5672)) + .WithEndpoint(hostPort: port, containerPort: 5672, name: MySqlServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "rabbitmq", Tag = "3" }) .WithDefaultPassword() .WithEnvironment("RABBITMQ_DEFAULT_USER", "guest") diff --git a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs index 5adfea1337..c813916731 100644 --- a/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs +++ b/src/Aspire.Hosting/RabbitMQ/RabbitMQServerResource.cs @@ -10,6 +10,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The RabbitMQ server password. public class RabbitMQServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString, IResourceWithEnvironment { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Redis server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// The RabbitMQ server password. /// @@ -19,7 +28,7 @@ public class RabbitMQServerResource(string name, string password) : ContainerRes /// Gets the connection string expression for the RabbitMQ server for the manifest. /// public string ConnectionStringExpression => - $"amqp://guest:{{{Name}.inputs.password}}@{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}"; + $"amqp://guest:{{{Name}.inputs.password}}@{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; /// /// Gets the connection string for the RabbitMQ server. @@ -27,12 +36,6 @@ public class RabbitMQServerResource(string name, string password) : ContainerRes /// A connection string for the RabbitMQ server in the form "amqp://user:password@host:port". public string? GetConnectionString() { - if (!this.TryGetAllocatedEndPoints(out var allocatedEndpoints)) - { - throw new DistributedApplicationException($"RabbitMQ resource \"{Name}\" does not have endpoint annotation."); - } - - var endpoint = allocatedEndpoints.Where(a => a.Name != "management").Single(); - return $"amqp://guest:{Password}@{endpoint.EndPointString}"; + return $"amqp://guest:{Password}@{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; } } diff --git a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs index b56f495352..9b0dc41369 100644 --- a/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs +++ b/src/Aspire.Hosting/Redis/RedisBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Lifecycle; using Aspire.Hosting.Redis; @@ -24,7 +23,7 @@ public static IResourceBuilder AddRedis(this IDistributedApplicat { var redis = new RedisResource(name); return builder.AddResource(redis) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 6379)) + .WithEndpoint(hostPort: port, containerPort: 6379, name: RedisResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Image = "redis", Tag = "7.2.4" }) .PublishAsContainer(); } diff --git a/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs b/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs index 87a197cfe6..2be00ca163 100644 --- a/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs +++ b/src/Aspire.Hosting/Redis/RedisCommanderConfigWriterHook.cs @@ -29,11 +29,9 @@ public Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, C foreach (var redisInstance in redisInstances) { - if (redisInstance.TryGetAllocatedEndPoints(out var allocatedEndpoints)) + if (redisInstance.PrimaryEndpoint.IsAllocated) { - var endpoint = allocatedEndpoints.Where(ae => ae.Name == "tcp").Single(); - - var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:host.docker.internal:{endpoint.Port}:0"; + var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:host.docker.internal:{redisInstance.PrimaryEndpoint.Port}:0"; hostsVariableBuilder.Append(hostString); } } diff --git a/src/Aspire.Hosting/Redis/RedisResource.cs b/src/Aspire.Hosting/Redis/RedisResource.cs index 37c118a019..9ae6c839fa 100644 --- a/src/Aspire.Hosting/Redis/RedisResource.cs +++ b/src/Aspire.Hosting/Redis/RedisResource.cs @@ -9,6 +9,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The name of the resource. public class RedisResource(string name) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Redis server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the connection string expression for the Redis server for the manifest. /// @@ -21,7 +30,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"{{{Name}.bindings.tcp.host}}:{{{Name}.bindings.tcp.port}}"; + return $"{PrimaryEndpoint.GetExpression(EndpointProperty.Host)}:{PrimaryEndpoint.GetExpression(EndpointProperty.Port)}"; } } @@ -51,13 +60,6 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionString(); } - if (!this.TryGetAnnotationsOfType(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Redis resource does not have endpoint annotation."); - } - - // We should only have one endpoint for Redis for local scenarios. - var endpoint = allocatedEndpoints.Single(); - return endpoint.EndPointString; + return $"{PrimaryEndpoint.Host}:{PrimaryEndpoint.Port}"; } } diff --git a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs index 7d1f6bbd4d..2667b0e920 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerBuilderExtensions.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Sockets; using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Utils; @@ -28,7 +27,7 @@ public static IResourceBuilder AddSqlServer(this IDistr var sqlServer = new SqlServerServerResource(name, password); return builder.AddResource(sqlServer) - .WithAnnotation(new EndpointAnnotation(ProtocolType.Tcp, port: port, containerPort: 1433)) + .WithEndpoint(hostPort: port, containerPort: 1433, name: MySqlServerResource.PrimaryEndpointName) .WithAnnotation(new ContainerImageAnnotation { Registry = "mcr.microsoft.com", Image = "mssql/server", Tag = "2022-latest" }) .WithDefaultPassword() .WithEnvironment("ACCEPT_EULA", "Y") diff --git a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs index 7e5073f1e0..13ecce9b8a 100644 --- a/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs +++ b/src/Aspire.Hosting/SqlServer/SqlServerServerResource.cs @@ -12,6 +12,15 @@ namespace Aspire.Hosting.ApplicationModel; /// The SQL Sever password. public class SqlServerServerResource(string name, string password) : ContainerResource(name), IResourceWithConnectionString { + internal const string PrimaryEndpointName = "tcp"; + + private EndpointReference? _primaryEndpoint; + + /// + /// Gets the primary endpoint for the Redis server. + /// + public EndpointReference PrimaryEndpoint => _primaryEndpoint ??= new(this, PrimaryEndpointName); + /// /// Gets the password for the SQL Server container resource. /// @@ -29,7 +38,7 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.ConnectionStringExpression; } - return $"Server={{{Name}.bindings.tcp.host}},{{{Name}.bindings.tcp.port}};User ID=sa;Password={{{Name}.inputs.password}};TrustServerCertificate=true"; + return $"Server={PrimaryEndpoint.GetExpression(EndpointProperty.Host)},{PrimaryEndpoint.GetExpression(EndpointProperty.Port)};User ID=sa;Password={{{Name}.inputs.password}};TrustServerCertificate=true"; } } @@ -59,19 +68,12 @@ public string? ConnectionStringExpression return connectionStringAnnotation.Resource.GetConnectionString(); } - if (!this.TryGetAnnotationsOfType(out var allocatedEndpoints)) - { - throw new DistributedApplicationException("Expected allocated endpoints!"); - } - - var endpoint = allocatedEndpoints.Single(); - // HACK: Use the 127.0.0.1 address because localhost is resolving to [::1] following // up with DCP on this issue. - return $"Server=127.0.0.1,{endpoint.Port};User ID=sa;Password={PasswordUtil.EscapePassword(Password)};TrustServerCertificate=true"; + return $"Server=127.0.0.1,{PrimaryEndpoint.Port};User ID=sa;Password={PasswordUtil.EscapePassword(Password)};TrustServerCertificate=true"; } - private readonly Dictionary _databases = new Dictionary(StringComparers.ResourceName); + private readonly Dictionary _databases = new(StringComparers.ResourceName); /// /// A dictionary where the key is the resource name and the value is the database name. diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs index 74d19aa2b8..a9147b295e 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepProvisionerTests.cs @@ -12,7 +12,7 @@ namespace Aspire.Hosting.Tests.Azure; public class AzureBicepProvisionerTests { [Fact] - public void SetParametersTranslatesParametersToARMCompatibleJsonParameters() + public async Task SetParametersTranslatesParametersToARMCompatibleJsonParameters() { var builder = DistributedApplication.CreateBuilder(); @@ -20,14 +20,14 @@ public void SetParametersTranslatesParametersToARMCompatibleJsonParameters() .WithParameter("name", "david"); var parameters = new JsonObject(); - BicepProvisioner.SetParameters(parameters, bicep0.Resource); + await BicepProvisioner.SetParametersAsync(parameters, bicep0.Resource); Assert.Single(parameters); Assert.Equal("david", parameters["name"]?["value"]?.ToString()); } [Fact] - public void SetParametersTranslatesCompatibleParameterTypes() + public async Task SetParametersTranslatesCompatibleParameterTypes() { var builder = DistributedApplication.CreateBuilder(); @@ -45,7 +45,7 @@ public void SetParametersTranslatesCompatibleParameterTypes() .WithParameter("param", param); var parameters = new JsonObject(); - BicepProvisioner.SetParameters(parameters, bicep0.Resource); + await BicepProvisioner.SetParametersAsync(parameters, bicep0.Resource); Assert.Equal(6, parameters.Count); Assert.Equal("john", parameters["name"]?["value"]?.ToString()); @@ -57,7 +57,7 @@ public void SetParametersTranslatesCompatibleParameterTypes() } [Fact] - public void ResourceWithTheSameBicepTemplateAndParametersHaveTheSameCheckSum() + public async Task ResourceWithTheSameBicepTemplateAndParametersHaveTheSameCheckSum() { var builder = DistributedApplication.CreateBuilder(); @@ -74,18 +74,18 @@ public void ResourceWithTheSameBicepTemplateAndParametersHaveTheSameCheckSum() .WithParameter("jsonObj", new JsonObject { ["key"] = "value" }); var parameters0 = new JsonObject(); - BicepProvisioner.SetParameters(parameters0, bicep0.Resource); + await BicepProvisioner.SetParametersAsync(parameters0, bicep0.Resource); var checkSum0 = BicepProvisioner.GetChecksum(bicep0.Resource, parameters0); var parameters1 = new JsonObject(); - BicepProvisioner.SetParameters(parameters1, bicep1.Resource); + await BicepProvisioner.SetParametersAsync(parameters1, bicep1.Resource); var checkSum1 = BicepProvisioner.GetChecksum(bicep1.Resource, parameters1); Assert.Equal(checkSum0, checkSum1); } [Fact] - public void ResourceWithSameTemplateButDifferentParametersHaveDifferentChecksums() + public async Task ResourceWithSameTemplateButDifferentParametersHaveDifferentChecksums() { var builder = DistributedApplication.CreateBuilder(); @@ -101,18 +101,18 @@ public void ResourceWithSameTemplateButDifferentParametersHaveDifferentChecksums .WithParameter("jsonObj", new JsonObject { ["key"] = "value" }); var parameters0 = new JsonObject(); - BicepProvisioner.SetParameters(parameters0, bicep0.Resource); + await BicepProvisioner.SetParametersAsync(parameters0, bicep0.Resource); var checkSum0 = BicepProvisioner.GetChecksum(bicep0.Resource, parameters0); var parameters1 = new JsonObject(); - BicepProvisioner.SetParameters(parameters1, bicep1.Resource); + await BicepProvisioner.SetParametersAsync(parameters1, bicep1.Resource); var checkSum1 = BicepProvisioner.GetChecksum(bicep1.Resource, parameters1); Assert.NotEqual(checkSum0, checkSum1); } [Fact] - public void GetCurrentChecksumSkipsKnownValuesForCheckSumCreation() + public async Task GetCurrentChecksumSkipsKnownValuesForCheckSumCreation() { var builder = DistributedApplication.CreateBuilder(); @@ -129,14 +129,14 @@ public void GetCurrentChecksumSkipsKnownValuesForCheckSumCreation() .WithParameter(AzureBicepResource.KnownParameters.PrincipalType, "type"); var parameters0 = new JsonObject(); - BicepProvisioner.SetParameters(parameters0, bicep0.Resource); + await BicepProvisioner.SetParametersAsync(parameters0, bicep0.Resource); var checkSum0 = BicepProvisioner.GetChecksum(bicep0.Resource, parameters0); // Save the old version of this resource's parameters to config var config = new ConfigurationManager(); config["Parameters"] = parameters0.ToJsonString(); - var checkSum1 = BicepProvisioner.GetCurrentChecksum(bicep1.Resource, config); + var checkSum1 = await BicepProvisioner.GetCurrentChecksumAsync(bicep1.Resource, config); Assert.Equal(checkSum0, checkSum1); } diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs index facab65bf1..70921bf4ac 100644 --- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Text.Json.Nodes; using Aspire.Hosting.Azure; +using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Azure.Provisioning.Storage; using Azure.ResourceManager.Storage.Models; @@ -170,17 +171,7 @@ public async Task WithReferenceAppInsightsSetsEnvironmentVariable() var serviceA = builder.AddProject("serviceA") .WithReference(appInsights); - // Call environment variable callbacks. - var annotations = serviceA.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(serviceA.Resource); Assert.True(config.ContainsKey("APPLICATIONINSIGHTS_CONNECTION_STRING")); Assert.Equal("myinstrumentationkey", config["APPLICATIONINSIGHTS_CONNECTION_STRING"]); @@ -369,8 +360,8 @@ public void AsAzurePostgresFlexibleServer() Assert.Equal("Aspire.Hosting.Azure.Bicep.postgres.bicep", azurePostgres.Resource.TemplateResourceName); Assert.Equal("postgres", postgres.Resource.Name); Assert.Equal("postgres", azurePostgres.Resource.Parameters["serverName"]); - Assert.Same(usr, azurePostgres.Resource.Parameters["administratorLogin"]); - Assert.Same(pwd, azurePostgres.Resource.Parameters["administratorLoginPassword"]); + Assert.Same(usr.Resource, azurePostgres.Resource.Parameters["administratorLogin"]); + Assert.Same(pwd.Resource, azurePostgres.Resource.Parameters["administratorLoginPassword"]); Assert.True(azurePostgres.Resource.Parameters.ContainsKey(AzureBicepResource.KnownParameters.KeyVaultName)); Assert.NotNull(databases); Assert.Equal(["dbName"], databases); @@ -409,15 +400,15 @@ public async Task PublishAsAzurePostgresFlexibleServer() Assert.Equal("Aspire.Hosting.Azure.Bicep.postgres.bicep", azurePostgres.Resource.TemplateResourceName); Assert.Equal("postgres", postgres.Resource.Name); Assert.Equal("postgres", azurePostgres.Resource.Parameters["serverName"]); - Assert.Same(usr, azurePostgres.Resource.Parameters["administratorLogin"]); - Assert.Same(pwd, azurePostgres.Resource.Parameters["administratorLoginPassword"]); + Assert.Same(usr.Resource, azurePostgres.Resource.Parameters["administratorLogin"]); + Assert.Same(pwd.Resource, azurePostgres.Resource.Parameters["administratorLoginPassword"]); Assert.True(azurePostgres.Resource.Parameters.ContainsKey(AzureBicepResource.KnownParameters.KeyVaultName)); Assert.NotNull(databases); Assert.Equal(["db"], databases); // Verify that when PublishAs variant is used, connection string acquisition // still uses the local endpoint. - var endpointAnnotation = new AllocatedEndpointAnnotation("dummy", System.Net.Sockets.ProtocolType.Tcp, "localhost", 1234, "tcp"); + var endpointAnnotation = new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, System.Net.Sockets.ProtocolType.Tcp, "localhost", 1234, "tcp"); postgres.WithAnnotation(endpointAnnotation); var expectedConnectionString = $"Host={endpointAnnotation.Address};Port={endpointAnnotation.Port};Username=postgres;Password={PasswordUtil.EscapePassword(postgres.Resource.Password)}"; Assert.Equal(expectedConnectionString, postgres.Resource.GetConnectionString()); diff --git a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs index 5656966c9f..59a8f9c5fa 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs +++ b/tests/Aspire.Hosting.Tests/Kafka/AddKafkaTests.cs @@ -48,7 +48,7 @@ public void KafkaCreatesConnectionString() appBuilder .AddKafka("kafka") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(KafkaServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 27017, diff --git a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs index 6301ad400c..faceb405e4 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs +++ b/tests/Aspire.Hosting.Tests/MongoDB/AddMongoDBTests.cs @@ -81,7 +81,7 @@ public void MongoDBCreatesConnectionString() appBuilder .AddMongoDB("mongodb") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "localhost", 27017, diff --git a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs index c69e3b0fdf..59e325b68b 100644 --- a/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs +++ b/tests/Aspire.Hosting.Tests/MySql/AddMySqlTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Xunit; using Aspire.Hosting.Utils; +using Aspire.Hosting.Tests.Utils; namespace Aspire.Hosting.Tests.MySql; @@ -42,16 +43,7 @@ public async Task AddMySqlContainerWithDefaultsAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -91,16 +83,7 @@ public async Task AddMySqlAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -116,7 +99,7 @@ public void MySqlCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -140,7 +123,7 @@ public void MySqlCreatesConnectionStringWithDatabase() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddMySql("mysql") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -229,7 +212,7 @@ public async Task SingleMySqlInstanceProducesCorrectMySqlHostsVariable() using var app = builder.Build(); // Add fake allocated endpoints. - mysql.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); + mysql.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); var model = app.Services.GetRequiredService(); var hook = new PhpMyAdminConfigWriterHook(); @@ -237,20 +220,11 @@ public async Task SingleMySqlInstanceProducesCorrectMySqlHostsVariable() var myAdmin = builder.Resources.Single(r => r.Name.EndsWith("-phpmyadmin")); - var envAnnotations = myAdmin.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(myAdmin); - Assert.Equal("host.docker.internal:5001", context.EnvironmentVariables["PMA_HOST"]); - Assert.NotNull(context.EnvironmentVariables["PMA_USER"]); - Assert.NotNull(context.EnvironmentVariables["PMA_PASSWORD"]); + Assert.Equal("host.docker.internal:5001", config["PMA_HOST"]); + Assert.NotNull(config["PMA_USER"]); + Assert.NotNull(config["PMA_PASSWORD"]); } [Fact] @@ -274,8 +248,8 @@ public void WithPhpMyAdminProducesValidServerConfigFile() var mysql2 = builder.AddMySql("mysql2").WithPhpMyAdmin(8081); // Add fake allocated endpoints. - mysql1.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); - mysql2.WithAnnotation(new AllocatedEndpointAnnotation("tcp", ProtocolType.Tcp, "host.docker.internal", 5002, "tcp")); + mysql1.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5001, "tcp")); + mysql2.WithAnnotation(new AllocatedEndpointAnnotation(MySqlServerResource.PrimaryEndpointName, ProtocolType.Tcp, "host.docker.internal", 5002, "tcp")); var myAdmin = builder.Resources.Single(r => r.Name.EndsWith("-phpmyadmin")); var volume = myAdmin.Annotations.OfType().Single(); diff --git a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs index 27bc78f45c..ba19400e67 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs +++ b/tests/Aspire.Hosting.Tests/Oracle/AddOracleDatabaseTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -40,16 +41,7 @@ public async Task AddOracleDatabaseWithDefaultsAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -89,16 +81,7 @@ public async Task AddOracleDatabaseAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -114,7 +97,7 @@ public void OracleCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracleDatabase("orcl") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(OracleDatabaseServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -139,7 +122,7 @@ public void OracleCreatesConnectionStringWithDatabase() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddOracleDatabase("orcl") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(OracleDatabaseServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -191,16 +174,7 @@ public async Task AddDatabaseToOracleDatabaseAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => diff --git a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs index 3a364a76b7..1b59070776 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs +++ b/tests/Aspire.Hosting.Tests/Postgres/AddPostgresTests.cs @@ -4,6 +4,7 @@ using System.Net.Sockets; using System.Text.Json; using Aspire.Hosting.Postgres; +using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -42,16 +43,7 @@ public async Task AddPostgresWithDefaultsAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -101,16 +93,7 @@ public async Task AddPostgresAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -136,7 +119,7 @@ public void PostgresCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); var postgres = appBuilder.AddPostgres("postgres") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -154,7 +137,7 @@ public void PostgresCreatesConnectionStringWithDatabase() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddPostgres("postgres") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(PostgresServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -206,16 +189,7 @@ public async Task AddDatabaseToPostgresAddsAnnotationMetadata() Assert.Equal("tcp", endpoint.Transport); Assert.Equal("tcp", endpoint.UriScheme); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => diff --git a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs index 3e23aa1960..98d641438b 100644 --- a/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Tests/ProjectResourceTests.cs @@ -3,6 +3,7 @@ using Aspire.Hosting.Publishing; using Aspire.Hosting.Tests.Helpers; +using Aspire.Hosting.Tests.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -28,16 +29,7 @@ public async Task AddProjectAddsEnvironmentVariablesAndServiceMetadata() var serviceMetadata = Assert.Single(resource.Annotations.OfType()); Assert.IsType(serviceMetadata); - var annotations = resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(resource); Assert.Collection(config, env => diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs index 462fcffe68..6b71f8d0eb 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs +++ b/tests/Aspire.Hosting.Tests/RabbitMQ/AddRabbitMQTests.cs @@ -49,7 +49,7 @@ public void RabbitMQCreatesConnectionString() appBuilder .AddRabbitMQ("rabbit") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(RabbitMQServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 27011, diff --git a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs index 4c4971851b..c0c536e972 100644 --- a/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs +++ b/tests/Aspire.Hosting.Tests/Redis/AddRedisTests.cs @@ -3,6 +3,7 @@ using System.Net.Sockets; using Aspire.Hosting.Redis; +using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.Utils; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -79,7 +80,7 @@ public void RedisCreatesConnectionString() var appBuilder = DistributedApplication.CreateBuilder(); appBuilder.AddRedis("myRedis") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(RedisResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 2000, @@ -148,18 +149,9 @@ public async Task SingleRedisInstanceProducesCorrectRedisHostsVariable() var commander = builder.Resources.Single(r => r.Name.EndsWith("-commander")); - var envAnnotations = commander.Annotations.OfType(); + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(commander); - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } - - Assert.Equal("myredis1:host.docker.internal:5001:0", context.EnvironmentVariables["REDIS_HOSTS"]); + Assert.Equal("myredis1:host.docker.internal:5001:0", config["REDIS_HOSTS"]); } [Fact] @@ -180,17 +172,8 @@ public async Task MultipleRedisInstanceProducesCorrectRedisHostsVariable() var commander = builder.Resources.Single(r => r.Name.EndsWith("-commander")); - var envAnnotations = commander.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(commander); - Assert.Equal("myredis1:host.docker.internal:5001:0,myredis2:host.docker.internal:5002:0", context.EnvironmentVariables["REDIS_HOSTS"]); + Assert.Equal("myredis1:host.docker.internal:5001:0,myredis2:host.docker.internal:5002:0", config["REDIS_HOSTS"]); } } diff --git a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs index 83056abe25..606c5461f7 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs +++ b/tests/Aspire.Hosting.Tests/SqlServer/AddSqlServerTests.cs @@ -1,6 +1,7 @@ // 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.Extensions.DependencyInjection; using System.Net.Sockets; @@ -41,16 +42,7 @@ public async Task AddSqlServerContainerWithDefaultsAddsAnnotationMetadata() Assert.Equal("mssql/server", containerAnnotation.Image); Assert.Equal("mcr.microsoft.com", containerAnnotation.Registry); - var envAnnotations = containerResource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in envAnnotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(containerResource); Assert.Collection(config, env => @@ -73,7 +65,7 @@ public void SqlServerCreatesConnectionString() appBuilder .AddSqlServer("sqlserver") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(SqlServerServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 1433, @@ -99,7 +91,7 @@ public void SqlServerDatabaseCreatesConnectionString() appBuilder .AddSqlServer("sqlserver") .WithAnnotation( - new AllocatedEndpointAnnotation("mybinding", + new AllocatedEndpointAnnotation(SqlServerServerResource.PrimaryEndpointName, ProtocolType.Tcp, "localhost", 1433, diff --git a/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs new file mode 100644 index 0000000000..ff6d7c21a1 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Utils/EnvironmentVariableEvaluator.cs @@ -0,0 +1,44 @@ +// 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.Tests.Utils; + +public static class EnvironmentVariableEvaluator +{ + public static async ValueTask> GetEnvironmentVariablesAsync(IResource resource, + DistributedApplicationOperation applicationOperation = DistributedApplicationOperation.Run) + { + var environmentVariables = new Dictionary(); + + if (resource.TryGetEnvironmentVariables(out var callbacks)) + { + var config = new Dictionary(); + var executionContext = new DistributedApplicationExecutionContext(applicationOperation); + var context = new EnvironmentCallbackContext(executionContext, config); + + foreach (var callback in callbacks) + { + await callback.Callback(context); + } + + foreach (var (key, expr) in config) + { + var value = (applicationOperation, expr) switch + { + (_, string s) => s, + (DistributedApplicationOperation.Run, IValueProvider provider) => await provider.GetValueAsync().ConfigureAwait(false), + (DistributedApplicationOperation.Publish, IManifestExpressionProvider provider) => provider.ValueExpression, + (_, null) => null, + _ => throw new InvalidOperationException($"Unsupported expression type: {expr.GetType()}") + }; + + if (value is not null) + { + environmentVariables[key] = value; + } + } + } + + return environmentVariables; + } +} diff --git a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs index e5caf32612..218d6af15d 100644 --- a/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs +++ b/tests/Aspire.Hosting.Tests/WithEnvironmentTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using Aspire.Hosting.Tests.Utils; using Xunit; namespace Aspire.Hosting.Tests; @@ -27,17 +28,7 @@ public async Task EnvironmentReferencingEndpointPopulatesWithBindingUrl() testProgram.Build(); - // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("myName")); Assert.Equal(1, servicesKeysCount); @@ -53,17 +44,7 @@ public async Task SimpleEnvironmentWithNameAndValue() testProgram.Build(); - // Call environment variable callbacks. - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("myName")); Assert.Equal(1, servicesKeysCount); @@ -82,16 +63,7 @@ public async Task EnvironmentCallbackPopulatesValueWhenCalled() environmentValue = "value2"; // Call environment variable callbacks. - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("myName")); Assert.Equal(1, servicesKeysCount); @@ -109,16 +81,7 @@ public async Task EnvironmentCallbackPopulatesValueWhenParameterResourceProvided testProgram.Build(); - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource); Assert.Contains(config, kvp => kvp.Key == "MY_PARAMETER" && kvp.Value == "MY_PARAMETER_VALUE"); } @@ -133,16 +96,8 @@ public async Task EnvironmentCallbackPopulatesWithExpressionPlaceholderWhenPubli testProgram.Build(); - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource, + DistributedApplicationOperation.Publish); Assert.Contains(config, kvp => kvp.Key == "MY_PARAMETER" && kvp.Value == "{parameter.value}"); } @@ -159,17 +114,7 @@ public async Task EnvironmentCallbackThrowsWhenParameterValueMissingInDcpMode() var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - var exception = await Assert.ThrowsAsync(async () => - { - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } - }); + var exception = await Assert.ThrowsAsync(async () => await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource)); Assert.Equal("Parameter resource could not be used because configuration key `Parameters:parameter` is missing.", exception.Message); } @@ -189,16 +134,7 @@ public async Task ComplexEnvironmentCallbackPopulatesValueWhenCalled() environmentValue = "value2"; // Call environment variable callbacks. - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("myName")); Assert.Equal(1, servicesKeysCount); diff --git a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs index fe8c55ed3c..113221be22 100644 --- a/tests/Aspire.Hosting.Tests/WithReferenceTests.cs +++ b/tests/Aspire.Hosting.Tests/WithReferenceTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Sockets; +using Aspire.Hosting.Tests.Utils; using Xunit; namespace Aspire.Hosting.Tests; @@ -28,16 +29,7 @@ public async Task ResourceWithSingleEndpointProducesSimplifiedEnvironmentVariabl testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(2, servicesKeysCount); @@ -78,16 +70,7 @@ public async Task ResourceWithConflictingEndpointsProducesFullyScopedEnvironment testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(2, servicesKeysCount); @@ -128,16 +111,7 @@ public async Task ResourceWithNonConflictingEndpointsProducesAllVariantsOfEnviro testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(4, servicesKeysCount); @@ -176,16 +150,7 @@ public async Task ResourceWithConflictingEndpointsProducesAllEnvironmentVariable testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(2, servicesKeysCount); @@ -222,16 +187,7 @@ public async Task ResourceWithEndpointsProducesAllEnvironmentVariables() testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(4, servicesKeysCount); @@ -252,18 +208,9 @@ public async Task ConnectionStringResourceThrowsWhenMissingConnectionString() testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - await Assert.ThrowsAsync(async () => { - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); }); } @@ -277,17 +224,7 @@ public async Task ConnectionStringResourceOptionalWithMissingConnectionString() testProgram.ServiceBBuilder.WithReference(resource, optional: true); testProgram.Build(); - // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(0, servicesKeysCount); @@ -304,18 +241,9 @@ public async Task ParameterAsConnectionStringResourceThrowsWhenConnectionStringS testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - var exception = await Assert.ThrowsAsync(async () => { - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); }); Assert.Equal("Connection string parameter resource could not be used because connection string `missingresource` is missing.", exception.Message); @@ -333,16 +261,7 @@ public async Task ParameterAsConnectionStringResourceInjectsConnectionStringWhen testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); Assert.Equal("test connection string", config["ConnectionStrings__resource"]); } @@ -358,16 +277,7 @@ public async Task ParameterAsConnectionStringResourceInjectsExpressionWhenPublis testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource, DistributedApplicationOperation.Publish); Assert.Equal("{resource.connectionString}", config["ConnectionStrings__resource"]); } @@ -383,16 +293,7 @@ public async Task ParameterAsConnectionStringResourceInjectsCorrectEnvWhenPublis testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Publish); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource, DistributedApplicationOperation.Publish); Assert.Equal("{resource.connectionString}", config["MY_ENV"]); } @@ -411,16 +312,7 @@ public async Task ConnectionStringResourceWithConnectionString() testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(1, servicesKeysCount); @@ -441,16 +333,7 @@ public async Task ConnectionStringResourceWithConnectionStringOverwriteName() testProgram.Build(); // Call environment variable callbacks. - var annotations = testProgram.ServiceBBuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceBBuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("ConnectionStrings__")); Assert.Equal(1, servicesKeysCount); @@ -481,16 +364,7 @@ public async Task WithReferenceHttpProduceEnvironmentVariables() testProgram.ServiceABuilder.WithReference("petstore", new Uri("https://petstore.swagger.io/")); // Call environment variable callbacks. - var annotations = testProgram.ServiceABuilder.Resource.Annotations.OfType(); - - var config = new Dictionary(); - var executionContext = new DistributedApplicationExecutionContext(DistributedApplicationOperation.Run); - var context = new EnvironmentCallbackContext(executionContext, config); - - foreach (var annotation in annotations) - { - await annotation.Callback(context); - } + var config = await EnvironmentVariableEvaluator.GetEnvironmentVariablesAsync(testProgram.ServiceABuilder.Resource); var servicesKeysCount = config.Keys.Count(k => k.StartsWith("services__")); Assert.Equal(1, servicesKeysCount);