Skip to content

Commit 9b06baf

Browse files
authored
Allow for nested children in the dashboard (#7604)
* Allow for nested children in the dashboard Originally the parent-child lookup contained the "root" parent and all the descendants in a flat list. This doesn't show up well in the dashboard because grandchildren are parented directly under their root. Fix this by making the parent-child lookup only contain direct children, and recursively parent the descendants on their direct parent. Fix #7580
1 parent d6cdb08 commit 9b06baf

File tree

4 files changed

+68
-21
lines changed

4 files changed

+68
-21
lines changed

src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,11 @@ await _notificationService.PublishUpdateAsync(child, s => s with
238238
StopTimeStamp = stopTimeStamp,
239239
Properties = s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parentName)
240240
}).ConfigureAwait(false);
241+
242+
// the parent name needs to be an instance name, not the resource name.
243+
// parent the children of the child under the first resource instance.
244+
await SetChildResourceAsync(child, child.GetResolvedResourceNames()[0], state, startTimeStamp, stopTimeStamp)
245+
.ConfigureAwait(false);
241246
}
242247
}
243248

@@ -253,6 +258,8 @@ await _notificationService.PublishUpdateAsync(child, s => s with
253258
{
254259
Properties = s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parentName)
255260
}).ConfigureAwait(false);
261+
262+
await SetExecutableChildResourceAsync(child).ConfigureAwait(false);
256263
}
257264
}
258265

src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,16 @@ internal static class RelationshipEvaluator
1212
{
1313
public static ILookup<IResource, IResource> GetParentChildLookup(DistributedApplicationModel model)
1414
{
15-
static IResource? SelectParentContainerResource(IResource resource) => resource switch
16-
{
17-
IResourceWithParent rp => SelectParentContainerResource(rp.Parent),
18-
IResource r when r.IsContainer() => r,
19-
_ => null
20-
};
21-
2215
// parent -> children lookup
2316
// Built from IResourceWithParent first, then from annotations.
2417
return model.Resources.OfType<IResourceWithParent>()
25-
.Select(x => (Child: (IResource)x, Root: SelectParentContainerResource(x.Parent)))
26-
.Where(x => x.Root is not null)
18+
.Select(x => (Child: (IResource)x, Parent: x.Parent))
19+
.Where(x => x.Parent is not null)
2720
.Concat(GetParentChildRelationshipsFromAnnotations(model))
28-
.ToLookup(x => x.Root!, x => x.Child);
21+
.ToLookup(x => x.Parent!, x => x.Child);
2922
}
3023

31-
private static IEnumerable<(IResource Child, IResource? Root)> GetParentChildRelationshipsFromAnnotations(DistributedApplicationModel model)
24+
private static IEnumerable<(IResource Child, IResource Parent)> GetParentChildRelationshipsFromAnnotations(DistributedApplicationModel model)
3225
{
3326
static bool TryGetParent(IResource resource, [NotNullWhen(true)] out IResource? parent)
3427
{
@@ -55,14 +48,7 @@ IResource r when TryGetParent(r, out var parent) => parent,
5548

5649
ValidateRelationships(result!);
5750

58-
static IResource? SelectRootResource(IResource? resource) => resource switch
59-
{
60-
IResource r when TryGetParent(r, out var parent) => SelectRootResource(parent) ?? parent,
61-
_ => null
62-
};
63-
64-
// translate the result to child -> root, which the dashboard expects
65-
return result.Select(x => (x.Child, Root: SelectRootResource(x.Child)));
51+
return result!;
6652
}
6753

6854
private static void ValidateRelationships((IResource Child, IResource Parent)[] relationships)

tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly()
8383
await appOrchestrator.RunApplicationAsync();
8484

8585
string? parentResourceId = null;
86+
string? childResourceId = null;
8687
string? childParentResourceId = null;
8788
string? child2ParentResourceId = null;
8889
string? nestedChildParentResourceId = null;
@@ -96,6 +97,7 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly()
9697
}
9798
else if (item.Resource == child.Resource)
9899
{
100+
childResourceId = item.ResourceId;
99101
childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
100102
}
101103
else if (item.Resource == nestedChild.Resource)
@@ -121,8 +123,8 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly()
121123
Assert.Equal(parentResourceId, childParentResourceId);
122124
Assert.Equal(parentResourceId, child2ParentResourceId);
123125

124-
// Nested child should have parent set to the root parent, not direct parent
125-
Assert.Equal(parentResourceId, nestedChildParentResourceId);
126+
// Nested child should be parented on the direct parent
127+
Assert.Equal(childResourceId, nestedChildParentResourceId);
126128
}
127129

128130
[Fact]
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.Orchestrator;
5+
using Microsoft.Extensions.DependencyInjection;
6+
using Xunit;
7+
8+
namespace Aspire.Hosting.Tests.Orchestrator;
9+
10+
public class RelationshipEvaluatorTests
11+
{
12+
[Fact]
13+
public void HandlesNestedChildren()
14+
{
15+
var builder = DistributedApplication.CreateBuilder();
16+
17+
var parentResource = builder.AddContainer("parent", "image");
18+
var childResource = builder.AddResource(new CustomChildResource("child", parentResource.Resource));
19+
var grandChildResource = builder.AddResource(new CustomChildResource("grandchild", childResource.Resource));
20+
var greatGrandChildResource = builder.AddResource(new CustomChildResource("greatgrandchild", grandChildResource.Resource));
21+
22+
var childWithAnnotationsResource = builder.AddContainer("child-with-annotations", "image")
23+
.WithParentRelationship(parentResource.Resource);
24+
25+
var grandChildWithAnnotationsResource = builder.AddContainer("grandchild-with-annotations", "image")
26+
.WithParentRelationship(childWithAnnotationsResource.Resource);
27+
28+
using var app = builder.Build();
29+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
30+
31+
var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
32+
Assert.Equal(4, parentChildLookup.Count);
33+
34+
Assert.Collection(parentChildLookup[parentResource.Resource],
35+
x => Assert.Equal(childResource.Resource, x),
36+
x => Assert.Equal(childWithAnnotationsResource.Resource, x));
37+
38+
Assert.Single(parentChildLookup[childResource.Resource], grandChildResource.Resource);
39+
Assert.Single(parentChildLookup[grandChildResource.Resource], greatGrandChildResource.Resource);
40+
41+
Assert.Empty(parentChildLookup[greatGrandChildResource.Resource]);
42+
43+
Assert.Single(parentChildLookup[childWithAnnotationsResource.Resource], grandChildWithAnnotationsResource.Resource);
44+
45+
Assert.Empty(parentChildLookup[grandChildWithAnnotationsResource.Resource]);
46+
}
47+
48+
private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent
49+
{
50+
public IResource Parent => parent;
51+
}
52+
}

0 commit comments

Comments
 (0)