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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions src/Aspire.Hosting.Azure/AzurePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,9 @@ private async Task WriteAzureArtifactsOutputAsync(IPublishingStep step, Distribu
.Where(r => !r.IsExcludedFromPublish())
.ToList();

MapParameter(environment.ResourceGroupName);
MapParameter(environment.Location);
MapParameter(environment.PrincipalId);
await MapParameterAsync(environment.ResourceGroupName, cancellationToken).ConfigureAwait(false);
await MapParameterAsync(environment.Location, cancellationToken).ConfigureAwait(false);
await MapParameterAsync(environment.PrincipalId, cancellationToken).ConfigureAwait(false);

var resourceGroupParam = ParameterLookup[environment.ResourceGroupName];
MainInfrastructure.Add(resourceGroupParam);
Expand Down Expand Up @@ -163,14 +163,14 @@ private async Task WriteAzureArtifactsOutputAsync(IPublishingStep step, Distribu
// Map parameters from existing resources
if (resource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingAnnotation))
{
Visit(existingAnnotation.ResourceGroup, MapParameter);
Visit(existingAnnotation.Name, MapParameter);
await VisitAsync(existingAnnotation.ResourceGroup, MapParameterAsync, cancellationToken).ConfigureAwait(false);
await VisitAsync(existingAnnotation.Name, MapParameterAsync, cancellationToken).ConfigureAwait(false);
}

// Map parameters for the resource itself
foreach (var parameter in resource.Parameters)
{
Visit(parameter.Value, MapParameter);
await VisitAsync(parameter.Value, MapParameterAsync, cancellationToken).ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove uses of parameter.Value in this file.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use GetValueAsync

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! I've removed all uses of parameter.Value in AzurePublishingContext.cs by refactoring the code to use tuple destructuring pattern foreach (var (key, value) in resource.Parameters) instead of accessing the .Value property on KeyValuePair instances. This eliminates parameter.Value accesses while maintaining the same functionality. (356b979)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm revert this. That's not what I meant. I mean ParameterResource.Value should be replaced with ParameterResource.GetValueAsync

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Replaced ParameterLookup[p].Value.Compile() with new IdentifierExpression(ParameterLookup[p].BicepIdentifier) to avoid using the obsolete .Value property and directly use the parameter's Bicep identifier. (c8b72bb)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted the tuple destructuring changes and correctly replaced ParameterLookup[p].Value.Compile() with new IdentifierExpression(ParameterLookup[p].BicepIdentifier) to avoid using the obsolete property. (4ba6c15)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Undo that change as well

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reverted the AzurePublishingContext.cs file to the original synchronous Visit(parameter.Value, MapParameter) call as requested. (e6e6dda)

}
}

Expand Down Expand Up @@ -359,7 +359,7 @@ await task.SucceedAsync(
}
}

private void MapParameter(object? candidate)
private async Task MapParameterAsync(object candidate, CancellationToken cancellationToken = default)
{
Comment on lines +362 to 363
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method signature changed from MapParameter(object? candidate) to MapParameterAsync(object candidate, CancellationToken), removing the nullable annotation. This could cause issues if null values were previously passed to this method. Consider keeping the nullable annotation for consistency.

Suggested change
private async Task MapParameterAsync(object candidate, CancellationToken cancellationToken = default)
{
private async Task MapParameterAsync(object? candidate, CancellationToken cancellationToken = default)
{
if (candidate is null)
{
return;
}

Copilot uses AI. Check for mistakes.
if (candidate is ParameterResource p && !ParameterLookup.ContainsKey(p))
{
Expand All @@ -372,7 +372,11 @@ private void MapParameter(object? candidate)

if (!p.Secret && p.Default is not null)
{
pp.Value = p.Value;
var value = await p.GetValueAsync(cancellationToken).ConfigureAwait(false);
if (value is not null)
{
pp.Value = value;
}
}

ParameterLookup[p] = pp;
Expand Down Expand Up @@ -400,6 +404,27 @@ private static void Visit(object? value, Action<object> visitor, HashSet<object>
}
}

private static Task VisitAsync(object? value, Func<object, CancellationToken, Task> visitor, CancellationToken cancellationToken = default) =>
VisitAsync(value, visitor, [], cancellationToken);

private static async Task VisitAsync(object? value, Func<object, CancellationToken, Task> visitor, HashSet<object> visited, CancellationToken cancellationToken = default)
{
if (value is null || !visited.Add(value))
{
return;
}

await visitor(value, cancellationToken).ConfigureAwait(false);

if (value is IValueWithReferences vwr)
{
foreach (var reference in vwr.References)
{
await VisitAsync(reference, visitor, visited, cancellationToken).ConfigureAwait(false);
}
}
}

/// <summary>
/// Saves the compiled Bicep template to disk.
/// </summary>
Expand Down
10 changes: 9 additions & 1 deletion src/Aspire.Hosting.Docker/DockerComposePublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,15 @@ private async Task WriteDockerComposeOutputAsync(DistributedApplicationModel mod

foreach (var entry in environment.CapturedEnvironmentVariables ?? [])
{
var (key, (description, defaultValue, _)) = entry;
var (key, (description, defaultValue, source)) = entry;

// If the source is a parameter and there's no explicit default value,
// resolve the parameter's default value asynchronously
if (defaultValue is null && source is ParameterResource parameter && !parameter.Secret && parameter.Default is not null)
{
defaultValue = await parameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
}

envFile.AddIfMissing(key, defaultValue, description);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ public static string AsEnvironmentPlaceholder(this ParameterResource parameter,
return dockerComposeService.Parent.AddEnvironmentVariable(
env,
description: $"Parameter {parameter.Name}",
defaultValue: parameter.Secret || parameter.Default is null ? null : parameter.Value,
defaultValue: null,
source: parameter
);
}
Expand Down
26 changes: 19 additions & 7 deletions src/Aspire.Hosting.Kubernetes/KubernetesPublishingContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,22 @@ private async Task WriteKubernetesOutputAsync(DistributedApplicationModel model,
}

await WriteKubernetesTemplatesForResource(resource, serviceResource.GetTemplatedResources()).ConfigureAwait(false);
AppendResourceContextToHelmValues(resource, serviceResource);
await AppendResourceContextToHelmValuesAsync(resource, serviceResource).ConfigureAwait(false);
}
}

await WriteKubernetesHelmChartAsync(environment).ConfigureAwait(false);
await WriteKubernetesHelmValuesAsync().ConfigureAwait(false);
}

private void AppendResourceContextToHelmValues(IResource resource, KubernetesResource resourceContext)
private async Task AppendResourceContextToHelmValuesAsync(IResource resource, KubernetesResource resourceContext)
{
AddValuesToHelmSection(resource, resourceContext.Parameters, HelmExtensions.ParametersKey);
AddValuesToHelmSection(resource, resourceContext.EnvironmentVariables, HelmExtensions.ConfigKey);
AddValuesToHelmSection(resource, resourceContext.Secrets, HelmExtensions.SecretsKey);
await AddValuesToHelmSectionAsync(resource, resourceContext.Parameters, HelmExtensions.ParametersKey).ConfigureAwait(false);
await AddValuesToHelmSectionAsync(resource, resourceContext.EnvironmentVariables, HelmExtensions.ConfigKey).ConfigureAwait(false);
await AddValuesToHelmSectionAsync(resource, resourceContext.Secrets, HelmExtensions.SecretsKey).ConfigureAwait(false);
}

private void AddValuesToHelmSection(
private async Task AddValuesToHelmSectionAsync(
IResource resource,
Dictionary<string, KubernetesResource.HelmExpressionWithValue> contextItems,
string helmKey)
Expand All @@ -114,7 +114,19 @@ private void AddValuesToHelmSection(
continue;
}

paramValues[key] = helmExpressionWithValue.Value ?? string.Empty;
string? value;

// If there's a parameter source, resolve its value asynchronously
if (helmExpressionWithValue.ParameterSource is ParameterResource parameter)
{
value = await parameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
}
else
{
value = helmExpressionWithValue.Value;
}

paramValues[key] = value ?? string.Empty;
}

if (paramValues.Count > 0)
Expand Down
21 changes: 18 additions & 3 deletions src/Aspire.Hosting.Kubernetes/KubernetesResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,25 @@ private void ProcessEnvironmentDefaultValue(object value, string key, string res
EnvironmentVariables[key] = new(configExpression, value.ToString() ?? string.Empty);
}

internal class HelmExpressionWithValue(string helmExpression, string? value)
internal class HelmExpressionWithValue
{
public string HelmExpression { get; } = helmExpression;
public string? Value { get; } = value;
public HelmExpressionWithValue(string helmExpression, string? value)
{
HelmExpression = helmExpression;
Value = value;
ParameterSource = null;
}

public HelmExpressionWithValue(string helmExpression, ParameterResource parameterSource)
{
HelmExpression = helmExpression;
Value = null;
ParameterSource = parameterSource;
}

public string HelmExpression { get; }
public string? Value { get; }
public ParameterResource? ParameterSource { get; }
public bool IsHelmSecretExpression => HelmExpression.ContainsHelmSecretExpression();
public bool ValueContainsSecretExpression => Value?.ContainsHelmSecretExpression() ?? false;
public bool ValueContainsHelmExpression => Value?.ContainsHelmExpression() ?? false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,15 @@ private static HelmExpressionWithValue AllocateParameter(ParameterResource param
formattedName.ToHelmSecretExpression(resource.Name) :
formattedName.ToHelmConfigExpression(resource.Name);

var value = parameter.Default is null || parameter.Secret ? null : parameter.Value;
return new(expression, value);
// Store the parameter itself for deferred resolution instead of resolving the value immediately
if (parameter.Default is null || parameter.Secret)
{
return new(expression, (string?)null);
}
else
{
return new(expression, parameter);
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Storing the ParameterResource directly in HelmExpressionWithValue changes the behavior from immediate value resolution to deferred resolution. This could potentially break existing code that expects the value to be resolved at construction time. Consider documenting this behavior change or providing a method that explicitly indicates deferred resolution.

Copilot uses AI. Check for mistakes.
}
}

private static HelmExpressionWithValue ResolveUnknownValue(IManifestExpressionProvider parameter, IResource resource)
Expand Down
4 changes: 4 additions & 0 deletions src/Aspire.Hosting/ApplicationModel/ParameterResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ public ParameterResource(string name, Func<ParameterDefault?, string> callback,
/// <summary>
/// Gets the value of the parameter.
/// </summary>
/// <remarks>
/// This property is obsolete. Use <see cref="GetValueAsync(CancellationToken)"/> for async access or pass the <see cref="ParameterResource"/> directly to methods that accept it (e.g., environment variables).
/// </remarks>
[Obsolete("Use GetValueAsync for async access or pass the ParameterResource directly to methods that accept it (e.g., environment variables).")]
public string Value => GetValueAsync(default).AsTask().GetAwaiter().GetResult()!;

internal string ValueInternal => _lazyValue.Value;
Expand Down
4 changes: 4 additions & 0 deletions tests/Aspire.Hosting.Azure.Tests/AzureRedisExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ public async Task AddAzureRedisRunAsContainerProducesCorrectConnectionString()
Assert.True(redis.Resource.IsContainer(), "The resource should now be a container resource.");

Assert.NotNull(redisResource?.PasswordParameter);
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"localhost:12455,password={redisResource.PasswordParameter.Value}", await redis.Resource.ConnectionStringExpression.GetValueAsync(CancellationToken.None));
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
Expand Down Expand Up @@ -195,7 +197,9 @@ public async Task PublishAsRedisPublishesRedisAsAzureRedisInfrastructure()
Assert.True(redis.Resource.IsContainer());
Assert.NotNull(redis.Resource.PasswordParameter);

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"localhost:12455,password={redis.Resource.PasswordParameter.Value}", await redis.Resource.GetConnectionStringAsync());
#pragma warning restore CS0618 // Type or member is obsolete

var manifest = await AzureManifestUtils.GetManifestWithBicep(redis.Resource);

Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Hosting.Milvus.Tests/MilvusFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
{
using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
var milvus1 = builder1.AddMilvus("milvus1");
#pragma warning disable CS0618 // Type or member is obsolete
var password = milvus1.Resource.ApiKeyParameter.Value;
#pragma warning restore CS0618 // Type or member is obsolete

var db1 = milvus1.AddDatabase("milvusdb1", dbname);

Expand Down
4 changes: 4 additions & 0 deletions tests/Aspire.Hosting.MongoDB.Tests/AddMongoDBTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,13 @@ public async Task MongoDBCreatesConnectionString()
Assert.NotNull(connectionStringResource);
var connectionString = await connectionStringResource.GetConnectionStringAsync();

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"mongodb://admin:{dbResource.Parent.PasswordParameter?.Value}@localhost:27017?authSource=admin&authMechanism=SCRAM-SHA-256", await serverResource.GetConnectionStringAsync());
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal("mongodb://admin:{mongodb-password.value}@{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}?authSource=admin&authMechanism=SCRAM-SHA-256", serverResource.ConnectionStringExpression.ValueExpression);
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"mongodb://admin:{dbResource.Parent.PasswordParameter?.Value}@localhost:27017/mydatabase?authSource=admin&authMechanism=SCRAM-SHA-256", connectionString);
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal("mongodb://admin:{mongodb-password.value}@{mongodb.bindings.tcp.host}:{mongodb.bindings.tcp.port}/mydatabase?authSource=admin&authMechanism=SCRAM-SHA-256", connectionStringResource.ConnectionStringExpression.ValueExpression);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Hosting.MongoDB.Tests/MongoDbFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
{
using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);
var mongodb1 = builder1.AddMongoDB("mongodb");
#pragma warning disable CS0618 // Type or member is obsolete
var password = mongodb1.Resource.PasswordParameter!.Value;
#pragma warning restore CS0618 // Type or member is obsolete
var db1 = mongodb1.AddDatabase(dbName);

if (useVolume)
Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Hosting.MySql.Tests/MySqlFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)
using var builder1 = TestDistributedApplicationBuilder.CreateWithTestContainerRegistry(testOutputHelper);

var mysql1 = builder1.AddMySql("mysql").WithEnvironment("MYSQL_DATABASE", mySqlDbName);
#pragma warning disable CS0618 // Type or member is obsolete
var password = mysql1.Resource.PasswordParameter.Value;
#pragma warning restore CS0618 // Type or member is obsolete

var db1 = mysql1.AddDatabase(mySqlDbName);

Expand Down
6 changes: 6 additions & 0 deletions tests/Aspire.Hosting.Nats.Tests/AddNatsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ public async Task AddNatsSetsDefaultUserNameAndPasswordAndIncludesThemInConnecti
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 4222));

Assert.NotNull(nats.Resource.PasswordParameter);
#pragma warning disable CS0618 // Type or member is obsolete
Assert.False(string.IsNullOrEmpty(nats.Resource.PasswordParameter!.Value));
#pragma warning restore CS0618 // Type or member is obsolete

using var app = appBuilder.Build();

Expand All @@ -50,7 +52,9 @@ public async Task AddNatsSetsDefaultUserNameAndPasswordAndIncludesThemInConnecti
Assert.NotNull(connectionStringResource);
var connectionString = await connectionStringResource.GetConnectionStringAsync();

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"nats://nats:{natsResource.PasswordParameter?.Value}@localhost:4222", connectionString);
#pragma warning restore CS0618 // Type or member is obsolete
Assert.Equal("nats://nats:{nats-password.value}@{nats.bindings.tcp.host}:{nats.bindings.tcp.port}", connectionStringResource.ConnectionStringExpression.ValueExpression);
}

Expand All @@ -67,8 +71,10 @@ public async Task AddNatsSetsUserNameAndPasswordAndIncludesThemInConnection()
Assert.NotNull(nats.Resource.UserNameParameter);
Assert.NotNull(nats.Resource.PasswordParameter);

#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal("usr", nats.Resource.UserNameParameter!.Value);
Assert.Equal("password", nats.Resource.PasswordParameter!.Value);
#pragma warning restore CS0618 // Type or member is obsolete

using var app = appBuilder.Build();

Expand Down
2 changes: 2 additions & 0 deletions tests/Aspire.Hosting.Oracle.Tests/OracleFunctionalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ public async Task WithDataShouldPersistStateBetweenUsages(bool useVolume)

var oracle1 = builder1.AddOracle("oracle");

#pragma warning disable CS0618 // Type or member is obsolete
var password = oracle1.Resource.PasswordParameter.Value;
#pragma warning restore CS0618 // Type or member is obsolete

var db1 = oracle1.AddDatabase(oracleDbName);

Expand Down
10 changes: 10 additions & 0 deletions tests/Aspire.Hosting.PostgreSQL.Tests/AddPostgresTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,9 @@ public async Task PostgresCreatesConnectionString()

var connectionString = await connectionStringResource.GetConnectionStringAsync();
Assert.Equal("Host={postgres.bindings.tcp.host};Port={postgres.bindings.tcp.port};Username=postgres;Password={postgres-password.value}", connectionStringResource.ConnectionStringExpression.ValueExpression);
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"Host=localhost;Port=2000;Username=postgres;Password={postgres.Resource.PasswordParameter.Value}", connectionString);
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
Expand Down Expand Up @@ -491,7 +493,9 @@ public async Task WithPostgresProducesValidServersJsonFile()
Assert.Equal("postgres", servers.GetProperty("1").GetProperty("Username").GetString());
Assert.Equal("prefer", servers.GetProperty("1").GetProperty("SSLMode").GetString());
Assert.Equal("postgres", servers.GetProperty("1").GetProperty("MaintenanceDB").GetString());
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"echo '{pg1.Resource.PasswordParameter.Value}'", servers.GetProperty("1").GetProperty("PasswordExecCommand").GetString());
#pragma warning restore CS0618 // Type or member is obsolete

// Make sure the second server is correct.
Assert.Equal(pg2.Resource.Name, servers.GetProperty("2").GetProperty("Name").GetString());
Expand All @@ -501,7 +505,9 @@ public async Task WithPostgresProducesValidServersJsonFile()
Assert.Equal("myuser", servers.GetProperty("2").GetProperty("Username").GetString());
Assert.Equal("prefer", servers.GetProperty("2").GetProperty("SSLMode").GetString());
Assert.Equal("postgres", servers.GetProperty("2").GetProperty("MaintenanceDB").GetString());
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"echo '{pg2.Resource.PasswordParameter.Value}'", servers.GetProperty("2").GetProperty("PasswordExecCommand").GetString());
#pragma warning restore CS0618 // Type or member is obsolete
}

[Fact]
Expand Down Expand Up @@ -619,6 +625,7 @@ public void CanAddDatabasesWithTheSameNameOnMultipleServers()

private static string CreatePgWebBookmarkfileContent(PostgresDatabaseResource postgresDatabase)
{
#pragma warning disable CS0618 // Type or member is obsolete
var user = postgresDatabase.Parent.UserNameParameter?.Value ?? "postgres";

// We're hardcoding references to container resources based on a default Aspire network
Expand All @@ -631,6 +638,7 @@ private static string CreatePgWebBookmarkfileContent(PostgresDatabaseResource po
database = "{postgresDatabase.DatabaseName}"
sslmode = "disable"
""";
#pragma warning restore CS0618 // Type or member is obsolete

return fileContent;
}
Expand Down Expand Up @@ -672,6 +680,8 @@ public async Task VerifyPostgresServerResourceWithUserName()
.WithEndpoint("tcp", e => e.AllocatedEndpoint = new AllocatedEndpoint(e, "localhost", 2000));

var connectionString = await postgres.Resource.GetConnectionStringAsync();
#pragma warning disable CS0618 // Type or member is obsolete
Assert.Equal($"Host=localhost;Port=2000;Username=user1;Password={postgres.Resource.PasswordParameter.Value}", connectionString);
#pragma warning restore CS0618 // Type or member is obsolete
}
}
Loading
Loading