Using Statement: Import PowerShell Classes from Modules
- Written by June Blender
- Last Updated: 23 January 2017
- Created: 28 September 2016
- Hits: 4055
Tested On: Windows PowerShell 5.0, 5.1, 220.127.116.11, 18.104.22.168, PowerShell (Mac OS X) 22.214.171.124
Beginning in Windows PowerShell 5.0, you can create PowerShell classes in scripts and modules. To create different versions of a class, it's best to place the class in a module and use the versioning features of the module to create versions of your class.
But, to import the classes defined in a module into your session, you can't use Import-Module or even #Requires -Module. Instead, use the Using statement, introduced in PowerShell 5.0, which imports the module, including its classes, into the caller's session. The Using statement is not yet documented, so this is a bit confusing. Let's examine it.
Import-Module Does Not Import Classes
Let's start with the standard module import command, Import-Module. With the advent of module auto-loading in PowerShell 3.0, Import-Module almost became obsolete. But, the introduction of side-by-side versioning in PowerShell 5.0 resurrected Import-Module and, in fact, made it your new best friend, because Import-Module has four version parameters that let you specify the version you want: ModuleVersion (minimum version), MaximumVersion, RequiredVersion, and FullyQualifiedName, which takes a ModuleSpecification object.
Import-Module would be a splendid tool to use to import classes. But, Import-Module does not import PowerShell classes defined in the module.
In this test module, ImportTester, I've created an ImportTester class and a Test-Function function. I've explicitly exported the function, but Export-ModuleMember doesn't have a Class parameter to export classes.
The ImportTester module has a module manifest, ImportTester.psd1. The value of the FunctionsToExport key is the Test-Function function. But, module manifests don't have a ClassesToExport key. In fact, there are no class-related keys in the module manifest.
I can use Import-Module to import the ImportTester module and explicitly import version 126.96.36.199. The module is imported and its function is exported, but its ImportTester class is not exported.
#Requires Does Not Import Classes
The #Requires statement is also really useful. Its value can be a module name string or a ModuleSpecification object that can include a version number. But, sadly, although #Requires imports the module, it doesn't import classes defined in the module.
In this test script, I'll use a #Requires statement with a ModuleSpecification object and run the script in the console. The #Requires statement imports the correct version of the module, including the Test-Function function, but it doesn't import the ImportTester class.
Using Module Statement Imports Classes
The Using statement, introduced in Windows PowerShell 5.0, has two uses. It imports assemblies (using namespace) and it imports modules (using module), including their classes. The assembly-importing feature is a convenience; the module-importing feature is a requirement, because there's no other way to import classes from script modules.
The Using statement has a module parameter that takes a module name string or a ModuleSpecification object.
Using namespace <namespace> Using module <string> Using module <Microsoft.PowerShell.Commands.ModuleSpecification>
Unlike #Requires, you can use a Using module statement in a script or interactively in the console, so let's use it to import the ImportTester module.
The import succeeds. The Using module statement imported the module, including its Test-Function function. And, unlike Import-Module and #Requires, the Using module statement imported the ImportTester class.
You can also create a version-specific Using module statement with a ModuleSpecification object. Again, it imports the ImportTester class.
Rules (and Gotchas) for the Using Module Statement
Here are the rules for the Using Module statement:
- Using module statement can be used in a script or module, or run interactively in the console.
- In a script or module, the Using module statement must be the first statement. No uncommented statement can precede it, including parameters.
- Its value can be a string (module name or path to module file) or a ModuleSpecification hashtable.
- Using module imports classes from the root module (ModuleToProcess) of a script module or binary module. The Using statement does not (consistently) import classes defined in nested modules or classes defined in scripts that are dot-sourced into the module. (These classes are, of course, available in module scope without the Using statement.) (Thanks to Brandon Olin for the info about scripts.)
- Using module statement cannot include any variables. Its values must be static.
PS C:\> $moduleName = 'ImportTester' PS C:\> Using module $moduleName At line:1 char:1
+ Using module $moduleName + ~~~~~~~~~~~~~~~~~~~~~~~~
'$moduleName' is not a valid value for using name. + CategoryInfo : ParserError: (:) , ParentContainsErrorRecordException
+ FullyQualifiedErrorId : InvalidValueForUsingItemName
To use variable values in a Using module statement, create a script block that includes the Using module statement and dot-source it into the script. (Thanks to Bartek Bielawski for this solution.)
Param ( [parameter(Mandatory)] [string] $ModuleName ) $scriptBody = "using module $ModuleName" $script = [ScriptBlock]::Create($scriptBody) . $script ...
- Using module statement can include a path to a module that is not in a .psd1 file, but the path must not include variables. Instead, use a relative path...
PS C:\> Using module $home\Documents\ClassTester\188.8.131.52\ClassTester.psd1 At line:1 char:1 + Using module $home\Documents\ClassTester\184.108.40.206\ClassTester.psd1 + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ '$home\Documents\ClassTester\220.127.116.11\ClassTester.psd1' is not a valid value for using name. + CategoryInfo : ParserError: (:) , ParentContainsErrorRecordException + FullyQualifiedErrorId : InvalidValueForUsingItemName PS C:\> cd $home PS C:\Users\User01> Using module .\Documents\ClassTester\18.104.22.168\ClassTester.psd1 PS C:\Users\User01>
Using namespace imports namespaces
Just for completeness, I want to mention the namespace parameter of Using, which imports the namespace from a .NET assembly into the current session.
Here it is in action. I want to create a ModuleSpecification object from a hash table. This isn't required for the FullyQualifiedName parameter, or for the #Requires or the Using statement. Those will take a hash table and convert it to a ModuleSpecification object for you. But, sometimes, you want to create a ModuleSpecification object explicitly.
To do so, by default, you need to type its full .NET name, Microsoft.PowerShell.Commands.ModuleSpecification, with its namespace, Microsoft.PowerShell.Commands.
To make it easier to type, you can use the Using namespace statement to import the namespace. Then, you don't need to type the full name.
But, this works only when the assembly that contains the namespace is available. That is, importing the namespace and using the type name works only when typing the fully-qualified name with the namespace already works.
This isn't the same as the Add-Type cmdlet, which adds a class to the session that is NOT available, even if you type its full name.
For example, I want to use System.Windows.Forms.MessageBox. But, because the type isn't available, Using namespace won't help.
In this case, you need Add-Type.
Which modules export classes?
If you're defining classes in a module and you want users to be able to use them, be sure to write about them in your help topics. Get-Help does not support PowerShell classes, but you can write about the classes that you define in an About topic, like this one.
Help is really critical. The Using statement isn't documented and it isn't (yet) well known, so users are quite likely to use Import-Module or #Requires, or rely on module auto-loading, and then be frustrated because the class isn't imported. Or, more likely, they won't even know that the class exists until they hit a feature that requires it.
To find the classes in a module, you can use the ImplementingAssembly property of imported modules, but it's not well-known either. There are no class-related parameters of Export-ModuleMember, Find-Module, or Get-InstalledModule, and no class-related keys in the module manifest. If you manage to learn about the class and use a Using statement to import the class, there's no help for classes other than help for the script or module in which the class is defined.
So, for now, read the help. And use the Using statement.