PowerShell classes behaviour inconsistent
When you create a binary PowerShell module, just by compiling a simple, barebones C# class DLL and adding it to the module path (even without manifest), the class objects inside the DLL will be immediately available for that session after the command 'import-module'.
When you create a 'classic script based' PowerShell module (using .psm1/.psd1) the classes inside the module are only available to that module internally, but not to the PS session by default, unless you run the 'using module ...' command in the PowerShell session.
That by itself is already inconsistent. It gets more confusing when you create a new object using New-WebserviceProxy. The associated classes from the online web service are from then on also available in the current session, without any extra loading.
So, classes in 'foreign' modules are autoloaded and available, while 'PowerShell native' classes are not. This seems to be bug or at least inconsistent behaviour.
It would be great if the 'Export-ModuleMember' command was expanded to support classes as well.
(I have a feeling this is an oversight. I spoke to someone at the PowerShell booth at Microsoft Ignite 2016 and the person was surprised classes inside text based .psm1 files are not available outside the module by default)
See also related comments on: https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/16504684-package-powershell-classes-in-a-module
Steps to reproduce
Create new PowerShell module with classes inside. Classes not available by default when importing module.
Expected behavior
Classes from binary PowerShell modules as well as classic, script based module should load in the same way.
Actual behavior
You need to explicitly call 'Using module ...' in the running PowerShell session in order to get access to the classes inside a script based PowerShell module.
Environment data
> $PSVersionTable
Name Value
---- -----
PSVersion 5.1.14393.206
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.14393.206
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
To add to what Steve's reported here, 'using' directives need to be placed at the top of a script, but this doesn't seem to stop you from placing them at the top of a secondary script and then invoking that script from the middle of your first one.
Main.ps1
Write-Host "This is the first line"
& LoaderScript.ps1
[SomeClass]::new()
LoaderScript.ps1
using ".\SomeModule.psm1"
SomeModule.psm1
Class SomeClass {
[string]$SomeProperty
}
I get what they're going for with 'using' - it does some much-needed things for IntelliSense when you get into class management, but wimbor's point remains that there is still a demand to load them dynamically.
I, too, expected there to be some form of Export-ModuleMember -Class
Thank you, great discussion!
Why there is a need in using module?
Using module looks like a duplicated facility with Import-Module.
Let's take a look.
- PowerShell is a dynamic language.
- Classes provide some parse-time checks. I.e. if you have a class that has a return value
[string]but doesn't return anything from anywhere, you will get an error on the module load (actually, during the module parsing, which happens, when you are trying to load it). It gives developers ability to find problems earlier. - Mixing parse time checks into dynamic language is not a picnic.
-
using moduleseparated fromImport-Modulebecause is in this parse-time vs runtime difference.Import-Modulecould take a variable value and load it in runtime, butusing modulecan take only constant expressions. Also, becauseusingstatement is allowed only at the beginning of the file, it greatly simplifies things: now you don't need to try to figure out (dynamic) scopes and visibility rules and can just use whole files as scopes. - If you want to use one class in the context of another class, i.e.
# A.psm1
class A{}
# B.psm1
Import-Module ./A.psm1
class B {
[A]getA() { return [A]::new() }
}
The type definition of [A] should be available at parse time to [B], so this code could not work reliable. That's why we need using module and that's why it's always at the top of the script: to simplify scoping rules for class names visibility.
Export-ModuleMember
The classes are exported out of the module. To see them as available types use using module. Should we also make it available as types for Import-Module? Yes, I think that's a good idea.
Note that it may still be confusing and inconsistent because after this change you would be able to write code:
# A.psm1
class A{
$foo = 'bar'
}
# B.psm1
Import-Module ./A.psm1
class B {
[A]getA() { return [A]::new() } # error, this is compile-time dispatching
}
[A]::foo # no error, this is runtime dispatching
These are the fundamental incompatibility between two approaches, but I think we can collaboratively improve the experience.
One question about using module. I have a simple module that declare a 'Computer' class.
@'
class Computer {
[string] $Name
[string] $OS
}
'@ >C:\Temp\Computer\1.0\Computer.psm1
I can write this into a script :
@'
using module C:\Temp\Computer\1.0\Computer.psm1
[Computer]::new()
'@ > c:\temp\using.ps1
But not this :
@'
using module C:\Temp\Computer\1.0\Computer.psm1
[Computer]::new()
$Object=Start-job {
using module C:\Temp\Computer\1.0\Computer.psm1
[Computer]::New()
} |
Wait-Job|
Receive-job -AutoRemoveJob -Wait
'@ > c:\temp\using.ps1
I get an exception :
At C:\temp\using.ps1:5 char:4
+ using module C:\Temp\Computer\1.0\Computer.psm1
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A 'using' statement must appear before any other statements in a script.
+ CategoryInfo : ParserError: (:) [], ParseException
+ FullyQualifiedErrorId : UsingMustBeAtStartOfScript
This code is not possible, I get the same exception :
@'
$Object=Start-job {
using module C:\Temp\Computer\1.0\Computer.psm1
[Computer]::New()
} |
Wait-Job|
Receive-job -AutoRemoveJob -Wait
'@ > c:\temp\Test.ps1
.\test.ps1
It is by design ?
This code is possible :
@'
$Object=Start-job {
Invoke-Expression "using module C:\Temp\Computer\1.0\Computer.psm1"
[Computer]::New()
} |
Wait-Job|
Receive-job -AutoRemoveJob -Wait
$object
'@ > c:\temp\Test2.ps1
.\test2.ps1
The use of Invoke-Expression can it be a workaround ?
PSversion 5.0.10586.117, Seven x64
@LaurentDardenne that's an excellent example, thank you for sharing it. UX indeed feels a little bit broken. One way to see it is "how to defer parse time check to runtime". I would not recommend use a temp file or invoke-expression. A little bit cleaner workaround would be creating scriptblock for Start-Job argument dynamically from string. Use [scriptblock]::create().
One design problem with allowing using module to be at beginning of any arbitrary scriptblock is that it would be significant complicate lookups and resolution.
You could use Start-Job -FilePath instead of Invoke-Expression.
using module confuse us because it is directive at parse time but looks like runtime term.
It would be reasoned to use #using module
I found an interesting way to break things, without having to involve modules at all.
Have a script file Skippy.ps1 with the following contents:
#Requires -Version 7
[Skippy]::Hi()
Then execute the following:
PS C:\> class Skippy { static [void] Hi() { Write-Host "skippy" } }
PS C:\> & "$Home\OneDrive\Desktop\skippy.ps1"
skippy
PS C:\> class Skippy { static [void] Hi() { Write-Host "skippy@@@" } }
PS C:\> & "$Home\OneDrive\Desktop\skippy.ps1"
skippy
PS C:\> # i edited Skippy.ps1 and added an extra blank line
PS C:\> & "$Home\OneDrive\Desktop\skippy.ps1"
skippy@@@
PS C:\>
You can see that the old version of the class gets called, until I make a meaningless change to the file and it picks up the new version.
Same behavior under Powershell version 5.1.
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
This issue has not had any activity in 6 months, if this is a bug please try to reproduce on the latest version of PowerShell and reopen a new issue and reference this issue if this is still a blocker for you.
This issue has been marked as "No Activity" as there has been no activity for 6 months. It has been closed for housekeeping purposes.