3

I have a c# source which uses the nuget package Vanara.PInvoke.Shell32. As expected, when I try to use this source in Powershell using Add-Type but it chokes on the "using Vanara.Pinvoke" statement

I've tried to use "Install-Package Vanara.PInvoke.Shell32" but it fails to install it

How can I make this module available in Powershell core?

Serge Weinstock
  • 1,235
  • 3
  • 12
  • 20
  • please add the EXACT, COMPLETE error msg to your Question ... and wrap it in code formatting markers so that it can be seen & easily read by all. – Lee_Dailey Jul 27 '21 at 12:38
  • OK, in short: 1) Copy the dlls to a folder. 2) run Add-Type -Path folder_path ? – Serge Weinstock Jul 27 '21 at 14:34
  • I've tried that. But I still have issues when trying to load my c# code using Add-Type. It still chokes on the "import Vanara.PInvoke" – Serge Weinstock Jul 27 '21 at 14:51
  • It works if I list explicitly all the assemblies: Add-Type -TypeDefinition $src -ReferencedAssemblies .\bin\Release\net6.0\publish\Vanara.PInvoke.Shell32.dll,"System.Windows", ... A littel too complex to me – Serge Weinstock Jul 27 '21 at 15:19

1 Answers1

1

It sounds that you've already downloaded the Vanara.PInvoke.Shell32 NuGet package and know the full path to the .dll file(s) housing the assembl(ies) of interest:

  • This answer shows how to download a NuGet package with all its dependencies for use in PowerShell (note that Install-Package, while capable of downloading NuGet packages in principle, doesn't also automatically packages that the targeted package depends on); the technique is also used in the demo code below.

Using the Vanara.PInvoke.*.dll assemblies from PowerShell code - by loading them into the session with Add-Type -LiteralPath and then making calls such as [Vanara.PInvoke.User32]::GetForegroundWindow() - seems to work without additional effort.

However, your use case requires using the assembly from ad hoc-compiled C# source code passed to Add-Type's -TypeDefinition parameter, and, as you have discovered, this requires substantially more effort, beyond just passing the paths to the Vanara.PInvoke.*.dll files to the -ReferencedAssemblies parameter, at least as of PowerShell 7.1:

  • Inexplicably, in order for a later Add-Type -TypeDefinition call to succeed, the assemblies from the NuGet package must first explicitly be loaded into the session with Add-Type -LiteralPath, by their full paths - this smells like a bug.

  • If the assemblies are .NET Standard DLLs, as in the case at hand, you must also pass the netstandard assembly to -ReferencedAssemblies when calling Add-Type -TypeDefinition.

  • For the code to run in both PowerShell editions, the helper .NET SDK project (see code below) should target --framework netstandard2.0, for instance.

  • By default, all assemblies (and their types) available by default in a PowerShell session itself can also be referenced in the C# source code passed to -TypeDefinition:

    • In Windows PowerShell any assemblies passed to -ReferencedAssemblies are added to the implicitly available types.
    • In PowerShell (Core) 7+, by contrast, using -ReferencedAssemblies excludes the normally implicitly available assemblies, so that all required ones must then be passed explicitly (e.g., System.Console in order to use Console.WriteLine()).

Demo:

The following is a self-contained, easily customizable sample with detailed comments that works in both Windows PowerShell and PowerShell (Core) 7+ and does the following:

  • downloads a given NuGet package on demand.
  • creates an aux. NET SDK project that references the package and publishes the project so that the relevant assemblies (*.dll) become readily available.
  • uses the package's assemblies first directly from PowerShell, and then via ad hoc-compiled C# code (passed to Add-Type -TypeDefinition).

Note:

  • The .NET SDK must be installed.

  • Ignore the broken syntax highlighting.

$ErrorActionPreference = 'Stop'; Set-StrictMode -Off

# -- BEGIN: CUSTOMIZE THIS PART.
  # Name of the NuGet package to download.
  $pkgName = 'Vanara.PInvoke.Shell32'

  # If the package assemblies are .NET Standard assemblies, the 'netstandard'
  # assembly must also be referenced - comment out this statement if not needed.
  # Note: .NET Standards are versioned, but seemingly just specifying 'netstandard'
  #       is enough, in both PowerShell editions. If needed, specify the fully qualified,
  #       version-appropriate assembly name explicitly; e.g., for .NET Standard 2.0:
  #          'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'
  #       In *PowerShell (Core) 7+* only, a shortened version such as 'netstandard, Version=2.0' works too.
  $netStandardAssemblyName = 'netstandard'

  # The target .NET framework to compile the helper .NET SDK project for.
  # Targeting a .NET Standard makes the code work in both .NET Framework and .NET (Core).  
  # If you uncomment this statement, the SDK's default is used, which is 'net5.0' as of this writing.
  $targetFrameworkArgs = '--framework', 'netstandard2.0'

  # Test command that uses the package from PowerShell.
  $testCmdFromPs = { [Vanara.PInvoke.User32]::GetForegroundWindow().DangerousGetHandle() }

  # C# source that uses the package, to be compiled ad-hoc.
  # Note: Modify only the designated locations.
  $csharpSourceCode = @'
    using System;
    // == Specify your `using`'s here.
    using Vanara.PInvoke;
    namespace demo {
      public static class Foo {
        // == Modify only this method; make sure it returns something, ideally the same thing as
        //    PowerShell test command.
        public static IntPtr Bar() { 
          return User32.GetForegroundWindow().DangerousGetHandle();
        }
      }
    }
'@

# -- END of customized part.

# Make sure the .NET SDK is installed.
$null = Get-command dotnet

# Helper function for invoking external programs.
function iu { $exe, $exeArgs = $args; & $exe $exeArgs; if ($LASTEXITCODE) { Throw "'$args' failed with exit code $LASTEXIDCODE." } }


# Create a 'NuGetFromPowerShellDemo' subdirectory in the TEMP directory and change to it.
Push-Location ($tmpDir = New-Item -Force -Type Directory ([IO.Path]::GetTempPath() + "/NuGetFromPowerShellDemo"))

try {
  
  # Create an aux. class-lib project that downloads the NuGet package of interest.
  if (Test-Path "bin\release\*\publish\$pkgName.dll") {
    Write-Verbose -vb "Reusing previously created aux. .NET SDK project for package '$pkgName'"
  }
  else {
    Write-Verbose -vb "Creating aux. .NET SDK project to download and unpack NuGet package '$pkgName'..."
    iu dotnet new classlib --force @targetFrameworkArgs >$null
    iu dotnet add package $pkgName >$null
    iu dotnet publish -c release >$null
  }

  # Determine the full paths of all the assemblies that were published (excluding the helper-project assembly).
  [array] $pkgAssemblyPaths = (Get-ChildItem bin\release\*\publish\*.dll -Exclude "$(Split-Path -Leaf $PWD).dll").FullName

  # Load the package assemblies into the session.
  # !! THIS IS NECESSARY EVEN IF YOU ONLY WANT TO REFERENCE THE PACKAGE
  # !! ALL YOU WANT DO TO IS TO USE THE PACKAGE TO AD HOC-COMPILE C# SOURCE CODE.
  # Write-Verbose -vb "Loading assembly file paths, from $($pkgAssemblyPaths[0] | Split-Path):`n$(($pkgAssemblyPaths | Split-Path -Leaf) -join "`n")"
  Add-Type -LiteralPath $pkgAssemblyPaths

  # Write-Verbose -vb 'Performing a test call FROM POWERSHELL...'
  & $testCmdFromPs

  # Determine the assemblies to pass to Add-Type -ReferencedAssemblies.
  # The NuGet package's assemblies.
  $requiredAssemblies = $pkgAssemblyPaths
  # Additionally, the approriate .NET Standard assembly may need to be referenced.
  if ($netStandardAssemblyName) { $requiredAssemblies += $netStandardAssemblyName }
  # Note: In *PowerShell (Core) 7+*, using -ReferencedAssemblies implicitly
  #       excludes the assemblies that are otherwise available by default, so you
  #       may have to specify additional assemblies, such as 'System.Console'.
  #       Caveat: In .NET (Core), types are often forwarded to other assemblies,
  #               in which case you must use the forwarded-to assembly; e.g.
  #               'System.Drawing.Primitives' rather than just 'System.Drawing' in
  #               order to use type System.Drawing.Point.
  #               What mitigates the problem is that failing to do so results in a 
  #               an error message that mentions the required, forwarded-to assembly.
  # E.g.:
  #  if ($IsCoreCLR) { $requiredAssemblies += 'System.Console' }

  Write-Verbose -vb 'Ad-hoc compiling C# CODE that uses the package assemblies...'
  Add-Type -ReferencedAssemblies $requiredAssemblies -TypeDefinition $csharpSourceCode
  
  Write-Verbose -vb 'Performing a test call FROM AD HOC-COMPILED C# CODE...'
  [demo.Foo]::Bar()

} 
finally {
  Pop-Location
  Write-Verbose -vb "To clean up the temp. dir, exit this session and run the following in a new session:`n`n  Remove-Item -LiteralPath '$tmpDir' -Recurse -Force"
}
mklement0
  • 382,024
  • 64
  • 607
  • 775