diff --git a/ILSpy.Tests/EntityReferenceResolveTests.cs b/ILSpy.Tests/EntityReferenceResolveTests.cs
new file mode 100644
index 000000000..b5af5d67b
--- /dev/null
+++ b/ILSpy.Tests/EntityReferenceResolveTests.cs
@@ -0,0 +1,66 @@
+// 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.Threading.Tasks;
+
+using Avalonia.Headless.NUnit;
+
+using AwesomeAssertions;
+
+using ICSharpCode.Decompiler.TypeSystem;
+
+using ILSpy;
+using ILSpy.TreeNodes;
+
+using NUnit.Framework;
+
+namespace ICSharpCode.ILSpy.Tests;
+
+///
+/// EntityReference.Resolve turns an unresolved (module, handle) reference into the live IEntity,
+/// the shared resolution used by code-view hover navigation and the assembly-tree node finder.
+///
+[TestFixture]
+public class EntityReferenceResolveTests
+{
+ [AvaloniaTest]
+ public async Task Resolve_Returns_The_Entity_For_A_Module_Handle_Reference()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ var typeNode = vm.AssemblyTreeModel.FindNode(
+ "System.Linq", "System.Linq", "System.Linq.Enumerable");
+ var entity = (IEntity)typeNode.Member!;
+ var module = entity.ParentModule!.MetadataFile!;
+
+ var reference = new EntityReference(module, entity.MetadataToken);
+ var resolved = reference.Resolve(vm.AssemblyTreeModel.AssemblyList!);
+
+ resolved.Should().NotBeNull("the (module, handle) pair resolves back to its entity");
+ resolved!.FullName.Should().Be(entity.FullName);
+ }
+
+ [AvaloniaTest]
+ public async Task Resolve_Returns_Null_For_An_Unknown_Module()
+ {
+ var (_, vm) = await TestHarness.BootAsync();
+ // A module path that isn't in the list can't be resolved -> null, not a throw.
+ var reference = new EntityReference("X:/does/not/exist.dll",
+ System.Reflection.Metadata.Ecma335.MetadataTokens.TypeDefinitionHandle(2));
+ reference.Resolve(vm.AssemblyTreeModel.AssemblyList!).Should().BeNull();
+ }
+}
diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
index c516de1a5..950074275 100644
--- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs
+++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs
@@ -598,14 +598,7 @@ namespace ILSpy.AssemblyTree
switch (reference)
{
case EntityReference unresolved:
- var module = unresolved.ResolveAssembly(AssemblyList!);
- if (module == null)
- return null;
- var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolved.Handle));
- if (token == null)
- return null;
- var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached);
- return FindTreeNode(typeSystem.MainModule.ResolveEntity(token.Value));
+ return FindTreeNode(unresolved.Resolve(AssemblyList!));
case ITypeDefinition type:
return FindTypeNode(assemblyListTreeNode, type);
diff --git a/ILSpy/EntityReference.cs b/ILSpy/EntityReference.cs
index 2303ee067..4da4f1516 100644
--- a/ILSpy/EntityReference.cs
+++ b/ILSpy/EntityReference.cs
@@ -18,8 +18,10 @@
using System.Diagnostics;
using System.Reflection.Metadata;
+using System.Reflection.Metadata.Ecma335;
using ICSharpCode.Decompiler.Metadata;
+using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX;
namespace ILSpy
@@ -53,5 +55,24 @@ namespace ILSpy
public MetadataFile? ResolveAssembly(AssemblyList context)
=> metadataFile ?? context.FindAssembly(Module)?.GetMetadataFileOrNull();
+
+ ///
+ /// Resolves this reference to a live within ,
+ /// or null when the module, the handle, or the entity can't be resolved. Builds a fresh
+ /// uncached per call -- the caller is resolving a single
+ /// handle, not walking the whole module.
+ ///
+ public IEntity? Resolve(AssemblyList context)
+ {
+ var module = ResolveAssembly(context);
+ if (module == null)
+ return null;
+ var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(Handle));
+ if (token == null)
+ return null;
+ var typeSystem = new DecompilerTypeSystem(module, module.GetAssemblyResolver(),
+ TypeSystemOptions.Default | TypeSystemOptions.Uncached);
+ return typeSystem.MainModule.ResolveEntity(token.Value);
+ }
}
}
diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs
index c08d3c858..10108869d 100644
--- a/ILSpy/TextView/DecompilerTextView.axaml.cs
+++ b/ILSpy/TextView/DecompilerTextView.axaml.cs
@@ -981,16 +981,7 @@ namespace ILSpy.TextView
if (DataContext is not DecompilerTabPageModel m || m.CurrentNode is not { } node)
return null;
var list = node.AncestorsAndSelf().OfType().FirstOrDefault()?.AssemblyList;
- if (list == null)
- return null;
- var resolvedModule = unresolved.ResolveAssembly(list);
- if (resolvedModule == null)
- return null;
- var token = MetadataTokenHelpers.TryAsEntityHandle(MetadataTokens.GetToken(unresolved.Handle));
- if (token == null)
- return null;
- var typeSystem = new DecompilerTypeSystem(resolvedModule, resolvedModule.GetAssemblyResolver(), TypeSystemOptions.Default | TypeSystemOptions.Uncached);
- return typeSystem.MainModule.ResolveEntity(token.Value);
+ return list == null ? null : unresolved.Resolve(list);
default:
return null;
}