Skip to content

Commit 69fb5a3

Browse files
JamesNKCopilot
andauthored
Match name with ResourceCommandService (#10370)
Co-authored-by: Copilot <[email protected]>
1 parent d04a2d4 commit 69fb5a3

File tree

3 files changed

+112
-2
lines changed

3 files changed

+112
-2
lines changed

src/Aspire.Hosting/ApplicationModel/ResourceCommandService.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@ internal ResourceCommandService(ResourceNotificationService resourceNotification
2525
/// <summary>
2626
/// Execute a command for the specified resource.
2727
/// </summary>
28-
/// <param name="resourceId">The id of the resource.</param>
28+
/// <remarks>
29+
/// <para>
30+
/// A resource id can be either the unique id of the resource or the displayed resource name.
31+
/// </para>
32+
/// <para>
33+
/// Projects, executables and containers typically have a unique id that combines the display name and a unique suffix. For example, a resource named <c>cache</c> could have a resource id of <c>cache-abcdwxyz</c>.
34+
/// This id is used to uniquely identify the resource in the app host.
35+
/// </para>
36+
/// <para>
37+
/// The resource name can be also be used to retrieve the resource state, but it must be unique. If there are multiple resources with the same name, then this method will not return a match.
38+
/// For example, if a resource named <c>cache</c> has multiple replicas, then specifing <c>cache</c> won't return a match.
39+
/// </para>
40+
/// </remarks>
41+
/// <param name="resourceId">The resource id. This id can either exactly match the unique id of the resource or the displayed resource name if the resource name doesn't have duplicates (i.e. replicas).</param>
2942
/// <param name="commandName">The command name.</param>
3043
/// <param name="cancellationToken">The cancellation token.</param>
3144
/// <returns>The <see cref="ExecuteCommandResult" /> indicates command success or failure.</returns>

src/Aspire.Hosting/ApplicationModel/ResourceNotificationService.cs

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,11 +434,25 @@ private async Task<ResourceEvent> WaitForResourceCoreAsync(string resourceName,
434434
/// <summary>
435435
/// Attempts to retrieve the current state of a resource by resourceId.
436436
/// </summary>
437-
/// <param name="resourceId">The resource id.</param>
437+
/// <remarks>
438+
/// <para>
439+
/// A resource id can be either the unique id of the resource or the displayed resource name.
440+
/// </para>
441+
/// <para>
442+
/// Projects, executables and containers typically have a unique id that combines the display name and a unique suffix. For example, a resource named <c>cache</c> could have a resource id of <c>cache-abcdwxyz</c>.
443+
/// This id is used to uniquely identify the resource in the app host.
444+
/// </para>
445+
/// <para>
446+
/// The resource name can be also be used to retrieve the resource state, but it must be unique. If there are multiple resources with the same name, then this method will not return a match.
447+
/// For example, if a resource named <c>cache</c> has multiple replicas, then specifing <c>cache</c> won't return a match.
448+
/// </para>
449+
/// </remarks>
450+
/// <param name="resourceId">The resource id. This id can either exactly match the unique id of the resource or the displayed resource name if the resource name doesn't have duplicates (i.e. replicas).</param>
438451
/// <param name="resourceEvent">When this method returns, contains the <see cref="ResourceEvent"/> for the specified resource id, if found; otherwise, <see langword="null"/>.</param>
439452
/// <returns><see langword="true"/> if specified resource id was found; otherwise, <see langword="false"/>.</returns>
440453
public bool TryGetCurrentState(string resourceId, [NotNullWhen(true)] out ResourceEvent? resourceEvent)
441454
{
455+
// Find exact match.
442456
if (_resourceNotificationStates.TryGetValue(resourceId, out var state))
443457
{
444458
if (state.LastSnapshot is { } snapshot)
@@ -448,6 +462,29 @@ public bool TryGetCurrentState(string resourceId, [NotNullWhen(true)] out Resour
448462
}
449463
}
450464

465+
// Fallback to finding match on resource name. If there are multiple resources with the same name (e.g. replicas) then don't match.
466+
KeyValuePair<string, ResourceNotificationState>? nameMatch = null;
467+
foreach (var matchingResource in _resourceNotificationStates.Where(s => string.Equals(s.Value.Resource.Name, resourceId, StringComparisons.ResourceName)))
468+
{
469+
if (nameMatch == null)
470+
{
471+
nameMatch = matchingResource;
472+
}
473+
else
474+
{
475+
// Second match found, so we can't return a match based on the name.
476+
nameMatch = null;
477+
break;
478+
}
479+
}
480+
481+
if (nameMatch is { } m && m.Value.LastSnapshot != null)
482+
{
483+
resourceEvent = new ResourceEvent(m.Value.Resource, m.Key, m.Value.LastSnapshot);
484+
return true;
485+
}
486+
487+
// No match.
451488
resourceEvent = null;
452489
return false;
453490
}

tests/Aspire.Hosting.Tests/ResourceCommandServiceTests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,29 @@ public async Task ExecuteCommandAsync_NoMatchingResource_Failure()
3030
Assert.Equal("Resource 'NotFoundResourceId' not found.", result.ErrorMessage);
3131
}
3232

33+
[Fact]
34+
public async Task ExecuteCommandAsync_ResourceNameMultipleMatches_Failure()
35+
{
36+
// Arrange
37+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
38+
39+
var custom = builder.AddResource(new CustomResource("myResource"));
40+
custom.WithAnnotation(new DcpInstancesAnnotation([
41+
new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0),
42+
new DcpInstance("myResource-efghwxyz", "efghwxyz", 1)
43+
]));
44+
45+
var app = builder.Build();
46+
await app.StartAsync();
47+
48+
// Act
49+
var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "NotFound");
50+
51+
// Assert
52+
Assert.False(result.Success);
53+
Assert.Equal("Resource 'myResource' not found.", result.ErrorMessage);
54+
}
55+
3356
[Fact]
3457
public async Task ExecuteCommandAsync_NoMatchingCommand_Failure()
3558
{
@@ -49,6 +72,43 @@ public async Task ExecuteCommandAsync_NoMatchingCommand_Failure()
4972
Assert.Equal("Command 'NotFound' not available for resource 'myResource'.", result.ErrorMessage);
5073
}
5174

75+
[Fact]
76+
public async Task ExecuteCommandAsync_ResourceNameMultipleMatches_Success()
77+
{
78+
// Arrange
79+
using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
80+
81+
var commandResourcesChannel = Channel.CreateUnbounded<string>();
82+
83+
var custom = builder.AddResource(new CustomResource("myResource"));
84+
custom.WithAnnotation(new DcpInstancesAnnotation([
85+
new DcpInstance("myResource-abcdwxyz", "abcdwxyz", 0)
86+
]));
87+
custom.WithCommand(name: "mycommand",
88+
displayName: "My command",
89+
executeCommand: async e =>
90+
{
91+
await commandResourcesChannel.Writer.WriteAsync(e.ResourceName);
92+
return new ExecuteCommandResult { Success = true };
93+
});
94+
95+
var app = builder.Build();
96+
await app.StartAsync();
97+
98+
// Act
99+
var result = await app.ResourceCommands.ExecuteCommandAsync("myResource", "mycommand");
100+
commandResourcesChannel.Writer.Complete();
101+
102+
// Assert
103+
Assert.True(result.Success);
104+
105+
var resolvedResourceNames = custom.Resource.GetResolvedResourceNames().ToList();
106+
await foreach (var resourceName in commandResourcesChannel.Reader.ReadAllAsync().DefaultTimeout())
107+
{
108+
Assert.True(resolvedResourceNames.Remove(resourceName));
109+
}
110+
}
111+
52112
[Fact]
53113
[QuarantinedTest("https://github.com/dotnet/aspire/issues/9832")]
54114
public async Task ExecuteCommandAsync_HasReplicas_Success_CalledPerReplica()

0 commit comments

Comments
 (0)