Skip to content

Commit 462c20d

Browse files
[release/9.4] Made changes to how parameter.Value works (#10357)
* Made changes to how parameter.Value works - Value now blocks (sync over async) waiting for value resolution if WaitForValueTcs is set. - Made GetValueAsync on ParameterResource public. - Changed most code outside of tests to use GetValueAsync instead of Value * Fix typo in comment regarding parameter access in WithHttpHealthCheck method * Add error handling for URL parameter resolution in external service --------- Co-authored-by: David Fowler <davidfowl@gmail.com>
1 parent 82fcb46 commit 462c20d

File tree

10 files changed

+158
-39
lines changed

10 files changed

+158
-39
lines changed

src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public async Task GetOrCreateResourceAsync(AzureBicepResource resource, Provisio
106106
if (BicepUtilities.GetExistingResourceGroup(resource) is { } existingResourceGroup)
107107
{
108108
var existingResourceGroupName = existingResourceGroup is ParameterResource parameterResource
109-
? parameterResource.Value
109+
? (await parameterResource.GetValueAsync(cancellationToken).ConfigureAwait(false))!
110110
: (string)existingResourceGroup;
111111
var response = await context.Subscription.GetResourceGroups().GetAsync(existingResourceGroupName, cancellationToken).ConfigureAwait(false);
112112
resourceGroup = response.Value;

src/Aspire.Hosting.MySql/MySqlBuilderExtensions.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -205,14 +205,14 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
205205
.WithHttpEndpoint(targetPort: 80, name: "http")
206206
.ExcludeFromManifest();
207207

208-
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(phpMyAdminContainer, (e, ct) =>
208+
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(phpMyAdminContainer, async (e, ct) =>
209209
{
210210
var mySqlInstances = builder.ApplicationBuilder.Resources.OfType<MySqlServerResource>();
211211

212212
if (!mySqlInstances.Any())
213213
{
214214
// No-op if there are no MySql resources present.
215-
return Task.CompletedTask;
215+
return;
216216
}
217217

218218
if (mySqlInstances.Count() == 1)
@@ -225,12 +225,12 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
225225
// This will need to be refactored once updated service discovery APIs are available
226226
context.EnvironmentVariables.Add("PMA_HOST", $"{endpoint.Resource.Name}:{endpoint.TargetPort}");
227227
context.EnvironmentVariables.Add("PMA_USER", "root");
228-
context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter.Value);
228+
context.EnvironmentVariables.Add("PMA_PASSWORD", singleInstance.PasswordParameter);
229229
});
230230
}
231231
else
232232
{
233-
var tempConfigFile = WritePhpMyAdminConfiguration(mySqlInstances);
233+
var tempConfigFile = await WritePhpMyAdminConfiguration(mySqlInstances, ct).ConfigureAwait(false);
234234

235235
try
236236
{
@@ -258,8 +258,6 @@ public static IResourceBuilder<T> WithPhpMyAdmin<T>(this IResourceBuilder<T> bui
258258
}
259259
}
260260
}
261-
262-
return Task.CompletedTask;
263261
});
264262

265263
configureContainer?.Invoke(phpMyAdminContainerBuilder);
@@ -346,7 +344,7 @@ public static IResourceBuilder<MySqlServerResource> WithInitFiles(this IResource
346344
return builder.WithContainerFiles(initPath, importFullPath);
347345
}
348346

349-
private static string WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResource> mySqlInstances)
347+
private static async Task<string> WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResource> mySqlInstances, CancellationToken cancellationToken)
350348
{
351349
// This temporary file is not used by the container, it will be copied and then deleted
352350
var filePath = Path.GetTempFileName();
@@ -360,14 +358,16 @@ private static string WritePhpMyAdminConfiguration(IEnumerable<MySqlServerResour
360358
foreach (var mySqlInstance in mySqlInstances)
361359
{
362360
var endpoint = mySqlInstance.PrimaryEndpoint;
361+
var pwd = await mySqlInstance.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
362+
363363
writer.WriteLine("$i++;");
364364
// PhpMyAdmin assumes MySql is being accessed over a default Aspire container network and hardcodes the resource address
365365
// This will need to be refactored once updated service discovery APIs are available
366366
writer.WriteLine($"$cfg['Servers'][$i]['host'] = '{endpoint.Resource.Name}:{endpoint.TargetPort}';");
367367
writer.WriteLine($"$cfg['Servers'][$i]['verbose'] = '{mySqlInstance.Name}';");
368368
writer.WriteLine($"$cfg['Servers'][$i]['auth_type'] = 'cookie';");
369369
writer.WriteLine($"$cfg['Servers'][$i]['user'] = 'root';");
370-
writer.WriteLine($"$cfg['Servers'][$i]['password'] = '{mySqlInstance.PasswordParameter.Value}';");
370+
writer.WriteLine($"$cfg['Servers'][$i]['password'] = '{pwd}';");
371371
writer.WriteLine($"$cfg['Servers'][$i]['AllowNoPassword'] = true;");
372372
writer.WriteLine();
373373
}

src/Aspire.Hosting.Nats/NatsBuilderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public static IResourceBuilder<NatsServerResource> AddNats(this IDistributedAppl
7373
AuthOpts = new()
7474
{
7575
Username = await nats.UserNameReference.GetValueAsync(ct).ConfigureAwait(false),
76-
Password = nats.PasswordParameter!.Value,
76+
Password = await nats.PasswordParameter!.GetValueAsync(ct).ConfigureAwait(false),
7777
}
7878
};
7979

src/Aspire.Hosting.PostgreSQL/PostgresBuilderExtensions.cs

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -203,18 +203,18 @@ public static IResourceBuilder<T> WithPgAdmin<T>(this IResourceBuilder<T> builde
203203

204204
pgAdminContainerBuilder.WithContainerFiles(
205205
destinationPath: "/pgadmin4",
206-
callback: (context, _) =>
206+
callback: async (context, cancellationToken) =>
207207
{
208208
var appModel = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
209209
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresServerResource>();
210210

211-
return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
211+
return [
212212
new ContainerFile
213213
{
214214
Name = "servers.json",
215-
Contents = WritePgAdminServerJson(postgresInstances),
215+
Contents = await WritePgAdminServerJson(postgresInstances, cancellationToken).ConfigureAwait(false),
216216
},
217-
]);
217+
];
218218
});
219219

220220
configureContainer?.Invoke(pgAdminContainerBuilder);
@@ -313,25 +313,25 @@ public static IResourceBuilder<PostgresServerResource> WithPgWeb(this IResourceB
313313

314314
pgwebContainerBuilder.WithContainerFiles(
315315
destinationPath: "/",
316-
callback: (context, _) =>
316+
callback: async (context, ct) =>
317317
{
318318
var appModel = context.ServiceProvider.GetRequiredService<DistributedApplicationModel>();
319319
var postgresInstances = builder.ApplicationBuilder.Resources.OfType<PostgresDatabaseResource>();
320320

321321
// Add the bookmarks to the pgweb container
322-
return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
322+
return [
323323
new ContainerDirectory
324324
{
325325
Name = ".pgweb",
326326
Entries = [
327327
new ContainerDirectory
328328
{
329329
Name = "bookmarks",
330-
Entries = WritePgWebBookmarks(postgresInstances),
330+
Entries = await WritePgWebBookmarks(postgresInstances, ct).ConfigureAwait(false)
331331
},
332332
],
333333
},
334-
]);
334+
];
335335
});
336336

337337
return builder;
@@ -489,21 +489,25 @@ public static IResourceBuilder<PostgresServerResource> WithHostPort(this IResour
489489
});
490490
}
491491

492-
private static IEnumerable<ContainerFileSystemItem> WritePgWebBookmarks(IEnumerable<PostgresDatabaseResource> postgresInstances)
492+
private static async Task<IEnumerable<ContainerFileSystemItem>> WritePgWebBookmarks(IEnumerable<PostgresDatabaseResource> postgresInstances, CancellationToken cancellationToken)
493493
{
494494
var bookmarkFiles = new List<ContainerFileSystemItem>();
495495

496496
foreach (var postgresDatabase in postgresInstances)
497497
{
498-
var user = postgresDatabase.Parent.UserNameParameter?.Value ?? "postgres";
498+
var user = postgresDatabase.Parent.UserNameParameter is null
499+
? "postgres"
500+
: await postgresDatabase.Parent.UserNameParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
501+
502+
var password = await postgresDatabase.Parent.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false) ?? "password";
499503

500504
// PgAdmin assumes Postgres is being accessed over a default Aspire container network and hardcodes the resource address
501505
// This will need to be refactored once updated service discovery APIs are available
502506
var fileContent = $"""
503507
host = "{postgresDatabase.Parent.Name}"
504508
port = {postgresDatabase.Parent.PrimaryEndpoint.TargetPort}
505509
user = "{user}"
506-
password = "{postgresDatabase.Parent.PasswordParameter.Value}"
510+
password = "{password}"
507511
database = "{postgresDatabase.DatabaseName}"
508512
sslmode = "disable"
509513
""";
@@ -518,7 +522,7 @@ private static IEnumerable<ContainerFileSystemItem> WritePgWebBookmarks(IEnumera
518522
return bookmarkFiles;
519523
}
520524

521-
private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource> postgresInstances)
525+
private static async Task<string> WritePgAdminServerJson(IEnumerable<PostgresServerResource> postgresInstances, CancellationToken cancellationToken)
522526
{
523527
using var stream = new MemoryStream();
524528
using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
@@ -531,6 +535,10 @@ private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource>
531535
foreach (var postgresInstance in postgresInstances)
532536
{
533537
var endpoint = postgresInstance.PrimaryEndpoint;
538+
var userName = postgresInstance.UserNameParameter is null
539+
? "postgres"
540+
: await postgresInstance.UserNameParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
541+
var password = await postgresInstance.PasswordParameter.GetValueAsync(cancellationToken).ConfigureAwait(false);
534542

535543
writer.WriteStartObject($"{serverIndex}");
536544
writer.WriteString("Name", postgresInstance.Name);
@@ -539,10 +547,10 @@ private static string WritePgAdminServerJson(IEnumerable<PostgresServerResource>
539547
// This will need to be refactored once updated service discovery APIs are available
540548
writer.WriteString("Host", endpoint.Resource.Name);
541549
writer.WriteNumber("Port", (int)endpoint.TargetPort!);
542-
writer.WriteString("Username", postgresInstance.UserNameParameter?.Value ?? "postgres");
550+
writer.WriteString("Username", userName);
543551
writer.WriteString("SSLMode", "prefer");
544552
writer.WriteString("MaintenanceDB", "postgres");
545-
writer.WriteString("PasswordExecCommand", $"echo '{postgresInstance.PasswordParameter.Value}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
553+
writer.WriteString("PasswordExecCommand", $"echo '{password}'"); // HACK: Generating a pass file and playing around with chmod is too painful.
546554
writer.WriteEndObject();
547555

548556
serverIndex++;

src/Aspire.Hosting.Redis/RedisBuilderExtensions.cs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,14 +158,14 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
158158
.WithHttpEndpoint(targetPort: 8081, name: "http")
159159
.ExcludeFromManifest();
160160

161-
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource, (e, ct) =>
161+
builder.ApplicationBuilder.Eventing.Subscribe<BeforeResourceStartedEvent>(resource, async (e, ct) =>
162162
{
163163
var redisInstances = builder.ApplicationBuilder.Resources.OfType<RedisResource>();
164164

165165
if (!redisInstances.Any())
166166
{
167167
// No-op if there are no Redis resources present.
168-
return Task.CompletedTask;
168+
return;
169169
}
170170

171171
var hostsVariableBuilder = new StringBuilder();
@@ -177,14 +177,13 @@ public static IResourceBuilder<RedisResource> WithRedisCommander(this IResourceB
177177
var hostString = $"{(hostsVariableBuilder.Length > 0 ? "," : string.Empty)}{redisInstance.Name}:{redisInstance.Name}:{redisInstance.PrimaryEndpoint.TargetPort}:0";
178178
if (redisInstance.PasswordParameter is not null)
179179
{
180-
hostString += $":{redisInstance.PasswordParameter.Value}";
180+
var password = await redisInstance.PasswordParameter.GetValueAsync(ct).ConfigureAwait(false);
181+
hostString += $":{password}";
181182
}
182183
hostsVariableBuilder.Append(hostString);
183184
}
184185

185186
resourceBuilder.WithEnvironment("REDIS_HOSTS", hostsVariableBuilder.ToString());
186-
187-
return Task.CompletedTask;
188187
});
189188

190189
configureContainer?.Invoke(resourceBuilder);
@@ -244,7 +243,7 @@ public static IResourceBuilder<RedisResource> WithRedisInsight(this IResourceBui
244243
context.EnvironmentVariables.Add($"RI_REDIS_ALIAS{counter}", redisInstance.Name);
245244
if (redisInstance.PasswordParameter is not null)
246245
{
247-
context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter.Value);
246+
context.EnvironmentVariables.Add($"RI_REDIS_PASSWORD{counter}", redisInstance.PasswordParameter);
248247
}
249248

250249
counter++;

src/Aspire.Hosting/ApplicationModel/ParameterResource.cs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@ public ParameterResource(string name, Func<ParameterDefault?, string> callback,
3131
/// <summary>
3232
/// Gets the value of the parameter.
3333
/// </summary>
34-
public string Value
34+
public string Value => GetValueAsync(default).AsTask().GetAwaiter().GetResult()!;
35+
36+
internal string ValueInternal
3537
{
3638
get
3739
{
@@ -79,14 +81,20 @@ internal string ConfigurationKey
7981
/// </summary>
8082
internal TaskCompletionSource<string>? WaitForValueTcs { get; set; }
8183

82-
async ValueTask<string?> IValueProvider.GetValueAsync(CancellationToken cancellationToken)
84+
/// <summary>
85+
/// Gets the value of the parameter asynchronously, waiting if necessary for the value to be set.
86+
/// </summary>
87+
/// <param name="cancellationToken">The cancellation token to observe while waiting for the value.</param>
88+
/// <returns>A task that represents the asynchronous operation, containing the value of the parameter.</returns>
89+
public async ValueTask<string?> GetValueAsync(CancellationToken cancellationToken)
8390
{
8491
if (WaitForValueTcs is not null)
8592
{
8693
// Wait for the value to be set if the task completion source is available.
8794
return await WaitForValueTcs.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
8895
}
8996

90-
return Value;
97+
// In publish mode, there's no WaitForValueTcs set.
98+
return ValueInternal;
9199
}
92100
}

src/Aspire.Hosting/ExternalServiceBuilderExtensions.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ private static IResourceBuilder<ExternalServiceResource> AddExternalServiceImpl(
8080
.WithInitialState(new CustomResourceSnapshot
8181
{
8282
ResourceType = "ExternalService",
83+
State = KnownResourceStates.Waiting,
8384
Properties = []
8485
})
8586
.ExcludeFromManifest();
@@ -104,7 +105,24 @@ private static IResourceBuilder<ExternalServiceResource> AddExternalServiceImpl(
104105
if (uri is null)
105106
{
106107
// If the URI is not set, it means we are using a parameterized URL
107-
var url = resource.UrlParameter?.Value;
108+
string? url;
109+
try
110+
{
111+
url = resource.UrlParameter is null
112+
? null
113+
: await resource.UrlParameter.GetValueAsync(ct).ConfigureAwait(false);
114+
}
115+
catch (Exception ex)
116+
{
117+
e.Logger.LogError(ex, "Failed to get value for URL parameter '{ParameterName}'", resource.UrlParameter?.Name);
118+
119+
await e.Notifications.PublishUpdateAsync(resource, snapshot => snapshot with
120+
{
121+
State = KnownResourceStates.FailedToStart
122+
}).ConfigureAwait(false);
123+
124+
return;
125+
}
108126

109127
if (!ExternalServiceResource.UrlIsValidForExternalService(url, out uri, out var message))
110128
{
@@ -182,6 +200,8 @@ public static IResourceBuilder<ExternalServiceResource> WithHttpHealthCheck(this
182200
{
183201
var uri = builder.Resource.Uri;
184202

203+
// OK accessing the parameter here synchronously as this should only activate once the resource is running
204+
185205
if (uri is null && !Uri.TryCreate(builder.Resource.UrlParameter?.Value, UriKind.Absolute, out uri)
186206
|| (uri?.Scheme != "http" && uri?.Scheme != "https"))
187207
{

src/Aspire.Hosting/Orchestrator/ParameterProcessor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private async Task ProcessParameterAsync(ParameterResource parameterResource)
6262
{
6363
try
6464
{
65-
var value = parameterResource.Value ?? "";
65+
var value = parameterResource.ValueInternal ?? "";
6666

6767
await notificationService.PublishUpdateAsync(parameterResource, s =>
6868
{

0 commit comments

Comments
 (0)