diff --git a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs index 60f840df9ee..d1bd69689cf 100644 --- a/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs +++ b/src/Aspire.Hosting/Orchestrator/ApplicationOrchestrator.cs @@ -238,6 +238,11 @@ await _notificationService.PublishUpdateAsync(child, s => s with StopTimeStamp = stopTimeStamp, Properties = s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parentName) }).ConfigureAwait(false); + + // the parent name needs to be an instance name, not the resource name. + // parent the children of the child under the first resource instance. + await SetChildResourceAsync(child, child.GetResolvedResourceNames()[0], state, startTimeStamp, stopTimeStamp) + .ConfigureAwait(false); } } @@ -253,6 +258,8 @@ await _notificationService.PublishUpdateAsync(child, s => s with { Properties = s.Properties.SetResourceProperty(KnownProperties.Resource.ParentName, parentName) }).ConfigureAwait(false); + + await SetExecutableChildResourceAsync(child).ConfigureAwait(false); } } diff --git a/src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs b/src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs index 44f0e17081c..3a782ba9f44 100644 --- a/src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs +++ b/src/Aspire.Hosting/Orchestrator/RelationshipEvaluator.cs @@ -12,23 +12,16 @@ internal static class RelationshipEvaluator { public static ILookup GetParentChildLookup(DistributedApplicationModel model) { - static IResource? SelectParentContainerResource(IResource resource) => resource switch - { - IResourceWithParent rp => SelectParentContainerResource(rp.Parent), - IResource r when r.IsContainer() => r, - _ => null - }; - // parent -> children lookup // Built from IResourceWithParent first, then from annotations. return model.Resources.OfType() - .Select(x => (Child: (IResource)x, Root: SelectParentContainerResource(x.Parent))) - .Where(x => x.Root is not null) + .Select(x => (Child: (IResource)x, Parent: x.Parent)) + .Where(x => x.Parent is not null) .Concat(GetParentChildRelationshipsFromAnnotations(model)) - .ToLookup(x => x.Root!, x => x.Child); + .ToLookup(x => x.Parent!, x => x.Child); } - private static IEnumerable<(IResource Child, IResource? Root)> GetParentChildRelationshipsFromAnnotations(DistributedApplicationModel model) + private static IEnumerable<(IResource Child, IResource Parent)> GetParentChildRelationshipsFromAnnotations(DistributedApplicationModel model) { static bool TryGetParent(IResource resource, [NotNullWhen(true)] out IResource? parent) { @@ -55,14 +48,7 @@ IResource r when TryGetParent(r, out var parent) => parent, ValidateRelationships(result!); - static IResource? SelectRootResource(IResource? resource) => resource switch - { - IResource r when TryGetParent(r, out var parent) => SelectRootResource(parent) ?? parent, - _ => null - }; - - // translate the result to child -> root, which the dashboard expects - return result.Select(x => (x.Child, Root: SelectRootResource(x.Child))); + return result!; } private static void ValidateRelationships((IResource Child, IResource Parent)[] relationships) diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs index 2cfc3708ef2..9569a8f5b2d 100644 --- a/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs +++ b/tests/Aspire.Hosting.Tests/Orchestrator/ApplicationOrchestratorTests.cs @@ -83,6 +83,7 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly() await appOrchestrator.RunApplicationAsync(); string? parentResourceId = null; + string? childResourceId = null; string? childParentResourceId = null; string? child2ParentResourceId = null; string? nestedChildParentResourceId = null; @@ -96,6 +97,7 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly() } else if (item.Resource == child.Resource) { + childResourceId = item.ResourceId; childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString(); } else if (item.Resource == nestedChild.Resource) @@ -121,8 +123,8 @@ public async Task WithParentRelationshipSetsParentPropertyCorrectly() Assert.Equal(parentResourceId, childParentResourceId); Assert.Equal(parentResourceId, child2ParentResourceId); - // Nested child should have parent set to the root parent, not direct parent - Assert.Equal(parentResourceId, nestedChildParentResourceId); + // Nested child should be parented on the direct parent + Assert.Equal(childResourceId, nestedChildParentResourceId); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/Orchestrator/RelationshipEvaluatorTests.cs b/tests/Aspire.Hosting.Tests/Orchestrator/RelationshipEvaluatorTests.cs new file mode 100644 index 00000000000..786a2e42aed --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Orchestrator/RelationshipEvaluatorTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Orchestrator; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Aspire.Hosting.Tests.Orchestrator; + +public class RelationshipEvaluatorTests +{ + [Fact] + public void HandlesNestedChildren() + { + var builder = DistributedApplication.CreateBuilder(); + + var parentResource = builder.AddContainer("parent", "image"); + var childResource = builder.AddResource(new CustomChildResource("child", parentResource.Resource)); + var grandChildResource = builder.AddResource(new CustomChildResource("grandchild", childResource.Resource)); + var greatGrandChildResource = builder.AddResource(new CustomChildResource("greatgrandchild", grandChildResource.Resource)); + + var childWithAnnotationsResource = builder.AddContainer("child-with-annotations", "image") + .WithParentRelationship(parentResource.Resource); + + var grandChildWithAnnotationsResource = builder.AddContainer("grandchild-with-annotations", "image") + .WithParentRelationship(childWithAnnotationsResource.Resource); + + using var app = builder.Build(); + var appModel = app.Services.GetRequiredService(); + + var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel); + Assert.Equal(4, parentChildLookup.Count); + + Assert.Collection(parentChildLookup[parentResource.Resource], + x => Assert.Equal(childResource.Resource, x), + x => Assert.Equal(childWithAnnotationsResource.Resource, x)); + + Assert.Single(parentChildLookup[childResource.Resource], grandChildResource.Resource); + Assert.Single(parentChildLookup[grandChildResource.Resource], greatGrandChildResource.Resource); + + Assert.Empty(parentChildLookup[greatGrandChildResource.Resource]); + + Assert.Single(parentChildLookup[childWithAnnotationsResource.Resource], grandChildWithAnnotationsResource.Resource); + + Assert.Empty(parentChildLookup[grandChildWithAnnotationsResource.Resource]); + } + + private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent + { + public IResource Parent => parent; + } +}