当前 PowerShell 实例的状态对于 C# 中的此操作无效

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

我正在为不同的 PS 脚本调用以下方法,我希望只创建一次 PowerShell 对象,为此我将 Powershell 对象设置为静态对象(参见下面的代码)。但它给了我错误

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

我该如何处理?优化我的以下代码的最佳方法是什么? 注意:如果我删除静态,下面的代码可以正常工作。

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;
        }
    }
}

这是从这里调用的:

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;
        }

没有理由直接在您的 C# 代码中创建 ScriptBlock 实例并与其交互 - PowerShell SDK 内部 使用它们:[1] 当您将一段 PowerShell 代码作为 string 传递给 PowerShell.AddScript() method, and are invoked via the PowerShell instance's, .Invoke()方法。

虽然你的间接获取脚本块的方式在C#代码中直接执行PowerShell 实例通过 .AddScript() 调用 (ps.AddScript( pSScript ); var scriptBlock = (await ps.InvokeAsync())[0].BaseObject as ScriptBlock;) 为您创建和输出它 does 给您一个脚本块,您可以调用直接从 C# 代码通过它的 .Invoke() 方法(如果你直接在 C# 代码中创建脚本块,你将无法调用它,因为没有连接到 PowerShell runspace), 此类调用仅提供 成功 输出 - 所有其他 PowerShell streams 的输出将是 lost - 也就是说,原始 PowerShell 实例的 .Streams 属性 不会反映此类输出,这尤其使非终止发生的错误无法访问,同样,.HadErrors属性也不会反映是否发生了非终止错误。所以这种做法应该避免.[2]

这是一个示例,通过 PowerShell.AddScript() 在幕后 隐式地创建脚本块 ]向它传递一个参数并调用它:

// 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);
  }

}

但是,以上内容不可重复使用,因为一旦您使用 .AddParameter(s).AddArgument() 添加参数或参数,你不能删除它们并指定不同的 来执行另一个调用 - 据我所知。

解决方法是使用PowerShell 管道输入(通过可选的input参数提供,您可以传递给PowerShell.Invoke(),因为 允许使用不同的输入重复调用。 但是,您的脚本块必须相应地构建:

// 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);
    }

  }

}

或者,如果可行,考虑在没有 脚本块的情况下,因为它们需要解析(尽管这是一次性开销case) 和 - 在 Windows - 受有效 execution policy, which could prevent their execution (though you can bypass such a restriction on a per-process basis, see this answer).

约束

如果没有脚本块,您将不得不使用 PowerShell.AddCommand() calls, separating multiple independent commands with PowerShell.AddStatement().

单独调用一个或多个命令
  • 如果单个命令或命令管道通过管道接受所有输入,您可以使用与上述相同的方法。

  • 否则 - 如果需要 .AddParameter(s) / .AddArgument() - 你必须先调用 ps.Commands.Clear() 重新添加命令每次(重复)调用;但是,与调用 .AddScript() 相比,这应该会引入很少的开销。


使可重用技术适应您的代码:

ClassDataRulesPSScripts,它使用静态PowerShell实例 并在其静态构造函数中添加一次脚本块。

  • 注意:考虑使用 实例 属性 并使 class 实现 IDisposable 以允许 [=184= 的用户] 控制 PowerShell 实例的生命周期。
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 });
  }

}

使用class的代码,通过管道传递参数:

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();
}

示例调用(obj 是包含上述方法的对象;假设一个简化的 EntityProperty class 和 属性 .Value):

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

上面的结果应该是这样的:

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

这是脚本块输出的第二个自定义对象的字符串表示形式。


[1]在PowerShell脚本代码中,相比之下,ScriptBlock个实例直接使用的,通常采用 script-block literals ({ ... }), invoked with &, the call operator.

的形式

[2] 下面是 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)"
即使调用产生了非终止错误,.HadErrors 报告 $false,并且 .Streams.Error 为空。