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 ;-)