Search code examples
c#powershellscriptblockpowershell-sdk

The state of the current PowerShell instance is not valid for this operation in C#


I am having below method which is being called for different PS scripts and I would like the PowerShell object to be created only once and for that I made the Powershell object as static(see below code). but then it gives me error

The state of the current PowerShell instance is not valid for this operation.

How should I handle this? what is the best way to optimize my below code? NB: Below code works fine if I remove static.

class DataRulesPSScripts
    {
        static PowerShell ps = PowerShell.Create();
        public IEnumerable<object> RunScriptBlock( ScriptBlock scriptBlock, Dictionary<string, object> scriptParameters )
        {
            var vars = scriptParameters.Select( p => new PSVariable( p.Key, p.Value ) ).ToList();
            return scriptBlock.InvokeWithContext( null, vars );
        }

        public async Task<ScriptBlock> CreateScriptBlock( string pSScript )
        {            
            ps.AddScript( pSScript );
            var scriptBlock = (await ps.InvokeAsync())[0].BaseObject as ScriptBlock;
            return scriptBlock;
        }
    }
}

this is called from here :

internal async Task<string> GeneratePartitionKey( Dictionary<string, EntityProperty> arg)
        {       
            var result =await GenerateKeys(arg);
            return result[0].ToString();
        }        

        internal async Task<string> GenerateRowKey( Dictionary<string, EntityProperty> arg )
        {
            var result = await GenerateKeys( arg );
            return result[1].ToString();
        }

        private async Task<List<object>> GenerateKeys( Dictionary<string, EntityProperty> arg )
        {
            var pars = new Dictionary<string, object>();
            pars.Add( "_", arg );
            DataRulesPSScripts ds = new DataRulesPSScripts();
            var scriptBlock = await ds.CreateScriptBlock( PSScript );
            var results = ds.RunScriptBlock( scriptBlock, pars ).ToList();
            return results;
        }

Solution

  • There is no reason to create and interact with ScriptBlock instances directly in your C# code - they are used internally by the PowerShell SDK:[1] They are internally created and stored when you pass a piece of PowerShell code as a string to the PowerShell.AddScript() method, and are invoked via the PowerShell instance's, .Invoke() method.

    While your indirect way of obtaining a script block for direct execution in C# code by letting a PowerShell instance create and output it for you via an .AddScript() call (ps.AddScript( pSScript ); var scriptBlock = (await ps.InvokeAsync())[0].BaseObject as ScriptBlock;) does give you a script block that you can call directly from C# code via its .Invoke() method (if you created the script block directly in C# code, you wouldn't be able to invoke it all, due to not being connected to a PowerShell runspace), such calls only provide success output - output from all other PowerShell streams would be lost - that is, the originating PowerShell instance's .Streams property wouldn't reflect such output, which notably makes non-terminating errors that occurred inaccessible, and, similarly, the .HadErrors property would not reflect whether non-terminating errors occurred. Therefore, this approach should be avoided.[2]

    Here's an example that creates a script bock implicitly, behind the scenes, via PowerShell.AddScript(), passes an argument to it and invokes it:

    // Define the script-block text.
    // It expects a single argument that is an object with .Name and .Code
    // properties, whose values are echoed.
    // NOTE: Do NOT include { ... }
    var scriptBlockText = "$obj = $args[0]; $obj.Name; $obj.Code";
    
    // Define an object to pass to the script block.
    var obj = new { Name = "Abc", Code = 42 };
    
    using (var ps = PowerShell.Create()) {
      
      // Add the script block and an argument to pass to it.
      ps
        .AddScript(scriptBlockText)
        .AddArgument(obj);
    
      // Invoke and echo the results.  
      foreach (var o in ps.Invoke()) {
        Console.WriteLine(o);
      }
    
    }
    

    However, the above isn't reusable, because once you've added arguments or parameters with .AddParameter(s) or .AddArgument(), you cannot remove them and specify different ones to perform another call - as far as I know.

    The workaround is to use PowerShell pipeline input (as provided via the optional input parameter you can pass to PowerShell.Invoke(), as that enables repeated invocations with different input. However, your script block must then be constructed accordingly:

    // Define the script-block text.
    // This time, expect the input to come via the *pipeline*, which
    // can be accessed via the $input enumerator.
    // NOTE: Do NOT include { ... }
    var scriptBlockText = "$obj = $($input); $obj.Name; $obj.Code";
    
    // Define two objects to pass to the script block, one each in 
    // two separate invocations:
    object[] objects = {
      new { Name = "Abc", Code = 42 },
      new { Name = "Def", Code = 43 }
    };
    
    using (var ps = PowerShell.Create()) {
      
      // Add the script block.
      ps.AddScript(scriptBlockText);
    
      // Perform two separate invocations.
      foreach (var obj in objects) {
    
        // For housekeeping, clean the previous non-success streams.
        ps.Streams.ClearStreams();
    
        // Invoke the script block with the object at hand and echo the results.
        // Note: The input argument must be an enumerable, so we wrap the object
        //       in an aux. array.
        foreach (var o in ps.Invoke(new[] { obj })) {
          Console.WriteLine(o);
        }
    
      }
    
    }
    

    Alternatively, if feasible, consider making do without script blocks, as they require parsing (albeit as one-time overhead in this case) and - on Windows - are subject to the effective execution policy, which could prevent their execution (though you can bypass such a restriction on a per-process basis, see this answer).

    Without script blocks, you'd have to invoke one or more commands individually, using PowerShell.AddCommand() calls, separating multiple independent commands with PowerShell.AddStatement().

    • If a single command or a pipeline of command accepts all input via the pipeline, you can use the same approach as above.

    • Otherwise - if .AddParameter(s) / .AddArgument() are needed - you'd have to call ps.Commands.Clear() and re-add the commands before every (repeat) invocation; however, compared to calling .AddScript(), this should introduce little overhead.


    Adaptation of the reusable technique to your code:

    Class DataRulesPSScripts, which uses a static PowerShell instance and adds the script block once, in its static constructor.

    • Note: Consider using an instance property instead and making the class implement IDisposable to allow users of the class control over the PowerShell instance's lifecycle.
    class DataRulesPSScripts
    {
      static PowerShell ps = PowerShell.Create();
      // The script-block text:
      // Note that $ParamA and $ParamB must correspond to the keys of the
      // dictionary passed to the script block on invocation via .InvokeAsync()
      static string PSScript = @"$argDict = $($input); & { param($ParamA, $ParamB) [pscustomobject] @{ Partition = $ParamA; Key = 1 }, [pscustomobject] @{ Row = $ParamB; Key = 2 } } @argDict";
    
      static DataRulesPSScripts() {
        // Add the script-block text only once, which therefore incurs the
        // overhead of parsing the text into a script block only once,
        // and allows repeated later invocations via .Invoke() with pipeline input.
        ps.AddScript(PSScript);
      }
      public async Task<IEnumerable<object>> RunScriptBlock(Dictionary<string, EntityProperty> scriptParameters)
      {
        // Pass the parameter dictionary as pipeline input.
        // Note: Since dictionaries are enumerable themselves, an aux. array
        //       is needed to pass the dictionary as a single object.
        return await ps.InvokeAsync<object>(new [] { scriptParameters });
      }
    
    }
    

    The code that uses the class, which passes the parameters via the pipeline:

    internal async Task<string> GeneratePartitionKey(Dictionary<string, EntityProperty> arg)
    {
      var result = await GenerateKeys(arg);
      return result[0].ToString();
    }
    
    internal async Task<string> GenerateRowKey(Dictionary<string, EntityProperty> arg)
    {
      var result = await GenerateKeys(arg);
      return result[1].ToString();
    }
    
    
    private async Task<List<object>> GenerateKeys(Dictionary<string, EntityProperty> args)
    {
      DataRulesPSScripts ds = new DataRulesPSScripts();
      var results = await ds.RunScriptBlock(args);
      return results.ToList();
    }
    

    Sample call (obj is the object that contains the methods above; assumes a simplified EntityProperty class with property .Value):

    Console.WriteLine(
      obj.GenerateRowKey(
        new Dictionary<string, EntityProperty> { ["ParamA"] = new EntityProperty { Value = "bar" },  ["ParamB"] = new EntityProperty { Value = "baz" } }
      ).Result
    );
    

    The above should yield something like:

    @{Row=demo.EntityProperty; Key=2}
    

    This is the string representation of the 2nd custom object output by the script block.


    [1] In PowerShell script code, by contrast, ScriptBlock instances are used directly, typically in the form of script-block literals ({ ... }), invoked with &, the call operator.

    [2] Here's a quick demonstration from PowerShell:
    $ps=[PowerShell]::Create(); $sb = $ps.AddScript("{ 'hi'; Get-Item nosuchfile }").Invoke()[0]; "call result: $($sb.Invoke())"; "had errors: $($ps.HadErrors)"; "error stream: $($ps.Streams.Error)"
    Even though the call produced a non-terminating error, .HadErrors reports $false, and the .Streams.Error is empty.