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
56 changes: 26 additions & 30 deletions src/Aspire.Dashboard/Model/ResourceOutgoingPeerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,35 +144,10 @@ private static bool ArePropertyValuesEquivalent(ResourceViewModel resource1, Res

public bool TryResolvePeer(KeyValuePair<string, string>[] 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<string, ResourceViewModel> resources, KeyValuePair<string, string>[] attributes, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch)
internal static bool TryResolvePeerCore(IDictionary<string, ResourceViewModel> resources, KeyValuePair<string, string>[] attributes, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch)
{
var address = OtlpHelpers.GetPeerAddress(attributes);
if (address != null)
Expand Down Expand Up @@ -205,22 +180,43 @@ internal static bool TryResolvePeerNameCore(IDictionary<string, ResourceViewMode
/// <summary>
/// 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.
/// </summary>
private static bool TryMatchAgainstResources(string peerAddress, IDictionary<string, ResourceViewModel> resources, [NotNullWhen(true)] out string? name, [NotNullWhen(true)] out ResourceViewModel? resourceMatch)
{
ResourceViewModel? foundResource = null;

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;
}
else if (!string.Equals(foundResource.Name, resource.Name, StringComparisons.ResourceName))
{
// Multiple different resources match - return false immediately
name = null;
resourceMatch = null;
return false;
}
break; // No need to check other addresses for this resource once we found a match
}
}
}

// Return true only if exactly one resource matched
if (foundResource is not null)
{
name = ResourceViewModel.GetResourceName(foundResource, resources);
resourceMatch = foundResource;
return true;
}

// Return false if no matches found
name = null;
resourceMatch = null;
return false;
Expand Down
113 changes: 112 additions & 1 deletion tests/Aspire.Dashboard.Tests/ResourceOutgoingPeerResolverTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ public void NameAndDisplayNameDifferent_MultipleInstances_ReturnName()

private static bool TryResolvePeerName(IDictionary<string, ResourceViewModel> resources, KeyValuePair<string, string>[] attributes, out string? peerName)
{
return ResourceOutgoingPeerResolver.TryResolvePeerNameCore(resources, attributes, out peerName, out _);
return ResourceOutgoingPeerResolver.TryResolvePeerCore(resources, attributes, out peerName, out _);
}

[Fact]
Expand Down Expand Up @@ -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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<string, ResourceViewModel>
{
["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<ResourceViewModelSubscription> subscribeResult) : IDashboardClient
{
public bool IsEnabled => true;
Expand Down
Loading