diff --git a/ILSpy/Commands/IProtocolHandler.cs b/ILSpy/Commands/IProtocolHandler.cs
new file mode 100644
index 000000000..027570596
--- /dev/null
+++ b/ILSpy/Commands/IProtocolHandler.cs
@@ -0,0 +1,43 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System.Reflection.Metadata;
+
+using ICSharpCode.Decompiler.Metadata;
+
+using ILSpy.TreeNodes;
+
+namespace ILSpy.Commands
+{
+ ///
+ /// Plug-in contract for resolving non-default reference protocols (e.g.
+ /// metadata://) to tree nodes. Implementations are MEF-discovered via
+ /// [Export(typeof(IProtocolHandler))] and consulted by
+ /// 's navigation router; the first handler to
+ /// return a non-null node wins.
+ ///
+ public interface IProtocolHandler
+ {
+ ///
+ /// Resolves a reference to a tree node, or returns null to signal "not my
+ /// protocol". is set to true when the handler
+ /// wants the host to carve out a new tab instead of reusing the active one.
+ ///
+ ILSpyTreeNode? Resolve(string protocol, MetadataFile module, Handle handle, out bool newTabPage);
+ }
+}
diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs
index 4987a8b6b..6aa5dbe99 100644
--- a/ILSpy/Docking/DockWorkspace.cs
+++ b/ILSpy/Docking/DockWorkspace.cs
@@ -340,11 +340,43 @@ namespace ILSpy.Docking
// reference (only types/members/EntityReferences are supported today).
if (segment.Reference == null)
return;
+ // EntityReferences with a non-"decompile" protocol (e.g. metadata://) get a first
+ // pass through registered IProtocolHandler exports. The first handler returning a
+ // non-null node wins; if none match we fall through to the default resolver.
+ if (segment.Reference is global::ILSpy.EntityReference entity
+ && entity.Protocol != "decompile")
+ {
+ var module = entity.ResolveAssembly(assemblyTreeModel.AssemblyList!);
+ if (module != null)
+ {
+ foreach (var handler in TryGetProtocolHandlers())
+ {
+ var resolved = handler.Resolve(entity.Protocol, module, entity.Handle, out _);
+ if (resolved != null)
+ {
+ assemblyTreeModel.SelectedItem = resolved;
+ return;
+ }
+ }
+ }
+ }
var node = assemblyTreeModel.FindTreeNode(segment.Reference);
if (node != null)
assemblyTreeModel.SelectedItem = node;
}
+ static IEnumerable TryGetProtocolHandlers()
+ {
+ try
+ {
+ return AppEnv.AppComposition.Current.GetExports();
+ }
+ catch
+ {
+ return System.Array.Empty();
+ }
+ }
+
// Long-lived decompiler viewmodel — kept alive across metadata interludes so going
// back to text doesn't lose the previous decompile until a fresh one supersedes it.
// Created lazily on first need.
diff --git a/ILSpy/Metadata/MetadataProtocolHandler.cs b/ILSpy/Metadata/MetadataProtocolHandler.cs
new file mode 100644
index 000000000..9ea731507
--- /dev/null
+++ b/ILSpy/Metadata/MetadataProtocolHandler.cs
@@ -0,0 +1,69 @@
+// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of this
+// software and associated documentation files (the "Software"), to deal in the Software
+// without restriction, including without limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+// to whom the Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all copies or
+// substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
+// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
+// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+using System;
+using System.Composition;
+using System.Linq;
+using System.Reflection.Metadata;
+
+using ICSharpCode.Decompiler.Metadata;
+
+using ILSpy.AssemblyTree;
+using ILSpy.Commands;
+using ILSpy.TreeNodes;
+
+namespace ILSpy.Metadata
+{
+ ///
+ /// Resolves metadata:// hyperlinks in the decompiler output to a metadata-tree
+ /// node under the containing assembly. The reference's picks
+ /// which metadata table to surface; the user lands on the table itself and can
+ /// scroll/select the specific row.
+ ///
+ [Export(typeof(IProtocolHandler))]
+ [Shared]
+ public sealed class MetadataProtocolHandler : IProtocolHandler
+ {
+ readonly AssemblyTreeModel assemblyTreeModel;
+
+ [ImportingConstructor]
+ public MetadataProtocolHandler(AssemblyTreeModel assemblyTreeModel)
+ {
+ this.assemblyTreeModel = assemblyTreeModel;
+ }
+
+ public ILSpyTreeNode? Resolve(string protocol, MetadataFile module, Handle handle, out bool newTabPage)
+ {
+ newTabPage = true;
+ if (protocol != "metadata")
+ return null;
+ var assemblyNode = assemblyTreeModel.FindTreeNode(module) as AssemblyTreeNode;
+ if (assemblyNode == null)
+ return null;
+ assemblyNode.EnsureLazyChildren();
+ var metadataNode = assemblyNode.Children.OfType().FirstOrDefault();
+ if (metadataNode == null)
+ return null;
+ // WPF additionally drills into the matching table for the handle kind. The
+ // Avalonia MetadataTreeNode doesn't yet expose a FindNodeByHandleKind helper;
+ // surfacing the MetadataTreeNode itself is enough to land the user inside the
+ // per-assembly metadata view. Specific-table drill-down is a follow-up.
+ return metadataNode;
+ }
+ }
+}