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;
}
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 thePowerShell.AddScript()
method, and are invoked via thePowerShell
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 originatingPowerShell
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: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 toPowerShell.Invoke()
, as that enables repeated invocations with different input. However, your script block must then be constructed accordingly: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 withPowerShell.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 callps.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 staticPowerShell
instance and adds the script block once, in its static constructor.IDisposable
to allow users of the class control over the PowerShell instance's lifecycle.The code that uses the class, which passes the parameters via the pipeline:
Sample call (
obj
is the object that contains the methods above; assumes a simplifiedEntityProperty
class with property.Value
):The above should yield something like:
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.