36

I've got a module setup to be like a library for a few other scripts. I can't figure out how to get a class declaration into the script scope calling Import-Module. I tried to arrange Export-Module with a -class argument, like the -function, but there isn't a -class available. Do I just have to declare the class in every script?

The setup:

  • holidays.psm1 in ~\documents\windows\powershell\modules\holidays\
  • active script calls import-module holidays
  • there is another function in holidays.psm1 that returns a class object correctly, but I don't know how to create new members of the class from the active script after importing

Here is what the class looks like:

Class data_block
{
    $array
    $rows
    $cols
    data_block($a, $r, $c)
    {
        $this.array = $a
        $this.rows = $r
        $this.cols = $c
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
jason
  • 423
  • 1
  • 5
  • 8
  • Does `Import-Module holidays -verbose` list your `data_block`? – Vesper Jun 25 '15 at 14:23
  • There is just 1 other function in there & it's all that shows up: `PS ~> import-module holidays -verbose VERBOSE: Importing function 'get-holidays'.` – jason Jun 25 '15 at 14:35
  • You can. Consider changing the accepted reply to : http://stackoverflow.com/a/38701492/2502814 – Vincent Aug 19 '16 at 00:37
  • Does `Export-Module` actually exist (not a rhetorical question)? Is it [`Export-ModuleMember`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/export-modulemember)? – Peter Mortensen Jun 18 '21 at 12:55

11 Answers11

30

PSA: There is a known issue that keeps old copies of classes in memory. It makes working with classes really confusing if you don't know about it. You can read about it here.


using is Prone to Pitfalls

The using keyword is prone to various pitfalls as follows:

  • The using statement does not work for modules not in PSModulePath unless you specify the module's full path in the using statement. This is rather surprising because although a module is available via Get-Module the using statement may not work depending on how the module was loaded.
  • The using statement can only be used at the very beginning of a "script". No combination of [scriptblock]::Create() or New-Module seems overcome this. A string passed to Invoke-Expression seems to act as a sort of standalone script; a using statement at the beginning of such a string sort of works. That is, Invoke-Expression "using module $path" can succeed but the scope into which the contents of the module are made available seems rather inscrutable. For example, if Invoke-Expression "using module $path" is used inside a Pester scriptblock, the classes inside the module are not available from the same Pester scriptblock.

The above statements are based on this set of tests.

ScriptsToProcess Prevents Access to Private Module Functions

Defining a class in a script referred to by the module manifest's ScriptsToProcess seems at first glance to export the class from the module. However, instead of exporting the class, it "creates the class in the global SessionState instead of the module's, so it...can't access private functions". As far as I can tell, using ScriptsToProcess is like defining the class outside the module in the following manner:

#  this is like defining c in class.ps1 and referring to it in ScriptsToProcess
class c {
    [string] priv () { return priv }
    [string] pub  () { return pub  }
}

# this is like defining priv and pub in module.psm1 and referring to it in RootModule
New-Module {
    function priv { 'private function' }
    function pub  { 'public function' }
    Export-ModuleMember 'pub'
} | Import-Module

[c]::new().pub()  # succeeds
[c]::new().priv() # fails

Invoking this results in

public function
priv : The term 'priv' is not recognized ...
+         [string] priv () { return priv } ...

The module function priv is inaccessible from the class even though priv is called from a class that was defined when that module was imported. This might be what you want, but I haven't found a use for it because I have found that class methods usually need access to some function in the module that I want to keep private.

.NewBoundScriptBlock() Seems to Work Reliably

Invoking a scriptblock bound to the module containing the class seems to work reliably to export instances of a class and does not suffer from the pitfalls that using does. Consider this module which contains a class and has been imported:

New-Module 'ModuleName' { class c {$p = 'some value'} } |
    Import-Module

Invoking [c]::new() inside a scriptblock bound to the module produces an object of type [c]:

PS C:\> $c = & (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()})
PS C:\> $c.p
some value

Idiomatic Alternative to .NewBoundScriptBlock()

It seems that there is a shorter, idiomatic alternative to .NewBoundScriptBlock(). The following two lines each invoke the scriptblock in the session state of the module output by Get-Module:

& (Get-Module 'ModuleName').NewBoundScriptBlock({[c]::new()})
& (Get-Module 'ModuleName') {[c]::new()}}

The latter has the advantage that it will yield flow of control to the pipeline mid-scriptblock when an object is written to the pipeline. .NewBoundScriptBlock() on the other hand collects all objects written to the pipeline and only yields once execution of the entire scriptblock has completed.

alx9r
  • 3,675
  • 4
  • 26
  • 55
  • 4
    Good to know, though while `using module` requires a _literal_ path (no variables allowed), it can also be a _relative_ path. Also, it makes sense not to default to whatever module by that name _happens to be loaded_. – mklement0 Mar 16 '17 at 18:15
  • I don't know if and when something changed, but loading modules from directories in `$env:PSModulePath` by _name only_ with `using module` does seem to work, even in the WinPS v5.1 version that comes with W11 22H2. – mklement0 Jun 05 '23 at 15:32
  • I think your W11 observation matches what the answer from 2016 claims. As I read this, the answer claims modules that are not in `PSModulePath` (like those loaded using the form [`Import-Module -Name c:\ps-test\modules\test`](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/import-module?view=powershell-5.1) do not load with the `using` statement. I'm pretty sure `using` modules in `PSModulePath` by name has more-or-less worked in all versions that implemented `using`. – alx9r Jun 05 '23 at 16:04
19

I found a way to load the classes without the need of "using module". In your MyModule.psd1 file use the line:

ScriptsToProcess = @('Class.ps1')

And then put your classes in the Class.ps1 file:

class MyClass {}

Update: Although you don't have to use "using module MyModule" with this method you still have to either:

  • Run "using module MyModule"
  • Or run "Import-Module MyModule"
  • Or call any function in your module (so it will auto import your module on the way)

Update2: This will load the Class to the current scope so if you import the Module from within a function for example the Class will not be accessible outside of the function. Sadly the only reliable method I see is to write your Class in C# and load it with Add-Type -Language CSharp -TypeDefinition 'MyClass...'.

ili
  • 405
  • 4
  • 6
  • 6
    This is a great way to expose public classes that are stored in individual ps1s or a subfolder of your module. Can be used as a with a wild card `ScriptsToProcess = @('publicClasses\*.ps1')` – Austin S. Jan 28 '18 at 00:16
  • 3
    It doesn't work: `Import-Module : The member 'ScriptsToProcess' in the module manifest is not valid: The path cannot be processed because it resolved to more than one file; only one file at a time can be processed.. Verify that a valid value is specified for this field in the 'C:\Stash\MyModule\MyModule.psd1' file.` – Mohamed Nuur Nov 22 '18 at 05:31
  • [`ScriptsToProcess` "specifies script (.ps1) files that run in the caller's session state"](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/new-modulemanifest?view=powershell-6) and it ["creates the class in the global SessionState instead of the module's, so it gets exported by default and can't access private functions"](https://github.com/PowerShell/PowerShell/issues/5942#issuecomment-364804062). The class is not defined in the module at all. Strictly-speaking, that's different from the original question of "how to export a class from a module". – alx9r Mar 18 '19 at 17:14
16

According to here and here, you can use classes defined in your module by doing the following in PowerShell 5:

using module holidays
Lars Truijens
  • 42,837
  • 6
  • 126
  • 143
  • 2
    `using` is amazingly frustrating. I have found it basically unusable except for the most trivial circumstances. It turns out though, that `.NewBoundScriptBlock()` works fine to export instances of a class. See my answer [here](http://stackoverflow.com/a/40441684/1404637). – alx9r Nov 05 '16 at 18:32
  • 2
    Danger! Danger! I've beaten my head against the wall countless times on this. Apparently not all 5.0's are the same in this regard. This DOES NOT WORK in 5.0.10514.6 (check using (Get-Host).Version). Update your copy of WinRM 5.0 and it will work (5.0.10586.117 works)! – Dave Markle Feb 05 '17 at 03:45
  • The second link, which states that you can make a function that creates instances of you class and call it to get objects of the class is a work around. – Dave F Feb 24 '21 at 10:37
8

The using statement is the way to go if it works for you. Otherwise this seems to work as well.

File testclass.psm1

Use a function to deliver the class

class abc{
    $testprop = 'It Worked!'
    [int]testMethod($num){return $num * 5}
}

function new-abc(){
    return [abc]::new()
}

Export-ModuleMember -Function new-abc

File someScript.ps1

Import-Module path\to\testclass.psm1
$testclass = new-abc
$testclass.testProp        # Returns 'It Worked!'
$testclass.testMethod(500) # Returns 2500


$testclass | gm


Name        MemberType Definition
----        ---------- ----------
Equals      Method     bool Equals(System.Object obj)
GetHashCode Method     int GetHashCode()
GetType     Method     type GetType()
testMethod  Method     int testMethod(System.Object num)
ToString    Method     string ToString()
testprop    Property   System.Object testprop {get;set;}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Jakobii
  • 508
  • 4
  • 7
  • What don't you like about it? – Espen Feb 07 '18 at 17:09
  • 1
    In my humble opinion, this is about the best way I've seen to modularize things in the bounds of an object and access it from a module. It may not isolate the scope for privatizing things in the class, but it's about the best we can do given some of the limitations of PS. – Mark S Aug 28 '19 at 14:19
  • I like this way to go, it's PS idiomatic and let me avoid using _using module_. – gsscoder Jul 16 '20 at 09:41
5

You pretty much cannot. According to about_Classes help:

Class keyword

Defines a new class. This is a true .NET Framework type. Class members are public, but only public within the module scope. You can't refer to the type name as a string (for example, New-Object doesn't work), and in this release, you can't use a type literal (for example, [MyClass]) outside the script/module file in which the class is defined.

This means, if you want to get yourself a data_block instance or use functions that operate those classes, make a function, say, New-DataBlock and make it return a new data_block instance, which you can then use to get class methods and properties (likely including static ones).

Community
  • 1
  • 1
Vesper
  • 18,599
  • 6
  • 39
  • 61
  • 2
    "You pretty much cannot": This used to be true, but in Powershell 5 you now can. Lars Tuijens' answer give the extra command to add after your Import-module. "using statement enables in PowerShell is the ability to import PowerShell Classes from PowerShell modules!" – Vincent Aug 19 '16 at 00:29
  • 1
    It seems like this was never quite accurate as it looks like you could always [`& (Get-Module ModuleName).NewBoundScriptBlock({[data_block]::new()]}` to export an instance](http://stackoverflow.com/a/40441684/1404637) even before `using` was available. – alx9r Nov 05 '16 at 18:27
  • 1
    Seconding that this hasn't been true since v5.1 at least: ```Classes that you want to be available to users outside of the module should be defined in the root module.``` https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_classes?view=powershell-5.1#importing-classes-from-a-powershell-module – JonoB Jun 23 '22 at 12:14
5

This certainly does not work as expected.
The idea in PowerShell 5 is that you can define your class in a separate file with a .psm1 extension.
Then you can load the definition with the command (e.g.):

using module C:\classes\whatever\path\to\file.psm1

This must be the first line in your script (after comments).

What causes so much pain is that even if the class definitions are called from a script, the modules are loaded for the entire session. You can see this by running:

Get-Module

You will see the name of the file you loaded. No matter if you run the script again, it will not reload the class definitions! (It won't even read the psm1 file.) This causes much gnashing of teeth.

Sometimes - sometimes - you can run this command before running the script, which will reload the module with refreshed class definitions:

Remove-Module  file

where file is the name without path or extension. However, to save your sanity I recommend restarting the PowerShell session. This is obviously cumbersome; Microsoft needs to clean this up somehow.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
0xG
  • 451
  • 5
  • 5
  • 2
    I've created this issue to address the problem of the `using module` statement not reloading the module after changes have been made to it. Please go thumbs up it to up-vote it. https://github.com/PowerShell/PowerShell/issues/7654 – deadlydog Aug 28 '18 at 23:57
  • 1
    https://windowsserver.uservoice.com/forums/301869-powershell/suggestions/35237182--using-module-statement-does-not-reload-module-af @deadlydog, I believe it was you who added this User Voice, as well. – JoePC Apr 03 '19 at 20:44
  • You should be able to reload a module with `Import-Module MyModule -Force`. Not perfect, but still a viable workaround. – curropar Dec 31 '19 at 11:32
4

I've encountered multiple issues regarding PowerShell classes in v5 as well.

I've decided to use the following workaround for now, as this is perfectly compatible with .NET and PowerShell:

Add-Type -Language CSharp -TypeDefinition @"
namespace My.Custom.Namespace {
    public class Example
    {
        public string Name { get; set; }
        public System.Management.Automation.PSCredential Credential { get; set; }
        // ...
    }
}
"@

The benefit is that you don't need a custom assembly to add a type definition. You can add the class definition inline in your PowerShell scripts or modules.

The only downside is that you will need to create a new runtime to reload the class definition after is has been loaded for the first time (just like loading assemblies in a C#/.NET domain).

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
oɔɯǝɹ
  • 7,219
  • 7
  • 58
  • 69
1

The way I've worked around this problem is to move your custom class definition into an empty .ps1 file with the same name (like you would in Java/C#), and then load it into both the module definition and your dependent code by dot sourcing. I know this isn't great, but to me it's better than having to maintain multiple definitions of the same class across multiple files...

Steve Rathbone
  • 458
  • 1
  • 5
  • 14
1

To update class definitions while developing, select the code for the class and press F8 to run the selected code. It is not as clean as the -Force option on the Import-Module command.

Seeing as using Module doesn't have that option and Remove-Module is sporadic at best, this is the best way I have found to develop a class and see the results without having to close down the PowerShell ISE and start it up again.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
DavSum
  • 11
  • 1
0

A surprising & cumbersome limitation of using module appears to be that any classes to expose outside of a module MUST be in the module's psm1 file itself.

A class definition to expose outside the module cannot be 'dotsourced' into the psm1 file from a separate ps1 file in the module

...this is as per the docs since v5.1 (to at least 7.2):

The using module statement imports classes from the root module (ModuleToProcess) of a script module or binary module. It does not consistently import classes defined in nested modules or classes defined in scripts that are dot-sourced into the module. Classes that you want to be available to users outside of the module should be defined in the root module.

So therefore, it seems the simplest options (as discussed in other answers) are:

  1. If you only need to reference class instances outside of its defining module, create public functions to return class instances:

    function Get-MyModulesClass { [MyModuleClass]::New() }

  2. To reference a class type outside of the module (eg specifing a function argument's type), the class must have been defined directly in the psm1 file of the module, and this psm1 file must have been included in your external script via using module (eg using module <relativePathToModulePsm1File>).

...and of course what doesn't help when figuring all this out is that classes don't reload so you need start a new powershell session every time you make a change to the classes.

Example Module

/MyLibaryModule/MyPrivateClass.ps1:

class MyPrivateClass { 
    [void] Test(){ Write-Host "Accessed private class methods!"}
}

/MyLibaryModule/MyLibraryModule.psm1

class MyPublicClass {} # Exposed classes MUST be defined in this file

. $PSScriptRoot\MyPrivateClass.ps1
function Get-MyPrivateClassInstance { [MyPrivateClass]::new()}

/ExampleScript.ps1

using module .\MyLibraryModule\MyLibraryModule.psm1 

[MyPublicClass]$myVar1 # Works 

[MyPrivateClass]$myVar2  # Errors 

Import-Module .\MyLibraryModule\MyLibraryModule.psm1 
$object = Get-MyPrivateClassInstance
$object.GetType().Name
$object.Test()  # works

Output

InvalidOperation: 
Line |
   5 |  [MyPrivateClass]$myVar2  # Errors
     |   ~~~~~~~~~~~~~~
     | Unable to find type [MyPrivateClass].
MyPrivateClass
Accessed private class methods!
JonoB
  • 343
  • 1
  • 9
0

I was having a similar problem and created this sample repo and setup various tests and found the following.

Using PowerShell classes

The only way to be able to reference the class type by name outside of the module (e.g. [data_block]::new()) is:

  1. The class must be defined directly in the psm1 file; it cannot be dot-sourced in.
  2. The module must be imported with using module.

If you use dot-sourcing or Import-Module, everything within the module itself can reference the class name fine, but if something outside of the module references the class name it will get a Unable to find type [data_block] error.

Using C# classes

Everything just works when using C# classes. You can define them in separate files and dot-source them into the .psm1 file, and they will be fully available within the module and to consumers of the module whether they use Import-Module or using module.

For more details, check out this blog post.


To answer your specific question:

I can't figure out how to get a class declaration into the script scope calling Import-Module

This is not possible. You need to either convert your PowerShell class into a C# class, or else use using module (see the docs) to import the module into your script instead of Import-Module.

This is what your sample class might look like as a C# class defined in PowerShell:

Add-Type -Language CSharp -TypeDefinition @"
  public class data_block
  {
    public int[] array { get; set; }
    public int rows { get; set; }
    public int cols { get; set; }

    data_block(int[] a, int r, int c)
    {
        this.array = a
        this.rows = r
        this.cols = c
    }
  }
"@
deadlydog
  • 22,611
  • 14
  • 112
  • 118