Browse Source

Add a per-assembly target-framework override for reference resolution

ILSpy resolves an assembly's references against the target framework it
detects from the TargetFrameworkAttribute. When that attribute is missing,
wrong, or the user wants to force a different framework, there was no way to
hint the correct one, so references could resolve against the wrong runtime
pack or framework directory.

A LoadedAssembly can now carry a TargetFrameworkIdOverride that short-circuits
detection (it is the single value every LoadedAssembly-based resolution path
reads), is persisted in the assembly-list XML, and is carried across a reload
so a runtime change re-resolves against the new framework. The "Set Target
Framework" context-menu entry edits it through a dialog with a free-form text
box and an always-visible list of common monikers to pick from (the app forces
overlay popups, so a dropdown would be clamped inside the small dialog); input
is validated and converted from the short TFM users know (net48) to the long
FrameworkName form the resolver consumes (.NETFramework,Version=v4.8) via
NuGet. The direct DetectTargetFrameworkId callers in the decompiler core
(project export, language version) intentionally keep reading the real
attribute; only reference resolution is overridden.

Resurrects a 2020 prototype (branch tfmoverride) re-implemented against the
current ILSpyX/Avalonia code, whose surrounding structures no longer matched.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3839/head
Siegfried Pammer 2 weeks ago committed by Siegfried Pammer
parent
commit
17b3539a20
  1. 1
      Directory.Packages.props
  2. 11
      ICSharpCode.Decompiler.Tests/packages.lock.json
  3. 11
      ICSharpCode.ILSpyCmd/packages.lock.json
  4. 13
      ICSharpCode.ILSpyX/AssemblyList.cs
  5. 40
      ICSharpCode.ILSpyX/LoadedAssembly.cs
  6. 12
      ILSpy.ReadyToRun/packages.lock.json
  7. 12
      ILSpy.Tests.Windows/packages.lock.json
  8. 148
      ILSpy.Tests/AssemblyList/TargetFrameworkOverrideTests.cs
  9. 1
      ILSpy.Tests/Editor/DecompilerViewTests.cs
  10. 76
      ILSpy.Tests/Views/TargetFrameworkConverterTests.cs
  11. 12
      ILSpy.Tests/packages.lock.json
  12. 133
      ILSpy/Commands/SetTargetFrameworkContextMenuEntry.cs
  13. 3
      ILSpy/ILSpy.csproj
  14. 38
      ILSpy/Properties/Resources.Designer.cs
  15. 12
      ILSpy/Properties/Resources.resx
  16. 28
      ILSpy/TreeNodes/AssemblyTreeNode.cs
  17. 20
      ILSpy/TreeNodes/ReferenceFolderTreeNode.cs
  18. 22
      ILSpy/Views/SetTargetFrameworkDialog.axaml
  19. 103
      ILSpy/Views/SetTargetFrameworkDialog.axaml.cs
  20. 117
      ILSpy/Views/TargetFrameworkConverter.cs
  21. 11
      ILSpy/packages.lock.json
  22. 12
      TestPlugin/packages.lock.json

1
Directory.Packages.props

@ -67,6 +67,7 @@
<PackageVersion Include="NSubstitute" Version="5.3.0" /> <PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NUnit" Version="4.6.1" /> <PackageVersion Include="NUnit" Version="4.6.1" />
<PackageVersion Include="NUnit3TestAdapter" Version="6.2.0" /> <PackageVersion Include="NUnit3TestAdapter" Version="6.2.0" />
<PackageVersion Include="NuGet.Frameworks" Version="7.6.0" />
<PackageVersion Include="NuGet.Protocol" Version="7.6.0" /> <PackageVersion Include="NuGet.Protocol" Version="7.6.0" />
<PackageVersion Include="PowerShellStandard.Library" Version="5.1.1" /> <PackageVersion Include="PowerShellStandard.Library" Version="5.1.1" />
<PackageVersion Include="System.Composition.AttributedModel" Version="$(NetPackageVersion)" /> <PackageVersion Include="System.Composition.AttributedModel" Version="$(NetPackageVersion)" />

11
ICSharpCode.Decompiler.Tests/packages.lock.json

@ -417,11 +417,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -1261,6 +1256,12 @@
"resolved": "10.0.9", "resolved": "10.0.9",
"contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA==" "contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"System.Composition.AttributedModel": { "System.Composition.AttributedModel": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[10.0.9, )", "requested": "[10.0.9, )",

11
ICSharpCode.ILSpyCmd/packages.lock.json

@ -311,11 +311,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -412,6 +407,12 @@
"resolved": "0.11.6", "resolved": "0.11.6",
"contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"System.Collections.Immutable": { "System.Collections.Immutable": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[10.0.9, )", "requested": "[10.0.9, )",

13
ICSharpCode.ILSpyX/AssemblyList.cs

@ -95,7 +95,10 @@ namespace ICSharpCode.ILSpyX
{ {
foreach (var asm in listElement.Elements("Assembly")) foreach (var asm in listElement.Elements("Assembly"))
{ {
OpenAssembly((string)asm); var loaded = OpenAssembly((string)asm);
// Lazy detection has not run yet, so seeding the override here makes it effective
// for the first resolution without a reload. Absent attribute -> null (auto-detect).
loaded.TargetFrameworkIdOverride = (string?)asm.Attribute("TargetFramework");
} }
this.dirty = false; // OpenAssembly() sets dirty, so reset it afterwards this.dirty = false; // OpenAssembly() sets dirty, so reset it afterwards
} }
@ -175,7 +178,10 @@ namespace ICSharpCode.ILSpyX
return new XElement( return new XElement(
"List", "List",
new XAttribute("name", this.ListName), new XAttribute("name", this.ListName),
assemblies.Where(asm => !asm.IsAutoLoaded).Select(asm => new XElement("Assembly", asm.FileName)) assemblies.Where(asm => !asm.IsAutoLoaded).Select(asm => new XElement("Assembly", asm.FileName,
asm.TargetFrameworkIdOverride != null
? new XAttribute("TargetFramework", asm.TargetFrameworkIdOverride)
: null))
); );
} }
@ -387,6 +393,9 @@ namespace ICSharpCode.ILSpyX
fileLoaders: manager?.LoaderRegistry, fileLoaders: manager?.LoaderRegistry,
applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols); applyWinRTProjections: ApplyWinRTProjections, useDebugSymbols: UseDebugSymbols);
newAsm.IsAutoLoaded = target.IsAutoLoaded; newAsm.IsAutoLoaded = target.IsAutoLoaded;
// Carry the framework hint across the reload (set before any resolution on the fresh
// instance) so the references re-resolve against the overridden target framework.
newAsm.TargetFrameworkIdOverride = target.TargetFrameworkIdOverride;
lock (lockObj) lock (lockObj)
{ {
this.assemblies.Remove(target); this.assemblies.Remove(target);

40
ICSharpCode.ILSpyX/LoadedAssembly.cs

@ -137,13 +137,27 @@ namespace ICSharpCode.ILSpyX
string? targetFrameworkId; string? targetFrameworkId;
/// <summary> /// <summary>
/// Returns a target framework identifier in the form '&lt;framework&gt;Version=v&lt;version&gt;'. /// Returns the effective target framework identifier in the form '&lt;framework&gt;,Version=v&lt;version&gt;'.
/// An explicit override wins over the detected value.
/// </summary>
public async Task<string> GetTargetFrameworkIdAsync()
{
// An explicit override wins over (and bypasses) detection, even once detection has
// already cached a value, so the user's framework hint drives reference resolution.
// The setter normalizes blank to null, so a non-null override is always a usable TFM.
if (TargetFrameworkIdOverride is { } tfmOverride)
return tfmOverride;
return await GetDetectedTargetFrameworkIdAsync().ConfigureAwait(false);
}
/// <summary>
/// Returns the detected target framework identifier in the form '&lt;framework&gt;,Version=v&lt;version&gt;'.
/// Returns an empty string if no TargetFrameworkAttribute was found /// Returns an empty string if no TargetFrameworkAttribute was found
/// or the file doesn't contain an assembly header, i.e., is only a module. /// or the file doesn't contain an assembly header, i.e., is only a module.
/// ///
/// Throws an exception if the file does not contain any .NET metadata (e.g. file of unknown format). /// Throws an exception if the file does not contain any .NET metadata (e.g. file of unknown format).
/// </summary> /// </summary>
public async Task<string> GetTargetFrameworkIdAsync() public async Task<string> GetDetectedTargetFrameworkIdAsync()
{ {
var value = LazyInit.VolatileRead(ref targetFrameworkId); var value = LazyInit.VolatileRead(ref targetFrameworkId);
if (value == null) if (value == null)
@ -373,6 +387,26 @@ namespace ICSharpCode.ILSpyX
/// </summary> /// </summary>
public string? PdbFileName { get; private set; } public string? PdbFileName { get; private set; }
string? targetFrameworkIdOverride;
/// <summary>
/// Overrides the target framework used to resolve this assembly's references, in the
/// '&lt;framework&gt;,Version=v&lt;version&gt;' form (e.g. ".NETFramework,Version=v4.8").
/// Null means the framework detected from the TargetFrameworkAttribute is used. Blank or
/// whitespace-only values (e.g. a hand-edited assembly-list XML with <c>TargetFramework=""</c>)
/// are normalized to null and incidental whitespace is trimmed, so an override never
/// suppresses detection with an empty effective TFM.
/// Set this before the assembly's references are first resolved (initial load) or follow
/// a change with a reload: the resolver is built once per LoadedAssembly and frozen.
/// </summary>
public string? TargetFrameworkIdOverride {
get => targetFrameworkIdOverride;
set {
var trimmed = value?.Trim();
targetFrameworkIdOverride = string.IsNullOrEmpty(trimmed) ? null : trimmed;
}
}
async Task<LoadResult> LoadAsync(Task<Stream?>? streamTask) async Task<LoadResult> LoadAsync(Task<Stream?>? streamTask)
{ {
using var stream = await PrepareStream(); using var stream = await PrepareStream();

12
ILSpy.ReadyToRun/packages.lock.json

@ -339,11 +339,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -1403,6 +1398,7 @@
"Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )", "Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )",
"Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )", "Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )",
"NuGet.Frameworks": "[7.6.0, )",
"NuGet.Protocol": "[7.6.0, )", "NuGet.Protocol": "[7.6.0, )",
"ProDataGrid": "[12.0.0, )", "ProDataGrid": "[12.0.0, )",
"Svg.Controls.Skia.Avalonia": "[12.0.0.11, )", "Svg.Controls.Skia.Avalonia": "[12.0.0.11, )",
@ -1573,6 +1569,12 @@
"resolved": "0.11.6", "resolved": "0.11.6",
"contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Protocol": { "NuGet.Protocol": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[7.6.0, )", "requested": "[7.6.0, )",

12
ILSpy.Tests.Windows/packages.lock.json

@ -561,11 +561,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -1623,6 +1618,7 @@
"Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )", "Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )",
"Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )", "Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )",
"NuGet.Frameworks": "[7.6.0, )",
"NuGet.Protocol": "[7.6.0, )", "NuGet.Protocol": "[7.6.0, )",
"ProDataGrid": "[12.0.0, )", "ProDataGrid": "[12.0.0, )",
"Svg.Controls.Skia.Avalonia": "[12.0.0.11, )", "Svg.Controls.Skia.Avalonia": "[12.0.0.11, )",
@ -1800,6 +1796,12 @@
"resolved": "0.11.6", "resolved": "0.11.6",
"contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Protocol": { "NuGet.Protocol": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[7.6.0, )", "requested": "[7.6.0, )",

148
ILSpy.Tests/AssemblyList/TargetFrameworkOverrideTests.cs

@ -0,0 +1,148 @@
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpyX;
using ICSharpCode.ILSpy.AppEnv;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
/// <summary>
/// The target-framework override lets the user hint the TFM used to resolve an assembly's
/// references instead of the one detected from its TargetFrameworkAttribute. These tests pin
/// the load-bearing behaviour: the override wins over detection, survives the assembly-list
/// XML round-trip, defaults to null for assemblies persisted without it, and is carried across
/// a reload (which is how a runtime change re-resolves against the new framework).
/// </summary>
[TestFixture]
public class TargetFrameworkOverrideTests
{
static AssemblyListManager Manager => AppComposition.Current.GetExport<SettingsService>().AssemblyListManager;
[AvaloniaTest]
public async Task Override_Wins_Over_Detected_Tfm_Even_After_Detection_Cached()
{
var list = Manager.CreateList("tfm-override-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmOverrideFixture"));
// Force detection to run and cache its result first, so the test also proves the override
// takes precedence over an already-cached detected value.
var detected = await asm.GetTargetFrameworkIdAsync();
asm.TargetFrameworkIdOverride = ".NETFramework,Version=v4.7.2";
(await asm.GetTargetFrameworkIdAsync()).Should().Be(".NETFramework,Version=v4.7.2");
detected.Should().NotBe(".NETFramework,Version=v4.7.2",
"the fixture is not a .NET Framework 4.7.2 assembly; the override is what changes the result");
}
[AvaloniaTest]
public void Override_Survives_SaveAsXml_Roundtrip()
{
var list = Manager.CreateList("tfm-roundtrip-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmRoundtripFixture"));
asm.TargetFrameworkIdOverride = ".NETCoreApp,Version=v6.0";
var reloaded = new AssemblyList(Manager, list.SaveAsXml());
reloaded.GetAssemblies().Should().ContainSingle()
.Which.TargetFrameworkIdOverride.Should().Be(".NETCoreApp,Version=v6.0");
}
[AvaloniaTest]
public void Assembly_Without_Override_Persists_No_Attribute_And_Loads_Null()
{
var list = Manager.CreateList("tfm-noattr-" + Guid.NewGuid().ToString("N"));
list.OpenAssembly(FixtureAssembly.Emit("TfmNoAttrFixture"));
var xml = list.SaveAsXml();
xml.Elements("Assembly").Should().OnlyContain(e => e.Attribute("TargetFramework") == null,
"no attribute is written when there is no override (backward-compatible XML)");
var reloaded = new AssemblyList(Manager, xml);
reloaded.GetAssemblies().Should().ContainSingle()
.Which.TargetFrameworkIdOverride.Should().BeNull();
}
[AvaloniaTest]
[TestCase("")]
[TestCase(" ")]
public async Task Blank_Override_Normalizes_To_Null_And_Falls_Back_To_Detected(string blank)
{
var list = Manager.CreateList("tfm-blank-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmBlankFixture"));
var detected = await asm.GetTargetFrameworkIdAsync();
// A blank/whitespace override must not suppress detection with an empty effective TFM.
asm.TargetFrameworkIdOverride = blank;
asm.TargetFrameworkIdOverride.Should().BeNull();
(await asm.GetTargetFrameworkIdAsync()).Should().Be(detected);
}
[AvaloniaTest]
public void Override_Is_Trimmed()
{
var list = Manager.CreateList("tfm-trim-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmTrimFixture"));
asm.TargetFrameworkIdOverride = " .NETCoreApp,Version=v8.0 ";
asm.TargetFrameworkIdOverride.Should().Be(".NETCoreApp,Version=v8.0");
}
[AvaloniaTest]
public async Task Blank_TargetFramework_Attribute_In_Xml_Loads_As_Null()
{
// A hand-edited assembly-list XML may carry TargetFramework="". Loading it must behave
// like no override at all, not pin the effective TFM to an empty string.
var list = Manager.CreateList("tfm-blankxml-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmBlankXmlFixture"));
var xml = list.SaveAsXml();
xml.Elements("Assembly").Single().SetAttributeValue("TargetFramework", "");
var reloaded = new AssemblyList(Manager, xml);
var loaded = reloaded.GetAssemblies().Should().ContainSingle().Subject;
loaded.TargetFrameworkIdOverride.Should().BeNull();
(await loaded.GetTargetFrameworkIdAsync()).Should().Be(await asm.GetTargetFrameworkIdAsync());
}
[AvaloniaTest]
public void ReloadAssembly_Carries_Override_To_New_Instance()
{
var list = Manager.CreateList("tfm-reload-" + Guid.NewGuid().ToString("N"));
var asm = list.OpenAssembly(FixtureAssembly.Emit("TfmReloadFixture"));
asm.TargetFrameworkIdOverride = ".NETStandard,Version=v2.0";
var reloaded = list.ReloadAssembly(asm);
((object?)reloaded).Should().NotBeNull();
reloaded!.Should().NotBeSameAs(asm);
reloaded.TargetFrameworkIdOverride.Should().Be(".NETStandard,Version=v2.0");
}
}

1
ILSpy.Tests/Editor/DecompilerViewTests.cs

@ -94,6 +94,7 @@ public class DecompilerViewTests
// Assert — every header and a representative referenced assembly land in the listing. // Assert — every header and a representative referenced assembly land in the listing.
tab.Text.Should().Contain("Detected TargetFramework-Id:"); tab.Text.Should().Contain("Detected TargetFramework-Id:");
tab.Text.Should().Contain("Effective TargetFramework-Id:");
tab.Text.Should().Contain("Detected RuntimePack:"); tab.Text.Should().Contain("Detected RuntimePack:");
tab.Text.Should().Contain("Referenced assemblies (in metadata order):"); tab.Text.Should().Contain("Referenced assemblies (in metadata order):");
tab.Text.Should().Contain("System.Runtime"); tab.Text.Should().Contain("System.Runtime");

76
ILSpy.Tests/Views/TargetFrameworkConverterTests.cs

@ -0,0 +1,76 @@
// 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 AwesomeAssertions;
using ICSharpCode.ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
/// <summary>
/// The Set-Target-Framework dialog accepts the short TFMs users know (net48, net6.0, ...) but
/// ILSpy resolves with the long FrameworkName form (.NETFramework,Version=v4.8). These tests
/// pin that conversion/validation, which is the part of the dialog worth unit-testing in
/// isolation from the window.
/// </summary>
[TestFixture]
public class TargetFrameworkConverterTests
{
[TestCase("net48", ".NETFramework,Version=v4.8")]
[TestCase("net472", ".NETFramework,Version=v4.7.2")]
[TestCase("net6.0", ".NETCoreApp,Version=v6.0")]
[TestCase("net8.0", ".NETCoreApp,Version=v8.0")]
[TestCase("netstandard2.0", ".NETStandard,Version=v2.0")]
[TestCase("netcoreapp3.1", ".NETCoreApp,Version=v3.1")]
public void Short_Tfm_Parses_To_FrameworkName(string input, string expected)
{
TargetFrameworkConverter.TryParseToFrameworkName(input, out var frameworkName, out _)
.Should().BeTrue();
frameworkName.Should().Be(expected);
}
[Test]
public void Long_FrameworkName_Is_Also_Accepted()
{
TargetFrameworkConverter.TryParseToFrameworkName(".NETCoreApp,Version=v6.0", out var frameworkName, out _)
.Should().BeTrue();
frameworkName.Should().Be(".NETCoreApp,Version=v6.0");
}
[TestCase("")]
[TestCase(" ")]
[TestCase("not-a-tfm")]
[TestCase("garbage123")]
public void Invalid_Input_Is_Rejected_With_An_Error(string input)
{
TargetFrameworkConverter.TryParseToFrameworkName(input, out var frameworkName, out var error)
.Should().BeFalse();
frameworkName.Should().BeNull();
error.Should().NotBeNullOrEmpty();
}
[TestCase(".NETFramework,Version=v4.8", "net48")]
[TestCase(".NETCoreApp,Version=v6.0", "net6.0")]
[TestCase(".NETStandard,Version=v2.0", "netstandard2.0")]
public void FrameworkName_Converts_To_Short_Folder_Name_For_Display(string frameworkName, string expected)
{
TargetFrameworkConverter.ToShortFolderName(frameworkName).Should().Be(expected);
}
}

12
ILSpy.Tests/packages.lock.json

@ -524,11 +524,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -1586,6 +1581,7 @@
"Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )", "Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )",
"Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )", "Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )",
"NuGet.Frameworks": "[7.6.0, )",
"NuGet.Protocol": "[7.6.0, )", "NuGet.Protocol": "[7.6.0, )",
"ProDataGrid": "[12.0.0, )", "ProDataGrid": "[12.0.0, )",
"Svg.Controls.Skia.Avalonia": "[12.0.0.11, )", "Svg.Controls.Skia.Avalonia": "[12.0.0.11, )",
@ -1775,6 +1771,12 @@
"resolved": "0.11.6", "resolved": "0.11.6",
"contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Protocol": { "NuGet.Protocol": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[7.6.0, )", "requested": "[7.6.0, )",

133
ILSpy/Commands/SetTargetFrameworkContextMenuEntry.cs

@ -0,0 +1,133 @@
// 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.Composition;
using System.Linq;
using System.Threading.Tasks;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.AssemblyTree;
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.TreeNodes;
using ICSharpCode.ILSpy.Views;
namespace ICSharpCode.ILSpy.Commands
{
/// <summary>
/// Right-click an assembly -> "Set Target Framework". Overrides the framework ILSpy resolves the
/// assembly's references against (instead of the one detected from its TargetFrameworkAttribute),
/// then reloads so the references re-resolve. Leaving the dialog blank clears the override.
/// </summary>
[ExportContextMenuEntry(Header = nameof(Resources.SetTargetFramework), Category = "Debug", Order = 420)]
[Shared]
public sealed class SetTargetFrameworkContextMenuEntry : IContextMenuEntry
{
readonly AssemblyTreeModel assemblyTreeModel;
[ImportingConstructor]
public SetTargetFrameworkContextMenuEntry(AssemblyTreeModel assemblyTreeModel)
{
this.assemblyTreeModel = assemblyTreeModel;
}
public bool IsEnabled(TextViewContext context) => true;
public bool IsVisible(TextViewContext context)
=> context.SelectedTreeNodes?.Length == 1
&& context.SelectedTreeNodes[0] is AssemblyTreeNode asm
&& asm.LoadedAssembly.IsLoadedAsValidAssembly;
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes?.FirstOrDefault() is not AssemblyTreeNode node)
return;
ExecuteAsync(node).HandleExceptions();
}
async Task ExecuteAsync(AssemblyTreeNode node)
{
var owner = UiContext.MainWindow;
if (owner == null)
return;
var assembly = node.LoadedAssembly;
var current = await assembly.GetTargetFrameworkIdAsync();
var result = await new SetTargetFrameworkDialog(current).ShowDialog<string?>(owner);
if (result == null)
return; // cancelled
var frameworkName = result.Length == 0 ? null : result;
if (frameworkName == assembly.TargetFrameworkIdOverride
|| (assembly.TargetFrameworkIdOverride == null && frameworkName == current))
return; // unchanged
ApplyOverride(assemblyTreeModel, node, frameworkName);
}
/// <summary>
/// Sets the override, reloads so references re-resolve against the new framework, and restores
/// the tree selection. Shared by "Set Target Framework" and "Reset Target Framework".
/// </summary>
internal static void ApplyOverride(AssemblyTreeModel assemblyTreeModel, AssemblyTreeNode node, string? frameworkName)
{
var assembly = node.LoadedAssembly;
// Snapshot the node path before ReloadAssembly swaps the LoadedAssembly out from under
// the tree, then re-select the equivalent node so the user keeps their position.
var path = AssemblyTreeModel.GetPathForNode(node);
assembly.TargetFrameworkIdOverride = frameworkName;
assembly.AssemblyList.ReloadAssembly(assembly.FileName);
var restored = assemblyTreeModel.FindNodeByPath(path, returnBestMatch: true);
if (restored != null)
assemblyTreeModel.SelectNode(restored);
assemblyTreeModel.RefreshDecompiledView();
}
}
/// <summary>
/// Right-click an assembly that carries a manual target-framework override -> "Reset Target
/// Framework". Clears the override and reloads so references re-resolve against the framework
/// detected from the TargetFrameworkAttribute again. Only shown while an override is in effect,
/// so the user can revert without reopening the dialog.
/// </summary>
[ExportContextMenuEntry(Header = nameof(Resources.ResetTargetFramework), Category = "Debug", Order = 421)]
[Shared]
public sealed class ResetTargetFrameworkContextMenuEntry : IContextMenuEntry
{
readonly AssemblyTreeModel assemblyTreeModel;
[ImportingConstructor]
public ResetTargetFrameworkContextMenuEntry(AssemblyTreeModel assemblyTreeModel)
{
this.assemblyTreeModel = assemblyTreeModel;
}
public bool IsEnabled(TextViewContext context) => true;
public bool IsVisible(TextViewContext context)
=> context.SelectedTreeNodes?.Length == 1
&& context.SelectedTreeNodes[0] is AssemblyTreeNode asm
&& asm.LoadedAssembly.IsLoadedAsValidAssembly
&& asm.LoadedAssembly.TargetFrameworkIdOverride != null;
public void Execute(TextViewContext context)
{
if (context.SelectedTreeNodes?.FirstOrDefault() is not AssemblyTreeNode node)
return;
SetTargetFrameworkContextMenuEntry.ApplyOverride(assemblyTreeModel, node, null);
}
}
}

3
ILSpy/ILSpy.csproj

@ -120,6 +120,9 @@
<!-- NuGet V3 feed client for the "Open from NuGet feed" dialog: search, version <!-- NuGet V3 feed client for the "Open from NuGet feed" dialog: search, version
enumeration, and download into the global packages folder. --> enumeration, and download into the global packages folder. -->
<PackageReference Include="NuGet.Protocol" /> <PackageReference Include="NuGet.Protocol" />
<!-- Parses/validates target-framework monikers for the "Set Target Framework" dialog. -->
<PackageReference Include="NuGet.Frameworks" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

38
ILSpy/Properties/Resources.Designer.cs generated

@ -2836,6 +2836,15 @@ namespace ICSharpCode.ILSpy.Properties {
} }
} }
/// <summary>
/// Looks up a localized string similar to Reset Target Framework.
/// </summary>
public static string ResetTargetFramework {
get {
return ResourceManager.GetString("ResetTargetFramework", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Reset to defaults. /// Looks up a localized string similar to Reset to defaults.
/// </summary> /// </summary>
@ -3006,7 +3015,34 @@ namespace ICSharpCode.ILSpy.Properties {
return ResourceManager.GetString("SelectPDB", resourceCulture); return ResourceManager.GetString("SelectPDB", resourceCulture);
} }
} }
/// <summary>
/// Looks up a localized string similar to Set Target Framework....
/// </summary>
public static string SetTargetFramework {
get {
return ResourceManager.GetString("SetTargetFramework", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Target framework:.
/// </summary>
public static string TargetFramework {
get {
return ResourceManager.GetString("TargetFramework", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Unknown target framework. Enter a moniker such as net48 or net6.0..
/// </summary>
public static string InvalidTargetFramework {
get {
return ResourceManager.GetString("InvalidTargetFramework", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Select target folder. /// Looks up a localized string similar to Select target folder.
/// </summary> /// </summary>

12
ILSpy/Properties/Resources.resx

@ -780,6 +780,9 @@ Are you sure you want to continue?</value>
<data name="InsertUsingDeclarations" xml:space="preserve"> <data name="InsertUsingDeclarations" xml:space="preserve">
<value>Insert using declarations</value> <value>Insert using declarations</value>
</data> </data>
<data name="InvalidTargetFramework" xml:space="preserve">
<value>Unknown target framework. Enter a moniker such as net48 or net6.0.</value>
</data>
<data name="ListDeleteConfirmation" xml:space="preserve"> <data name="ListDeleteConfirmation" xml:space="preserve">
<value>Are you sure that you want to delete the selected assembly list?</value> <value>Are you sure that you want to delete the selected assembly list?</value>
</data> </data>
@ -976,6 +979,9 @@ Do you want to continue?</value>
<data name="RenameList" xml:space="preserve"> <data name="RenameList" xml:space="preserve">
<value>Rename list</value> <value>Rename list</value>
</data> </data>
<data name="ResetTargetFramework" xml:space="preserve">
<value>Reset Target Framework</value>
</data>
<data name="ResetToDefaults" xml:space="preserve"> <data name="ResetToDefaults" xml:space="preserve">
<value>Reset to defaults</value> <value>Reset to defaults</value>
</data> </data>
@ -1039,6 +1045,9 @@ Do you want to continue?</value>
<data name="SelectVersionDropdownTooltip" xml:space="preserve"> <data name="SelectVersionDropdownTooltip" xml:space="preserve">
<value>Select version of language to output (Alt+E)</value> <value>Select version of language to output (Alt+E)</value>
</data> </data>
<data name="SetTargetFramework" xml:space="preserve">
<value>Set Target Framework...</value>
</data>
<data name="SettingsChangeRestartRequired" xml:space="preserve"> <data name="SettingsChangeRestartRequired" xml:space="preserve">
<value>You must restart ILSpy for the change to take effect.</value> <value>You must restart ILSpy for the change to take effect.</value>
</data> </data>
@ -1126,6 +1135,9 @@ Do you want to continue?</value>
<data name="TabSize" xml:space="preserve"> <data name="TabSize" xml:space="preserve">
<value>Tab size:</value> <value>Tab size:</value>
</data> </data>
<data name="TargetFramework" xml:space="preserve">
<value>Target framework:</value>
</data>
<data name="Theme" xml:space="preserve"> <data name="Theme" xml:space="preserve">
<value>Theme</value> <value>Theme</value>
</data> </data>

28
ILSpy/TreeNodes/AssemblyTreeNode.cs

@ -27,6 +27,8 @@ using Avalonia.Controls;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Media; using Avalonia.Media;
using AvaloniaEdit.Highlighting;
using ICSharpCode.Decompiler; using ICSharpCode.Decompiler;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Metadata;
@ -38,11 +40,12 @@ using ICSharpCode.ILSpyX.TreeView;
using ICSharpCode.ILSpy; using ICSharpCode.ILSpy;
using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Controls.TreeView;
using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.Languages;
namespace ICSharpCode.ILSpy.TreeNodes namespace ICSharpCode.ILSpy.TreeNodes
{ {
public sealed class AssemblyTreeNode : ILSpyTreeNode public sealed class AssemblyTreeNode : ILSpyTreeNode, IRichTextNode
{ {
readonly LoadedAssembly assembly; readonly LoadedAssembly assembly;
string? loadError; string? loadError;
@ -228,6 +231,29 @@ namespace ICSharpCode.ILSpy.TreeNodes
public override object Text => assembly.Text; public override object Text => assembly.Text;
/// <summary>
/// The label already embeds the effective target framework in parentheses (see
/// <see cref="LoadedAssembly.Text"/>, e.g. "Foo (1.0.0.0, .NETFramework, v4.8)"). When that
/// framework comes from a user override, this bolds just that TFM fragment so overridden
/// assemblies stand out without bolding the whole row. Returns null (plain <see cref="Text"/>)
/// when there is no override.
/// </summary>
public RichText? CreateRichText()
{
if (assembly.TargetFrameworkIdOverride is not { } tfmOverride)
return null;
string label = assembly.Text;
// LoadedAssembly.Text renders the TFM as the effective id with "Version=" replaced by a
// space; reproduce that and locate it so only the framework fragment is emphasized.
string fragment = tfmOverride.Replace("Version=", " ");
int start = label.LastIndexOf(fragment, StringComparison.Ordinal);
if (start < 0)
return null;
var model = new RichTextModel();
model.SetFontWeight(start, fragment.Length, FontWeight.Bold);
return new RichText(label, model);
}
// ToString is the stable identity used by SessionSettings.ActiveTreeViewPath — must not // ToString is the stable identity used by SessionSettings.ActiveTreeViewPath — must not
// depend on the active language. The full file path uniquely identifies the assembly. // depend on the active language. The full file path uniquely identifies the assembly.
public override string ToString() => assembly.FileName; public override string ToString() => assembly.FileName;

20
ILSpy/TreeNodes/ReferenceFolderTreeNode.cs

@ -17,6 +17,7 @@
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using Avalonia.Threading; using Avalonia.Threading;
@ -25,6 +26,7 @@ using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.ILSpy.Properties; using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpy.Languages; using ICSharpCode.ILSpy.Languages;
using ICSharpCode.ILSpyX;
namespace ICSharpCode.ILSpy.TreeNodes namespace ICSharpCode.ILSpy.TreeNodes
{ {
@ -61,9 +63,10 @@ namespace ICSharpCode.ILSpy.TreeNodes
public override void Decompile(Language language, ITextOutput output, DecompilationOptions options) public override void Decompile(Language language, ITextOutput output, DecompilationOptions options)
{ {
var targetFramework = parentAssembly.LoadedAssembly.GetTargetFrameworkIdAsync().GetAwaiter().GetResult(); var loadedAssembly = parentAssembly.LoadedAssembly;
var runtimePack = parentAssembly.LoadedAssembly.GetRuntimePackAsync().GetAwaiter().GetResult(); var (detectedTargetFramework, effectiveTargetFramework, runtimePack) = GetFrameworkInfoAsync(loadedAssembly).GetAwaiter().GetResult();
output.WriteLine($"Detected TargetFramework-Id: {targetFramework}"); output.WriteLine($"Detected TargetFramework-Id: {detectedTargetFramework}");
output.WriteLine($"Effective TargetFramework-Id: {effectiveTargetFramework}");
output.WriteLine($"Detected RuntimePack: {runtimePack}"); output.WriteLine($"Detected RuntimePack: {runtimePack}");
// Children realise lazily on the UI thread; we may run from a background decompile. // Children realise lazily on the UI thread; we may run from a background decompile.
@ -86,5 +89,16 @@ namespace ICSharpCode.ILSpy.TreeNodes
output.WriteLine(); output.WriteLine();
} }
} }
// One await chain instead of three separate blocking GetResult() calls: each blocked wait
// can park a thread-pool thread, so resolving all three framework values in a single async
// method and blocking once is cheaper.
static async Task<(string Detected, string Effective, string RuntimePack)> GetFrameworkInfoAsync(LoadedAssembly loadedAssembly)
{
var detected = await loadedAssembly.GetDetectedTargetFrameworkIdAsync().ConfigureAwait(false);
var effective = await loadedAssembly.GetTargetFrameworkIdAsync().ConfigureAwait(false);
var runtimePack = await loadedAssembly.GetRuntimePackAsync().ConfigureAwait(false);
return (detected, effective, runtimePack);
}
} }
} }

22
ILSpy/Views/SetTargetFrameworkDialog.axaml

@ -0,0 +1,22 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ICSharpCode.ILSpy.Views.SetTargetFrameworkDialog"
Width="360" SizeToContent="Height" CanResize="False"
WindowStartupLocation="CenterOwner"
Title="ILSpy">
<Grid Margin="12,10" RowDefinitions="Auto,Auto,Auto,Auto,Auto">
<TextBlock Grid.Row="0" Name="PromptText" Margin="0,0,0,6" TextWrapping="Wrap" />
<TextBox Grid.Row="1" Name="FrameworkBox" />
<!-- The presets sit in an always-visible list rather than a dropdown: the app forces
OverlayPopups (see Program.cs), so a popup would be clamped to this small dialog and
blanket it. A fixed-height ListBox scrolls when the presets overflow. -->
<ListBox Grid.Row="2" Name="PresetList" Height="150" Margin="0,6,0,0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" />
<TextBlock Grid.Row="3" Name="ErrorText" Margin="0,6,0,0"
Foreground="Red" TextWrapping="Wrap" IsVisible="False" />
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6" Margin="0,12,0,0">
<Button Name="OkButton" Content="OK" IsDefault="True" MinWidth="72" />
<Button Name="CancelButton" Content="Cancel" IsCancel="True" MinWidth="72" />
</StackPanel>
</Grid>
</Window>

103
ILSpy/Views/SetTargetFrameworkDialog.axaml.cs

@ -0,0 +1,103 @@
// 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.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ICSharpCode.ILSpy.Views
{
/// <summary>
/// Prompts for a target-framework moniker used to override how an assembly's references are
/// resolved. A text box accepts the short TFMs users know (net48, net6.0, ...) for free-form
/// entry, with the common ones offered in an adjacent list to pick from; input is
/// validated/converted to the long FrameworkName form via <see cref="TargetFrameworkConverter"/>.
/// Leaving it blank clears the override.
/// </summary>
public partial class SetTargetFrameworkDialog : Window
{
// Suggestions only; any valid TFM the user types is accepted.
static readonly IReadOnlyList<string> CommonFrameworks = new[] {
"net11.0", "net10.0", "net9.0", "net8.0", "net7.0", "net6.0", "net5.0",
"netcoreapp3.1", "netcoreapp3.0", "netcoreapp2.1",
"netstandard2.1", "netstandard2.0",
"net48", "net472", "net471", "net47", "net462", "net461", "net46",
};
TextBox frameworkBox = null!;
ListBox presetList = null!;
TextBlock errorText = null!;
public SetTargetFrameworkDialog()
{
InitializeComponent();
frameworkBox = this.FindControl<TextBox>("FrameworkBox")!;
presetList = this.FindControl<ListBox>("PresetList")!;
errorText = this.FindControl<TextBlock>("ErrorText")!;
this.FindControl<TextBlock>("PromptText")!.Text = Properties.Resources.TargetFramework;
presetList.ItemsSource = CommonFrameworks;
// Picking a preset fills the box; typing stays free-form, so any TFM the user knows is
// accepted even when it isn't in the list.
presetList.SelectionChanged += (_, _) => {
if (presetList.SelectedItem is string preset)
frameworkBox.Text = preset;
};
// Double-clicking a preset is "choose and confirm", like committing a combo selection.
presetList.DoubleTapped += (_, _) => {
if (presetList.SelectedItem is string)
OnOk();
};
((Button)this.FindControl<Button>("OkButton")!).Click += (_, _) => OnOk();
((Button)this.FindControl<Button>("CancelButton")!).Click += (_, _) => Close(null);
}
public SetTargetFrameworkDialog(string? currentFrameworkName) : this()
{
Title = Properties.Resources.SetTargetFramework;
var shortName = TargetFrameworkConverter.ToShortFolderName(currentFrameworkName);
if (!string.IsNullOrEmpty(shortName))
{
frameworkBox.Text = shortName;
// Highlight the matching preset (no-op when the current value isn't one of them).
presetList.SelectedItem = CommonFrameworks.FirstOrDefault(f => f == shortName);
}
}
void OnOk()
{
var text = frameworkBox.Text;
if (string.IsNullOrWhiteSpace(text))
{
// Blank clears the override (revert to auto-detection).
Close(string.Empty);
return;
}
if (!TargetFrameworkConverter.TryParseToFrameworkName(text, out var frameworkName, out var error))
{
errorText.Text = error;
errorText.IsVisible = true;
return;
}
Close(frameworkName);
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
}

117
ILSpy/Views/TargetFrameworkConverter.cs

@ -0,0 +1,117 @@
// 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.Collections.Generic;
using ICSharpCode.ILSpy.Properties;
using NuGet.Frameworks;
namespace ICSharpCode.ILSpy.Views
{
/// <summary>
/// Bridges the short target-framework monikers users type (net48, net6.0, netstandard2.0)
/// and the long FrameworkName form ILSpy resolves references with
/// (.NETFramework,Version=v4.8). Parsing/validation goes through NuGet so the dialog accepts
/// exactly what the ecosystem considers a valid TFM.
/// </summary>
internal static class TargetFrameworkConverter
{
// NuGet's parser is lenient and coerces arbitrary text into a generic framework, so accept
// only identifiers ILSpy's resolver actually understands (NuGet keeps .NET 5+ as .NETCoreApp).
static readonly HashSet<string> SupportedIdentifiers = new(StringComparer.OrdinalIgnoreCase) {
FrameworkConstants.FrameworkIdentifiers.Net, // .NETFramework
FrameworkConstants.FrameworkIdentifiers.NetCoreApp, // .NETCoreApp (incl. net5.0+)
FrameworkConstants.FrameworkIdentifiers.NetStandard, // .NETStandard
};
/// <summary>
/// Parses a short TFM (e.g. "net48") or an already-long FrameworkName (e.g.
/// ".NETFramework,Version=v4.8") into the long FrameworkName form. Returns false with a
/// human-readable <paramref name="error"/> when the input is blank or not a recognised
/// framework.
/// </summary>
public static bool TryParseToFrameworkName(string? input, out string? frameworkName, out string? error)
{
frameworkName = null;
error = null;
if (string.IsNullOrWhiteSpace(input))
{
error = Resources.InvalidTargetFramework;
return false;
}
var framework = TryParse(input.Trim());
if (framework == null || framework.IsUnsupported || framework.IsAgnostic || framework.IsAny
|| !SupportedIdentifiers.Contains(framework.Framework))
{
error = Resources.InvalidTargetFramework;
return false;
}
frameworkName = framework.DotNetFrameworkName;
return true;
}
/// <summary>
/// Converts a long FrameworkName to its short folder name for display (e.g.
/// ".NETFramework,Version=v4.8" -> "net48"), or returns null if it cannot be parsed.
/// </summary>
public static string? ToShortFolderName(string? frameworkName)
{
if (string.IsNullOrWhiteSpace(frameworkName))
return null;
try
{
var framework = NuGetFramework.ParseFrameworkName(frameworkName, DefaultFrameworkNameProvider.Instance);
if (framework.IsUnsupported)
return null;
return framework.GetShortFolderName();
}
catch (ArgumentException)
{
return null;
}
}
// Accepts both the short folder form ("net48") and the long FrameworkName form
// (".NETFramework,Version=v4.8"); NuGet's two parsers cover one each.
static NuGetFramework? TryParse(string input)
{
try
{
var framework = NuGetFramework.Parse(input);
if (!framework.IsUnsupported)
return framework;
}
catch (ArgumentException)
{
// fall through to the FrameworkName parser
}
try
{
return NuGetFramework.ParseFrameworkName(input, DefaultFrameworkNameProvider.Instance);
}
catch (ArgumentException)
{
return null;
}
}
}
}

11
ILSpy/packages.lock.json

@ -167,6 +167,12 @@
"resolved": "10.0.9", "resolved": "10.0.9",
"contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA==" "contentHash": "g41l/30G3K4B/d/L8kjux0+30e27c8D0FVQ/PFCpbekgfDpj9mnDhieP67EqXWvl1EWNeZh2rpR4F5B/jcDOHA=="
}, },
"NuGet.Frameworks": {
"type": "Direct",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Protocol": { "NuGet.Protocol": {
"type": "Direct", "type": "Direct",
"requested": "[7.6.0, )", "requested": "[7.6.0, )",
@ -538,11 +544,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",

12
TestPlugin/packages.lock.json

@ -321,11 +321,6 @@
"System.Security.Cryptography.ProtectedData": "8.0.0" "System.Security.Cryptography.ProtectedData": "8.0.0"
} }
}, },
"NuGet.Frameworks": {
"type": "Transitive",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Packaging": { "NuGet.Packaging": {
"type": "Transitive", "type": "Transitive",
"resolved": "7.6.0", "resolved": "7.6.0",
@ -1385,6 +1380,7 @@
"Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )", "Microsoft.DiaSymReader.Converter.Xml": "[1.1.0-beta2-22171-02, )",
"Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )", "Microsoft.DiaSymReader.Native": "[17.0.0-beta1.21524.1, )",
"Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[10.0.9, )",
"NuGet.Frameworks": "[7.6.0, )",
"NuGet.Protocol": "[7.6.0, )", "NuGet.Protocol": "[7.6.0, )",
"ProDataGrid": "[12.0.0, )", "ProDataGrid": "[12.0.0, )",
"Svg.Controls.Skia.Avalonia": "[12.0.0.11, )", "Svg.Controls.Skia.Avalonia": "[12.0.0.11, )",
@ -1546,6 +1542,12 @@
"resolved": "0.11.6", "resolved": "0.11.6",
"contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA==" "contentHash": "f33RkDtZO8VlGXCtmQIviOtxgnUdym9xx/b1p9h91CRGOsJFxCFOFK1FDbVt1OCf1aWwYejUFa2MOQyFWTFjbA=="
}, },
"NuGet.Frameworks": {
"type": "CentralTransitive",
"requested": "[7.6.0, )",
"resolved": "7.6.0",
"contentHash": "rJ7QtKN45XzLXCrMATve6eFLiUyUGEkA1rFSb6U6Fw6laM4hEAcKOrcdbgWlcFUlCK2158qP1LF00hg/ivF3nw=="
},
"NuGet.Protocol": { "NuGet.Protocol": {
"type": "CentralTransitive", "type": "CentralTransitive",
"requested": "[7.6.0, )", "requested": "[7.6.0, )",

Loading…
Cancel
Save