Inspecting .NET assemblies with dnlib

Ever wondered what's in a .NET assembly dll? Don't want to load it into the current AppDomain or litter the Global Assembly Cache? Let's see how we can safely explore assembly metadata with PowerShell...

A simple question about reflection...

Yesterday I encountered this question on StackOverflow, titled "Get all Methods by Attribute from a .NET Core DLL using Reflection". It was closed as off-topic, but I found the question interesting, so let's dig in. The question, paraphrased, was:

I have a DLL containing a .NET Core assembly, and I want to discover all methods decorated with a specific attribute and return the method names - how can I do that in PowerShell?

Excellent question!

Reflection against .NET types in PowerShell

As you may be aware, .NET has strong support for reflection, that is: the type system in .NET is instrumented to allow objects to expose their own type information. We can leverage this to easily discover type metadata for any assembly available to the runtime. As an example, here's how you could easily enumerate all public enum types in every single assembly currently loaded by your PowerShell host application:

$AllAssemblies = [AppDomain]::CurrentDomain.GetAssemblies()
$AllTypes      = $AllAssemblies.GetTypes()
$AllEnums      = $AllTypes.Where({$_.IsEnum})
$PublicEnums   = $AllEnums.Where({$_.IsPublic})

At this point $PublicEnums will be an array of [type] objects, each representing a public enum type. I won't go into much more detail about how to use the [type] API to navigate the type system, but I invite you to watch the first half of this recording from PSConfEU 2018, and check out the code behind Patrick Meinecke's ImpliedReflection module.

ReflectionOnlyLoad

There might be situations where you definitely don't want to load an assembly into PowerShell before inspecting it:

  • Code from and unknown source - does it actually look as advertised?
  • Weak type name references in my scritps that might resolve a conflicting type
  • Auto-completion for type names will slow down with more public types loaded

The .NET Framework CLR has a wonderful facility for inspecting assemblies without allowing them to execute or influence the GAC, called a "Reflection-Only Context". This is used by loading an assembly via the Assembly.ReflectionOnlyLoad*() methods which in turn return a "shallow" Assembly object, the metadata of which we can then inspect:

PS ~> Add-Type 'public class A { }' -OT Library -OA .\testAssembly.dll
PS ~> [A] # test if we can resolve `A`
Unable to find type [A].
At line:1 char:1
+ [A]
+ ~~~
    + CategoryInfo          : InvalidOperation: (A:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

PS ~> $testAssembly = # Let's load the assembly with ReflectionOnly*
>> [System.Reflection.Assembly]::ReflectionOnlyLoadFrom("$pwd\testAssembly.dll")
>>
PS ~> $testAssembly.GetType('A') # and test that we can inspect [A]

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    A                                        System.Object

PS ~> [A] # current app domain still unaffected
Unable to find type [A].
At line:1 char:1
+ [A]
+ ~~~
    + CategoryInfo          : InvalidOperation: (A:TypeName) [], RuntimeException
    + FullyQualifiedErrorId : TypeNotFound

Reflection-Only and .NET Core

Unfortunately, this facility has not been ported to .NET Core, for a variety of reasons. Loading previous versions of the runtime into a reflection-only context is, as an example, not supported.

.NET Core comes with a more "low-level" facility - the MetadataReader class - to read the raw type metadata from an assembly file. I really want to explore the Reflection.Metadata API at some point, but for the purposes of this walkthrough I'm going to do something else, because:

  • The System.Reflection.MetaData API is a bit more complicated than ReflectionOnly
  • The Metadata API is not available in .NET Framework, and I want this to work in Windows PowerShell as well

Did you do something cool with MetadataReader in PowerShell Core? I'd love to check it out, hit me up on twitter!

dnlib to the rescue!

While looking for existing tools leveraging ICSharpCode.Decompiler - the IL decompiler behind ILSpy - I stumbled across a different library written by the author of dnSpy, called dnlib, and it seems to check all the boxes:

  • Reads metadata from assemblies
  • Doesn't meddle with the GAC
  • Compiles against .NET Standard!

So let's give it a try! (NOTE: dnlib is written in C# 8.0, and will require the compiler that ships with Visual Studio 2019, so head over and download+install the Community Edition if you don't have it already)

Download dnlib

dnlib is available on GitHub, so let's start by cloning the git repo:

PS ~\GitHub> git clone https://github.com/0xd4d/dnlib.git
Cloning into 'dnlib'...
remote: Enumerating objects: 35, done.
remote: Counting objects: 100% (35/35), done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 16989 (delta 15), reused 25 (delta 10), pack-reused 16954
Receiving objects: 100% (16989/16989), 3.73 MiB | 3.95 MiB/s, done.
Resolving deltas: 100% (14191/14191), done.

Compile from source

Next up, we'll need to compile the library from source. Navigate to the root of the repo and kick off the build process with dotnet build

PS ~\GitHub\dnlib> dotnet build
Microsoft (R) Build Engine version 16.3.0+0f4c62fea for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

  Restore completed in 376.48 ms for C:\Users\IISResetMe\GitHub\dnlib\src\dnlib.csproj.
  Restore completed in 442.14 ms for C:\Users\IISResetMe\GitHub\dnlib\Examples\Examples.csproj.
  dnlib -> C:\Users\IISResetMe\GitHub\dnlib\src\bin\Debug\net45\dnlib.dll
  dnlib -> C:\Users\IISResetMe\GitHub\dnlib\src\bin\Debug\netstandard2.0\dnlib.dll
  Examples -> C:\Users\IISResetMe\GitHub\dnlib\Examples\bin\Debug\net45\Examples.exe
  Examples -> C:\Users\IISResetMe\GitHub\dnlib\Examples\bin\Debug\netcoreapp2.1\Examples.dll

Build succeeded.
    0 Warning(s)
    0 Error(s)

Time Elapsed 00:00:13.89

Sweet, now we can start playing with it!

Put it to work!

I had a gander at the Examples included in the repo, and the very first example shows how to load and inspect the current runtime assembly (mscorlib) via [dnlib.DotNet.ModuleDefMD]::Load(). The Load() method in question has an overload with a filename parameter - sounds even better!

With this in mind, I put together the following PowerShell script to enumerate all types in a given assembly file and print the names of all properties with the [Parameter()] attribute attached:

# Create a new library with a no-op cmdlet type
Add-Type @'
using System;
using System.Management.Automation;

[Cmdlet(VerbsCommon.Get, "Greeting")]
public class MyCommand : Cmdlet
{
  [Parameter(Mandatory = true)]
  public string MyName { get; set; }
}
'@ -OT Library -OA MyModule.dll -ReferencedAssemblies System.Management.Automation

# Load the dll with dnlib
$MyModule = [dnlib.DotNet.ModuleDefMD]::Load("$pwd\MyModule.dll", [dnlib.DotNet.ModuleContext]::new())

# Enumerate all Types
$Types = $MyModule.GetTypes()

# For every type definition, inspect the properties 
# Filter their CustomAttributes against the attribute name we expect
# Finally output the property name(s)
$Types |ForEach-Object {
  $Params = $_.Properties |Where-Object {
    $_.CustomAttributes.AttributeType.FullName -eq [Parameter].FullName
  }
  $Params.Name
}

With the example above, we should expect output like this:

String Data                        Length DataLength
------ ----                        ------ ----------
MyName {77, 121, 78, 97, 109, 101}      6          6

Notice that rather than returning the [string] value of the Name of the MyName property, it returns the full details including the raw bytes it read form the assembly metadata - pretty cool, huh?

But, you know what's even cooler? The example above works with both PowerShell Core and Windows PowerShell ;-)