ILSpy vs dnSpy vs dotPeek: Which .NET Decompiler, and What to Use on a Mac
On this page
Short answer (checked 2026-11-02): on a Mac or Linux, use ILSpy. Since 11.0 it ships a native macOS ARM64 build, and its command-line twin ilspycmd runs anywhere .NET does. Use dnSpyEx on Windows when you need to step through .NET code without source. Use dotPeek on Windows if you already live in JetBrains tools and want a free decompiler that can also act as a symbol server. The original dnSpy repository has been archived since 2020; dnSpyEx is the maintained continuation.
What I ran and what I didn’t. I tested on an Apple Silicon Mac with no Windows machine or VM. I ran ILSpy 11.2 (GUI and ilspycmd) myself. dnSpyEx’s GUI and debugger are Windows-only, so I did not run them; I did get its command-line decompiler, dnSpy.Console, to run on macOS through an unsupported workaround described below. dotPeek ships Windows installers only, so I did not run it at all. Everything said about the dnSpyEx debugger and about dotPeek comes from the vendors’ documentation, cited inline.
The three tools at a glance
| ILSpy | dnSpyEx | dotPeek | |
|---|---|---|---|
| Latest release (checked 2026-11-02) | 11.2, 2026-10-27 | 6.6.0, 2026-06-20 | 2026.2.4, 2026-10-30 |
| Licence | MIT (open source) | GPL-3.0 (open source) | Free of charge, proprietary |
| Platforms | Windows, macOS (ARM64), Linux | Windows (WPF) | Windows (x64, x86, ARM64 installers) |
| Decompile to C# | Yes (run here) | Yes (console engine run here via a workaround) | Yes (vendor docs; not run) |
| Debugger | No | Yes (vendor docs; not run) | No; can serve symbols to a debugger (vendor docs) |
| Command line | ilspycmd dotnet tool |
dnSpy.Console |
Not covered on its product page |
| Single-file bundles | Yes | Not documented in the sources I used | Not documented in the sources I used |
Sources: the ILSpy 11.2, 11.1 and 11.0 release notes, the dnSpyEx 6.6.0 release notes and README, and JetBrains’ dotPeek page. The dotPeek version and platforms come from JetBrains’ public release feed, which on 2026-11-02 listed only Windows downloads (x64, x86, ARM64 and a web installer).
Where each project stands in 2026
ILSpy: now cross-platform
ILSpy 11.0 (August 2026) ported the UI from WPF to Avalonia. In the project’s words, “With this port, ILSpy is now available on Linux and Mac in addition to Windows,” and the WPF front end was removed. The 11.2 release page (2026-10-27) offers Windows installers for x64 and ARM64, Linux zip, .deb and .rpm packages, and a macOS ARM64 zip. That makes ILSpy the practical dnSpy alternative on Mac and Linux for browsing and decompiling. It does not add a debugger.
11.0 also targeted agent use of ilspycmd: it added --member for decompiling a single member and --dump-table for metadata tables. The release notes say downloads of the dotnet tool exceed 70k a week and credit agents such as Claude for the growth.
Visual Studio’s own debugger decompiler is built on ILSpy. Microsoft’s decompilation page says source generation “is based on the open source ILSpy project” and has been available since Visual Studio 2019 16.5.
dnSpy and dnSpyEx
The original dnSpy/dnSpy repository on GitHub is archived, with its last push on 2020-12-20. dnSpyEx describes itself as “an unofficial continuation of the dnSpy project which is a debugger and .NET assembly editor.” Version 6.6.0 is its first stable release in two years. It moved to .NET 10, added a dnSpy-net build that uses an installed .NET 10 runtime, added a String References window, and fixed decompiler crashes for runtime-async methods.
Two points from that release page matter when you choose:
- It is a Windows WPF application. 6.6.0 “detects when it is being run under Wine on Linux” and disables hardware acceleration to improve compatibility. That is a compatibility tweak, not a Linux or macOS build.
- The maintainer states there “will not be an official dnSpy AI integration, or an MCP server.”
Its debugger is the reason most people pick it. The README lists debugging .NET Framework, .NET and Unity assemblies “no source code required”, breakpoints in any assembly, and locals, watch and call-stack windows. dnSpyEx can also edit and save assemblies; use that only on your own code or code you are authorized to examine.
dotPeek
JetBrains describes dotPeek as “a free-of-charge standalone tool based on ReSharper’s bundled decompiler.” It decompiles to C# or IL, can save a decompiled assembly as a Visual Studio project, and “can also perform as a symbol server” so that the Visual Studio debugger can step into decompiled library code. JetBrains Rider, which does run on macOS, has its own integrated decompiler and can debug decompiled external code; per the Rider 2026.2 external code debugging help, stepping into external methods is not supported on ARM64 macOS. Rider is a separate, licensed IDE, and I did not test it.
Same DLL, decompiled
To compare output I wrote a small library with a record, a LINQ chain, an async method and a switch expression, and built it in Release with the .NET 10.0.403 SDK (Demo.dll, 10,240 bytes):
public record Order(int Id, string Customer, decimal Total, bool Paid);
public static class OrderReport
{
public static IEnumerable<string> TopCustomers(IEnumerable<Order> orders, int count) =>
orders.Where(o => o.Paid)
.GroupBy(o => o.Customer)
.OrderByDescending(g => g.Sum(o => o.Total))
.Take(count)
.Select(g => $"{g.Key}: {g.Sum(o => o.Total):0.00}");
public static async Task<int> CountLinesAsync(string path, CancellationToken ct = default)
{
using var reader = new StreamReader(path);
var lines = 0;
while (await reader.ReadLineAsync(ct) is not null)
lines++;
return lines;
}
public static string Describe(Order o) => o switch
{
{ Paid: false } => "unpaid",
{ Total: > 1000m } => "large",
_ => "normal",
};
}
ILSpy 11.2
ilspycmd -t Demo.Order Demo.dll returned the record as one line, exactly like the source:
public record Order(int Id, string Customer, decimal Total, bool Paid);
ilspycmd -t Demo.OrderReport Demo.dll returned this (the using lines are omitted):
public static IEnumerable<string> TopCustomers(IEnumerable<Order> orders, int count)
{
return from g in (from o in orders
where o.Paid
group o by o.Customer into g
orderby g.Sum((Order o) => o.Total) descending
select g).Take(count)
select $"{g.Key}: {g.Sum((Order o) => o.Total):0.00}";
}
public static async Task<int> CountLinesAsync(string path, CancellationToken ct = default(CancellationToken))
{
using StreamReader reader = new StreamReader(path);
int lines = 0;
while (await reader.ReadLineAsync(ct) != null)
{
lines++;
}
return lines;
}
public static string Describe(Order o)
{
if ((object)o != null)
{
if (!o.Paid)
{
return "unpaid";
}
if (o.Total > 1000m)
{
return "large";
}
}
return "normal";
}
The record and the async method came back as you would write them; the compiler-generated state machine is hidden. The LINQ chain is correct but rewritten in query syntax, with explicit lambda parameter types. The switch expression became nested if statements with a (object)o != null check: same logic, different shape. One type took 0.43 s and about 105 MB of peak memory on an Apple M5 Pro.
The ILSpy 11.2 GUI (ILSpy_macos-arm64_11.2.0.10050.zip) also opened Demo.dll on macOS and listed it as Demo (1.0.0.0, .NETCoreApp, v10.0). One Mac gotcha: the ILSpy.app bundle is ad-hoc signed rather than notarized, and spctl rejected it. I downloaded it with curl, which sets no quarantine flag, so it opened; if you download it through a browser, expect Gatekeeper to block it until you allow it.
dnSpyEx 6.6.0 (console engine on macOS, via a workaround)
dnSpyEx publishes a framework-dependent build, dnSpy-net.zip, which contains dnSpy.Console.dll. Run as shipped on macOS, it fails because it asks for the Windows desktop runtime:
$ dotnet bin/dnSpy.Console.dll --help
You must install or update .NET to run this application.
Framework: 'Microsoft.WindowsDesktop.App', version '10.0.0' (arm64)
No frameworks were found.
In a copy of the folder I deleted the Microsoft.WindowsDesktop.App entry from dnSpy.Console.runtimeconfig.json. The console tool then started and decompiled. This is not a supported way to run dnSpyEx: it says nothing about the GUI or the debugger, and it may break in any release. It does show what dnSpyEx’s decompiler engine produces with its default options. dnSpy.Console -t Demo.OrderReport Demo.dll, with the per-member token comments removed:
public static IEnumerable<string> TopCustomers(IEnumerable<Order> orders, int count)
{
return Enumerable.Select<IGrouping<string, Order>, string>(Enumerable.Take<IGrouping<string, Order>>(Enumerable.OrderByDescending<IGrouping<string, Order>, decimal>(Enumerable.GroupBy<Order, string>(Enumerable.Where<Order>(orders, (Order o) => o.Paid), (Order o) => o.Customer), (IGrouping<string, Order> g) => Enumerable.Sum<Order>(g, (Order o) => o.Total)), count), delegate(IGrouping<string, Order> g)
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler;
defaultInterpolatedStringHandler..ctor(2, 2);
defaultInterpolatedStringHandler.AppendFormatted(g.Key);
defaultInterpolatedStringHandler.AppendLiteral(": ");
defaultInterpolatedStringHandler.AppendFormatted<decimal>(Enumerable.Sum<Order>(g, (Order o) => o.Total), "0.00");
return defaultInterpolatedStringHandler.ToStringAndClear();
});
}
public static Task<int> CountLinesAsync(string path, CancellationToken ct = default(CancellationToken))
{
OrderReport.<CountLinesAsync>d__1 <CountLinesAsync>d__;
<CountLinesAsync>d__.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
<CountLinesAsync>d__.path = path;
<CountLinesAsync>d__.ct = ct;
<CountLinesAsync>d__.<>1__state = -1;
<CountLinesAsync>d__.<>t__builder.Start<OrderReport.<CountLinesAsync>d__1>(ref <CountLinesAsync>d__);
return <CountLinesAsync>d__.<>t__builder.Task;
}
public static string Describe(Order o)
{
if (o != null)
{
if (!o.Paid)
{
return "unpaid";
}
if (o.Total > 1000m)
{
return "large";
}
}
return "normal";
}
Each member carries a comment such as // Token: 0x06000017 RID: 23 RVA: 0x00002440 File Offset: 0x00000640, which is handy when you need to map C# back to bytes in the file. Compared with ILSpy 11.2 on this .NET 10 build:
- Async: dnSpyEx showed the compiler’s state-machine stub instead of
async/await, even though its--asyncoption defaults to on. - String interpolation: it expanded
$"..."into theDefaultInterpolatedStringHandlercalls the compiler emits, including the non-C#..ctor(2, 2). - Records:
Ordercame back as a full class withEqualityContract,PrintMembers,Equals,GetHashCodeandDeconstruct, well over 100 lines instead of one. - LINQ: nested static
Enumerable.*calls rather than query or fluent syntax. - Switch expression: almost the same as ILSpy, without the
(object)cast.
Both produced correct logic; ILSpy’s output is much closer to what a person writes in modern C#. Speed was similar for one type (0.36 s and about 82 MB peak for dnSpyEx). I could not check whether the dnSpyEx GUI on Windows behaves differently.
dotPeek
Not run: there is no macOS or Linux build. If you have Windows, decompile the same Demo.dll and look at the same four constructs first: records, async, interpolated strings and pattern matching. They are where decompilers, and versions of the same decompiler, differ most.
Debugging your own code without its source
The common legitimate case: a crash happens inside a library you depend on, or inside an old build of your own app whose PDB is lost. I did not run any of the following; they summarise the vendors’ documentation.
- Try source first. If the package has Source Link or you still have the PDB, a normal debugger shows the real source. Microsoft’s decompilation page points to Source Link for libraries and NuGet packages that enable it.
- Visual Studio 2022 17.7 and later automatically decompiles external .NET code when you step into it, if Just My Code is off, or when you double-click a frame in the Call Stack window. You can set breakpoints in the generated code. Microsoft lists limits: optimized builds may bind breakpoints to the wrong line, local names may be missing, and async code may decompile incompletely.
- dnSpyEx offers the same “debug without source” ability as a standalone Windows tool, across .NET Framework, .NET and Unity, per its README.
- dotPeek can act as a symbol server so that the Visual Studio debugger gets source for assemblies you have no PDB for, per JetBrains.
ILSpy itself does not debug. ilspycmd -genpdb can generate a portable PDB from decompiled code, which a debugger can then load. I did not test that path.
Automating with ilspycmd
ilspycmd installs as a .NET tool:
dotnet tool install ilspycmd --version 11.2.0.10050 --tool-path ./tools
./tools/ilspycmd --version
# ilspycmd: 11.2.0.10050
# ICSharpCode.Decompiler: 11.2.0.10050
Options I used: -t <type> for one type, -m <token or doc id> for one member, -l c to list classes, -p -o <dir> for a compilable project, --bundle-entry and -d for single-file bundles (see is this EXE a .NET app?), and --disable-updatecheck for scripts. Given a NativeAOT binary or a native apphost it fails with PE file does not contain any managed metadata. The 11.2 release notes add opt-in Native AOT packages of ilspycmd for linux-x64 and win-x64 and report 4x to 7x faster calls on Windows; there is no macOS AOT package, so I used the normal managed tool.
When you do not need a decompiler
Many questions are answered by metadata alone: target framework, references, version, P/Invoke imports, or whether the file is .NET at all. This site’s .NET metadata and IL viewer shows those and the IL of each method in the browser, without uploading the file. It does not produce C#. For the framework and version questions, see check a .NET DLL’s target framework.
Which should you pick?
- On macOS or Linux, or in CI and agent workflows: ILSpy and
ilspycmd. It has no debugger. - On Windows and you must step through code without source: Visual Studio’s built-in decompilation if you have it, otherwise dnSpyEx.
- Already using ReSharper or Rider, on Windows: dotPeek is a natural fit, and its symbol server helps the Visual Studio debugger.
- The file is NativeAOT: none of them; there is no IL. Use native tools.
FAQ
Can I run dnSpyEx on a Mac with Wine? The 6.6.0 release notes only mention detecting Wine on Linux. I did not try Wine on macOS.
Can obfuscated assemblies be read? Decompilers show what the IL contains. Renamed symbols stay renamed and restructured control flow stays restructured. Only analyze code you own or are authorized to examine.