From 1673909da134614a1f1374602ed242ec26ece46c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?S=C3=A9bastien=20Ros?=
<1165805+sebastienros@users.noreply.github.com>
Date: Fri, 21 Aug 2026 14:16:30 -0700
Subject: [PATCH] Fix dashboard graph multi-path icons
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../ResourceGraph/ResourceGraphMapper.cs | 19 +++++++++++---
.../Model/ResourceGraphMapperTests.cs | 26 +++++++++++++++++++
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs b/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs
index 501afdf034d..ccc40dfce33 100644
--- a/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs
+++ b/src/Aspire.Dashboard/Model/ResourceGraph/ResourceGraphMapper.cs
@@ -86,8 +86,21 @@ private static string ResolvedEndpointText(DisplayedUrl? endpoint)
public static string GetIconPathData(Icon icon)
{
- var p = icon.Content;
- var e = XElement.Parse(p);
- return e.Attribute("d")!.Value;
+ // Fluent UI icon content is an SVG fragment. Most icons contain one path:
+ //
+ // Some icons, such as DocumentMultiple, contain sibling paths:
+ //
+ // Wrap the fragment so XML parsing accepts both shapes, then combine the path data into one compound SVG path.
+ var iconContent = XElement.Parse($"");
+ var pathData = iconContent.Elements()
+ .Select(e => e.Attribute("d")?.Value ?? throw new InvalidOperationException($"Icon '{icon.Name}' contains an element without path data."))
+ .ToArray();
+
+ if (pathData.Length == 0)
+ {
+ throw new InvalidOperationException($"Icon '{icon.Name}' doesn't contain path data.");
+ }
+
+ return string.Join(' ', pathData);
}
}
diff --git a/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs b/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs
index ab705fabbce..b89dff237ee 100644
--- a/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs
+++ b/tests/Aspire.Dashboard.Tests/Model/ResourceGraphMapperTests.cs
@@ -2,12 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Collections.Immutable;
+using System.Xml.Linq;
using Aspire.Dashboard.Model;
using Aspire.Dashboard.Model.ResourceGraph;
using Aspire.Dashboard.Resources;
using Aspire.Tests.Shared.DashboardModel;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;
+using Icons = Microsoft.FluentUI.AspNetCore.Components.Icons;
namespace Aspire.Dashboard.Tests.Model;
@@ -156,4 +158,28 @@ public void MapResource_ReferenceToResourceExcludedFromGraph_Ignored()
Assert.Empty(dto.ReferencedNames);
}
+
+ [Fact]
+ public void GetIconPathData_SinglePath_ReturnsPathData()
+ {
+ var icon = new Icons.Filled.Size24.Box();
+ var expectedPathData = XElement.Parse(icon.Content).Attribute("d")!.Value;
+
+ var pathData = ResourceGraphMapper.GetIconPathData(icon);
+
+ Assert.Equal(expectedPathData, pathData);
+ }
+
+ [Fact]
+ public void GetIconPathData_MultiplePaths_ReturnsCombinedPathData()
+ {
+ var icon = new Icons.Filled.Size24.DocumentMultiple();
+ var iconContent = XElement.Parse($"");
+ var expectedPaths = iconContent.Elements().Select(e => e.Attribute("d")!.Value).ToArray();
+ Assert.Equal(3, expectedPaths.Length);
+
+ var pathData = ResourceGraphMapper.GetIconPathData(icon);
+
+ Assert.Equal(string.Join(' ', expectedPaths), pathData);
+ }
}