Implement PowerShell PSProvider *in* PowerShell - powershell

I'm looking to implement a PowerShell Provider in PowerShell.
I keep thinking that if I just define the types, then import them into my session (import-module), I should be able to have them available.
For example, this does not work but its along the path of what I'd like to implement.
I'm obviously missing quite a bit...anyone know if this is possible?
# EnvironmentProvider.ps1
$reference_assemblies = (
"System.Management.Automation, Version=1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"
# "System.Configuration.Install, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"
)
$source = #"
namespace Providers
{
using System.Management.Automation;
using System.Management.Automation.Provider;
[CmdletProvider("Environments", ProviderCapabilities.None)]
public class EnvironmentProvider : DriveCmdletProvider
{
protected override PSDriveInfo NewDrive(PSDriveInfo drive)
{
return new EnvironmentDriveInfo(drive);
}
protected override object NewDriveDynamicParameters()
{
return base.NewDriveDynamicParameters();
}
}
public class EnvironmentDriveInfo : PSDriveInfo
{
public EnvironmentDriveInfo(PSDriveInfo driveInfo) : base(driveInfo)
{
}
}
}
"#
# -ea silentlycontinue in case its already loaded
#
add-type -referencedassemblies $referenced_assemblies -typedefinition $source -language CSharp -erroraction silentlycontinue
After import-module, I try to create the drive "environments":
new-psdrive -psprovider Environments -name "Environments" -root ""
errors with:
New-PSDrive : Cannot find a provider with the name 'Environments'.
Assuming the provider actually worked, maybe have it return a list of environments: dev, qa, staging, production.
Then I'd like to be able to re-use this through:
c:\adminlib>import-module .\EnvironmentProvider.ps1
c:\adminlib>environments:
environments:>ls
dev
qa
staging
production
environments:> cd production
environments\production> [execute actions against production]
environments\production:> cd dev
environments\dev:> [execute actions against dev, etc]

I would strongly recommend looking at the stuff Oisin wrote, suspect for people like you, who can grab their head around it, that could be very good reference on how-to. Or maybe what to avoid? ;)
You can find it on codeplex: http://psprovider.codeplex.com/

I know it's been some time since you asked the question, but I've been searching for that same answer myself. As it happens, re-reading the Samples in msdn finally got me my answer, and given the frustration quotient I thought I'd share:
The assembly containing the provider needs to be imported using Import-Module (not merely the module containing the add-type declaration). This can be done using two ways:
Option 1:
Use the parameter of Add-Type that builds the runtime assembly as a .dll file and import the file.
Option 2:
Import the runtime assembly from memory. This is how I did that with the standard msdn samples:
[appdomain]::CurrentDomain.GetAssemblies() | Where {$_.ExportedTypes -ne $null} | Where {($_.ExportedTypes | Select -ExpandProperty "Name") -contains "AccessDBProvider"} | Import-Module
Replace the Provider name in the where filter with your own.
Cheers,
Fred

Related

How to load StackExchange.Redis.dll into powershell?

I'm trying to create a powershell script to clear a redis cache, it's in Azure but I don't think that's relevant. I've seen 2 examples which I'm trying to copy where people have loaded StackExchange.Redis.dll into their script: https://www.powershellgallery.com/packages/Saritasa.Redis/1.2.0/Content/Saritasa.Redis.psm1 and Clearing Azure Redis Cache using PowerShell during deployment.
I've downloaded the current StackExchange.Redis.dll from nuget.org. I've tried to load it on 2 servers, one with .Net 4.61 installed, the other with .Net 4.8. I get the same problem on both.
If I try to use [System.Reflection.Assembly]::LoadFrom I get as below:
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> dir
Directory: E:\redis\stackexchange.redis.2.2.88\lib\net461
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 05/11/2021 00:42 637440 StackExchange.Redis.dll
-a--- 05/11/2021 00:42 705989 StackExchange.Redis.xml
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> [System.Reflection.Assembly]::LoadFrom("E:\redis\stackexchange.redis.
2.2.88\lib\net461\StackExchange.Redis.dll")
GAC Version Location
--- ------- --------
False v4.0.30319 E:\redis\stackexchange.redis.2.2.88\lib\net461\StackExchange.Redis.dll
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> [StackExchange.Redis.ConnectionMultiplexer]::Connect($myConnectionStr
ing)
Unable to find type [StackExchange.Redis.ConnectionMultiplexer]: make sure that the assembly containing this type is
loaded.
At line:1 char:1
+ [StackExchange.Redis.ConnectionMultiplexer]::Connect($myConnectionString)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (StackExchange.R...tionMultiplexer:TypeName) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
PS E:\redis\stackexchange.redis.2.2.88\lib\net461>
If I try to use Add-Type I get:
PS E:\redis\stackexchange.redis.2.2.88\lib\net461> Add-Type -AssemblyName .\StackExchange.Redis.dll
Add-Type : Could not load file or assembly '.\\StackExchange.Redis.dll' or one of its dependencies. The given assembly
name or codebase was invalid. (Exception from HRESULT: 0x80131047)
At line:1 char:1
+ Add-Type -AssemblyName .\StackExchange.Redis.dll
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Add-Type], FileLoadException
+ FullyQualifiedErrorId : System.IO.FileLoadException,Microsoft.PowerShell.Commands.AddTypeCommand
PS E:\redis\stackexchange.redis.2.2.88\lib\net461>
I've looked through the dependencies in nuget.org, I saw one non-Microsoft one called Pipelines.Sockets.Unofficial which I also downloaded and got the same thing. There's a whole hierarchy of other dependencies which I think are all part of .Net, surely I haven't got to download them all if .Net is installed on the server? Thanks for any help, I've been trying all day!
I'll put some things I learned above my code in case someone as inexperienced as me reads this. These points aren't in any order:
To download a nuget package for use by powershell, the easiest way is to use the Install-Package cmdlet, for example:
Install-Package -Name System.IO.Pipelines -ProviderName NuGet -SkipDependencies -Destination C:\Stackexchange.Redis-packages -RequiredVersion 5.0.1
Note that -SkipDependencies is needed because without it Install-Package gave me an error message about a circular dependency, described in https://endjin.com/blog/2020/12/how-to-consume-a-nuget-package-in-powershell. You have to download the dependencies yourself!
The alternative is: in nuget.org click the download link to download the .nupkg file, rename it to .zip, extract the files, then in File Explorer right-click the dll file, click Properties, Unblock.
I thought this app was great for showing the DLL dependencies of a DLL, it shows the whole heirarchy of dependencies and if they're found or not https://github.com/lucasg/Dependencies
To get the assembly versions of dll files in powershell:
Get-ChildItem -Filter *.dll -Recurse | Select-Object Name,#{n='FileVersion';e={$_.VersionInfo.FileVersion}},#{n='AssemblyVersion';e={[Reflection.AssemblyName]::GetAssemblyName($_.FullName).Version}}
from Get file version and assembly version of DLL files in the current directory and all sub directories
When loading DLLs, Add-Type behaves differently from [System.Reflection.Assembly]::LoadFrom(). Add-Type seems to load dependencies which can be useful. If Add-Type -Literalpath <dllFileName> fails with an error message “Add-Type : Unable to load one or more of the requested types. Retrieve the LoaderExceptions property for more
information” you can get the DLL it was looking for with $error[0].Exception.GetBaseException().LoaderExceptions https://www.reddit.com/r/PowerShell/comments/7a4vw6/addtype_how_do_i_retrieve_the_loaderexceptions/
To get round the situation where DLLs in the heirarchy have a dependency on different versions of the same DLL (which seems to be very common) this guy's solution is simply fantastic. I couldn't find any alternative and it seems to work perfectly https://www.azurefromthetrenches.com/powershell-binding-redirects-and-visual-studio-team-services/
The DLLs I used
I ended up with these DLL files in a folder:
Name FileVersion AssemblyVersion
---- ----------- ---------------
Microsoft.Bcl.AsyncInterfaces.dll 6.0.21.52210 6.0.0.0
Pipelines.Sockets.Unofficial.dll 2.2.0.45337 1.0.0.0
StackExchange.Redis.dll 2.2.88.56325 2.0.0.0
System.Buffers.dll 4.6.28619.01 4.0.3.0
System.IO.Pipelines.dll 5.0.120.57516 5.0.0.1
System.Memory.dll 4.6.28619.01 4.0.1.1
System.Numerics.Vectors.dll 4.6.26515.06 4.1.4.0
System.Runtime.CompilerServices.Unsafe.dll 6.0.21.52210 6.0.0.0
System.Threading.Channels.dll 6.0.21.52210 6.0.0.0
System.Threading.Tasks.Extensions.dll 4.6.28619.01 4.2.0.1
The code It's not finished, but shows the Stackexhange.Redis DLL loaded and used.
# Code to load the DLLs needed for Stackexchange.Redis.dll and clear the cache
# Basically copied from https://www.azurefromthetrenches.com/powershell-binding-redirects-and-visual-studio-team-services/
$DllPath = 'C:\Stackexchange.Redis-packages\combined DLLS'
$redisHostName = '<my cache name here>.redis.cache.windows.net'
$redisConnectionString = '<my cache name here>.redis.cache.windows.net:6380,password=<my cache password here>,ssl=True,abortConnect=False'
# Load DLL assemblies into memory, required by the event handler below
$SystemBuffersDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Buffers.dll")
$SystemRuntimeCompilerServicesUnsafeDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Runtime.CompilerServices.Unsafe.dll")
$SystemMemoryDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Memory.dll")
$SystemSystemThreadingTasksExtensionsDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Threading.Tasks.Extensions.dll")
$SystemIoPipelinesDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.IO.Pipelines.dll")
$MicrosoftBclAsyncInterfacesDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\Microsoft.Bcl.AsyncInterfaces.dll")
$PipelinesSocketsUnofficialDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\Pipelines.Sockets.Unofficial.dll")
$SystemThreadingChannelsDll = [System.Reflection.Assembly]::LoadFrom("$DllPath\System.Threading.Channels.dll")
# Event handler to be run when the AssemblyResolve event occurs
$onAssemblyResolveEventHandler = [System.ResolveEventHandler] {
param($sender, $e)
Write-Verbose "Assembly resolve event for $($e.Name)"
$dllName = $e.Name.Split(',')[0]
switch ($dllName) {
'System.Buffers' {return $SystemBuffersDll}
'System.Runtime.CompilerServices.Unsafe' {return $SystemRuntimeCompilerServicesUnsafeDll}
'System.Memory' {return $SystemMemoryDll}
'System.Threading.Tasks.Extensions' {return $SystemSystemThreadingTasksExtensionsDll}
'Microsoft.Bcl.AsyncInterfaces' {return $MicrosoftBclAsyncInterfacesDll}
'Pipelines.Sockets.Unofficial' {return $PipelinesSocketsUnofficialDll}
'System.Threading.Channels' {return $SystemThreadingChannelsDll}
'System.Numerics.Vectors' {return $SystemNumericsVectorsDll}
'System.IO.Pipelines' {return $SystemIoPipelinesDll}
}
foreach($assembly in [System.AppDomain]::CurrentDomain.GetAssemblies()) {
if ($assembly.FullName -eq $e.Name) {
return $assembly
}
}
return $null
}
# Set up the handler above to be triggered when the AssemblyResolve event occurs
[System.AppDomain]::CurrentDomain.add_AssemblyResolve($onAssemblyResolveEventHandler)
# Load StackExchange.Redis.dll, prefer Add-Type because it seems to include dependencies, LoadFrom doesn't so might get an error later
Add-Type -LiteralPath "$DllPath\StackExchange.Redis.dll"
$redis = [StackExchange.Redis.ConnectionMultiplexer]::Connect("$redisConnectionString, allowAdmin=true")
$redisServer = $redis.GetServer($redisHostName, 6380)
# $rs.FlushAllDatabases(async=true)
$redisServer.FlushAllDatabases()
# Detach the event handler (not detaching can lead to stack overflow issues when closing PS)
[System.AppDomain]::CurrentDomain.remove_AssemblyResolve($onAssemblyResolveEventHandler)

Visual Studio Code cannot import assemblies containing Hyper-V data types to build PowerShell objects

Problem Description and Background
I am building some custom PowerShell objects that require the Hyper-V data types. I found the Hyper-V data types by using commands
[System.AppDomain]::CurrentDomain.GetAssemblies().FullName | ? {$_ -like "*hyper*" -or $_ -like "*virt*"}
Get-TypeData -TypeName Microsoft.HyperV.*
The current best practice about importing assemblies into powershell scripts is that seems to recommend Add-Type instead of using assembly. There is more discussion about using assembly on the boards but I have been able to reproduce the error with both methods
I then created a custom type BMachine in PowerShell Visual Studio Code
using assembly 'C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.Virtualization.Client.Common.Types\v4.0_10.0.0.0__31bf3856ad364e35\Microsoft.Virtualization.Client.Common.Types.dll'
using assembly 'C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.HyperV.PowerShell.Objects\v4.0_10.0.0.0__31bf3856ad364e35\Microsoft.HyperV.PowerShell.Objects.dll'
class BMachine {
[String] $Name
[IPAddress] $Address
[Microsoft.HyperV.PowerShell.VirtualMachine] $Machine
BMachine ([String] $name) {
$this.Name = $name
$drivePath = "Machines\" + $name + "\" + $name + ".vhdx"
$this.Machine = New-VM -Generation 2 -Name $name -MemoryStartupBytes 1GB -NewVHDPath $drivePath -NewVHDSizeBytes 40GB -Path ("C\Machines\" + $name + "\")
}
}
$machine = [BMachine]::new( $(Get-Content \users\griot\Dropbox\scripts\single_words.txt | Get-Random) )
$machine
When I run the code in Visual Studio I always get the error Unable to find type [Microsoft.HyperV.PowerShell.VirtualMachine] You can see it in the screenshot
How Did I Narrow it Down to a Visual Studio Code Issue
I get the same error when using Add-Type, Using Assembly, or even trying Import Hyper-V
Uninstalling VS Code and blowing away all my configs before a restart did not fix the issue
Reinstalling the PowerShell extensions did not fix it
I still got the parse error when I installed the PowerShell Pro Tools extension (licensed)
However running script in Windows Terminal runs with no problem
Output From PowerShell 7 Terminal
2021-08-03 14:36 BMachine> .\BMachine.ps1
Name Address Machine
shopmate VirtualMachine (Name = 'shopmate') [Id = 'f26280fd-80c4-4e7b-a713-de4b4b31885d']
2021-08-03 14:36 BMachine>
My Question
What can I do so VS Code detects and imports builtin Microsoft data types and avoid getting the parse errors I am seeing?
The problem is not with Visual Studio. The assemblies needed to create custom HyperV objects are not in HyperV or virtualization assemblies. All the PowerShell Hyper-V cmdlets are just wrapppers for commands and classes in Microsoft.Management.Infrastructure Namespace. Most PowerShell commands having to do with physical machines, networking or hardware do not have access to those objects. You have to use the CIM classes and instances.

How to install a nuget package such as it can be loaded from Powershell

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?
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"
}

Remove Class from Memory in PowerShell

I've created a class called "Application" and loaded it in my main script with:
Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;
However if I ONLY make changes to the class file and run the code in PowerShell ISE none of the changes are applied. It's almost as if the class is still in memory even though I've used -FORCE.
I've also tried to remove the module before loading it and the same issue happens:
Remove-Module "Application" -ErrorAction Ignore -FORCE;
Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;
If I make a single character change in my main script then it reloads the class! But I shouldn't have to modify the main script to force PowerShell to reload the class, that just seems silly.
Is there a way to remove the Application class from memory if it exists?
NOTE: Files with just functions in them work file. This only applies to Class imports.
Addition: In the console, if I run the Remove-Module command it runs successfully but I can STILL create new objects with:
$appDetails = [Application]::new($applicationID);
Doesn't make sense to me...
MAIN SCRIPT:
# Application Details
# -----------------
#ID
$applicationID = 1;
############################################
#
# Load Supporting Scripts
#
############################################
try
{
Remove-Module "Application" -ErrorAction Ignore -FORCE;
Remove-Module "Common" -ErrorAction Ignore -FORCE;
Remove-Module "ServerData" -ErrorAction Ignore -FORCE;
Import-Module -NAME "C:\PowerShell_Scripts\Common.ps1" -GLOBAL -FORCE;
Import-Module -NAME "C:\PowerShell_Scripts\ServerData.ps1" -GLOBAL -FORCE;
Import-Module -NAME "C:\PowerShell_Scripts\Class\Application.ps1" -GLOBAL -FORCE;
}
catch
{
Write-Host "`nError: Cannot load required PowerShell scripts. Ensure C:\PowerShell_Scripts\ exists and has the required files." -ForegroundColor Red;
EXIT;
}
############################################
#
# Load the SharePoint Snapin Module.
#
############################################
LoadSharePointModule;
############################################
#
# Display component details to user.
#
############################################
#Create object of "Application" to get app details based on the ID.
$appDetails = [Application]::new($applicationID);
Write-Host "Ending ......";
APPLICATION CLASS FILE
Class Application
{
#Class Properties
[STRING] $appName;
[INT32] $appID;
[INT32] $versionMajor;
[INT32] $versionOS;
[INT32] $versionCentraAdmin;
[INT32] $versionMain;
[INT32] $versionGUI;
[INT32] $versionWorkflow;
[INT32] $versionForm;
[INT32] $versionVS;
[INT32] $versionOther;
[INT32] $versionFull;
[OBJECT] $spDevSite;
[OBJECT] $versionList;
#Constructor: Setup class properties.
Application ([INT32] $appID)
{
Write-Host "`nGathering application details ..." -ForegroundColor Yellow;
try
{
#Get the SharePoint Developer site Object.
$this.spDevSite = Get-SPWeb -ErrorAction Stop $GLOBAL:spDevURL;
}
catch
{
Write-Host "`nUnable to connect to SharePoint Developer site!: $($GLOBAL:spDevURL)";
#EXIT;
}
#Assign class property.
$this.appID = $appID;
}
}
I have deliberately set the URL for $GLOBAL:spDevURL; so that the Constructor fails for this test. It fails normally and displays
Write-Host "`nUnable to connect to SharePoint Developer site!: $($GLOBAL:spDevURL)";
But if I make a change to this line and run the script, the change is not applied.
The Known Issue
There is a known issue in PowerShell 5.0 and 5.1 that explains this behavior. The issue was acknowledged by DongBo Wang on the PowerShell 6 team in November 2016. He wrote the following:
"The module analysis result is stored in a cache with the module file path as the key and the PSModuleInfo object as the value. The cache entries are not properly invalidated based on the LastWriteTime of the module file, and thus same cached value got reused."
In other words, PowerShell 5.0, 5.1, and 6.0 keeps (and uses) old copies of classes in memory when it shouldn't.
Implications
This issue causes considerable problems for development using PowerShell classes if you do not compensate for it. I wrote a test that covers about 100 of the scenarios where class reloading is important. Vaguely speaking, in about 17 of those scenarios PowerShell 5.0 and 5.1 doesn't reload the class when it should. This means using the same session across edits creates a real likelihood the interpreter will have cached duplicate copies of the same or similar classes. That makes behavior unpredictable and causes strange results that cannot be troubleshot.
Workaround
I have found that you can still be productive developing using PowerShell classes. You just need to perform each test run in a fresh PowerShell session when a project involves PowerShell classes whose source the PowerShell interpreter may consider to have changed. The customary way to do this is to invoke your test command from your PowerShell console by invoking powershell.exe:
powershell.exe -Command { Invoke-Pester }
That's not a terribly inefficient test-edit-test cycle if you've got tight unit tests. If you need to step through code, you'll need to launch a fresh copy of ISE each time you make an edit.
With this workaround, I have found the productivity impact of this bug to be manageable. I developed this and this entirely using this workaround. Each of those projects involve a significant amount of code involving PowerShell classes.

Loading PowerShell Cmdlet Assembly with Module Manifest

I am trying to make my way through the PowerShell documentation and have hit a point of confusion.
I have creating a cmdlet assembly using the sample code located here.
I can load the module by issuing the command:
Import-Module -Name *NameOfAssembly*
This is, of course, if the assembly is located in a folder where PowerShell can find it.
If I create a module manifest, the only way I have been able to get the module manifest to load the assembly is to add the assembly on the RequiredModules line of the manifest. The documentation (located here) states that this doesn't actually load any modules. From what I have observed this is contradictory to what actually happens. Am I reading / understanding this incorrectly? If not, what am I missing? Is there a better way to get a cmdlet assembly (or assemblies) (I think they are called binary modules) deployed?
First of all - same as it works with script modules, you can get loading of NameOfModule.dll by simply putting it in NameOfModule subfolder of any folders listed in $env:PSModulePath
$dir = mkdir $profile\..\Modules\Greetings -Force
$dllPath = Join-Path -Path $dir.FullName -ChildPath Greetings.dll
Add-Type #'
using System.Management.Automation;
namespace SendGreeting
{
[Cmdlet(VerbsCommunications.Send, "Greeting")]
public class SendGreetingCommand : Cmdlet
{
[Parameter(Mandatory=true)]
public string Name
{
get { return name; }
set { name = value; }
}
private string name;
protected override void ProcessRecord()
{
WriteObject("Hello " + name + "!");
}
}
}
'# -OutputAssembly $dllPath
Import-Module Greetings -PassThru
In case you really need manifest (e.g. for some metadata, or external files) you have two options, depending on PowerShell version:
v2 - ModulesToProcess (works with newer versions, but causes warning)
v3+ - RootModule (fails on v2)
The key you've used, RequiredModules, is there to provide a way to name your dependencies. E.g. modules that your code depends on. RequiredAssemblies kind-of works, because adding any assembly that contains PowerShell cmdlets "just works" - but that approach kind of "hides" from future user where you defined your commands.