Show waiting dependency details in dashboard#17089
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves the Aspire Dashboard’s “Waiting” experience by publishing a new resource.waitingFor snapshot property while dependency waits are active, then using it to show which dependency resources are still blocking startup (including replica-resolved names and links when available). It also incorporates the same metadata into wait cancellation/timeout diagnostics.
Changes:
- Adds
KnownProperties.Resource.WaitingForplus shared snapshot helpers to set/remove resource snapshot properties. - Updates
ResourceNotificationService.WaitForDependenciesAsyncto publish/update/clearresource.waitingForduring waits and to include waiting dependency details in cancellation messages. - Updates dashboard rendering to display “Waiting for dependencies: …” (with inline resource links when possible) and regenerates localization resources; adds/updates tests accordingly.
Reviewed changes
Copilot reviewed 25 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Hosting.Tests/ResourceNotificationTests.cs | Adds tests asserting resource.waitingFor is published/updated/cleared and included in cancellation diagnostics. |
| tests/Aspire.Dashboard.Tests/Model/ResourceStateViewModelTests.cs | Adds coverage for waiting tooltip formatting when waiting dependencies are present. |
| tests/Aspire.Dashboard.Components.Tests/Controls/ResourceDetailsTests.cs | Adds component test ensuring the resource details panel renders waiting dependency names and links. |
| src/Shared/Model/KnownProperties.cs | Adds KnownProperties.Resource.WaitingFor constant (resource.waitingFor). |
| src/Shared/CustomResourceSnapshotExtensions.cs | Adds RemoveResourceProperty helper for snapshot property cleanup. |
| src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs | Publishes/updates/clears resource.waitingFor during dependency waits; clears stale waiting metadata when leaving Waiting; enriches cancellation messages with waiting dependency details. |
| src/Aspire.Dashboard/Resources/xlf/Columns.cs.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.de.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.es.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.fr.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.it.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.ja.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.ko.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.pl.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.pt-BR.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.ru.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.tr.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hans.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/xlf/Columns.zh-Hant.xlf | Adds new localization unit for “Waiting for dependencies: {0}.” |
| src/Aspire.Dashboard/Resources/Columns.resx | Adds the new neutral waiting string resource and comment describing {0}. |
| src/Aspire.Dashboard/Resources/Columns.Designer.cs | Regenerates designer accessor for StateColumnResourceWaitingFor. |
| src/Aspire.Dashboard/Model/ResourceViewModelExtensions.cs | Adds TryGetWaitingForDependencies extension for reading waiting dependency list from resource properties. |
| src/Aspire.Dashboard/Model/ResourceStateViewModel.cs | Updates waiting tooltip to include dependency list when resource.waitingFor is present. |
| src/Aspire.Dashboard/Components/Controls/ResourceDetails.razor.cs | Routes state-description rendering through a custom value component that can link waiting dependencies. |
| src/Aspire.Dashboard/Components/Controls/PropertyValues/ResourceStateDescriptionValue.razor.cs | New component: splits localized waiting format into prefix/suffix and builds linked/non-linked dependency entries. |
| src/Aspire.Dashboard/Components/Controls/PropertyValues/ResourceStateDescriptionValue.razor | New component markup: renders prefix + comma-separated dependency links/text + suffix with highlighting. |
Files not reviewed (1)
- src/Aspire.Dashboard/Resources/Columns.Designer.cs: Language not supported
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 17089Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 17089" |
|
PR testing update: ran the pr-testing workflow in the repo-local container runner for PR #17089. Result: passed for the targeted container smoke scenario. I did not find a PR behavior issue in the waiting dependency flow. Verification:
Setup notes from the container run:
Those setup notes did not appear to be regressions in this PR's waiting dependency changes. |
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
PR testing results✅ Verified PR #17089 with the dogfood CLI.
Scenarios exercised:
Targeted regression tests passed: Overall result: ✅ verified. |
davidfowl
left a comment
There was a problem hiding this comment.
Undo the HTTP2 change in basket service and remove the Start anyway from the details tab.
Co-authored-by: Copilot <[email protected]>
|
Addressed in a1238ec: removed the BasketService HTTP/2 override and hid the Start command from the resource details menu while keeping it available in the resources list/context menu. |
| } | ||
| }, | ||
| "AllowedHosts": "*", | ||
| "Kestrel": { |
There was a problem hiding this comment.
The removed Kestrel HTTP/2-only default was blocking the new HTTP health check on BasketService. This playground change needs WithHttpHealthCheck("/health", endpointName: "http") so the dashboard can show healthy/starting dependency details for the waiting-state demo.
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
Co-authored-by: Copilot <[email protected]>
|
@JamesNK addressed the remaining waiting dependency feedback in 9c20407.
|
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
…g-state-dependency-details # Conflicts: # tests/Aspire.Cli.Tests/Backchannel/ResourceSnapshotMapperTests.cs
Co-authored-by: Copilot <[email protected]>
| private void AddCommandMenuItems(List<MenuButtonItem> menuItems, ResourceViewModel resource, EventCallback<CommandViewModel> commandSelected, Func<ResourceViewModel, CommandViewModel, bool> isCommandExecuting, bool showStartCommand) | ||
| { | ||
| var menuCommands = resource.Commands | ||
| .Where(c => showStartCommand || !c.Name.Equals(CommandViewModel.StartCommand, StringComparisons.CommandName)) |
| ? resource.Properties.OrderBy(p => p.Key).ToDistinctDictionary( | ||
| p => p.Key, | ||
| p => p.Value.Value.TryConvertToString(out var value) ? value : null) | ||
| p => ConvertPropertyValueToJsonNode(p.Value.Value)) |
There was a problem hiding this comment.
If I export backsetcache.json I get many nulls. See resource.appArgsSensitivit and container.ports:
{
"name": "basketcache-fudydpcx",
"displayName": "basketcache",
"resourceType": "Container",
"uid": "basketcache-fudydpcx",
"state": "RuntimeUnhealthy",
"creationTimestamp": "2026-05-19T04:58:38+00:00",
"source": "docker.io/library/redis:8.6",
"urls": [
{
"name": "secondary",
"url": "tcp://localhost:58333/"
},
{
"name": "tcp",
"url": "rediss://localhost:58335/"
}
],
"volumes": [
{
"source": "testshop.apphost-8e2ec1c54c-basketcache-data",
"target": "/data",
"mountType": "volume"
}
],
"properties": {
"container.args": [],
"container.command": "/bin/sh",
"container.id": null,
"container.image": "docker.io/library/redis:8.6",
"container.lifetime": "Session",
"container.ports": [
null,
null
],
"resource.appArgs": [
"redis-server",
"--requirepass",
"$REDIS_PASSWORD",
"--save",
"60",
"1",
"--tls-port",
"6379",
"--port",
"6380",
"--tls-ca-cert-file",
"/usr/lib/ssl/aspire/cert.pem",
"--tls-cert-file",
"/usr/lib/ssl/aspire/private/C2D9E727877F50178B850CE95801C230203C8C2D.crt",
"--tls-key-file",
"/usr/lib/ssl/aspire/private/C2D9E727877F50178B850CE95801C230203C8C2D.key",
"--tls-auth-clients",
"no"
],
"resource.appArgsSensitivity": [
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
],
"resource.connectionString": "localhost:58335,password=-hAd+t1xBM{mNm}RF0j!Q~,ssl=true",
"resource.createTime": "2026-05-19T04:58:38.0000000Z",
"resource.displayName": "basketcache",
"resource.exitCode": null,
"resource.healthState": null,
"resource.name": "basketcache-fudydpcx",
"resource.startTime": null,
"resource.state": "RuntimeUnhealthy",
"resource.stopTime": null,
"resource.type": "Container",
"resource.uid": "basketcache-fudydpcx"
},
"environment": {
"REDIS_PASSWORD": "-hAd+t1xBM{mNm}RF0j!Q~",
"SSL_CERT_DIR": "/usr/lib/ssl/aspire/certs:/etc/ssl/certs:/usr/local/share/ca-certificates:/etc/pki/tls/certs"
},
"healthReports": {
"basketcache_check": {
"description": "",
"exceptionMessage": ""
}
},
"commands": {}
}Compare with before:
{
"name": "basketcache-fturxbyq",
"displayName": "basketcache",
"resourceType": "Container",
"uid": "basketcache-fturxbyq",
"state": "RuntimeUnhealthy",
"creationTimestamp": "2026-05-19T05:17:54+00:00",
"source": "docker.io/library/redis:8.6",
"urls": [
{
"name": "secondary",
"url": "tcp://localhost:51760/"
},
{
"name": "tcp",
"url": "rediss://localhost:51756/"
}
],
"volumes": [
{
"source": "testshop.apphost-8e2ec1c54c-basketcache-data",
"target": "/data",
"mountType": "volume"
}
],
"properties": {
"container.args": null,
"container.command": "/bin/sh",
"container.id": null,
"container.image": "docker.io/library/redis:8.6",
"container.lifetime": "Session",
"container.ports": null,
"resource.appArgs": null,
"resource.appArgsSensitivity": null,
"resource.connectionString": "localhost:51756,password=-hAd+t1xBM{mNm}RF0j!Q~,ssl=true",
"resource.createTime": "2026-05-19T05:17:54.0000000Z",
"resource.displayName": "basketcache",
"resource.exitCode": null,
"resource.healthState": null,
"resource.name": "basketcache-fturxbyq",
"resource.startTime": null,
"resource.state": "RuntimeUnhealthy",
"resource.stopTime": null,
"resource.type": "Container",
"resource.uid": "basketcache-fturxbyq"
},
"environment": {
"REDIS_PASSWORD": "-hAd+t1xBM{mNm}RF0j!Q~",
"SSL_CERT_DIR": "/usr/lib/ssl/aspire/certs:/etc/ssl/certs:/usr/local/share/ca-certificates:/etc/pki/tls/certs"
},
"healthReports": {
"basketcache_check": {
"description": "",
"exceptionMessage": ""
}
},
"commands": {}
}Not correctly converting JSON properties was here earlier, I can see container.ports is null when it should have value [ 6380, 6379 ] but it's now much worse.
| <value>Resource is waiting for dependencies.</value> | ||
| </data> | ||
| <data name="StateColumnResourceWaitingFor" xml:space="preserve"> | ||
| <value>Waiting for dependencies: {0}.</value> |
There was a problem hiding this comment.
Don't put fullstop in messages than end with :
| /// This allows for extensibility without changing the schema. | ||
| /// </summary> | ||
| public Dictionary<string, string?> Properties { get; init; } = []; | ||
| public Dictionary<string, JsonNode?> Properties { get; init; } = []; |
There was a problem hiding this comment.
What happens to old aspire CLI versions? I'm guessing they'll error.
JamesNK
left a comment
There was a problem hiding this comment.
Post-merge review: 4 issues found — 1 data loss bug (NumberValue/BoolValue not handled in JSON export), 1 breaking wire format change (Properties dict type), 1 formatting issue (trailing period), 1 performance concern (repeated ToArray allocation in tooltip path).
| <value>Resource is waiting for dependencies.</value> | ||
| </data> | ||
| <data name="StateColumnResourceWaitingFor" xml:space="preserve"> | ||
| <value>Waiting for dependencies: {0}.</value> |
There was a problem hiding this comment.
Nit: The period after {0} reads awkwardly because the interpolated value is a list following a colon (e.g., "Waiting for dependencies: nginx, redis."). Consider removing the trailing period: "Waiting for dependencies: {0}".
| return array; | ||
| } | ||
|
|
||
| return null; |
There was a problem hiding this comment.
Bug: ConvertPropertyValueToJsonNode doesn't handle NumberValue or BoolValue. The TryConvertToString extension only returns true for Value.KindOneofCase.StringValue. Properties with numeric values (e.g., container.ports holding port numbers like 6379) or boolean values fall through to return null, silently losing data in the exported JSON.
Fix:
private static JsonNode? ConvertPropertyValueToJsonNode(Google.Protobuf.WellKnownTypes.Value value)
{
if (value.TryConvertToString(out var stringValue))
{
return JsonValue.Create(stringValue);
}
if (value.HasNumberValue)
{
return JsonValue.Create(value.NumberValue);
}
if (value.HasBoolValue)
{
return JsonValue.Create(value.BoolValue);
}
if (value.ListValue is not null)
{
var array = new JsonArray();
foreach (var element in value.ListValue.Values)
{
array.Add(ConvertPropertyValueToJsonNode(element));
}
return array;
}
return null;
}| /// This allows for extensibility without changing the schema. | ||
| /// </summary> | ||
| public Dictionary<string, string?> Properties { get; init; } = []; | ||
| public Dictionary<string, JsonNode?> Properties { get; init; } = []; |
There was a problem hiding this comment.
Breaking wire format change: Properties changed from Dictionary<string, string?> to Dictionary<string, JsonNode?>. Older Aspire CLI versions that deserialize this field as Dictionary<string, string?> will fail when they receive array-valued or object-valued properties. Is there versioning or capability negotiation to handle this, or will older CLI versions need to be updated?
| dependencies = default; | ||
| return false; | ||
| } | ||
|
|
There was a problem hiding this comment.
Performance: TryGetResolvedWaitingForDependencies calls allResources.ToArray() on every invocation. In Resources.razor, this is passed via _resourceByName.Values in the column's TooltipText callback, meaning it allocates a new array on every tooltip hover for every resource row. Consider accepting IReadOnlyCollection<ResourceViewModel> or caching the materialized collection to avoid repeated allocations during rendering.




Description
When a resource is waiting on dependencies, the dashboard currently shows a generic waiting state without naming the resources that are still blocking startup. This change publishes a
resource.waitingForsnapshot property while dependency waits are active and uses it to show the waiting dependencies in the resource details state description.User-facing usage
When the dependency resource is visible in the dashboard, its name is rendered as an inline link to that resource. Replicated dependencies use resolved replica names, so each replica disappears from the waiting list as it satisfies the wait condition.
The same metadata is also included in cancellation diagnostics for waits, so timeout/cancellation messages show the state, health, and exit code of resources that were still blocking the wait.
Implementation details:
KnownProperties.Resource.WaitingForand helper support for setting/removing resource snapshot properties.resource.waitingForwhile dependency waits are active, including per-replica updates for replicated dependencies.Fixes #16774
Validation:
./restore.shMSBUILDTERMINALLOGGER=false ./dotnet.sh build src/Aspire.Dashboard/Aspire.Dashboard.csproj /t:UpdateXlf /p:SkipNativeBuild=trueMSBUILDTERMINALLOGGER=false ./dotnet.sh test --project tests/Aspire.Hosting.Tests/Aspire.Hosting.Tests.csproj --no-launch-profile -- --filter-method "*.WaitForDependenciesPublishesAndUpdatesWaitingForDependencies" --filter-method "*.WaitForDependenciesPublishesResolvedWaitingForDependenciesForReplicas" --filter-method "*.PublishUpdateClearsWaitingForDependenciesWhenResourceLeavesWaiting" --filter-method "*.CancellationMessageIncludesWaitingForDependencies" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"MSBUILDTERMINALLOGGER=false ./dotnet.sh test --project tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj --no-launch-profile -- --filter-class "*.ResourceStateViewModelTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"MSBUILDTERMINALLOGGER=false ./dotnet.sh test --project tests/Aspire.Dashboard.Components.Tests/Aspire.Dashboard.Components.Tests.csproj --no-launch-profile -- --filter-method "*.Render_StateDescription_ShowsWaitingForDependenciesAsResourceDetailEntry" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"git --no-pager diff --checkChecklist
<remarks />and<code />elements on your triple slash comments?