Skip to content

Commit 4717477

Browse files
authored
Add WithChildRelationship methods for parent-child relationships (#11840)
* Add WithChildRelationship methods to manage parent-child resource relationships * Add tests for WithChildRelationship to validate parent-child resource lookups
1 parent 8510e11 commit 4717477

File tree

3 files changed

+321
-6
lines changed

3 files changed

+321
-6
lines changed

src/Aspire.Hosting/ResourceBuilderExtensions.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2239,6 +2239,70 @@ public static IResourceBuilder<T> WithParentRelationship<T>(
22392239
return builder.WithRelationship(parent, KnownRelationshipTypes.Parent);
22402240
}
22412241

2242+
/// <summary>
2243+
/// Adds a <see cref="ResourceRelationshipAnnotation"/> to the resource annotations to add a parent-child relationship.
2244+
/// </summary>
2245+
/// <typeparam name="T">The type of the resource.</typeparam>
2246+
/// <param name="builder">The resource builder.</param>
2247+
/// <param name="child">The child of <paramref name="builder"/>.</param>
2248+
/// <returns>A resource builder.</returns>
2249+
/// <remarks>
2250+
/// <para>
2251+
/// The <c>WithChildRelationship</c> method is used to add child relationships to the resource. Relationships are used to link
2252+
/// resources together in UI.
2253+
/// </para>
2254+
/// <example>
2255+
/// This example shows adding a relationship between two resources.
2256+
/// <code lang="C#">
2257+
/// var builder = DistributedApplication.CreateBuilder(args);
2258+
///
2259+
/// var parameter = builder.AddParameter("parameter");
2260+
///
2261+
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
2262+
/// .WithChildRelationship(parameter);
2263+
/// </code>
2264+
/// </example>
2265+
/// </remarks>
2266+
public static IResourceBuilder<T> WithChildRelationship<T>(
2267+
this IResourceBuilder<T> builder,
2268+
IResourceBuilder<IResource> child) where T : IResource
2269+
{
2270+
child.WithRelationship(builder.Resource, KnownRelationshipTypes.Parent);
2271+
return builder;
2272+
}
2273+
2274+
/// <summary>
2275+
/// Adds a <see cref="ResourceRelationshipAnnotation"/> to the resource annotations to add a parent-child relationship.
2276+
/// </summary>
2277+
/// <typeparam name="T">The type of the resource.</typeparam>
2278+
/// <param name="builder">The resource builder.</param>
2279+
/// <param name="child">The child of <paramref name="builder"/>.</param>
2280+
/// <returns>A resource builder.</returns>
2281+
/// <remarks>
2282+
/// <para>
2283+
/// The <c>WithChildRelationship</c> method is used to add child relationships to the resource. Relationships are used to link
2284+
/// resources together in UI.
2285+
/// </para>
2286+
/// <example>
2287+
/// This example shows adding a relationship between two resources.
2288+
/// <code lang="C#">
2289+
/// var builder = DistributedApplication.CreateBuilder(args);
2290+
///
2291+
/// var parameter = builder.AddParameter("parameter");
2292+
///
2293+
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
2294+
/// .WithChildRelationship(parameter.Resource);
2295+
/// </code>
2296+
/// </example>
2297+
/// </remarks>
2298+
public static IResourceBuilder<T> WithChildRelationship<T>(
2299+
this IResourceBuilder<T> builder,
2300+
IResource child) where T : IResource
2301+
{
2302+
var childBuilder = builder.ApplicationBuilder.CreateResourceBuilder(child);
2303+
return builder.WithChildRelationship(childBuilder);
2304+
}
2305+
22422306
/// <summary>
22432307
/// Specifies the icon to use when displaying the resource in the dashboard.
22442308
/// </summary>

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

Lines changed: 166 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,10 @@ public async Task GrandChildResourceWithConnectionString()
350350

351351
var parentResource = builder.AddResource(new ParentResourceWithConnectionString("parent"));
352352
var childResource = builder.AddResource(
353-
new ChildResourceWithConnectionString("child", new Dictionary<string, string> { {"Namespace", "ns"} }, parentResource.Resource)
353+
new ChildResourceWithConnectionString("child", new Dictionary<string, string> { { "Namespace", "ns" } }, parentResource.Resource)
354354
);
355355
var grandChildResource = builder.AddResource(
356-
new ChildResourceWithConnectionString("grand-child", new Dictionary<string, string> { {"Database", "db"} }, childResource.Resource)
356+
new ChildResourceWithConnectionString("grand-child", new Dictionary<string, string> { { "Database", "db" } }, childResource.Resource)
357357
);
358358

359359
await using var app = builder.Build();
@@ -589,10 +589,10 @@ await events.PublishAsync(new OnResourceChangedContext(
589589

590590
// Parent should have the new state
591591
Assert.Equal(KnownResourceStates.FailedToStart, parentState);
592-
592+
593593
// Child container (has own lifetime) should NOT receive parent state
594594
Assert.NotEqual(KnownResourceStates.Running, childContainerState);
595-
595+
596596
// Custom child (does not have own lifetime) SHOULD receive parent state
597597
Assert.Equal(KnownResourceStates.FailedToStart, customChildState);
598598
}
@@ -635,11 +635,171 @@ await events.PublishAsync(new OnResourceChangedContext(
635635

636636
// Parent should have the new state
637637
Assert.Equal(KnownResourceStates.FailedToStart, parentState);
638-
638+
639639
// Child project (has own lifetime) should NOT receive parent state
640640
Assert.NotEqual(KnownResourceStates.Running, childProjectState);
641-
641+
642642
// Custom child (does not have own lifetime) SHOULD receive parent state
643643
Assert.Equal(KnownResourceStates.FailedToStart, customChildState);
644644
}
645+
646+
[Fact]
647+
public async Task WithChildRelationshipUsingResourceBuilderSetsParentPropertyCorrectly()
648+
{
649+
var builder = DistributedApplication.CreateBuilder();
650+
651+
var parent = builder.AddContainer("parent", "image");
652+
var child = builder.AddContainer("child", "image");
653+
var child2 = builder.AddContainer("child2", "image");
654+
655+
parent.WithChildRelationship(child)
656+
.WithChildRelationship(child2);
657+
658+
using var app = builder.Build();
659+
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();
660+
661+
var events = new DcpExecutorEvents();
662+
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();
663+
664+
var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
665+
await appOrchestrator.RunApplicationAsync();
666+
667+
string? parentResourceId = null;
668+
string? childParentResourceId = null;
669+
string? child2ParentResourceId = null;
670+
var watchResourceTask = Task.Run(async () =>
671+
{
672+
await foreach (var item in resourceNotificationService.WatchAsync())
673+
{
674+
if (item.Resource == parent.Resource)
675+
{
676+
parentResourceId = item.ResourceId;
677+
}
678+
else if (item.Resource == child.Resource)
679+
{
680+
childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
681+
}
682+
else if (item.Resource == child2.Resource)
683+
{
684+
child2ParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
685+
}
686+
687+
if (parentResourceId != null && childParentResourceId != null && child2ParentResourceId != null)
688+
{
689+
return;
690+
}
691+
}
692+
});
693+
694+
await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));
695+
696+
await watchResourceTask.DefaultTimeout();
697+
698+
Assert.Equal(parentResourceId, childParentResourceId);
699+
Assert.Equal(parentResourceId, child2ParentResourceId);
700+
}
701+
702+
[Fact]
703+
public async Task WithChildRelationshipUsingResourceSetsParentPropertyCorrectly()
704+
{
705+
var builder = DistributedApplication.CreateBuilder();
706+
707+
var parent = builder.AddContainer("parent", "image");
708+
var child = builder.AddContainer("child", "image");
709+
var child2 = builder.AddContainer("child2", "image");
710+
711+
parent.WithChildRelationship(child.Resource)
712+
.WithChildRelationship(child2.Resource);
713+
714+
using var app = builder.Build();
715+
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();
716+
717+
var events = new DcpExecutorEvents();
718+
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();
719+
720+
var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
721+
await appOrchestrator.RunApplicationAsync();
722+
723+
string? parentResourceId = null;
724+
string? childParentResourceId = null;
725+
string? child2ParentResourceId = null;
726+
var watchResourceTask = Task.Run(async () =>
727+
{
728+
await foreach (var item in resourceNotificationService.WatchAsync())
729+
{
730+
if (item.Resource == parent.Resource)
731+
{
732+
parentResourceId = item.ResourceId;
733+
}
734+
else if (item.Resource == child.Resource)
735+
{
736+
childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
737+
}
738+
else if (item.Resource == child2.Resource)
739+
{
740+
child2ParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
741+
}
742+
743+
if (parentResourceId != null && childParentResourceId != null && child2ParentResourceId != null)
744+
{
745+
return;
746+
}
747+
}
748+
});
749+
750+
await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));
751+
752+
await watchResourceTask.DefaultTimeout();
753+
754+
Assert.Equal(parentResourceId, childParentResourceId);
755+
Assert.Equal(parentResourceId, child2ParentResourceId);
756+
}
757+
758+
[Fact]
759+
public async Task WithChildRelationshipWorksWithProjects()
760+
{
761+
var builder = DistributedApplication.CreateBuilder();
762+
763+
var parentProject = builder.AddProject<ProjectA>("parent-project");
764+
var childProject = builder.AddProject<ProjectB>("child-project");
765+
766+
parentProject.WithChildRelationship(childProject);
767+
768+
using var app = builder.Build();
769+
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();
770+
771+
var events = new DcpExecutorEvents();
772+
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();
773+
774+
var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
775+
await appOrchestrator.RunApplicationAsync();
776+
777+
string? parentProjectResourceId = null;
778+
string? childProjectParentResourceId = null;
779+
var watchResourceTask = Task.Run(async () =>
780+
{
781+
await foreach (var item in resourceNotificationService.WatchAsync())
782+
{
783+
if (item.Resource == parentProject.Resource)
784+
{
785+
parentProjectResourceId = item.ResourceId;
786+
}
787+
else if (item.Resource == childProject.Resource)
788+
{
789+
childProjectParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
790+
}
791+
792+
if (parentProjectResourceId != null && childProjectParentResourceId != null)
793+
{
794+
return;
795+
}
796+
}
797+
});
798+
799+
await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));
800+
801+
await watchResourceTask.DefaultTimeout();
802+
803+
Assert.Equal(parentProjectResourceId, childProjectParentResourceId);
804+
}
645805
}

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,97 @@ public void HandlesNestedChildren()
4444
Assert.Empty(parentChildLookup[grandChildWithAnnotationsResource.Resource]);
4545
}
4646

47+
[Fact]
48+
public void WithChildRelationshipUsingResourceBuilderCreatesCorrectParentChildLookup()
49+
{
50+
var builder = DistributedApplication.CreateBuilder();
51+
52+
var parentResource = builder.AddContainer("parent", "image");
53+
var child1Resource = builder.AddContainer("child1", "image");
54+
var child2Resource = builder.AddContainer("child2", "image");
55+
56+
parentResource.WithChildRelationship(child1Resource)
57+
.WithChildRelationship(child2Resource);
58+
59+
using var app = builder.Build();
60+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
61+
62+
var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
63+
Assert.Equal(1, parentChildLookup.Count);
64+
65+
Assert.Collection(parentChildLookup[parentResource.Resource],
66+
x => Assert.Equal(child1Resource.Resource, x),
67+
x => Assert.Equal(child2Resource.Resource, x));
68+
}
69+
70+
[Fact]
71+
public void WithChildRelationshipUsingResourceCreatesCorrectParentChildLookup()
72+
{
73+
var builder = DistributedApplication.CreateBuilder();
74+
75+
var parentResource = builder.AddContainer("parent", "image");
76+
var child1Resource = builder.AddContainer("child1", "image");
77+
var child2Resource = builder.AddContainer("child2", "image");
78+
79+
parentResource.WithChildRelationship(child1Resource.Resource)
80+
.WithChildRelationship(child2Resource.Resource);
81+
82+
using var app = builder.Build();
83+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
84+
85+
var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
86+
Assert.Equal(1, parentChildLookup.Count);
87+
88+
Assert.Collection(parentChildLookup[parentResource.Resource],
89+
x => Assert.Equal(child1Resource.Resource, x),
90+
x => Assert.Equal(child2Resource.Resource, x));
91+
}
92+
93+
[Fact]
94+
public void WithChildRelationshipAndWithParentRelationshipWorkTogether()
95+
{
96+
var builder = DistributedApplication.CreateBuilder();
97+
98+
var parentResource = builder.AddContainer("parent", "image");
99+
var child1Resource = builder.AddContainer("child1", "image");
100+
var child2Resource = builder.AddContainer("child2", "image")
101+
.WithParentRelationship(parentResource);
102+
103+
parentResource.WithChildRelationship(child1Resource);
104+
105+
using var app = builder.Build();
106+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
107+
108+
var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
109+
Assert.Equal(1, parentChildLookup.Count);
110+
111+
Assert.Collection(parentChildLookup[parentResource.Resource],
112+
x => Assert.Equal(child1Resource.Resource, x),
113+
x => Assert.Equal(child2Resource.Resource, x));
114+
}
115+
116+
[Fact]
117+
public void WithChildRelationshipHandlesNestedRelationships()
118+
{
119+
var builder = DistributedApplication.CreateBuilder();
120+
121+
var grandParentResource = builder.AddContainer("grandparent", "image");
122+
var parentResource = builder.AddContainer("parent", "image");
123+
var childResource = builder.AddContainer("child", "image");
124+
125+
grandParentResource.WithChildRelationship(parentResource);
126+
parentResource.WithChildRelationship(childResource);
127+
128+
using var app = builder.Build();
129+
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();
130+
131+
var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
132+
Assert.Equal(2, parentChildLookup.Count);
133+
134+
Assert.Single(parentChildLookup[grandParentResource.Resource], parentResource.Resource);
135+
Assert.Single(parentChildLookup[parentResource.Resource], childResource.Resource);
136+
}
137+
47138
private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent
48139
{
49140
public IResource Parent => parent;

0 commit comments

Comments
 (0)