James Ko February 2016

What's the best way to get the namespaces in a WinMD file?

I'm looking to get all of the namespaces in a WinMD file programmatically. I would prefer a PowerShell or C#-based solution since I need it to be in a script, but any language will do as long as it gets the job done.

Here is the code I have right now, using Assembly.ReflectionOnlyLoadFrom:

var domain = AppDomain.CurrentDomain;
ResolveEventHandler assemblyHandler = (o, e) => Assembly.ReflectionOnlyLoad(e.Name);
EventHandler<NamespaceResolveEventArgs> namespaceHandler = (o, e) =>
{
    string file = WindowsRuntimeMetadata
        .ResolveNamespace(e.NamespaceName, Array.Empty<string>())
        .FirstOrDefault();

    if (file == null)
        return;

    var assembly = Assembly.ReflectionOnlyLoadFrom(file);
    e.ResolvedAssemblies.Add(assembly);
};

try
{
    // Load it! (plain .NET assemblies)
    return Assembly.LoadFrom(path);
}
catch
{
    try
    {
        // Hook up the handlers
        domain.ReflectionOnlyAssemblyResolve += assemblyHandler;
        WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve += namespaceHandler;

        // Load it again! (WinMD components)
        return Assembly.ReflectionOnlyLoadFrom(path);
    }
    finally
    {
        // Detach the handlers
        domain.ReflectionOnlyAssemblyResolve -= assemblyHandler;
        WindowsRuntimeMetadata.ReflectionOnlyNamespaceResolve -= namespaceHandler;
    }
}

For some reason, it doesn't seem to be working. When I run it, I'm getting a ReflectionTypeLoadException when I try to load WinMD files. (You can see this question for the full details.)

So my question is, what's the best way to go about doing this, if the Reflection APIs aren't working? How do tools like Visual Studio or ILSpy do this when you hit F12 on a WinRT type? Is there any way to do this from PowerShell?

Answers


James Ko February 2016

Ended up taking up @PetSerAl's suggestion for using Mono.Cecil, which is actually pretty solid. Here's the approach I ended up taking (written in PowerShell):

# Where the real work happens
function Get-Namespaces($assembly)
{
    Add-CecilReference
    $moduleDefinition = [Mono.Cecil.ModuleDefinition]
    $module = $moduleDefinition::ReadModule($assembly)
    return $module.Types | ? IsPublic | % Namespace | select -Unique
}

function Extract-Nupkg($nupkg, $out)
{
    Add-Type -AssemblyName 'System.IO.Compression.FileSystem' # PowerShell lacks native support for zip

    $zipFile = [IO.Compression.ZipFile]
    $zipFile::ExtractToDirectory($nupkg, $out)
}

function Add-CecilReference
{
    $url = 'https://www.nuget.org/api/v2/package/Mono.Cecil'
    $directory = $PSScriptRoot, 'bin', 'Mono.Cecil' -Join '\'
    $nupkg = Join-Path $directory 'Mono.Cecil.nupkg'
    $assemblyPath = $directory, 'lib', 'net45', 'Mono.Cecil.dll' -Join '\'

    if (Test-Path $assemblyPath)
    {
        # Already downloaded it from a previous script run/function call
        Add-Type -Path $assemblyPath
        return
    }

    ri -Recurse -Force $directory 2>&1 | Out-Null
    mkdir -f $directory | Out-Null # prevent this from being interpreted as a return value
    iwr $url -OutFile $nupkg
    Extract-Nupkg $nupkg -Out $directory
    Add-Type -Path $assemblyPath
}

You can find the full contents of the script here.

Post Status

Asked in February 2016
Viewed 3,568 times
Voted 12
Answered 1 times

Search




Leave an answer