A rebuilt WPF project only resolves its own pack URIs when every entry of
"<AssemblyName>.g.resources" comes back under the resource ID it had before.
The file on disk cannot carry that ID: it is sanitized for the file system,
and the ID itself is escaped. Verified against a WPF assembly built for this:
the WPF build tasks re-escape whatever LogicalName they are given, so the item
has to hand them the decoded name, and an entry left as EmbeddedResource
rebuilds into a manifest resource of its own instead of into ".g.resources".
The adjustment is made where the items are collected, so it covers the base
class and both hosts that plug their own BAML handling into it without
widening the WriteResourceToFile or IResourceFileHandler contracts.
The Resource build action this gives them is also what #2253 asks for.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The SDK-style writer only ever emitted EmbeddedResource items, so every other
item type the export produced never reached the project file. XAML recovered
from BAML lands in Page items: ILSpy wrote the .xaml files to disk and the
project referenced none of them, leaving an exported WPF project that cannot
rebuild.
Explicit items collide with the SDK's own globs - a UseWPF project globs
**/*.xaml into Page, and NETSDK1022 rejects the duplicate - so each include is
preceded by a remove of the same item type, the pattern the EmbeddedResource
path already uses. Setting EnableDefaultPageItems=false would work as well, but
it is WPF-specific and switches off a glob for the whole project, whereas the
remove is per item, applies to any item type, and keeps the SDK's item
definitions (XamlRuntime) in effect.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
WPF's build tasks key every Page and Resource item in
"<AssemblyName>.g.resources" by the item's relative path, lower-cased and
run through Uri.GetComponents(Path, UriEscaped) - so a folder named
"Resource Test" arrives as "resource%20test", and a folder named in any
non-ASCII script arrives as a run of UTF-8 percent escapes. Those escapes
are not part of the name; sanitizing them turned "resource%20test" into
"resource-20test" and any Chinese or umlaut folder into a line of hex.
Measured against a WPF assembly built for this: only space, '#', '{', '}'
and non-ASCII bytes are ever escaped, and Uri.UnescapeDataString is the
exact inverse - anything the escaper leaves alone contains no percent
sign, and a literal one arrives as %25. Only the WPF-generated containers
are affected, so a percent sign in any other .resources file stays part
of the name.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
Obfuscators put arbitrary characters into BAML strings, and XML 1.0 has no
representation for most control characters - a numeric character reference is invalid
for them too. Writing such a document threw ArgumentException from XmlWriter, which
loses the resource on project export and shows an exception instead of the page in the
UI. The escapes are spelled the way the C# output spells them, so one convention covers
both languages. Namespace URIs have to be escaped where the XNamespace is created rather
than in the final pass: the URI is baked into every element name built from it, so
patching only the xmlns declaration would desync the two. Characters XML can carry stay
untouched, so ordinary documents decompile byte-identically.
Every BAML stream of an assembly lives in one .resources container, and the recovery
around resource writing sat outside the loop over its entries, so a single page that
could not be written discarded every other page sharing the container with it.
Assisted-by: Claude:claude-opus-5:Claude Code
One member the decompiler could not handle aborted the whole export, so a
single unsupported method in a large assembly left the user with nothing: no
sources, no .csproj, no way around it. Recovering silently would trade that
for a worse outcome - broken output nobody knows is broken - so every failure
is recorded, written where the content would have gone, and pointed at the
issue tracker.
The recovery has to hold for anything the export touches, not just method
bodies: a file that cannot be created, a resource that cannot be decoded, an
output visitor that throws mid-type. Each of those costs its own unit and
nothing else, and the units behind a failure are still produced - dropping
them would make the export look complete when it is not.
Consumers that relied on the exception keep their failure signal: ilspycmd
exits non-zero and lists the failures, the PowerShell cmdlets raise an error
record per failure, and the round-trip suite asserts the export reported none
- otherwise a crash on a method its own tests never call would ship green.
Assisted-by: Claude:claude-opus-5[1m]:Claude Code
The language version appears in two places that share a name but not a
concept, which repeatedly reads as one confused API: on
DecompilerSettings it is a construction shortcut (SetLanguageVersion
initializes the feature flags once and the version is not stored, so
the flags are the only state and the call is deliberately one-way),
while on WholeProjectDecompiler it is an export parameter (the
LangVersion stamped into the project file, defaulting to
GetMinimumRequiredVersion() and rejected below it as a safety net
against exporting uncompilable projects). Spell both roles out in the
XML docs so the distinction no longer has to be reverse-engineered.
Assisted-by: Claude:claude-fable-5:Claude Code
The LanguageVersion setter's InvalidOperationException is a safety net
against exporting a project whose LangVersion cannot compile the
emitted code, but it only fires at assignment time: Settings is mutable
and shared, so enabling a feature after assigning the version slipped
past the check. Re-validating at the start of DecompileProject closes
that gap while keeping the setter's immediate feedback.
Assisted-by: Claude:claude-fable-5:Claude Code
* Make IProjectFileWriter implementations public and extensible
* Fix nullable error
* Fix delegate invocation to prevent race conditions
Refactored code to assign WriteCustomPropertyGroup and WriteCustomItemGroup delegates to local variables before null checks and invocation. This ensures thread safety by avoiding race conditions if the delegates are modified by other threads.
* Replace events with a GetCustomProperties virtual method
* Remove I prefix
The events introduced in #2519 timed only the five per-entity DoDecompile
overloads, allocated a Stopwatch and the member's FullName even when no
trace session was attached, and reported whole milliseconds, which rounds
almost every member to zero. Flat one-shot events also gave PerfView and
dotnet-trace no way to show durations or nesting, and the actually
expensive stages (type system initialization, assembly resolution probing,
the IL/AST transform pipelines, whole-project decompilation) were not
instrumented at all.
Start/Stop event pairs let trace viewers derive duration and nesting from
event timestamps, keywords let a session enable only the areas of
interest, and every call site is gated on IsEnabled() so tracing costs a
branch when disabled. Per-transform events are Verbose because of their
volume; unlike the STEP/Stepper mechanism they work in Release builds.
EventSource is in-box for netstandard2.0 and flows over both ETW and
EventPipe, so this stays cross-platform with no new dependency.
Assisted-by: Claude:claude-fable-5:Claude Code
Windows maps CON, PRN, AUX, NUL, COM1-9 and LPT1-9 to devices -- on many
builds even with an extension appended, so a type named Con made both
whole-project export and the save dialog fail with IOException '\\.\Con'.
CleanUpName only checked for reserved names after re-appending the file
extension, where they never match, and the save-dialog default-name
helpers did not check them at all. The escape appends the underscore to
the base name (con_.txt, not con.txt_) because device-name parsing
ignores everything after the first dot, and is applied per path segment
so reserved directory names produced by namespaces are covered too. The
ILSpy.Tests.Windows fixture verifies on a real Windows filesystem that
the escaped names are creatable.
Assisted-by: Claude:claude-fable-5:Claude Code
- Set 'LogicalName' attribute for all decompiled resources. This makes it possible to correctly recompile projects with resource names that are not valid filenames.
- Set Generator and SubType properties for XAML files.
With the built-in support for long paths in .NET 6.0, we no longer need to check for the registry key. The only limitation that remains is maxSegmentLength, which seems to be 255 on all commonly used file systems/all platforms. Also there is no need to differentiate between Windows and other platforms.
- Windows Explorer in Windows 10 seems to be fine with files generated by ILSpy that have names longer than 260 characters.
- Notepad++ and other applications seem to use 8.3 path syntax to access the file.
- Visual Studio 2022 does not like the long path names, affected users should raise an issue with MS. ILSpy generates proper paths.
When C++/CLI project produce assembly it generates target attribute like this
```
[assembly: TargetFramework(".NETCoreApp,Version=7.0", FrameworkDisplayName = "")]
```
when C# generates `TargetFrameworkAttribute` it produce slightly different format
```
[assembly: TargetFramework(".NETCoreApp,Version=v7.0", FrameworkDisplayName = "")]
```