I've already read this question and even though it has lots of back and forth, I tried my best to follow the suggestions, but still not sure how to work around this.
I have defined multiple classes in the ScriptsToProcess
.ps1
file like this:
The problem is that when I install the module in a clean state VM (where the module never existed before) and try to use the cmdlets, I get an error saying the type is not found, but if I close and reopen PowerShell or use Import-Module
with -Force
parameter then everything is okay.
I need to know how I can work around this problem without introducing too many repetitive code.
I'm using PowerShell 7.5
I don't fully understand the VM-related problem, but perhaps the following approach bypasses the problem:
Define your class
es as part of your script's root module (*.psm1
, referenced via the RootModule
module-manifest entry) instead of your current approach of using a *.ps1
file that is loaded into the caller's scope via the ScriptsToProcess
manifest entry.
ScriptsToProcess
entry (at least not for exporting your class
es and enum
s).By itself, this makes your classes only available to callers that import the module with a parse-time using module
statement, but note the following:
The linked documentation notes (emphasis added):
[
using module
] doesn't consistently import classes or enumerations defined in nested modules or in scripts that are dot-sourced into the root module. Define classes and enumerations that you want to be available to users outside of the module directly in the root module.
Only the importer and its descendent scopes see the imported module's class
es (and enum
s).
Because using module
is a parse-time statement, the target module path must not contain variables, but modules discoverable via $env:PSModulePath
can be referenced by name only (e.g. using module MyModule
); relative paths are resolved against the caller's own file-system location (rather than against the current directory).
Once a module is imported with using module
in a given session, it cannot be forcefully re-imported; that is, if you want to modify your class
and/or enum
definitions, you'll need to start a new session to see the changes.
However, the Exporting classes with type accelerators section of the about_Classes help topic describes a workaround that makes classes of your choice available session-globally, in all scopes and runspaces, and also works with module auto-loading and Import-Module
The sample code below demonstrates this approach, but comes with the following caveats:
As with the using module
-based approach, you won't be able to reload modified class
definitions in a given session - start a new session instead.
Unlike with the using module
-based approach (which you may still choose to combine with the suggested approach), the (possibly implicit) importer must not rely on the class
es to be available at parse time, i.e. must not try to reference them via type literals in class
definition of its own - see this answer for details.
class
and enum
definitions in a nested module or a *.ps1
file dot-sourced from your root module, this is best avoided if you want your module to also support the using module
importing technique for parse-time availability of the classes.Because the suggested approach (potentially) exposes the class
es to other runspaces too, they should be decorated with the [NoRunspaceAffinity()
attribute, which is available in v7.4+ only. That said, the approach is safe to use in earlier versions as long as only a single runspace (the default runspace) is used.
The linked help topic also includes event-based code for attempting to remove exported class
definitions when the module is unloaded (Remove-Module
), however, this does not work (as of PowerShell 7.4.1), and has therefore been omitted in the code below.
Note that with either approach above, class
es and enum
s won't be available until after your module has been imported into a given session.
That is, unlike functions and cmdlets in auto-loading modules, they aren't discoverable prior to import.
Example *.psm1
root-module content:
# Since the quasi-exported class will be available *process-wide*
# and therefore also in *other runspaces*, be sure to define it with
# the [NoRunspaceAffinity()] attribute.
# Caveat: **v7.4+ only**
[NoRunspaceAffinity()]
class SomeClass { # Note: 'SomeClass' is both its .Name and .FullName.
[int] Get() { return 42 }
}
# Define the types to export with type accelerators.
# Note: Unlike the `using module` approach, this approach allows
# you to *selectively* export `class`es and `enum`s.
$exportableTypes = @(
[SomeClass]
)
# Get the non-public TypeAccelerators class for defining new accelerators.
$typeAcceleratorsClass = [psobject].Assembly.GetType('System.Management.Automation.TypeAccelerators')
# Add type accelerators for every exportable type.
$existingTypeAccelerators = $typeAcceleratorsClass::Get
foreach ($type in $exportableTypes) {
# !! $TypeAcceleratorsClass::Add() quietly ignores attempts to redefine existing
# !! accelerators with different target types, so we check explicitly.
$existing = $existingTypeAccelerators[$type.FullName]
if ($null -ne $existing -and $existing -ne $type) {
throw "Unable to register type accelerator [$($type.FullName)], because it is already defined with a different type ([$existing])."
}
$typeAcceleratorsClass::Add($type.FullName, $type)
}
Any (possibly implicit) importer of the module associated with this *.psm1
will have the [SomeClass]
class available to them at runtime.
That is, [SomeClass]::new().Get()
in the caller's scope and any scope thereafter should yield 42