Skip to content
← All posts
· Delta1 Labs Glass.NETDecompilerGuide.NET

Decompile a single-file or self-contained .NET app with Glass.NET

You published a .NET app, you drag the .exe onto a decompiler, and nothing readable comes out. The problem is not the decompiler — it is that dotnet publish has several formats and most do not hand you a managed assembly where you expect it. Here is where your IL actually lives in a self-contained, single-file, ReadyToRun or Native AOT build, and how to open it in Glass.NET.

You published a .NET app. Now you want to read what actually shipped — maybe it is your own release and the source is long gone, maybe it is a tool whose behaviour you are investigating. So you drag the .exe onto Glass.NET and… nothing useful comes back. No namespaces, no methods, no C# — just a stub, or a flat refusal. It is tempting to conclude the decompiler is broken. It is not. The problem is that dotnet publish has several output formats, and most of them do not put a decompilable managed assembly at the path you instinctively reach for.

Once you know which format you are holding and where its managed IL really lives, the decompile itself is the easy part. This post walks the formats — self-contained, single-file, ReadyToRun and Native AOT — says exactly where the readable IL is in each (or honestly, when there is none), and then runs the normal Glass workflow on it. Glass.NET is free and strictly static: it reads the managed assembly’s metadata and IL and reconstructs C#, and it never runs the app it inspects. That matters doubly here, because a published app is precisely the thing you would otherwise have had to execute to find out what it does.

The thing that confuses everyone: the apphost is not your code

Modern dotnet publish wraps your program in a native launcher called the apphost — the MyApp.exe you double-click. The apphost is a small native bootstrapper, and its only job is to start the .NET runtime and point it at your managed assembly. It is native machine code; it contains none of your C#. A decompiler reads managed assemblies — IL plus metadata — so aiming it at the apphost .exe aims it at the wrong file entirely.

Your code is in a managed .dll. Whether that DLL is sitting next to the apphost or hidden inside it is the only real variable, and it is decided by the publish format. So the whole task reduces to one question: where is the managed DLL? Here is the answer, format by format.

Self-contained: open the managed DLL, not the .exe

A self-contained publish ships your app together with its own private copy of the .NET runtime, so it runs on a machine with no .NET installed. Open the publish folder and you will see a crowd of files: the native apphost MyApp.exe, dozens of runtime assemblies (System.*.dll, Microsoft.*.dll), some native libraries (*.dll, *.so, *.dylib) — and, sitting right there on disk, the one you want: your own managed assembly, MyApp.dll.

That MyApp.dll is exactly what the C# compiler emitted — ordinary IL and full metadata. Open it in Glass (File ▸ Open Assembly, or drag it onto the window), not the .exe. The apphost beside it is a red herring; ignore it. Self-contained is not single-file unless you also asked for that, so by default nothing is bundled and nothing needs extracting — the managed DLL is already on disk, ready to read. If you are unsure which of the many DLLs is yours, sort by name and look for your assembly name, or open a candidate and check the Assembly Information view for your product attributes rather than a System.* identity.

Single-file: extract the bundle first, then open the DLL

A single-file publish (PublishSingleFile) goes one step further. It takes those managed assemblies and embeds them inside the apphost .exe as resources, so you distribute one file instead of a folder. The managed IL is still in there, byte-for-byte unchanged — but it is packaged inside the native host, so there is no MyApp.dll on disk for Glass to open. Glass reads managed assemblies; it does not crack a native bundle open for you. So you extract the bundle first with the standard .NET mechanism, then open the DLL that falls out.

The extractor is built into the runtime. Point DOTNET_BUNDLE_EXTRACT_BASE_DIR at a directory and run the app once; the host unpacks its embedded assemblies there before it launches:

# single-file app: unpack the embedded assemblies to disk
DOTNET_BUNDLE_EXTRACT_BASE_DIR=./extracted ./MyApp
# ./extracted/<appname>/<hash>/MyApp.dll  — plain managed IL, ready for Glass

One honest caveat: this approach runs the app to make it self-extract. For your own release that is no problem. For a binary you do not trust, do not run it — reach instead for a standalone bundle-extractor tool that reads the documented bundle layout and writes the embedded DLLs to disk without launching the executable. Either way you end up with MyApp.dll on disk, and from that point Glass stays fully static: it only ever opens the already-extracted DLL. Drop that MyApp.dll onto Glass and you are back to an ordinary decompile.

ReadyToRun: still IL underneath, decompiles fine

ReadyToRun (PublishReadyToRun) is an easy one to worry about unnecessarily. R2R precompiles a native image of your methods ahead of time to cut startup cost — but it keeps the original IL alongside that native image as a fallback. The result is still a perfectly ordinary managed assembly with full metadata and IL. Glass reads an R2R assembly exactly like any other DLL; the precompiled native code is simply ignored, and you decompile the IL as usual. R2R changes how fast your app starts, not whether it can be read.

Native AOT: there is nothing to decompile — for anyone

Native AOT (PublishAot) is the honest exception, and it is worth being blunt about. AOT compiles your IL all the way to native machine code ahead of time and discards the IL. The published artifact is a true native binary: no managed assembly, no metadata, no IL. There is nothing for Glass — or ILSpy, or dotPeek, or any .NET decompiler — to reconstruct, because the thing a decompiler reads has been compiled away. This is not a Glass limitation; it is the nature of the format, and no decompiler can round-trip native code back into the C# that produced it. If you must analyze a NativeAOT binary you are in native reverse-engineering territory — a native disassembler — not .NET decompilation. (One practical escape hatch: many real apps are only partly AOT, and any managed assembly that was left out of the AOT image is back to being a normal, decompilable DLL. The protection trade-offs behind all this are covered in obfuscating Native AOT and single-file apps.)

Then it is the normal Glass workflow

Once you have the managed DLL on disk — straight out of a self-contained folder, or extracted from a single-file bundle — everything after that is the ordinary Glass flow, the same one in how to decompile a .NET DLL to C#:

  • Open it and orient. Glass loads the metadata and lays the assembly out as a namespaces ▸ types ▸ members tree. Glance at the Assembly Information view first — target framework and referenced assemblies — to confirm you grabbed your assembly and not a runtime one that rode along in the publish folder.
  • Decompile to C#. Click any type or member and Glass reconstructs readable C# on demand. You do not need a matching PDB; the metadata carries the real type and method names, and Glass synthesizes local names when symbols are absent.
  • Drop to IL when the C# is ambiguous. Switch the same member to the IL view when a reconstructed async method or iterator reads oddly — the IL is what actually runs, so it is the ground truth. (Reading IL walks through it.)
  • Export to a project. When you want the code on disk to grep, diff or open in an IDE, File ▸ Export to Project (or the glass export CLI) writes a .csproj plus the reconstructed .cs files. The full walkthrough is in exporting an assembly to a buildable project.

The takeaway

The publish format is a packaging decision. It changes where your managed IL lives, not whether it can be read. Self-contained leaves the DLL on disk — open it, not the apphost. Single-file hides it inside the apphost — extract the bundle, then open the DLL. ReadyToRun keeps the IL — open the assembly directly. Native AOT compiles the IL away — and then there is genuinely nothing to decompile, for Glass or anyone else. Know which one you are holding and the decompile is two clicks away. And because Glass.NET is free and strictly static, you read the whole thing without running a single line of it. Download Glass.NET, point it at the managed DLL, and read what shipped.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.