Skip to content

Fix blocking ParameterResource.Value calls to prevent deadlocks in Aspire 9.4+ #763

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 30, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ private static IResourceBuilder<T> WithJolokiaHealthCheck<T>(
{
Uri baseUri = new Uri(endpoint.Url, UriKind.Absolute);
string userName = (await builder.Resource.UserNameReference.GetValueAsync(ct))!;
string password = builder.Resource.PasswordParameter.Value;
string password = (await ((IValueProvider)builder.Resource.PasswordParameter).GetValueAsync(ct))!;
basicAuthentication = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{userName}:{password}"));
uri = new UriBuilder(baseUri)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ public static IResourceBuilder<MinioContainerResource> AddMinioContainer(
.WithImageRegistry(MinioContainerImageTags.Registry)
.WithHttpEndpoint(targetPort: 9000, port: port, name: MinioContainerResource.PrimaryEndpointName)
.WithHttpEndpoint(targetPort: consoleTargetPort, name: MinioContainerResource.ConsoleEndpointName)
.WithEnvironment(RootUserEnvVarName, resource.RootUser.Value)
.WithEnvironment(RootPasswordEnvVarName, resource.PasswordParameter.Value)
.WithEnvironment(RootUserEnvVarName, $"{resource.RootUser}")
.WithEnvironment(RootPasswordEnvVarName, $"{resource.PasswordParameter}")
.WithArgs("server", "/data", "--console-address", $":{consoleTargetPort}");

var endpoint = builderWithResource.Resource.GetEndpoint(MinioContainerResource.PrimaryEndpointName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
context.EnvironmentVariables.Add($"LABEL_mysql{counter}", mySqlServerResource.Name);
context.EnvironmentVariables.Add($"SERVER_mysql{counter}", mySqlServerResource.Name);
context.EnvironmentVariables.Add($"USER_mysql{counter}", "root");
context.EnvironmentVariables.Add($"PASSWORD_mysql{counter}", mySqlServerResource.PasswordParameter.Value);
context.EnvironmentVariables.Add($"PASSWORD_mysql{counter}", mySqlServerResource.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_mysql{counter}", mySqlServerResource.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_mysql{counter}", "mysql@dbgate-plugin-mysql");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,16 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,

foreach (var postgresServer in postgresInstances)
{
var user = postgresServer.UserNameParameter?.Value ?? "postgres";
var userParameter = postgresServer.UserNameParameter is null
? ReferenceExpression.Create($"postgres")
: ReferenceExpression.Create($"{postgresServer.UserNameParameter}");

// DbGate assumes Postgres is being accessed over a default Aspire container network and hardcodes the resource address
// This will need to be refactored once updated service discovery APIs are available
context.EnvironmentVariables.Add($"LABEL_postgres{counter}", postgresServer.Name);
context.EnvironmentVariables.Add($"SERVER_postgres{counter}", postgresServer.Name);
context.EnvironmentVariables.Add($"USER_postgres{counter}", user);
context.EnvironmentVariables.Add($"PASSWORD_postgres{counter}", postgresServer.PasswordParameter.Value);
context.EnvironmentVariables.Add($"USER_postgres{counter}", userParameter);
context.EnvironmentVariables.Add($"PASSWORD_postgres{counter}", postgresServer.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_postgres{counter}", postgresServer.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_postgres{counter}", "postgres@dbgate-plugin-postgres");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,

// DbGate assumes Redis is being accessed over a default Aspire container network and hardcodes the resource address
var redisUrl = redisResource.PasswordParameter is not null ?
$"redis://:{redisResource.PasswordParameter.Value}@{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}" : $"redis://{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort}";
ReferenceExpression.Create($"redis://:{redisResource.PasswordParameter}@{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}") :
ReferenceExpression.Create($"redis://{redisResource.Name}:{redisResource.PrimaryEndpoint.TargetPort?.ToString()}");

context.EnvironmentVariables.Add($"LABEL_redis{counter}", redisResource.Name);
context.EnvironmentVariables.Add($"URL_redis{counter}", redisUrl);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private static void ConfigureDbGateContainer(EnvironmentCallbackContext context,
context.EnvironmentVariables.Add($"LABEL_sqlserver{counter}", sqlServerResource.Name);
context.EnvironmentVariables.Add($"SERVER_sqlserver{counter}", sqlServerResource.Name);
context.EnvironmentVariables.Add($"USER_sqlserver{counter}", "sa");
context.EnvironmentVariables.Add($"PASSWORD_sqlserver{counter}", sqlServerResource.PasswordParameter.Value);
context.EnvironmentVariables.Add($"PASSWORD_sqlserver{counter}", sqlServerResource.PasswordParameter);
context.EnvironmentVariables.Add($"PORT_sqlserver{counter}", sqlServerResource.PrimaryEndpoint.TargetPort!.ToString()!);
context.EnvironmentVariables.Add($"ENGINE_sqlserver{counter}", "mssql@dbgate-plugin-mssql");

Expand Down
2 changes: 1 addition & 1 deletion src/Shared/Dapr/Core/DaprComponentMetadataAnnotation.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Aspire.Hosting.ApplicationModel;

namespace CommunityToolkit.Aspire.Hosting.Dapr;
internal sealed record DaprComponentConfigurationAnnotation(Action<DaprComponentSchema> Configure) : IResourceAnnotation;
internal sealed record DaprComponentConfigurationAnnotation(Func<DaprComponentSchema, Task> Configure) : IResourceAnnotation;
2 changes: 1 addition & 1 deletion src/Shared/Dapr/Core/DaprComponentSecretAnnotation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

namespace CommunityToolkit.Aspire.Hosting.Dapr;

internal record DaprComponentSecretAnnotation(string Key, string Value) : IResourceAnnotation;
internal record DaprComponentSecretAnnotation(string Key, ParameterResource Value) : IResourceAnnotation;
15 changes: 8 additions & 7 deletions src/Shared/Dapr/Core/DaprDistributedApplicationLifecycleHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net.Sockets;
using System.Threading.Tasks;
using static CommunityToolkit.Aspire.Hosting.Dapr.CommandLineArgs;

namespace CommunityToolkit.Aspire.Hosting.Dapr;
Expand Down Expand Up @@ -81,7 +82,7 @@ public async Task BeforeStartAsync(DistributedApplicationModel appModel, Cancell
{
foreach (var secretAnnotation in secretAnnotations)
{
secrets[secretAnnotation.Key] = secretAnnotation.Value;
secrets[secretAnnotation.Key] = (await ((IValueProvider)secretAnnotation).GetValueAsync(cancellationToken))!;
}
// We need to append the secret store path to the resources path
onDemandResourcesPaths.TryGetValue("secretstore", out var secretStorePath);
Expand Down Expand Up @@ -491,7 +492,7 @@ private async Task<string> GetComponentAsync(DaprComponentResource component, Fu
{
// We should try to read content from a known location (such as aspire root directory)
logger.LogInformation("Unvalidated configuration {specType} for component '{ComponentName}'.", component.Type, component.Name);
return await contentWriter(GetDaprComponent(component, component.Type)).ConfigureAwait(false);
return await contentWriter(await GetDaprComponent(component, component.Type)).ConfigureAwait(false);
}
private async Task<string> GetBuildingBlockComponentAsync(DaprComponentResource component, Func<string, Task<string>> contentWriter, string defaultProvider, CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -544,19 +545,19 @@ private static async Task<string> GetDefaultContent(DaprComponentResource compon
string defaultContent = await File.ReadAllTextAsync(defaultContentPath, cancellationToken).ConfigureAwait(false);
string yaml = defaultContent.Replace($"name: {component.Type}", $"name: {component.Name}");
DaprComponentSchema content = DaprComponentSchema.FromYaml(yaml);
ConfigureDaprComponent(component, content);
await ConfigureDaprComponent(component, content);
return content.ToString();
}


private static string GetDaprComponent(DaprComponentResource component, string type)
private static async Task<string> GetDaprComponent(DaprComponentResource component, string type)
{
var content = new DaprComponentSchema(component.Name, type);
ConfigureDaprComponent(component, content);
await ConfigureDaprComponent(component, content);
return content.ToString();
}

private static void ConfigureDaprComponent(DaprComponentResource component, DaprComponentSchema content)
private static async Task ConfigureDaprComponent(DaprComponentResource component, DaprComponentSchema content)
{
if (component.TryGetAnnotationsOfType<DaprComponentSecretAnnotation>(out var secrets) && secrets.Any())
{
Expand All @@ -566,7 +567,7 @@ private static void ConfigureDaprComponent(DaprComponentResource component, Dapr
{
foreach (var annotation in annotations)
{
annotation.Configure(content);
await annotation.Configure(content);
}
}
}
Expand Down
18 changes: 16 additions & 2 deletions src/Shared/Dapr/Core/DaprMetadataResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static IResourceBuilder<IDaprComponentResource> WithMetadata(this IResour
Name = name,
Value = value
});
return Task.CompletedTask;
}));


Expand All @@ -42,7 +43,7 @@ public static IResourceBuilder<IDaprComponentResource> WithMetadata(this IResour
{
if (parameterResource.Secret)
{
return builder.WithAnnotation(new DaprComponentSecretAnnotation(parameterResource.Name, parameterResource.Value))
return builder.WithAnnotation(new DaprComponentSecretAnnotation(parameterResource.Name, parameterResource))
.WithAnnotation(new DaprComponentConfigurationAnnotation(schema =>
{
var existing = schema.Spec.Metadata.Find(m => m.Name == name);
Expand All @@ -59,9 +60,22 @@ public static IResourceBuilder<IDaprComponentResource> WithMetadata(this IResour
Key = parameterResource.Name
}
});
return Task.CompletedTask;
}));
}

return builder.WithMetadata(name, parameterResource.Value);
return builder.WithAnnotation(new DaprComponentConfigurationAnnotation(async schema =>
{
var existing = schema.Spec.Metadata.Find(m => m.Name == name);
if (existing is not null)
{
schema.Spec.Metadata.Remove(existing);
}
schema.Spec.Metadata.Add(new DaprComponentSpecMetadataValue
{
Name = name,
Value = (await ((IValueProvider)parameterResource).GetValueAsync(default))!
});
}));
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.Net.Sockets;
using Aspire.Components.Common.Tests;
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;

namespace CommunityToolkit.Aspire.Hosting.DbGate.Tests;

public class AddDbGateTests
{
[Fact]
Expand Down Expand Up @@ -226,7 +228,7 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()

var mysqlResource1 = mysqlResourceBuilder1.Resource;

var mysqlResourceBuilder2 =builder.AddMySql("mysql2")
var mysqlResourceBuilder2 = builder.AddMySql("mysql2")
.WithDbGate();

var mysqlResource2 = mysqlResourceBuilder2.Resource;
Expand Down Expand Up @@ -295,10 +297,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_postgres1", item.Key);
Assert.Equal("postgres", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_postgres1", item.Key);
Assert.Equal(postgresResource1.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)postgresResource1.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand All @@ -325,10 +328,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_postgres2", item.Key);
Assert.Equal("postgres", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_postgres2", item.Key);
Assert.Equal(postgresResource2.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)postgresResource2.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand All @@ -345,12 +349,15 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("LABEL_redis1", item.Key);
Assert.Equal(redisResource1.Name, item.Value);
},
item =>
async item =>
{
var redisUrl = redisResource1.PasswordParameter is not null ?
$"redis://:{redisResource1.PasswordParameter.Value}@{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}" : $"redis://{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}";
var expectedRedisUrl = redisResource1.PasswordParameter switch
{
IValueProvider parameter => $"redis://:{await parameter.GetValueAsync(default)}@{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}",
_ => $"redis://{redisResource1.Name}:{redisResource1.PrimaryEndpoint.TargetPort}"
};
Assert.Equal("URL_redis1", item.Key);
Assert.Equal(redisUrl, item.Value);
Assert.Equal(expectedRedisUrl, item.Value);
},
item =>
{
Expand All @@ -362,12 +369,15 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("LABEL_redis2", item.Key);
Assert.Equal(redisResource2.Name, item.Value);
},
item =>
async item =>
{
var redisUrl = redisResource2.PasswordParameter is not null ?
$"redis://:{redisResource2.PasswordParameter.Value}@{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}" : $"redis://{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}";
var expectedRedisUrl = redisResource2.PasswordParameter switch
{
IValueProvider parameter => $"redis://:{await parameter.GetValueAsync(default)}@{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}",
_ => $"redis://{redisResource2.Name}:{redisResource2.PrimaryEndpoint.TargetPort}"
};
Assert.Equal("URL_redis2", item.Key);
Assert.Equal(redisUrl, item.Value);
Assert.Equal(expectedRedisUrl, item.Value);
},
item =>
{
Expand All @@ -389,10 +399,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_sqlserver1", item.Key);
Assert.Equal("sa", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_sqlserver1", item.Key);
Assert.Equal(sqlserverResource1.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)sqlserverResource1.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand All @@ -419,10 +430,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_sqlserver2", item.Key);
Assert.Equal("sa", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_sqlserver2", item.Key);
Assert.Equal(sqlserverResource2.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)sqlserverResource2.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand All @@ -449,10 +461,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_mysql1", item.Key);
Assert.Equal("root", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_mysql1", item.Key);
Assert.Equal(mysqlResource1.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)mysqlResource1.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand All @@ -479,10 +492,11 @@ public async Task WithDbGateShouldAddAnnotationsForMultipleDatabaseTypes()
Assert.Equal("USER_mysql2", item.Key);
Assert.Equal("root", item.Value);
},
item =>
async item =>
{
Assert.Equal("PASSWORD_mysql2", item.Key);
Assert.Equal(mysqlResource2.PasswordParameter.Value, item.Value);
var expectedPassword = await ((IValueProvider)mysqlResource2.PasswordParameter).GetValueAsync(default);
Assert.Equal(expectedPassword, item.Value);
},
item =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ public void WithAuthTokenStringParameterEnvironmentVariable()
var context = new EnvironmentCallbackContext(new DistributedApplicationExecutionContext(new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run)));
environment.Callback(context);

Assert.Equal("your-ngrok-auth-token", ((ParameterResource)context.EnvironmentVariables["NGROK_AUTHTOKEN"]).Value);
var parameterResource = (ParameterResource)context.EnvironmentVariables["NGROK_AUTHTOKEN"];
Assert.NotNull(parameterResource);
// Verify it's the expected parameter resource without calling .Value
Assert.Equal("ngrok-authtoken", parameterResource.Name);
}

[Fact]
Expand Down
Loading