locked
Using a Class in a Script Block RRS feed

  • Question

  • This might be a little out there, but I'm trying to create an instance of a class that I have written and then call a function on it inside a scriptblock *without* putting the entire class in the scriptblock.  Like this but a lot more sophisticated (which is why I'm using a class):

    class FooBar {
        [string]foo() {
            return "bar"
        }
    }
    
    $ScriptBlock = {$FooBar = New-Object FooBar; $FooBar.foo()}

    Now, because I'm creating a class, I can't just have the class in a module and use import-module in the scriptblock (doesn't import classes) or use "using" in the script block (apparently the powershell interpreter doesn't care about context with that command's location - it freaks out if it has anything in front of it whatsoever).

    Additionally, the scriptblock create method evaluates the contents of what you feed it, so doing this doesn't work either:

    $ScriptBlock = [ScriptBlock]::Create($FooBar = New-Object FooBar; $FooBar.Foo())

    So... I'm stuck.  Any ideas for how to get this to work?

    ***Edit: invoking the scriptblock directly *does* work, my problem specifically is that I'm feeding the scriptblock to Start-Job, and that is where I'm running into problems.  So add this code to the sample above:

    Start-Job -name "FooBar" -ScriptBlock $ScriptBlock
    Receive-Job -name "FooBar"

    ...and you'll get this:

    Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
    --     ----            -------------   -----         -----------     --------             -------
    25     FooBar          BackgroundJob   Running       True            localhost            $FooBar = New-Object F...
    Cannot find type [FooBar]: verify that the assembly containing this type is loaded.
        + CategoryInfo          : InvalidType: (:) [New-Object], PSArgumentException
        + FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand
        + PSComputerName        : localhost
    
    You cannot call a method on a null-valued expression.
        + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        + FullyQualifiedErrorId : InvokeMethodOnNull
        + PSComputerName        : localhost
    ***Second Edit: This problem with the scriptblock being used to create a job is *not* solved by importing a module inside the scriptblock, because you cannot do that.  So, if you do this:


    $ScriptBlock = {using module "c:\FooBar.psm1"; $FooBar = New-Object FooBar; $FooBar.foo()}
    
    Start-Job -name "FooBar" -ScriptBlock $ScriptBlock
    Receive-Job -name "FooBar"

    This happens:
    A 'using' statement must appear before any other statements in a script.
        + CategoryInfo          : ParserError: (:) [], ParseException
        + FullyQualifiedErrorId : UsingMustBeAtStartOfScript

    Again, the central point of the question is that I don't want to put the whole class into the scriptblock.  Does anyone have any ideas as to how that may be accomplished?
    Wednesday, April 11, 2018 12:23 AM

Answers

  • You can also do this:

    $sb = [scriptblock]::Create('
    using module demo
    $foobar = [foobar]::New()
    $foobar.foo()
    ')
    Start-Job -ScriptBlock $sb |Receive-Job -Wait
    

    Which effectively hides the line from the parser.


    \_(ツ)_/

    Wednesday, April 11, 2018 9:12 PM

All replies

  • This works with no issues:

    class FooBar {
        [string]foo() {
            return "bar"
        }
    }
    $ScriptBlock = {
        $FooBar = New-Object FooBar
        $FooBar.foo()
    }
    $ScriptBlock.Invoke()

    "Create" takes a string and not code.

    PS D:\scripts>  [ScriptBlock]::Create
    
    OverloadDefinitions
    -------------------
    static scriptblock Create(string script)

    There is no issue with importing a module with classes.

    What is it that you are trying to do?  Forget about code and just describe the desired results.


    \_(ツ)_/

    Wednesday, April 11, 2018 12:39 AM
  • To use a class in a module you need to define the module like this:

    using module demo
    
    $foobar = [foobar]::New()
    $foobar.foo()

    The class must be defined outside of any function or advanced function.


    \_(ツ)_/


    • Edited by jrv Wednesday, April 11, 2018 12:50 AM
    Wednesday, April 11, 2018 12:49 AM
  • To use a class from a module in a script block:

    # define this at the top of your script.
    using module demo
    
    ...
    ...
    
    $ScriptBlock = {
        $foobar = [foobar]::New()
        $foobar.foo()
    }
    $ScriptBlock.Invoke()


    \_(ツ)_/

    Wednesday, April 11, 2018 12:52 AM
  • Herp-a-derp.  Invoking it directly works, but using the script block as input to start-job does not.  I was only using Start-Job for testing the script blocks, because that's ultimately what I'll be using for this.  So, add this to the bottom of the script:
    Start-Job -name "FooBar" -ScriptBlock $ScriptBlock
    Receive-Job -name "FooBar"
    And this is what PowerShell throws back at you:

    Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
    --     ----            -------------   -----         -----------     --------             -------
    25     FooBar          BackgroundJob   Running       True            localhost            $FooBar = New-Object F...
    Cannot find type [FooBar]: verify that the assembly containing this type is loaded.
        + CategoryInfo          : InvalidType: (:) [New-Object], PSArgumentException
        + FullyQualifiedErrorId : TypeNotFound,Microsoft.PowerShell.Commands.NewObjectCommand
        + PSComputerName        : localhost
    
    You cannot call a method on a null-valued expression.
        + CategoryInfo          : InvalidOperation: (:) [], RuntimeException
        + FullyQualifiedErrorId : InvokeMethodOnNull
        + PSComputerName        : localhost
    


    Wednesday, April 11, 2018 8:11 PM
  • Jobs run in separate Power Shell instances and all code and modules must be included I n the script or file.  You cannot pass code into a job and a job cannot access variables in the calling runspace.

    This is basic PowerShell and you cannot get around it in any way.

    If the class is in a module then you must reference it inside of the job.


    \_(ツ)_/

    Wednesday, April 11, 2018 8:16 PM
  • That's what I'm getting at - the way that I would reference a class in a module in a script block would be with a using command, but if you do that here, powershell freaks out because it expects using to only occur at the beginning of the script.  

    So, if you have this:

    $ScriptBlock = {using module "c:\FooBar.psm1"; $FooBar = New-Object FooBar; $FooBar.foo()}
    
    Start-Job -name "FooBar" -ScriptBlock $ScriptBlock
    Receive-Job -name "FooBar"

    You get this:

    A 'using' statement must appear before any other statements in a script.
        + CategoryInfo          : ParserError: (:) [], ParseException
        + FullyQualifiedErrorId : UsingMustBeAtStartOfScript



    Wednesday, April 11, 2018 8:29 PM
  • That being said, how do I solve this without putting the entire class in the scriptblock?
    Wednesday, April 11, 2018 8:30 PM
  • Don't use a script block.  Use a file to run the job.


    \_(ツ)_/

    Wednesday, April 11, 2018 8:45 PM
  • I don't want to use a file because that's not best practice - and it really doesn't seem like a powershell-y way to do this.  There should be some way to incorporate a class from a module in a job from a single script.  

    Wednesday, April 11, 2018 8:57 PM
  • You can also do this:

    $sb = [scriptblock]::Create('
    using module demo
    $foobar = [foobar]::New()
    $foobar.foo()
    ')
    Start-Job -ScriptBlock $sb |Receive-Job -Wait
    

    Which effectively hides the line from the parser.


    \_(ツ)_/

    Wednesday, April 11, 2018 9:12 PM
  • That's what I was looking for - thank you!
    Wednesday, April 11, 2018 9:15 PM
  • I don't want to use a file because that's not best practice - and it really doesn't seem like a powershell-y way to do this.  There should be some way to incorporate a class from a module in a job from a single script.  


    Nonsense.  Script files are one of the best ways to run a script in a job and are in no way in violation of BP.  Who told you that silliness?

    \_(ツ)_/

    Wednesday, April 11, 2018 9:15 PM
  • If you use script files, then you have the added overheard of generating and cleaning up your script files.  I've done it before and it can get ugly and confusing - and it's more code to debug.  In one of the actual usecases I'm going to be applying this to, I'd be generating and managing 1500+ script files over the course of a couple days.  In another, the script is not going to be run in a controlled environment - some coworkers of mine will be using it, so generating and deleting temporary files could cause conflicts depending on the file structure of their workstation, and might also cause confusion and distress for some of the less technically inclined of them.  

    Basically, not using generated script files reduces the amount of code I need to write and makes the code I do write more reliable, with less guessing involved.  

    Wednesday, April 11, 2018 9:25 PM
  • Ok.  I understand.  It is best practice to never use script files.  I guess I just need to learn to type faster and more.

    I understand keeping simple things in one script but...when a situation is improved by an external file, then we should use an external script file.  It is a matter of technical choice.  It has nothing to do with BP.

     


    \_(ツ)_/

    Wednesday, April 11, 2018 9:28 PM