Powershell: Powershell 7.x : Cannot use type from dll (built for netstandard2.0)

Created on 6 Mar 2020  路  15Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

Provided that there is a dotnet core sdk istalled, run script as below.
What script does:

  • Displays some debug stuff (dotnet core version, powershell version)
  • Crates some directories e.g. where published library will be copied
  • Creates ad hoc dot net core class lib project (targeted for netstandard2.0)
  • Compiles the project into dll
  • Loads the dll into powershell
  • Calls function Hello from the dll and expects to receive predefined output (== "Works!")
Function ShowErrorDetails
{
    param(
        $ErrorRecord = $Error[0]
    )

    $out=""
    $out+= $($ErrorRecord | Format-List -Property * -Force | out-string)
    $out+= $($ErrorRecord.InvocationInfo | Format-List -Property * | out-string)
    $Exception = $ErrorRecord.Exception
    for ($depth = 0; $Exception -ne $null; $depth++)
    {
        $out+= $("$depth" * 80)
        $out+= $($Exception | Format-List -Property * -Force | out-string)
        $Exception = $Exception.InnerException
    }
    # emit return object
    $out
}

 $myLibName="MyLib"
 $myLibDllName="${myLibName}.dll"
 $csProjName = "${myLibName}.csproj"
 $thisDir = Get-Location  
 $workdir = Join-Path $thisDir "workdir"
 $libraryCodeDir = Join-Path $workdir $myLibName
 $csProjFilePath = Join-Path $libraryCodeDir $csProjName 
 $publishedOutputDir = Join-Path $workdir "published"
 $dllFilePath = Join-Path $publishedOutputDir $myLibDllName

 Write-Host "thisDir            : '$thisDir'"
 Write-Host "myLibName          : '$myLibName'"
 Write-Host "csProjName         : '$csProjName'"
 Write-Host "workdir            : '$workdir'"
 Write-Host "libraryCodeDir     : '$libraryCodeDir'"
 Write-Host "csProjFilePath     : '$csProjFilePath'"
 Write-Host "publishedOutputDir : '$publishedOutputDir'"
 Write-Host "myLibDllName       : '$myLibDllName'"
 Write-Host "dllFilePath        : '$dllFilePath'"

 Write-host "------ >>>>>   Powershell version : "
 $PsVersionTable | Out-Host

 Write-Host "------ >>>>>   Dotnet core version: "
 dotnet --info

 Write-host "------ >>>>>   Creating $libraryCodeDir..."
 New-item $libraryCodeDir -ItemType Directory -Force

 Write-Host "`n------ >>>>>   Creating library template in '$libraryCodeDir'"
 dotnet new classlib --name $myLibName --output $libraryCodeDir --framework "netstandard2.0" --force


$expectedOutput = "Works!"

$codeContent = @"
public static class MyClass
{ 
    public static string Hello()
    {
      return "${expectedOutput}";
    }
}
"@

try 
{
  Set-Content -Path $libraryCodeDir/Class1.cs -Value $codeContent
  dotnet publish -c Debug -o ${publishedOutputDir} ${csProjFilePath}
   # if compilation was not successful
  if($global:LASTEXITCODE -ne 0 )
  {
    Write-Error "------ >>>>>   Compilation failed, cannot continue with the script.."
    Exit 1
  }
  else
  {
    Write-Host "------ >>>>>   OK: compilation successful"
  }

  Write-Host "------ >>>>>   Loading library..."
  $bytes = [System.IO.File]::ReadAllBytes($dllFilePath)
  $loadedLib = [System.Reflection.Assembly]::Load($bytes)
  $loadedLib.GetTypes() | Out-Host
  $functionCallResult = [MyClass]::Hello()

  if($functionCallResult -eq  $expectedOutput )
  {
     Write-Host "------ >>>>>   SUCCESS: Function call returned expected result"
  }
  else
  {
    Write-Error "------ >>>>>   FAILURE: Function call unxpected result '${functionCallResult}' while expecting '${expectedOutput}'"
    Exit 1
  }
}
catch {
  ShowErrorDetails | Out-Host
}

Expected behavior

After the script finishes , user should see line like

------ >>>>>   SUCCESS: Function call returned expected result

Actual behavior

I've added a debug function ShowErrorDetails to show full error details (code taken from book Windows Powershell in action 3rd edition) if something goes wrong.
Powershell 7.0 cannot find the type from the library and at the end of error output it prints something like:

00000000000000000000000000000000000000000000000000000000000000000000000000000000
ErrorRecord                 : Unable to find type [MyClass].
WasThrownFromThrowStatement : False
TargetSite                  : Void CheckActionPreference(System.Management.Automation.Language.FunctionContext, System.Exception)
StackTrace                  :    at System.Management.Automation.ExceptionHandlingOps.CheckActionPreference(FunctionContext funcContext, Exception exception)
                                 at System.Management.Automation.Interpreter.ActionCallInstruction`2.Run(InterpretedFrame frame)
                                 at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
                                 at System.Management.Automation.Interpreter.EnterTryCatchFinallyInstruction.Run(InterpretedFrame frame)
Message                     : Unable to find type [MyClass].
Data                        : {System.Management.Automation.Interpreter.InterpretedFrameInfo}
InnerException              :
HelpLink                    :
Source                      : System.Management.Automation
HResult                     : -2146233087

Environment data

The issue is related only for powershell 7:
I tested it on :
Windows

Name                           Value
----                           -----
PSVersion                      7.0.0
PSEdition                      Core
GitCommitId                    7.0.0
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

Ubuntu:


Name                           Value
----                           -----
PSVersion                      7.0.0
PSEdition                      Core
GitCommitId                    7.0.0
OS                             Linux 5.3.0-40-generic #32~18.04.1-Ubuntu SMP Mon Feb 3 14:05:59 UTC 2020
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

I have verified that this correctly WORKS platforms such as
windows powershell 5.1

Name                           Value
----                           -----
PSVersion                      5.1.17763.1007
PSEdition                      Desktop
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}
BuildVersion                   10.0.17763.1007
CLRVersion                     4.0.30319.42000
WSManStackVersion              3.0
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1

windows powershell core 6.2.4

Name                           Value
----                           -----
PSVersion                      6.2.4
PSEdition                      Core
GitCommitId                    6.2.4
OS                             Microsoft Windows 10.0.17763
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

ubuntu powrshell core 6.2.4

Name                           Value
----                           -----
PSVersion                      6.2.4
PSEdition                      Core
GitCommitId                    6.2.4
OS                             Linux 5.3.0-40-generic #32~18.04.1-Ubuntu SMP Mon Feb 3 14:05:59 UTC 2020
Platform                       Unix
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0鈥
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0

ubuntu_pwsh6.2.4_output.txt
ubuntu_pwsh7_output.txt
windows_powershell_output.txt
windows_pwsh6.2.4_output.txt
windows_pwsh7_output.txt

Issue-Question WG-Engine

Most helpful comment

Sorry that I missed this one for so long :(
To be honest, I didn't realize Assembly.Load(byte[]) loads the assembly to an anonymous load context. I did know Assembly.LoadFile does that, but thought it was fine to not discover it because Assembly.LoadFile is useful only when you are trying to load a conflict assembly file and then do pure reflection on the returned assembly.

I think @SeeminglyScience's proposal makes sense. Let me look into this more and reply back.

All 15 comments

Looks like Assembly.Load doesn't load into the default assembly load context (which is where PowerShell now looks for type resolution).

Here's a work around:

$bytes = [System.IO.File]::ReadAllBytes($dllFilePath)
$loadedLib =  [Runtime.Loader.AssemblyLoadContext]::Default.LoadFromStream(
    [IO.MemoryStream]::new($bytes))

@SeeminglyScience Did that changed between pwsh 6.x and 7.x ?

Yeah, it changed in #11088

@daxian-dbw Maybe resolution could look at AssemblyLoadContext.All and either pick out the ALCs with the name "Assembly.Load(byte[], ...)" or that are of the (non-public) type System.Runtime.Loader.IndividualAssemblyLoadContext?

We discussed this in #11088 and intermediate conclusion was that it is by-design. Really there are two methods which load in non default context. .Net Core implements in such manner and it is not clear why we should change this and de-facto revert #11088.

.Net Core implements in such manner and it is not clear why we should change this

So that the types can be resolved by PowerShell, which would avoid making Assembly.Load essentially impossible to use from PowerShell (and breaking all code that uses it).

and de-facto revert #11088.

The way I proposed would still filter out assemblies explicitly loaded in custom assembly load contexts. Nothing would need to be reverted.

It is .Net Core behaviour for ALC.

I think #11088 is right direction to switch to ALC from AppDomain since we are discussing (1) support dll-s with different versions, (2) resolving name conflicts from different dll-s, (3) dll unloading.

It is .Net Core behaviour for ALC.

I think #11088 is right direction to switch to ALC from AppDomain since we are discussing (1) support dll-s with different versions, (2) resolving name conflicts from different dll-s, (3) dll unloading.

I agree. That's why I proposed a solution that doesn't interfere with that.

I updated my PowerShell from 6 to 7 a few days ago, and found out that a lot of my scripts have stopped working.

Is there a workaround? Can I use some other method than Assembly.Load, so that PowerShell finds the types? (I use the overload getting a byte array, so that the .dll file isn't locked and can be deleted/overwritten.)

Sorry that I missed this one for so long :(
To be honest, I didn't realize Assembly.Load(byte[]) loads the assembly to an anonymous load context. I did know Assembly.LoadFile does that, but thought it was fine to not discover it because Assembly.LoadFile is useful only when you are trying to load a conflict assembly file and then do pure reflection on the returned assembly.

I think @SeeminglyScience's proposal makes sense. Let me look into this more and reply back.

There was another issue where we discussed this that I'd like to link to update the original opener, but I can't find it. Let me know if you remember it

That conversation was in #11088 which introduced this regression. I already notified @stevenebutler who reported it.

:tada:This issue was addressed in #12203, which has now been successfully released as v7.1.0-preview.2.:tada:

Handy links:

:tada:This issue was addressed in #12203, which has now been successfully released as v7.0.1.:tada:

Handy links:

Was this page helpful?
0 / 5 - 0 ratings