2

I'm looking for a way to retrieve the target framework attribute (e.g. .NETCoreApp,Version=v2.1) from a DLL when using PowerShell Core, ideally without loading the DLL directly into the main session.

I can do this in Windows PowerShell 5, as it has access to the ReflectionOnlyLoadFrom method...

$dllPath = 'C:\Temp\ADALV3\microsoft.identitymodel.clients.activedirectory.2.28.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll'

[Reflection.Assembly]::ReflectionOnlyLoadFrom($dllPath).CustomAttributes |
Where-Object {$_.AttributeType.Name -eq 'TargetFrameworkAttribute'} |
Select -ExpandProperty ConstructorArguments |
Select -ExpandProperty value

However, I realise that this approach isn't available in .NET Core.

Editor's note: Even though the documentation (as of this writing) misleadingly suggests that the ReflectionOnlyLoadFrom method is available in .NET Core, it is not, as explained here.

From what I've seen, it looks likely that I should be able to access the custom attributes that hold the target framework attribute by using an instance of the System.Reflection.Metadata.MetadataReader class that's available in .NET Core (a couple of examples of this in use can be found here: https://csharp.hotexamples.com/examples/System.Reflection.Metadata/MetadataReader/GetCustomAttribute/php-metadatareader-getcustomattribute-method-examples.html ). However, all the constructors for this type seem to use a Byte* type, as the following shows when running from PowerShell Core:

([type] 'System.Reflection.Metadata.MetadataReader').GetConstructors() | % {$_.GetParameters() | ft}

I have no idea how to create a Byte* type in any version of PowerShell. Perhaps there's a method in System.Reflection.Metadata that I should be using before creating the MetadataReader object, but I haven't found it yet.

Apologies for the length of this question, but I'm hoping by sharing my notes I'll help in tracking down the solution. Any advice on how this target framework information can be obtained using PowerShell Core?

mklement0
  • 382,024
  • 64
  • 607
  • 775
ZenoArrow
  • 697
  • 7
  • 21
  • Sure, I have no issue using a different method. As I mentioned in my question, it seems that the methods provided by System.Reflection.Metadata could prove to be an adequate replacement (for my use case at least), and this is bundled with PowerShell Core. The problem I have is that I don't know how to use System.Reflection.Metadata from PowerShell Core. – ZenoArrow Feb 16 '19 at 23:48
  • The only other workaround I could think of (that wouldn't require importing an external library) would be to parse the PE metadata in the DLL, which I'm prepared to do, but either way I'd like to find an elegant solution. – ZenoArrow Feb 16 '19 at 23:55
  • 1
    A tip, as an aside: In PSv5+, the simplest way to inspect a given type's constructor signatures is to call the static `::new` method _without `(...)`_: `[System.Reflection.Metadata.MetadataReader]::new` – mklement0 Feb 17 '19 at 20:52
  • As for creating a `Byte*` instance: I don't think obtaining _raw memory pointers_ is possible in PowerShell - perhaps (costly) on-demand compilation of C# code via `Add-Type` is possible, based on code demonstrated in the answers to https://stackoverflow.com/q/9946550/45375. – mklement0 Feb 17 '19 at 21:20
  • An alternative way is to use Mono Cecil (also tons of examples). – Lex Li Feb 18 '19 at 02:17
  • mklement0 and Lex Li, thank you to both of you for your suggestions. I was hoping to find a solution that worked directly with the libraries available in PowerShell Core, and seem to have found something that works now. – ZenoArrow Feb 19 '19 at 21:19

1 Answers1

1

After quite a bit of work, I managed to put together a PowerShell script that works (without external dependencies) in PowerShell Core that pulls in the target framework from a DLL:

$dllPath = 'C:\Temp\ADALV3\microsoft.identitymodel.clients.activedirectory.2.28.4\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.WindowsForms.dll'

$stream = [System.IO.File]::OpenRead($dllPath)

$peReader = [System.Reflection.PortableExecutable.PEReader]::new($stream, [System.Reflection.PortableExecutable.PEStreamOptions]::LeaveOpen -bor [System.Reflection.PortableExecutable.PEStreamOptions]::PrefetchMetadata)

$metadataReader = [System.Reflection.Metadata.PEReaderExtensions]::GetMetadataReader($peReader)

$assemblyDefinition = $metadataReader.GetAssemblyDefinition()

$assemblyCustomAttributes = $assemblyDefinition.GetCustomAttributes()

$metadataCustomAttributes = $assemblyCustomAttributes | % {$metadataReader.GetCustomAttribute($_)}

foreach ($attribute in $metadataCustomAttributes) {

    $ctor = $metadataReader.GetMemberReference([System.Reflection.Metadata.MemberReferenceHandle]$attribute.Constructor)
    $attrType = $metadataReader.GetTypeReference([System.Reflection.Metadata.TypeReferenceHandle]$ctor.Parent)
    $attrName = $metadataReader.GetString($attrType.Name)
    $attrValBytes = $metadataReader.GetBlobContent($attribute.Value)
    $attrVal = [System.Text.Encoding]::UTF8.GetString($attrValBytes)

    if($attrName -eq 'TargetFrameworkAttribute') {Write-Output "AttributeName: $attrName, AttributeValue: $attrVal"}

}

$peReader.Dispose()

I'm mostly happy with it, the only issue I'd still like to sort out is that I'm getting some unhandled characters in the string output. I'll try to get rid of them.

Current script output

ZenoArrow
  • 697
  • 7
  • 21
  • If anyone else sees this and just wants the target framework string. the second byte in the array is the string length and the string starts from index 3 `var attValBytes = metadataReader.GetBlobContent(mdCustom.Value);` `var stringLength = attValBytes[2];` `var attVal = System.Text.Encoding.UTF8.GetString(attValBytes, 3, stringLength);` – Marlon Aug 16 '22 at 15:03