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