From 4c8244d3bb7f4deb77fba1b1f966cf0bc0872186 Mon Sep 17 00:00:00 2001
From: ds5678 <49847914+ds5678@users.noreply.github.com>
Date: Thu, 23 Jul 2026 18:53:35 -0700
Subject: [PATCH 1/5] Make IProjectFileWriter implementations public and
extensible
---
.../ProjectDecompiler/IProjectFileWriter.cs | 15 ++
.../ProjectFileWriterDefault.cs | 11 +-
.../ProjectFileWriterSdkStyle.cs | 175 +++++++++++++-----
.../WholeProjectDecompiler.cs | 2 +-
4 files changed, 151 insertions(+), 52 deletions(-)
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
index f9c1c33512..82220a3228 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
@@ -38,4 +38,19 @@ public interface IProjectFileWriter
/// The module being decompiled.
void Write(TextWriter target, IProjectInfoProvider project, IEnumerable files, MetadataFile module);
}
+ public static class IProjectFileWriterExtensions
+ {
+ extension(IProjectFileWriter)
+ {
+ ///
+ /// Gets an instance of based on the specified .
+ ///
+ /// The settings to use for getting the project file writer.
+ /// An instance of .
+ public static IProjectFileWriter FromSettings(DecompilerSettings settings)
+ {
+ return settings.UseSdkStyleProjectFormat ? ProjectFileWriterSdkStyle.Default : ProjectFileWriterDefault.Instance;
+ }
+ }
+ }
}
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
index 4447be86dd..78f815feb5 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs
@@ -32,13 +32,16 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
///
/// A implementation that creates the projects in the default format.
///
- sealed class ProjectFileWriterDefault : IProjectFileWriter
+ public sealed class ProjectFileWriterDefault : IProjectFileWriter
{
///
- /// Creates a new instance of the class.
+ /// Gets the singleton instance of the class.
///
- /// A new instance of the class.
- public static IProjectFileWriter Create() => new ProjectFileWriterDefault();
+ public static ProjectFileWriterDefault Instance { get; } = new();
+
+ ProjectFileWriterDefault()
+ {
+ }
///
public void Write(
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
index 0f79025246..2e6f670221 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
@@ -26,12 +26,14 @@
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Util;
+#nullable enable
+
namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
{
///
/// A implementation that creates the projects in the SDK style format.
///
- sealed class ProjectFileWriterSdkStyle : IProjectFileWriter
+ public class ProjectFileWriterSdkStyle : IProjectFileWriter
{
const string AspNetCorePrefix = "Microsoft.AspNetCore";
const string PresentationFrameworkName = "PresentationFramework";
@@ -57,10 +59,18 @@ sealed class ProjectFileWriterSdkStyle : IProjectFileWriter
enum ProjectType { Default, WinForms, Wpf, Web }
///
- /// Creates a new instance of the class.
+ /// Gets the default instance of the class.
+ ///
+ public static ProjectFileWriterSdkStyle Default { get; } = new();
+
+ ///
+ /// Occurs when a custom property group can be written to the project file.
+ ///
+ protected event Action, MetadataFile>? WriteCustomPropertyGroup;
+ ///
+ /// Occurs when a custom item group can be written to the project file.
///
- /// A new instance of the class.
- public static IProjectFileWriter Create() => new ProjectFileWriterSdkStyle();
+ protected event Action, MetadataFile>? WriteCustomItemGroup;
///
public void Write(
@@ -76,36 +86,52 @@ public void Write(
}
}
- static void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable files, MetadataFile module)
+ void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable files, MetadataFile module)
{
xml.WriteStartElement("Project");
var projectType = GetProjectType(module);
xml.WriteAttributeString("Sdk", GetSdkString(projectType));
- PlaceIntoTag("PropertyGroup", xml, () => WriteAssemblyInfo(xml, module, project, projectType));
- PlaceIntoTag("PropertyGroup", xml, () => WriteProjectInfo(xml, project));
- PlaceIntoTag("PropertyGroup", xml, () => WriteMiscellaneousPropertyGroup(xml, files));
- PlaceIntoTag("ItemGroup", xml, () => WriteResources(xml, files));
- PlaceIntoTag("ItemGroup", xml, () => WriteReferences(xml, module, project, projectType));
-
- xml.WriteEndElement();
- }
-
- static void PlaceIntoTag(string tagName, XmlTextWriter xml, Action content)
- {
- xml.WriteStartElement(tagName);
- try
+ using (new Group(xml, "PropertyGroup"))
{
- content();
+ WriteAssemblyInfo(xml, module, project, projectType);
}
- finally
+ using (new Group(xml, "PropertyGroup"))
{
- xml.WriteEndElement();
+ WriteProjectInfo(xml, project);
+ }
+ using (new Group(xml, "PropertyGroup"))
+ {
+ WriteMiscellaneousPropertyGroup(xml, files);
+ }
+ if (WriteCustomPropertyGroup != null)
+ {
+ using (new Group(xml, "PropertyGroup"))
+ {
+ WriteCustomPropertyGroup.Invoke(xml, project, files, module);
+ }
+ }
+ using (new Group(xml, "ItemGroup"))
+ {
+ WriteResources(xml, files);
+ }
+ using (new Group(xml, "ItemGroup"))
+ {
+ WriteReferences(xml, module, project);
+ }
+ if (WriteCustomItemGroup != null)
+ {
+ using (new Group(xml, "ItemGroup"))
+ {
+ WriteCustomItemGroup.Invoke(xml, project, files, module);
+ }
}
+
+ xml.WriteEndElement();
}
- static void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType)
+ void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType)
{
xml.WriteElementString("AssemblyName", module.Name);
@@ -116,9 +142,9 @@ static void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectIn
CorFlags flags;
if (module is PEFile { Reader.PEHeaders: var headers } peFile)
{
- WriteOutputType(xml, headers.IsDll, headers.PEHeader.Subsystem, projectType);
+ WriteOutputType(xml, headers.IsDll, headers.PEHeader!.Subsystem, projectType);
platformName = TargetServices.GetPlatformName(peFile);
- flags = headers.CorHeader.Flags;
+ flags = headers.CorHeader!.Flags;
}
else
{
@@ -129,16 +155,7 @@ static void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectIn
WriteDesktopExtensions(xml, projectType);
- var targetFramework = TargetServices.DetectTargetFramework(module);
- if (targetFramework.Identifier == ".NETFramework" && targetFramework.VersionNumber == 200)
- targetFramework = TargetServices.DetectTargetFrameworkNET20(module, project.AssemblyResolver, targetFramework);
-
- if (targetFramework.Moniker == null)
- {
- throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead.");
- }
-
- xml.WriteElementString("TargetFramework", targetFramework.Moniker);
+ xml.WriteElementString("TargetFramework", GetTargetFrameworkMoniker(module, project));
// 'AnyCPU' is default, so only need to specify platform if it differs
if (platformName != AnyCpuString)
@@ -152,6 +169,27 @@ static void WriteAssemblyInfo(XmlTextWriter xml, MetadataFile module, IProjectIn
}
}
+ ///
+ /// Gets the target framework moniker for the specified module and project.
+ ///
+ /// The module for which to get the target framework moniker.
+ /// The project information provider.
+ /// The target framework moniker.
+ /// Thrown if the target framework moniker cannot be determined.
+ protected virtual string GetTargetFrameworkMoniker(MetadataFile module, IProjectInfoProvider project)
+ {
+ var targetFramework = TargetServices.DetectTargetFramework(module);
+ if (targetFramework.Identifier == ".NETFramework" && targetFramework.VersionNumber == 200)
+ targetFramework = TargetServices.DetectTargetFrameworkNET20(module, project.AssemblyResolver, targetFramework);
+
+ if (targetFramework.Moniker == null)
+ {
+ throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead.");
+ }
+
+ return targetFramework.Moniker;
+ }
+
static void WriteOutputType(XmlTextWriter xml, bool isDll, Subsystem moduleSubsystem, ProjectType projectType)
{
if (!isDll)
@@ -251,14 +289,28 @@ static void WriteResources(XmlTextWriter xml, IEnumerable files
}
}
- static void WriteReferences(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project, ProjectType projectType)
+ void WriteReferences(XmlTextWriter xml, MetadataFile module, IProjectInfoProvider project)
+ {
+ foreach (var reference in GetReferences(module, project))
+ {
+ WriteReference(xml, reference, project);
+ }
+ }
+
+ ///
+ /// Gets the assembly references for the specified module and project, excluding implicit references and shared assemblies.
+ ///
+ /// The module for which to get the assembly references.
+ /// The project information provider.
+ /// An enumerable of assembly references.
+ protected virtual IEnumerable GetReferences(MetadataFile module, IProjectInfoProvider project)
{
bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == ".NETCoreApp";
var targetPacks = new HashSet();
if (isNetCoreApp)
{
targetPacks.Add("Microsoft.NETCore.App");
- switch (projectType)
+ switch (GetProjectType(module))
{
case ProjectType.WinForms:
case ProjectType.Wpf:
@@ -270,25 +322,38 @@ static void WriteReferences(XmlTextWriter xml, MetadataFile module, IProjectInfo
break;
}
}
-
- foreach (var reference in module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name)))
+ foreach (var reference in module.AssemblyReferences)
{
- if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string runtimePack) && targetPacks.Contains(runtimePack))
+ if (ImplicitReferences.Contains(reference.Name))
{
continue;
}
-
- xml.WriteStartElement("Reference");
- xml.WriteAttributeString("Include", reference.Name);
-
- var asembly = project.AssemblyResolver.Resolve(reference);
- if (asembly != null && !project.AssemblyReferenceClassifier.IsGacAssembly(reference))
+ if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string runtimePack) && targetPacks.Contains(runtimePack))
{
- xml.WriteElementString("HintPath", FileUtility.GetRelativePath(project.TargetDirectory, asembly.FileName));
+ continue;
}
+ yield return reference;
+ }
+ }
- xml.WriteEndElement();
+ ///
+ /// Writes an assembly reference to the project file.
+ ///
+ /// The XML writer used to write the project file.
+ /// The assembly reference to write.
+ /// The project information provider.
+ protected virtual void WriteReference(XmlTextWriter xml, AssemblyReference reference, IProjectInfoProvider project)
+ {
+ xml.WriteStartElement("Reference");
+ xml.WriteAttributeString("Include", reference.Name);
+
+ var assembly = project.AssemblyResolver.Resolve(reference);
+ if (assembly != null && !project.AssemblyReferenceClassifier.IsGacAssembly(reference))
+ {
+ xml.WriteElementString("HintPath", FileUtility.GetRelativePath(project.TargetDirectory, assembly.FileName));
}
+
+ xml.WriteEndElement();
}
static string GetSdkString(ProjectType projectType)
@@ -327,5 +392,21 @@ static ProjectType GetProjectType(MetadataFile module)
return ProjectType.Default;
}
+
+ readonly struct Group : IDisposable
+ {
+ readonly XmlTextWriter xml;
+
+ public Group(XmlTextWriter xml, string name)
+ {
+ this.xml = xml;
+ xml.WriteStartElement(name);
+ }
+
+ public void Dispose()
+ {
+ xml.WriteEndElement();
+ }
+ }
}
}
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
index 18061b9d63..067c030109 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs
@@ -130,7 +130,7 @@ protected WholeProjectDecompiler(
AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver));
AssemblyReferenceClassifier = assemblyReferenceClassifier ?? new AssemblyReferenceClassifier();
DebugInfoProvider = debugInfoProvider;
- this.projectWriter = projectWriter ?? (Settings.UseSdkStyleProjectFormat ? ProjectFileWriterSdkStyle.Create() : ProjectFileWriterDefault.Create());
+ this.projectWriter = projectWriter ?? IProjectFileWriter.FromSettings(settings);
}
// per-run members
From 1fe9ff94cd5a960042f49b47f2613bc001b5e034 Mon Sep 17 00:00:00 2001
From: ds5678 <49847914+ds5678@users.noreply.github.com>
Date: Thu, 23 Jul 2026 18:59:01 -0700
Subject: [PATCH 2/5] Fix nullable error
---
.../CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
index 2e6f670221..dcf332700d 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
@@ -328,7 +328,7 @@ protected virtual IEnumerable GetReferences(MetadataFile modu
{
continue;
}
- if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string runtimePack) && targetPacks.Contains(runtimePack))
+ if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string? runtimePack) && targetPacks.Contains(runtimePack))
{
continue;
}
From 28fc5678b2ac8e31cf1107397e0dc19dd1615049 Mon Sep 17 00:00:00 2001
From: ds5678 <49847914+ds5678@users.noreply.github.com>
Date: Sun, 26 Jul 2026 15:38:56 -0700
Subject: [PATCH 3/5] 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.
---
.../ProjectDecompiler/ProjectFileWriterSdkStyle.cs | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
index dcf332700d..5bef268f3e 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
@@ -105,11 +105,12 @@ void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable
Date: Tue, 28 Jul 2026 23:48:31 -0700
Subject: [PATCH 4/5] Replace events with a GetCustomProperties virtual method
---
.../ProjectFileWriterSdkStyle.cs | 38 +++++++++----------
1 file changed, 18 insertions(+), 20 deletions(-)
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
index 5bef268f3e..e7d75c568d 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs
@@ -63,15 +63,6 @@ enum ProjectType { Default, WinForms, Wpf, Web }
///
public static ProjectFileWriterSdkStyle Default { get; } = new();
- ///
- /// Occurs when a custom property group can be written to the project file.
- ///
- protected event Action, MetadataFile>? WriteCustomPropertyGroup;
- ///
- /// Occurs when a custom item group can be written to the project file.
- ///
- protected event Action, MetadataFile>? WriteCustomItemGroup;
-
///
public void Write(
TextWriter target,
@@ -105,12 +96,15 @@ void Write(XmlTextWriter xml, IProjectInfoProvider project, IEnumerable
+ /// Gets custom properties to be added to the project file. Override this method to provide additional properties.
+ ///
+ /// The project information provider.
+ /// The collection of project item information.
+ /// The metadata file representing the module.
+ /// An enumerable of custom properties as name-value pairs. Null if no custom properties are provided.
+ protected virtual IEnumerable<(string, string)>? GetCustomProperties(IProjectInfoProvider project, IEnumerable files, MetadataFile module)
+ {
+ return null;
+ }
+
static void WriteResources(XmlTextWriter xml, IEnumerable files)
{
// remove phase
From 692e93e7af579b60a8247eccd074d5c347f9b7fe Mon Sep 17 00:00:00 2001
From: ds5678 <49847914+ds5678@users.noreply.github.com>
Date: Wed, 29 Jul 2026 12:52:02 -0700
Subject: [PATCH 5/5] Remove I prefix
---
.../CSharp/ProjectDecompiler/IProjectFileWriter.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
index 82220a3228..ebbe969172 100644
--- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
+++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs
@@ -38,7 +38,7 @@ public interface IProjectFileWriter
/// The module being decompiled.
void Write(TextWriter target, IProjectInfoProvider project, IEnumerable files, MetadataFile module);
}
- public static class IProjectFileWriterExtensions
+ public static class ProjectFileWriterExtensions
{
extension(IProjectFileWriter)
{