Types in PowerShell 2 modules - powershell

I've implemented a small PowerShell module which brings a custom type with it. I defined the type in the .psm1 file as a C# class and added it with Add-Type. Now, when I add the module and remove it again, the type is still there which probably isn't quite right (it prevents re-adding the module, for example). The documentation for Remove-Module states that types defined in assemblies loaded by the module are unloaded as well. But my module doesn't bring in an assembly, just a tiny-ish single type in source code form.
I could just put the type into its own DLL and mark it as an assembly to load in the module manifest, but I like how currently all the source code is readily visible. Distributing a DLL with the module might just raise suspicion why it needs an executable file.
Is there something I can hook onto to remove the type somehow when unloading the module? Or should I just ignore potential errors with Add-Type to at least being able to re-add the module once removed from a session? I'd rather avoid putting a DLL in there (probably overkill anyway for that tiny module).

The docs on Remove-Module also say that the assembly is not unloaded. This is a fundamental issue with .NET and the CLR. Once an assembly is loaded into an AppDomain it can't be unloaded. So creating your own DLL (managed assembly) isn't going to help.
I'm not sure there is much you can do here short of avoiding Add-Type and creating your custom type using new-object psobject -prop #{...} and $obj.psobject.typenames.insert(0, 'newtypename').

Related

Auto-Loading Cmdlets as part of a Module dynamically

So, I was thinking: I have a lot (!) of custom cmdlets, but I don't want them to load all of them in my profile, because, naturally, that will take a lot of time. (I work fast, so my tools need to be fast.) But also, I don't want to always load them manually, because, well that's annoying. (And again ... I work fast!)
Luckily, there's a neat functionality called "Auto-Loading" which will totally solve my problem ... kind of. I just have to put my cmdlets into script modules, and they will be loaded automatically, right?
It seems though, there need to be some requirements met, for PowerShell to "detect" the cmdlets belonging to a module. The easiest way I know so far, is to simply put each cmdlet in a single ps1 file and then create a manifest somewhat like this:
#{
ModuleVersion = "1.0"
NestedModules = #(
"Get-Something.ps1",
"Remove-Something.ps1",
"Test-Something.ps1",
"Run-Something.ps1",
"Invoke-Something.ps1",
"Start-Something.ps1",
"Stop-Something.ps1"
),
CmdletsToExport = #(
"Get-Something",
"Remove-Something",
"Test-Something",
"Run-Something",
"Invoke-Something",
"Start-Something",
"Stop-Something"
)
}
This does work. But as I said .. I work fast, and I'm lazy. It would be much easier, if the module could discover its members dynamically. Basically, all ps1 files in the same folder. This would work pretty easily with some code in the psm1 file. But then, the Auto-Loading would not work. I understand PowerShell does need to know the exported cmdlets before hand.
Is there any other, dynamic way to do that, other than specifying them explicitly in the psd1 module manifest?
If you are worried about the size or organization of your module, create different modules for the different common subjects of your cmdlets, and put your functions into the psm1 for each module. Then make sure you add the cmdlet definitions to CmdletsToExport in the module manifest (psd1). If you have a centralized feed, distributing your modules becomes easy, and if you simply want all of them at a given time you can create a top level module that lists your other modules as dependencies so they get installed when you install the top level module. Unless you have hundreds to thousands of cmdlets to export as part of a single module, you shouldn't need to worry about performance problems.
For each module you have on the PSModulePath, you can use wildcards in the CmdletsToExport and FunctionsToExport sections of your manifest for auto-complete to work, if you don't want to export them name by name.
Edit: Technically, you can call Set-PSReadLineKeyHandler and Register-ArgumentCompleter yourself, however, you have the same problem now in that you need to add this to the $profile for each user or machine where you want the autocomplete to work, in addition to shipping your code there in the first place. So it doesn't really solve your issue.

How can I import/load a .dll file to use in a PowerShell script without getting "TypeNotFound" error?

[void] CreateSession() {
try {
# Load WinSCP .NET assembly
Add-Type -Path (Join-Path $PSScriptRoot "\winscp\WinSCPnet.dll")
# Setup session options
$this.sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = [WinSCP.Protocol]::Sftp
In the above section of code I encounter the "TypeNotFound" error message regarding "[WinSCP.Protocol]".
14 | Protocol = [WinSCP.Protocol]::Sftp
| ~~~~~~~~~~~~~~~
| Unable to find type [WinSCP.Protocol].
The .dll file can load correctly, I have verified this previously. I know what is happening is that the PowerShell "parser" is throwing an error because it doesn't recognize the WinSCP library at load time. I have tried adding a module and manifest, but I cannot find a simple example of how to do this properly. Also, it doesn't matter if I'm running PowerShell 5.x or 7.x. All I am wanting is to load this DLL so I can use classes/functions from it. Why is loading a DLL so hard in PowerShell?
What do I need to do to get this WinSCP DLL to load at runtime and not throw an error?
Note on possible duplicates
A very similar question was asked on this site a couple years ago by someone, but there are no answers to it.
Note on suggested duplicate
I am looking for a real example for how to load a DLL file into a script. The linked question does not appropriately do that. Why do I need to create a manifest module thing to import the DLL?
tl;dr:
The problem stems from trying to reference the just-loaded WinSCP types in a class definition, via type literals, such as [WinSCP.Protocol], as explained below.
The problem can be bypassed by not using classes at all, and using functions instead.
I am looking for a real example for how to load a DLL file into a script.
Add-Type -Path / -LiteralPath, as shown in your code does just that:
It loads the specified .NET assembly and makes its public types available in the calling session, just like the similar using assembly statement.
However, since you're using a class definition
attempting to reference a type from an assembly you are loading from the same script file via a type literal (e.g, [WinSCP.Protocol]), the class definition fails:
At script-file parse time, all types being referenced by a class definition must already have been loaded into the session, as of PowerShell 7.3.1.[1]
Removing this counterintuitive requirement for the using assembly statement was green-lit in 2017, but hasn't been implemented as of this writing: see GitHub issue #3641.
This applies equally to referencing types via type literals in the class body (e.g. [WinSCP.Protocol]) and deriving a class from another type, including implementing an interface (e.g. class Foo : WinSCP.Protocol { ... }).
Workarounds:
This answer offers two solutions:
A (multi-file) module-based solution.
A (suboptimal) Invoke-Expression-based solution.
This answer offers a simple two-script solution:
One script that loads the dependent assembly first, and then dot-sources another that contains the class definition based on the dependent assembly's types. 
A workaround isn't always needed, namely if you avoid use of type literals, such as [WinSCP.Protocol]
With respect to [WinSCP.SessionOptions], you're already doing that by using New-Object WinSCP.SessionOptions instead of the more modern (PSv5+) [WinSCP.SessionOptions]::new()
You can also avoid it for the [WinSCP.Protocol]::Sftp enumeration value by simply using a string - 'Sftp' instead - at least for the code snippet shown this would solve your problem; here's a simplified example:
class Foo {
# Note: Do NOT use [WinSCP.SessionOptions] here.
[object] $sessionOptions
[void] CreateSession() {
# Load WinSCP .NET assembly
Add-Type -LiteralPath (Join-Path $PSScriptRoot "\winscp\WinSCPnet.dll")
# Set up session options
# Note the use of *string* 'Sftp' in lieu of [WinSCP.Protocol]::Sftp
$this.sessionOptions = New-Object WinSCP.SessionOptions -Property #{
Protocol = 'Sftp'
}
}
}
Now you can instantiate [Foo] as you normally would - either with New-Object Foo or, preferable with [Foo]::new() (once [Foo] itself is successfully defined, it's fine to refer to it by a type literal, outside class definitions).
[1] Classes were a relatively late addition to the PowerShell language, and, unfortunately, there are still many problems to be worked out - see the list of pending issues in GitHub issue #6652. Note, however, that feature parity with, say, C# classes was never the aim.
The easiest solution, and the one I have resolved to use, is to just use functions and not classes in PowerShell.
Just use functions, not classes in PowerShell.

Classes loaded as dependency in imported module are not imported into script

I have some PowerShell modules and scripts that looks something like this:
MyClassModule.psm1
class MyClass {
#...
}
MyUtilityModule.psm1
using module MyClassModule
# Some code here
MyScript.ps1
using module MyUtilityModule
[MyClass]::new()
My questions are:
Why does this script fail if the class is imported as a dependency in MyUtilityModule?
Are imports always only local to the direct script they are imported into?
Is there any way to make them global for a using statement?
If it makes any difference I need to write with backwards compatibility to at least PowerShell version 5.1
This is all about scope. The user experience is directly influenced by how one loads a class. We have two ways to do so:
import-Module
Is the command that allows loading the contents of a module into the session.
...
It must be called before the function you want to call that is located in that specific module. But not necessarly at the beginning/top of your
script.
Using Module
...
The using statement must be located at the very top of your script. It also must be the very first statement of your script (Except for comments). This
make loading the module ‘conditionally’ impossible.
Comand Type, Can be called anywhere in script, internal functions, public functions, Enums, Classes
Import-Module, Yes, No, Yes, No, No
using Module, No, No, Yes, Yes, Yes
See these references for further details
How to write Powershell modules with classes
https://stephanevg.github.io/powershell/class/module/DATA-How-To-Write-powershell-Modules-with-classes
What is this Module Scope in PowerShell that you Speak of?
https://mikefrobbins.com/2017/06/08/what-is-this-module-scope-in-powershell-that-you-speak-of

Looking for BP on PS using module, import-module, import using dot-sourcing and Add-PSSnapin

Background: I am looking for Best Practices for building PowerShell framework of my own. As I was used to be .NET programmer I like to keep source files small and organize code in classes and libraries.
Question: I am totally confused with using module, import-module, sourcing using dot-import and Add-PSSnapin. Sometimes it works. Sometimes it does not. Some includes work when running from ISE/VS2015 but fail when running via cmd powershell -command "& './myscript.ps1'". I want to include/import classes and functions. I also would like to use type and namespace aliases. Using them with includes produces even weirdest results, but sometimes somehow they work.
Edit: let me be more specific:
Local project case (all files in one dir): main.ps1, common_autorun.ps1, _common.psm1, _specific.psm1.
How to include these modules into main script using relative paths?
_specific.psm1 also rely on _common.psm1.
There are ScriptBlocks passed between modules that may contain calls to classes defined in parent context.
common_autorun.ps1 contains solely type accelerators and namespace imports as described here.
Modules contain mainly classes with static methods as I am not yet used to PowerShell style of programming where functions do not have predicted returns.
As I understand my problems are related to context and scope. Unfortunately these PowerShell concepts are not well documented for v5 classes.
Edit2: Simplified sample:
_common.psm1 contains watch
_specific.psm1 contains getDiskSpaceInfoUNC
main.ps1 contains just:
watch 'getDiskSpaceInfoUNC "\\localhost\d$"'
What includes/imports should I put into these files in order this code to work both in ISE and powershell.exe -command "& './main.ps1'"?
Of cause this works perfectly when both functions are defined in main.ps1.

Powershell - Missing System.Collections.Generic in C:\windows\assembly\GAC_MSIL

I'm trying to use this code in Powershell:
Add-Type -AssemblyName "System.Collections.Generic"
However I get this error:
Add-Type : Cannot add type. One or more required assemblies are missing.
I've looked in C:\windows\assembly\GAC_MSIL and can see that there is no folder named System.Collections.Generic.
Do I need to download this library, if so where?
There is no System.Collections.Generic assembly. That is a namespace. A large portion of the types in that namespace are in the core library assembly, mscorlib.dll which is already available in Powershell since Powershell is .Net.
Go to MSDN for the namespace, find the type you are trying to use, and you can see the assembly it is in, and remember that there is not necessarily a one to one relationship between assemblies and namespaces.
Using generic types is a bit involved in Powershell and can depend on using reflection, or formatting complex type names.
See this stackoverflow question for some more details.
Never mind,
not sure why that doesn't work, but turns out I don't need it anyway.
I had this code which for some reason wasn't working intially because it couldn't find [Collections.Generic.List[String]], but now it seems to work:
[string[]] $csvUserInfo = #([IO.File]::ReadAllLines($script:EmailListCsvFile))
[Collections.Generic.List[String]]$x = $csvUserInfo
I would answer by another question, Why do you need this assembly ?
Can you try to replace your code by :
$x = [IO.File]::ReadAllLines($script:EmailListCsvFile)
In PowerShell it should work.