PowerShell icon indicating copy to clipboard operation
PowerShell copied to clipboard

PowerShell classes behaviour inconsistent

Open wimbor opened this issue 9 years ago • 12 comments

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                                                                     

wimbor avatar Oct 10 '16 19:10 wimbor

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

rumbawls avatar Oct 19 '16 15:10 rumbawls

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 module separated from Import-Module because is in this parse-time vs runtime difference. Import-Module could take a variable value and load it in runtime, but using module can take only constant expressions. Also, because using statement 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.

vors avatar Oct 22 '16 04:10 vors

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 avatar Oct 23 '16 09:10 LaurentDardenne

@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.

vors avatar Oct 23 '16 19:10 vors

You could use Start-Job -FilePath instead of Invoke-Expression.

lzybkr avatar Oct 25 '16 13:10 lzybkr

using module confuse us because it is directive at parse time but looks like runtime term. It would be reasoned to use #using module

iSazonov avatar Oct 25 '16 14:10 iSazonov

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.

User1785604260 avatar Mar 27 '23 22:03 User1785604260

Same behavior under Powershell version 5.1.

LaurentDardenne avatar Mar 28 '23 13:03 LaurentDardenne

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.