dnlib: reading and rewriting .NET assemblies from C#
Reads and writes .NET assemblies and modules
At a glance
- What is it?
- dnlib is an MIT-licensed C# library that loads .NET modules and assemblies, exposes their metadata and IL, and writes them back out. It suits tool authors who need to inspect or modify binaries, not application developers looking for a general reflection helper.
- Who is it for?
- Adopt dnlib if you are building a tool that has to inspect or rewrite .NET metadata and IL, and you are comfortable working against the source in the repository rather than a written manual. Do not adopt it if you only need to load types at runtime; System.Reflection covers that without a binary writer.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 122 days ago.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What dnlib is for, and who ends up using it
dnlib solves a narrow problem: a .NET assembly is a PE file with a metadata blob and IL method bodies inside it, and the runtime's reflection API gives you almost no way to change any of that and save it back. dnlib gives you an object model over the whole file. You load a module, walk its types, methods, fields and instructions, mutate them, and write a new file. The README describes it plainly as a ".NET module/assembly reader/writer library".
The audience is tool authors. If you are writing an IL editor, a packer, an obfuscator, a deobfuscator, a patcher that rewrites method bodies, or a build step that post-processes an assembly, this is the layer you build on. If you are writing an application and want to inspect loaded types, System.Reflection is the right tool and dnlib is overhead you do not need. The library is also not a decompiler; it gives you the metadata and the instruction stream, and what you print from that is your problem.
The ModuleDefMD pipeline: load, mutate, write
Everything starts with ModuleDefMD. Its Load() methods accept a file path, a byte array, a Stream, a memory address (an HINSTANCE), or a System.Reflection.Module instance. If the input is not a .NET module or assembly, Load() throws BadImageFormatException, which is the documented failure mode for bad input.
The important architectural detail is the ModuleContext. The README's examples create one with ModuleDef.CreateModuleContext() and pass it to Load() rather than calling the single-argument overload. That context carries the assembly and type resolvers, and the README is explicit that for .NET Core assemblies you have to disable GAC loading and add .NET Core reference assembly search paths, because the default resolver assumes the classic GAC layout. Get this wrong and resolution of referenced types silently looks in the wrong places.
Once loaded, module.Assembly gives you the AssemblyDef, and the dnlib.DotNet.Emit namespace (needed only if you touch method bodies) gives you the instruction model. Writing is symmetric: module.Write(path) for IL-only assemblies, module.NativeWrite(path) for C++/CLI assemblies that carry native code. The README recommends branching on module.IsILOnly to pick between them at runtime rather than hardcoding one call.
The metadata model is mutable, so a typical tool loads a module, edits the type or method graph in memory, and writes the result. The README does not document rollback or an undo mechanism, so treat the in-memory graph as the only copy and re-load from disk if an edit goes wrong.
Installing dnlib and loading your first assembly
dnlib ships as a NuGet package, which is the installation path the README badge points at. Add it to a project with the CLI, then reference the two namespaces the README says matter: dnlib.DotNet for the module model and dnlib.DotNet.Emit if you intend to read or write method bodies.
dotnet add package dnlibWith the package restored, the smallest useful program loads a module through an explicitly created ModuleContext and prints the assembly identity. The README's own example does exactly this, and the printed value comes from the AssemblyDef's ToString().
using dnlib.DotNet;
ModuleContext modCtx = ModuleDef.CreateModuleContext();
ModuleDefMD module = ModuleDefMD.Load(@"C:\path\to\file.exe", modCtx);
AssemblyDef asm = module.Assembly;
Console.WriteLine("Assembly: {0}", asm);What you should see is a line naming the assembly. If the file is not a managed assembly, Load throws BadImageFormatException instead, which is the signal that you pointed it at the wrong file rather than that the library failed.
Saving is one call, and the README notes the C++/CLI branch. If you are only handling C# or VB output, the first form is enough.
module.Write(@"C:\saved-assembly.dll");For obfuscated Unity or Mono assemblies the README gives a specific instruction: create a ModuleCreationOptions instance, set its Runtime property to CLRRuntimeReaderKind.Mono, and pass those options into Load(). That path is not the default and is easy to miss when a Mono assembly fails to load cleanly.
PDB handling and the Windows-only writer
PDB support is where dnlib's platform story gets uneven. Reading is broad: the README states that dnlib has a managed Windows PDB reader that works on all operating systems, and that PDB files are read from disk by default unless you change that through ModuleCreationOptions.
Writing is narrower. Windows PDBs can only be written on Windows; portable PDBs can be written on any OS, and dnlib supports Windows PDBs, portable PDBs and embedded portable PDBs. So a cross-platform tool that must emit debug symbols has to choose portable PDBs, or accept that its Windows PDB path is Windows-only. That constraint is a property of the format and the native writer, not a bug, but it shapes what you can promise users.
Enabling PDB output means constructing ModuleWriterOptions (or NativeModuleWriterOptions) and setting WritePdb to true before calling Write. By default the PDB takes the output assembly's name with a .pdb extension. The README warns about one coupling: if you initialize PdbStream, you should also initialize PdbFileName, because the PDB file name is written into the PE file itself. Setting one without the other produces an output whose debug directory points somewhere inconsistent.
For the native writers, dnlib prefers Microsoft.DiaSymReader.Native when present and falls back to diasymreader.dll from .NET Framework. The README is clear that dnlib does not add a reference to the Microsoft.DiaSymReader.Native NuGet package; you have to add it yourself if you want that path, and PdbReaderOptions and PdbWriterOptions let you disable either implementation.
Strong name signing, including the enhanced variants
dnlib can strong name sign on write. The README's sequence is: load or create the module, create a ModuleWriterOptions, open a StrongNameKey from an .snk file, call opts.InitializeStrongNameSigning(mod, signatureKey), then call mod.Write with those options. The Initialize call is what wires up the required properties, so skipping it and setting fields by hand is not the documented route.
Enhanced strong name signing has two documented shapes. Without key migration you supply a StrongNameKey and a StrongNamePublicKey and call InitializeEnhancedStrongNameSigning(mod, signatureKey, signaturePubKey). With key migration you supply four values: signature key and public key, plus a separate identity key and identity public key, and call the four-argument overload. The README points at an MSDN article for the background on enhanced strong naming rather than explaining the cryptography itself, which is a fair division of labour but means you should understand what key migration does to your assembly identity before choosing that overload. The distinction matters because the identity key and the signature key serve different roles, and the API makes you supply both explicitly.
Where dnlib is the wrong choice
The clearest limitation is documentation depth. There is no manual in the repository. What exists is the README's worked examples, seven files under Examples/ (Example1.cs through Example7.cs plus Program.cs), and the source under src/. The README covers loading, saving, PDBs and strong naming, and then stops. For anything past that, such as how a particular metadata table is modelled or what a writer option does to the output, the source is the reference. If your team needs prose documentation before adopting a dependency, this is a real cost.
A second boundary is scope. dnlib reads and writes assemblies; it does not decompile them, and it does not run them. If your goal is to display C# from a binary, you are building the decompiler on top of dnlib's metadata and IL, which is a substantially larger project than wiring up a library. Related searches for decompilers and ILSpy point at tools in that space, not at dnlib itself.
Finally, the resolver defaults assume a desktop .NET layout. The README's own comment about disabling GAC loading and adding .NET Core reference assembly search paths for .NET Core assemblies is the tell: out of the box, resolution behaviour is tuned for one runtime family, and modern targets require configuration. Tools that process a wide range of assemblies should expect to spend time on resolver setup rather than on the metadata edits they actually care about.
How dnlib differs from Mono.Cecil and System.Reflection
Mono.Cecil is the obvious alternative and the comparison is about the object model rather than the feature list. Both read and write .NET assemblies and both expose IL. Cecil is the older, more widely embedded option in the .NET tooling ecosystem, and it has a reputation for a stable API surface. dnlib's README positions the library around its writer options: explicit ModuleWriterOptions and NativeModuleWriterOptions objects, separate IL-only and native write paths chosen via IsILOnly, and PDB control through WritePdb, PdbFileName and PdbStream. If you need to write C++/CLI assemblies, the NativeWrite path is called out directly, and that is a concrete difference in what the writer handles.
System.Reflection is not a competitor for this job at all. It gives you read-only access to loaded types and cannot emit a modified assembly. dnlib's Load() can accept a System.Reflection.Module, which is a convenience bridge, but the point of the library is the write side that reflection lacks. Choose dnlib when mutation and re-emission are the requirement; choose reflection when you only need to inspect what is already loaded.
Maintenance, licensing and what upgrading costs
The repository is not archived, and the last push was on 2026-06-02. Recent tagged releases are v4.5.0 on 2025-05-15, v4.4.0 on 2024-01-16 and v4.3.0 on 2023-11-05, so the release cadence is measured in months to a year rather than weeks. That is normal for a library whose surface is a file format, but it means you should not expect quick turnarounds on issues.
The licence is MIT, recorded in LICENSE.txt at the repository root. MIT is permissive: it allows use in closed-source products provided the copyright notice and permission notice are preserved. That is a description of the licence text, not legal advice; if you are redistributing dnlib inside a commercial product, have your own counsel confirm the notice requirements.
Upgrade cost is mostly API surface. The public model is large, and the README's examples show how much of it you touch for ordinary tasks: ModuleContext, ModuleDefMD, AssemblyDef, ModuleWriterOptions, StrongNameKey and the Emit namespace. A version bump that changes any of those affects every call site. The repository ships a build.ps1 and dnlib.sln, so building from source is straightforward if you need to patch or pin a behaviour. The README does not document a compatibility policy or a deprecation window, so pinning a known-good version and reading the release notes before moving is the honest approach.
Editorial conclusion
Adopt dnlib if you are building a tool that has to inspect or rewrite .NET metadata and IL, and you are comfortable working against the source in the repository rather than a written manual. Do not adopt it if you only need to load types at runtime; System.Reflection covers that without a binary writer. Before committing, verify that ModuleDefMD.Load returns a module for a representative sample of your target assemblies, including any obfuscated Unity or Mono builds, and confirm which PDB formats you actually need to write on your target OS, since Windows PDB writing is limited to Windows.
Frequently asked questions
How do I install dnlib?
dnlib is distributed as a NuGet package, which is what the README's NuGet badge links to. Add it to a project with the .NET CLI and then reference dnlib.DotNet, plus dnlib.DotNet.Emit if you need to read or write method bodies.
How do I use dnlib to open a .NET assembly?
Create a ModuleContext with ModuleDef.CreateModuleContext(), pass it to ModuleDefMD.Load() along with a file path, byte array, Stream, memory address or System.Reflection.Module, and read the Assembly property for the AssemblyDef. If the file is not a .NET module or assembly, Load throws BadImageFormatException.
Does dnlib write PDB files on Linux?
Portable PDBs can be written on any OS, but Windows PDBs can only be written on Windows. Reading Windows PDBs is not restricted, because dnlib includes a managed Windows PDB reader that supports all operating systems.
Can dnlib write C++/CLI assemblies?
Yes. The README says to call NativeWrite() instead of Write() for C++/CLI assemblies, and shows branching on module.IsILOnly at runtime to choose between the two.
Does dnlib strong name sign assemblies?
It does. You create a ModuleWriterOptions, open a StrongNameKey, call InitializeStrongNameSigning(mod, signatureKey), and then write the module with those options. Enhanced strong name signing has separate overloads, with and without key migration.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/0xd4d-dnlib)