Importing multiple versions of Powershell Modules loads them all?

Viewed 1220

Importing multiple versions of modules (this is PS7):

Import-Module -Name MyModule -RequiredVersion 1.0.28
Import-Module -Name MyModule -RequiredVersion 1.0.29

Will load both versions into your session:

Get-Module -Name MyModule 

Outputs:

ModuleType Version    PreRelease Name
---------- -------    ---------- ---- 
Script     1.0.28                MyModule
Script     1.0.29                MyModule

This becomes quite confusing when you're trying to write CI/CD scripts, because someone may have multiple versions loaded locally. In my case I want to ensure a specific version is loaded before continuing.

Which version does PS actually use in this scenario? Is it the last loaded rather than the highest version?

Is there an easy way to ensure a specific version is being used, short of looping over all other versions and removing them?

2 Answers

Which version does PS actually use in this scenario?

The most recently imported version takes precedence, which the output of Get-Member indicates in reverse order of precedence (as of PowerShell Core 7.2.0-preview.5); that is, the last version listed in Get-Module output for a given module is the one in effect (for commands of the same name).

For a given command, you can use Get-Command to see what module (version) it comes from.[1]

Caveat: The most recently imported version only takes precedence in the sense that it shadows commands of the same name from previously imported versions. Therefore, if a previously imported version has additional commands not defined (anymore) by the most recently imported version, they are still in effect.


Is there an easy way to ensure a specific version is being used, short of looping over all other versions and removing them?

If you want to ensure that no other versions are loaded, use Remove-Module <name>, which removes all versions of the module with the given name from the session.

By contrast, if you wanted to unload a specific version, you'd pass a fully qualified module name to Remove-Module's -FullyQualifiedName parameter; e.g.:

Remove-Module -FullyQualifiedName @{ ModuleName = 'MyModule'; RequiredVersion = '1.0.28' }

Note the use of the RequiredVersion key to specify the exact version to unload; by contrast, using key ModuleVersion would unload the given version and all higher ones.


[1] Note that you can even get a reference to a currently shadowed command by requesting it via its specific module version using the -FullyQualifiedModule parameter (e.g.
Get-Command Get-Foo -FullyQualifiedModule @{ ModuleName='Foo'; RequiredVersion='1.0.28' }
), which you can then invoke with &, the call operator. However, due to the bug detailed in GitHub issue #15298, present as of PowerShell Core 7.2.0-preview.5, this only works if the targeted module version was explicitly imported first (e.g., Import-Module Foo -RequiredVersion 1.0.28).

Adding more details that I've had trouble with to mklement0's great answer:

  • Get-Module returns a list of ExportedFunctions,ExportedCmdlets, and ExportedCommands for each loaded module version. Perfect!

  • Be wary of nested modules! Powershell will separate information for the named module, and list any secondary modules under the NestedModules property of the one you asked for.

  • You can specify which commands and module version when you import-module, which can let you manage your running environment better than just importing the entire module at once.

Examples:

# Here I have two versions loaded, but only one has any exported cmdlets:
Get-Module ExchangeOnlineManagement | Format-List Version,ExportedFunctions,ExportedCmdlets,ExportedCommands,NestedModules

Version           : 2.0.4
ExportedFunctions : {[Connect-ExchangeOnline, Connect-ExchangeOnline], …}
ExportedCmdlets   : {[Get-EXOCasMailbox, Get-EXOCasMailbox], …}
ExportedCommands  : {[Get-EXOCasMailbox, Get-EXOCasMailbox], …}
NestedModules     : {Microsoft.Exchange.Management.RestApiClient,
                    Microsoft.Exchange.Management.ExoPowershellGalleryModule}

Version           : 2.0.5
ExportedFunctions : {[Connect-ExchangeOnline, Connect-ExchangeOnline], …}
ExportedCmdlets   : {}
ExportedCommands  : {[Connect-ExchangeOnline, Connect-ExchangeOnline], …}
NestedModules     : {}

# I can see function Connect-ExchangeOnline was loaded twice, so to see which version is active:
Get-Command Connect-ExchangeOnline 

CommandType : Function
Name        : Connect-ExchangeOnline
Version     : 2.0.5
Source      : ExchangeOnlineManagement

# I use this to see current loaded assemblies, and module version should be in the path
[appdomain]::CurrentDomain.GetAssemblies() |
    Where { $_.Modules.FullyQualifiedName.contains('ExchangeOnlineManagement') } |
    Select -ExpandProperty Modules |
    Format-List Name,Assembly,FullyQualifiedName

Name               : Microsoft.Exchange.Management.AdminApiProvider.dll
Assembly           : Microsoft.Exchange.Management.AdminApiProvider, Version=17.0.0.0
FullyQualifiedName : \path\to\Modules\ExchangeOnlineManagement\2.0.4\netCore\Microsoft.Exch 
                     ange.Management.AdminApiProvider.dll

Name               : Microsoft.Exchange.Management.ExoPowershellGalleryModule.dll
Assembly           : Microsoft.Exchange.Management.ExoPowershellGalleryModule, Version=17.0.0.0
FullyQualifiedName : \path\to\Modules\ExchangeOnlineManagement\2.0.4\netCore\Microsoft.Exch 
                     ange.Management.ExoPowershellGalleryModule.dll

Related