From 080723ab432a2df97e0d28e55b0ecbefac585c42 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 06:29:46 +0000 Subject: [PATCH 1/5] Initial plan From 18b403d57bcbf83e879dbccfd577a562339afa70 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 06:44:05 +0000 Subject: [PATCH 2/5] Update TryMatchAgainstResources to return false for multiple matches and add comprehensive tests Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../Model/ResourceOutgoingPeerResolver.cs | 22 +++- .../ResourceOutgoingPeerResolverTests.cs | 111 ++++++++++++++++++ 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs index 15b9401fef0..3216d646640 100644 --- a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs +++ b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs @@ -205,22 +205,38 @@ internal static bool TryResolvePeerNameCore(IDictionary /// Checks if a transformed peer address matches any of the resource addresses using their cached addresses. /// Applies the same transformations to resource addresses for consistent matching. + /// Returns true only if exactly one resource matches; false if no matches or multiple matches are found. /// private static bool TryMatchAgainstResources(string peerAddress, IDictionary resources, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch) { + ResourceViewModel? foundResource = null; + var matchCount = 0; + foreach (var (_, resource) in resources) { foreach (var resourceAddress in resource.CachedAddresses) { if (DoesAddressMatch(resourceAddress, peerAddress)) { - name = ResourceViewModel.GetResourceName(resource, resources); - resourceMatch = resource; - return true; + if (foundResource is null) + { + foundResource = resource; + } + matchCount++; + break; // No need to check other addresses for this resource once we found a match } } } + // Return true only if exactly one resource matched + if (matchCount == 1 && foundResource is not null) + { + name = ResourceViewModel.GetResourceName(foundResource, resources); + resourceMatch = foundResource; + return true; + } + + // Return false if no matches or multiple matches found name = null; resourceMatch = null; return false; diff --git a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs index 32fe3c48a5a..e52204d4945 100644 --- a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs +++ b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs @@ -357,6 +357,117 @@ private static ResourceViewModel CreateResourceWithParameterValue(string name, s properties: properties); } + [Fact] + public void MultipleResourcesMatch_SqlServerAddresses_ReturnsFalse() + { + // Arrange - Multiple SQL Server resources with same address + var resources = new Dictionary + { + ["sqlserver1"] = CreateResource("sqlserver1", "localhost", 1433), + ["sqlserver2"] = CreateResource("sqlserver2", "localhost", 1433) + }; + + // Act & Assert - Both resources would match "localhost:1433" + // so this should return false (ambiguous match) + Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:1433")], out var name)); + Assert.Null(name); + } + + [Fact] + public void MultipleResourcesMatch_RedisAddresses_ReturnsFalse() + { + // Arrange - Multiple Redis resources with equivalent addresses + var resources = new Dictionary + { + ["redis-cache"] = CreateResource("redis-cache", "localhost", 6379), + ["redis-session"] = CreateResource("redis-session", "localhost", 6379) + }; + + // Act & Assert - Both resources would match "localhost:6379" + // so this should return false (ambiguous match) + Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:6379")], out var name)); + Assert.Null(name); + } + + [Fact] + public void MultipleResourcesMatch_SqlServerCommaFormat_ReturnsFalse() + { + // Arrange - Multiple SQL Server resources where comma format would match both + var resources = new Dictionary + { + ["sqldb1"] = CreateResource("sqldb1", "localhost", 1433), + ["sqldb2"] = CreateResource("sqldb2", "localhost", 1433) + }; + + // Act & Assert - SQL Server comma format "localhost,1433" should match both resources + // so this should return false (ambiguous match) + Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost,1433")], out var name)); + Assert.Null(name); + } + + [Fact] + public void MultipleResourcesMatch_MixedPortFormats_ReturnsFalse() + { + // Arrange - Resources with same logical address but different port formats + var resources = new Dictionary + { + ["db-primary"] = CreateResource("db-primary", "dbserver", 5432), + ["db-replica"] = CreateResource("db-replica", "dbserver", 5432) + }; + + // Act & Assert - Should be ambiguous since both resources have same address + Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("server.address", "dbserver"), KeyValuePair.Create("server.port", "5432")], out var name)); + Assert.Null(name); + } + + [Fact] + public void MultipleResourcesMatch_AddressTransformation_ReturnsFalse() + { + // Arrange - Multiple resources with exact same address (not just after transformation) + var resources = new Dictionary + { + ["web-frontend"] = CreateResource("web-frontend", "localhost", 8080), + ["web-backend"] = CreateResource("web-backend", "localhost", 8080) + }; + + // Act & Assert - Both resources have identical cached address "localhost:8080" + // so this should return false (ambiguous match) + Assert.False(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "localhost:8080")], out var name)); + Assert.Null(name); + } + + [Fact] + public void MultipleResourcesMatch_ViaTransformation_ReturnsFirstMatch() + { + // Arrange - Resources that become ambiguous after address transformation + // Note: This test documents current behavior where transformation order matters + var resources = new Dictionary + { + ["sql-primary"] = CreateResource("sql-primary", "localhost", 1433), + ["sql-replica"] = CreateResource("sql-replica", "127.0.0.1", 1433) + }; + + // Act & Assert - Due to transformation order, this currently finds sql-replica first + // before the transformation that would make sql-primary match as well + Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "127.0.0.1:1433")], out var name)); + Assert.Equal("sql-replica", name); + } + + [Fact] + public void SingleResourceAfterTransformation_ReturnsTrue() + { + // Arrange - Only one resource that matches after address transformation + var resources = new Dictionary + { + ["unique-service"] = CreateResource("unique-service", "localhost", 8080), + ["other-service"] = CreateResource("other-service", "remotehost", 9090) + }; + + // Act & Assert - Only the first resource should match "127.0.0.1:8080" after transformation + Assert.True(TryResolvePeerName(resources, [KeyValuePair.Create("peer.service", "127.0.0.1:8080")], out var name)); + Assert.Equal("unique-service", name); + } + private sealed class MockDashboardClient(Task subscribeResult) : IDashboardClient { public bool IsEnabled => true; From b2cb3684cb185d6e39f7b9297bc2d19c98f7d264 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 06:52:39 +0000 Subject: [PATCH 3/5] Simplify TryMatchAgainstResources to use foundResource instead of matchCount Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../Model/ResourceOutgoingPeerResolver.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs index 3216d646640..69110b291b1 100644 --- a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs +++ b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs @@ -210,7 +210,6 @@ internal static bool TryResolvePeerNameCore(IDictionary resources, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch) { ResourceViewModel? foundResource = null; - var matchCount = 0; foreach (var (_, resource) in resources) { @@ -222,21 +221,27 @@ private static bool TryMatchAgainstResources(string peerAddress, IDictionary Date: Tue, 15 Jul 2025 07:01:11 +0000 Subject: [PATCH 4/5] Remove duplicated TryResolvePeerNameCore method and use TryResolvePeerCore instead Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../Model/ResourceOutgoingPeerResolver.cs | 29 ++----------------- .../ResourceOutgoingPeerResolverTests.cs | 2 +- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs index 69110b291b1..773ec210839 100644 --- a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs +++ b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs @@ -144,35 +144,10 @@ private static bool ArePropertyValuesEquivalent(ResourceViewModel resource1, Res public bool TryResolvePeer(KeyValuePair[] attributes, out string? name, out ResourceViewModel? matchedResource) { - var address = OtlpHelpers.GetPeerAddress(attributes); - if (address != null) - { - // Apply transformers to the peer address cumulatively - var transformedAddress = address; - - // First check exact match - if (TryMatchAgainstResources(transformedAddress, _resourceByName, out name, out matchedResource)) - { - return true; - } - - // Then apply each transformer cumulatively and check - foreach (var transformer in s_addressTransformers) - { - transformedAddress = transformer(transformedAddress); - if (TryMatchAgainstResources(transformedAddress, _resourceByName, out name, out matchedResource)) - { - return true; - } - } - } - - name = null; - matchedResource = null; - return false; + return TryResolvePeerCore(_resourceByName, attributes, out name, out matchedResource); } - internal static bool TryResolvePeerNameCore(IDictionary resources, KeyValuePair[] attributes, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch) + internal static bool TryResolvePeerCore(IDictionary resources, KeyValuePair[] attributes, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch) { var address = OtlpHelpers.GetPeerAddress(attributes); if (address != null) diff --git a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs index e52204d4945..fc6f851e93f 100644 --- a/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs +++ b/tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs @@ -217,7 +217,7 @@ public void NameAndDisplayNameDifferent_MultipleInstances_ReturnName() private static bool TryResolvePeerName(IDictionary resources, KeyValuePair[] attributes, out string? peerName) { - return ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, attributes, out peerName, out _); + return ResourceOutgoingPeerResolver.TryResolvePeerCore(resources, attributes, out peerName, out _); } [Fact] From 511781fd2a08c0801b785a7deaebdbd51424639f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 07:19:37 +0000 Subject: [PATCH 5/5] Use Name property comparison instead of reference equality for resource matching Co-authored-by: JamesNK <303201+JamesNK@users.noreply.github.com> --- src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs index 773ec210839..500036fc27c 100644 --- a/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs +++ b/src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs @@ -196,7 +196,7 @@ private static bool TryMatchAgainstResources(string peerAddress, IDictionary