Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions src/Aspire.Hosting/ResourceBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,70 @@ public static IResourceBuilder<T> WithParentRelationship<T>(
return builder.WithRelationship(parent, KnownRelationshipTypes.Parent);
}

/// <summary>
/// Adds a <see cref="ResourceRelationshipAnnotation"/> to the resource annotations to add a parent-child relationship.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="child">The child of <paramref name="builder"/>.</param>
/// <returns>A resource builder.</returns>
/// <remarks>
/// <para>
/// The <c>WithChildRelationship</c> method is used to add child relationships to the resource. Relationships are used to link
/// resources together in UI.
/// </para>
/// <example>
/// This example shows adding a relationship between two resources.
/// <code lang="C#">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var parameter = builder.AddParameter("parameter");
///
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
/// .WithChildRelationship(parameter);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<T> WithChildRelationship<T>(
this IResourceBuilder<T> builder,
IResourceBuilder<IResource> child) where T : IResource
{
child.WithRelationship(builder.Resource, KnownRelationshipTypes.Parent);
return builder;
}

/// <summary>
/// Adds a <see cref="ResourceRelationshipAnnotation"/> to the resource annotations to add a parent-child relationship.
/// </summary>
/// <typeparam name="T">The type of the resource.</typeparam>
/// <param name="builder">The resource builder.</param>
/// <param name="child">The child of <paramref name="builder"/>.</param>
/// <returns>A resource builder.</returns>
/// <remarks>
/// <para>
/// The <c>WithChildRelationship</c> method is used to add child relationships to the resource. Relationships are used to link
/// resources together in UI.
/// </para>
/// <example>
/// This example shows adding a relationship between two resources.
/// <code lang="C#">
/// var builder = DistributedApplication.CreateBuilder(args);
///
/// var parameter = builder.AddParameter("parameter");
///
/// var backend = builder.AddProject&lt;Projects.Backend&gt;("backend");
/// .WithChildRelationship(parameter.Resource);
/// </code>
/// </example>
/// </remarks>
public static IResourceBuilder<T> WithChildRelationship<T>(
this IResourceBuilder<T> builder,
IResource child) where T : IResource
{
var childBuilder = builder.ApplicationBuilder.CreateResourceBuilder(child);
return builder.WithChildRelationship(childBuilder);
}

/// <summary>
/// Specifies the icon to use when displaying the resource in the dashboard.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,10 +350,10 @@ public async Task GrandChildResourceWithConnectionString()

var parentResource = builder.AddResource(new ParentResourceWithConnectionString("parent"));
var childResource = builder.AddResource(
new ChildResourceWithConnectionString("child", new Dictionary<string, string> { {"Namespace", "ns"} }, parentResource.Resource)
new ChildResourceWithConnectionString("child", new Dictionary<string, string> { { "Namespace", "ns" } }, parentResource.Resource)
);
var grandChildResource = builder.AddResource(
new ChildResourceWithConnectionString("grand-child", new Dictionary<string, string> { {"Database", "db"} }, childResource.Resource)
new ChildResourceWithConnectionString("grand-child", new Dictionary<string, string> { { "Database", "db" } }, childResource.Resource)
);

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

// Parent should have the new state
Assert.Equal(KnownResourceStates.FailedToStart, parentState);

// Child container (has own lifetime) should NOT receive parent state
Assert.NotEqual(KnownResourceStates.Running, childContainerState);

// Custom child (does not have own lifetime) SHOULD receive parent state
Assert.Equal(KnownResourceStates.FailedToStart, customChildState);
}
Expand Down Expand Up @@ -635,11 +635,171 @@ await events.PublishAsync(new OnResourceChangedContext(

// Parent should have the new state
Assert.Equal(KnownResourceStates.FailedToStart, parentState);

// Child project (has own lifetime) should NOT receive parent state
Assert.NotEqual(KnownResourceStates.Running, childProjectState);

// Custom child (does not have own lifetime) SHOULD receive parent state
Assert.Equal(KnownResourceStates.FailedToStart, customChildState);
}

[Fact]
public async Task WithChildRelationshipUsingResourceBuilderSetsParentPropertyCorrectly()
{
var builder = DistributedApplication.CreateBuilder();

var parent = builder.AddContainer("parent", "image");
var child = builder.AddContainer("child", "image");
var child2 = builder.AddContainer("child2", "image");

parent.WithChildRelationship(child)
.WithChildRelationship(child2);

using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var events = new DcpExecutorEvents();
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();

var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
await appOrchestrator.RunApplicationAsync();

string? parentResourceId = null;
string? childParentResourceId = null;
string? child2ParentResourceId = null;
var watchResourceTask = Task.Run(async () =>
{
await foreach (var item in resourceNotificationService.WatchAsync())
{
if (item.Resource == parent.Resource)
{
parentResourceId = item.ResourceId;
}
else if (item.Resource == child.Resource)
{
childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
}
else if (item.Resource == child2.Resource)
{
child2ParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
}

if (parentResourceId != null && childParentResourceId != null && child2ParentResourceId != null)
{
return;
}
}
});

await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));

await watchResourceTask.DefaultTimeout();

Assert.Equal(parentResourceId, childParentResourceId);
Assert.Equal(parentResourceId, child2ParentResourceId);
}

[Fact]
public async Task WithChildRelationshipUsingResourceSetsParentPropertyCorrectly()
{
var builder = DistributedApplication.CreateBuilder();

var parent = builder.AddContainer("parent", "image");
var child = builder.AddContainer("child", "image");
var child2 = builder.AddContainer("child2", "image");

parent.WithChildRelationship(child.Resource)
.WithChildRelationship(child2.Resource);

using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var events = new DcpExecutorEvents();
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();

var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
await appOrchestrator.RunApplicationAsync();

string? parentResourceId = null;
string? childParentResourceId = null;
string? child2ParentResourceId = null;
var watchResourceTask = Task.Run(async () =>
{
await foreach (var item in resourceNotificationService.WatchAsync())
{
if (item.Resource == parent.Resource)
{
parentResourceId = item.ResourceId;
}
else if (item.Resource == child.Resource)
{
childParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
}
else if (item.Resource == child2.Resource)
{
child2ParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
}

if (parentResourceId != null && childParentResourceId != null && child2ParentResourceId != null)
{
return;
}
}
});

await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));

await watchResourceTask.DefaultTimeout();

Assert.Equal(parentResourceId, childParentResourceId);
Assert.Equal(parentResourceId, child2ParentResourceId);
}

[Fact]
public async Task WithChildRelationshipWorksWithProjects()
{
var builder = DistributedApplication.CreateBuilder();

var parentProject = builder.AddProject<ProjectA>("parent-project");
var childProject = builder.AddProject<ProjectB>("child-project");

parentProject.WithChildRelationship(childProject);

using var app = builder.Build();
var distributedAppModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var events = new DcpExecutorEvents();
var resourceNotificationService = ResourceNotificationServiceTestHelpers.Create();

var appOrchestrator = CreateOrchestrator(distributedAppModel, notificationService: resourceNotificationService, dcpEvents: events);
await appOrchestrator.RunApplicationAsync();

string? parentProjectResourceId = null;
string? childProjectParentResourceId = null;
var watchResourceTask = Task.Run(async () =>
{
await foreach (var item in resourceNotificationService.WatchAsync())
{
if (item.Resource == parentProject.Resource)
{
parentProjectResourceId = item.ResourceId;
}
else if (item.Resource == childProject.Resource)
{
childProjectParentResourceId = item.Snapshot.Properties.SingleOrDefault(p => p.Name == KnownProperties.Resource.ParentName)?.Value?.ToString();
}

if (parentProjectResourceId != null && childProjectParentResourceId != null)
{
return;
}
}
});

await events.PublishAsync(new OnResourcesPreparedContext(CancellationToken.None));

await watchResourceTask.DefaultTimeout();

Assert.Equal(parentProjectResourceId, childProjectParentResourceId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,97 @@ public void HandlesNestedChildren()
Assert.Empty(parentChildLookup[grandChildWithAnnotationsResource.Resource]);
}

[Fact]
public void WithChildRelationshipUsingResourceBuilderCreatesCorrectParentChildLookup()
{
var builder = DistributedApplication.CreateBuilder();

var parentResource = builder.AddContainer("parent", "image");
var child1Resource = builder.AddContainer("child1", "image");
var child2Resource = builder.AddContainer("child2", "image");

parentResource.WithChildRelationship(child1Resource)
.WithChildRelationship(child2Resource);

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
Assert.Equal(1, parentChildLookup.Count);

Assert.Collection(parentChildLookup[parentResource.Resource],
x => Assert.Equal(child1Resource.Resource, x),
x => Assert.Equal(child2Resource.Resource, x));
}

[Fact]
public void WithChildRelationshipUsingResourceCreatesCorrectParentChildLookup()
{
var builder = DistributedApplication.CreateBuilder();

var parentResource = builder.AddContainer("parent", "image");
var child1Resource = builder.AddContainer("child1", "image");
var child2Resource = builder.AddContainer("child2", "image");

parentResource.WithChildRelationship(child1Resource.Resource)
.WithChildRelationship(child2Resource.Resource);

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
Assert.Equal(1, parentChildLookup.Count);

Assert.Collection(parentChildLookup[parentResource.Resource],
x => Assert.Equal(child1Resource.Resource, x),
x => Assert.Equal(child2Resource.Resource, x));
}

[Fact]
public void WithChildRelationshipAndWithParentRelationshipWorkTogether()
{
var builder = DistributedApplication.CreateBuilder();

var parentResource = builder.AddContainer("parent", "image");
var child1Resource = builder.AddContainer("child1", "image");
var child2Resource = builder.AddContainer("child2", "image")
.WithParentRelationship(parentResource);

parentResource.WithChildRelationship(child1Resource);

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
Assert.Equal(1, parentChildLookup.Count);

Assert.Collection(parentChildLookup[parentResource.Resource],
x => Assert.Equal(child1Resource.Resource, x),
x => Assert.Equal(child2Resource.Resource, x));
}

[Fact]
public void WithChildRelationshipHandlesNestedRelationships()
{
var builder = DistributedApplication.CreateBuilder();

var grandParentResource = builder.AddContainer("grandparent", "image");
var parentResource = builder.AddContainer("parent", "image");
var childResource = builder.AddContainer("child", "image");

grandParentResource.WithChildRelationship(parentResource);
parentResource.WithChildRelationship(childResource);

using var app = builder.Build();
var appModel = app.Services.GetRequiredService<DistributedApplicationModel>();

var parentChildLookup = RelationshipEvaluator.GetParentChildLookup(appModel);
Assert.Equal(2, parentChildLookup.Count);

Assert.Single(parentChildLookup[grandParentResource.Resource], parentResource.Resource);
Assert.Single(parentChildLookup[parentResource.Resource], childResource.Resource);
}

private sealed class CustomChildResource(string name, IResource parent) : Resource(name), IResourceWithParent
{
public IResource Parent => parent;
Expand Down