Runspace 中的 C# Powershell - 如何让 "Format-List" 工作?

C# Powershell in Runspace - How can i get "Format-List" to work?

我目前正在用 C# 分别使用“System.Management.Automation”和“System.Management.Automation.Runspaces”中的 Powershell 命令和运行空间为我们的 Exchange Online 租户编写联系人管理器。它可以很好地向 GAL 添加联系人。但是我一直在编辑联系人。

我需要使用 Powershell 命令获取联系方式。我可以执行的代码如下所示:

var command = new PSCommand();
command.AddCommand("Get-Contact");
command.AddParameter("Identity", "someContact");

但是:当然,这只给我联系人姓名。我需要扩展该命令。我需要执行的等效本机 Powershell 命令如下所示:

Get-Contact -Identity "someContact" | Format-List

当我尝试以某种方式从上方将“格式列表”添加到命令方案时 - 例如:

var command = new PSCommand();
command.AddCommand("Get-Contact");
command.AddParameter("Identity", "someContact");
command.AddCommand("Format-List");

我得到一个异常,告诉我“Format-List”不是 Cmdlet 的名称或任何东西......我也尝试用 AddParameter 或什至 AddArgument 添加它 - none 这些工作,我总是以错误结束。

使用 Google 我在 Whosebug 上找到了线程,人们在其中通过“AddScript()”-Command 传递了一个脚本。但是当我做这样的事情时:

AddScript("Get-Contact -Identity 'someContact' | Format-List");

它告诉我,语法无法识别,因为 Remote-Powershell 在无语言模式下 运行。我不知道如何更改语言模式,如果可能的话。

以下是我用来在我们的 Exchange Online 租户上执行 Remote-Powershell-Commands 的完整代码:

        // sPass Variable in SecureString umwandeln (Passwort muss ein SecureString
        // sein, sonst wird es von WSManConnectionInfo nicht akzeptiert!)
        SecureString ssPass = new NetworkCredential("", sPass).SecurePassword;

        // Exchange Online Credentials vorbereiten
        PSCredential credential = new PSCredential(sUserAndMail, ssPass);

        // Connection zu Exchange Online mit der URL vorbereiten und Authentication Mode auf Basic setzen
        WSManConnectionInfo wsEOConnInfo = new WSManConnectionInfo(new Uri(sURI), sSchema, credential);
        wsEOConnInfo.AuthenticationMechanism = AuthenticationMechanism.Basic;
        wsEOConnInfo.IdleTimeout = 60000;

        // Runspace erstellen, in dem die Powershell-Befehle ausgeführt werden
        using (Runspace runspace = RunspaceFactory.CreateRunspace(wsEOConnInfo))
        {
            // Connection herstellen
            runspace.Open();

            // Prüfen, ob Connection existiert. Wenn ja, Commands ausführen
            if (runspace.RunspaceStateInfo.State == RunspaceState.Opened)
            {
                PowerShell ps = PowerShell.Create();

                // Zunächst die ExecutionPolicy auf RemoteSigned für den aktuellen Benutzer setzen. 
                // Andernfalls stehen die MailContact-Befehle der Remote-Powershell nicht zur Verfügung.
                ps.AddCommand("Set-ExecutionPolicy").AddParameter("ExecutionPolicy", "RemoteSigned").AddParameter("Scope", "CurrentUser");
                ps.Invoke();

                // Nun den Befehl auf Basis des Use-Cases zusammenstellen
                switch (SearchCaseValue)
                {
                    case 1:
                    {
                        // Hier den Befehl zum Suchen des Kontakts auf Basis des Namens
                        var command = new PSCommand();
                        command.AddCommand("Get-Contact");
                        command.AddParameter("Identity", "*" + tb_SearchTerm.Text + "*");

                        // Kommando zusammensetzen und die Ausführung in diesem Runspace festlegen
                        ps.Commands = command;
                        ps.Runspace = runspace;

                        // Kommando ausführen
                        try
                        {
                            // Den Output des Invokes einer Collection zum Zugriff auf die Inhalte zuweisen
                            Collection<PSObject> psOutput = ps.Invoke();

                            // Einen neuen StringBuilder instantiieren
                            var sb = new System.Text.StringBuilder();

                            // Wenn der Inhalt der Collection nicht leer ist, dann jede Zeile des Powershell-Ouputs
                            // aus der Collection in neue Zeilen des StringBuilders schreiben
                            foreach (PSObject outputItem in psOutput)
                            {
                                lb_SearchResults.Items.Add(outputItem.ToString());
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message.ToString());
                        }

                        // Hintergrundfarbe der Listbox ändern, da nun Ergebnisse darin angezeigt werden
                        // und den Button zum Reset der Ergebnisse aktivieren
                        lb_SearchResults.BackColor = Color.White;
                        panel_SearchInProgress.Visible = false;
                        bt_ResetSearchResults.Enabled = true;

                        // Runspace Schließen
                        runspace.Close();
                        break;
                    }
                    case 2:
                    {
                        // Hier den Befehl zum Suchen des Kontakts auf Basis der eMail-Adresse erstellen
                        var command = new PSCommand();
                        command.AddCommand("Get-Contact");
                        command.AddParameter("Filter", "((WindowsEmailAddress -like '*" + tb_SearchTerm.Text + "*'))");

                        // Kommando zusammensetzen und die Ausführung in diesem Runspace festlegen
                        ps.Commands = command;
                        ps.Runspace = runspace;

                        // Kommando ausführen
                        try
                        {
                            // Den Output des Invokes einer Collection zum Zugriff auf die Inhalte zuweisen
                            Collection<PSObject> psOutput = ps.Invoke();

                            // Einen neuen StringBuilder instantiieren
                            var sb = new System.Text.StringBuilder();

                            // Wenn der Inhalt der Collection nicht leer ist, dann jede Zeile des Powershell-Ouputs
                            // aus der Collection in neue Zeilen des StringBuilders schreiben
                            foreach (PSObject outputItem in psOutput)
                            {
                                lb_SearchResults.Items.Add(outputItem.ToString());
                            }
                        }
                        catch (Exception ex)
                        {
                            MessageBox.Show(ex.Message.ToString());
                        }

                        // Hintergrundfarbe der Listbox ändern, da nun Ergebnisse darin angezeigt werden
                        // und den Button zum Reset der Ergebnisse aktivieren
                        lb_SearchResults.BackColor = Color.White;
                        panel_SearchInProgress.Visible = false;
                        bt_ResetSearchResults.Enabled = true;

                        // Runspace Schließen
                        runspace.Close();
                        break;
                    }
                }    
            }
            // Runspace schließen, falls nicht bereits geschehen. Wichtig, da in Exchange Online
            // nur maximal 3 Runspaces (Connections) gleichzeitig offen sein dürfen!
            runspace.Dispose();
        }

我希望您发现这段摘录对解决问题很有用。抱歉里面有德语评论。我需要跟踪我所做的事情,你知道吗?! :-)

所以...你能告诉我如何在不使用脚本的情况下将“格式列表”传递给远程 Powershell 吗?

非常感谢您的提前帮助! 斯蒂芬

帮我理解为什么需要将联系人对象传递给 Format-Table。这基本上是在破坏接触对象本身。 Format-Table 只是一种将 PS 对象布局到 PS 主机的方法,一旦将对象传递给此函数,该对象将失去其所有属性和方法。

我将向您展示我所说的 AD 用户对象的示例。

无格式-Table:

PS C:\> $aduser=Get-ADuser -Filter *|select -First 1

PS C:\> $aduser.GetType()

IsPublic IsSerial Name                                     BaseType                                                         
-------- -------- ----                                     --------                                                         
True     False    ADUser                                   Microsoft.ActiveDirectory.Management.ADAccount                   

PS C:\> $aduser.psobject.Properties.Name
DistinguishedName
Enabled
GivenName
Name
ObjectClass
ObjectGUID
SamAccountName
SID
Surname
UserPrincipalName
PropertyNames
AddedProperties
RemovedProperties
ModifiedProperties
PropertyCount

格式-Table:

PS C:\> $aduser=Get-ADuser -Filter *|select -First 1|format-table

PS C:\> $aduser.GetType()

IsPublic IsSerial Name                                     BaseType                                                         
-------- -------- ----                                     --------                                                         
True     True     Object[]                                 System.Array                                                     

PS C:\> $aduser.psobject.Properties.name
Count
Length
LongLength
Rank
SyncRoot
IsReadOnly
IsFixedSize
IsSynchronized

我希望这是有道理的。

PSObject returned by ps.Invoke() 包含对象的所有属性。您可以获得所有属性的值。 试试这个

ICollection<PSObject> psOutput = ps.Invoke();
foreach (PSObject outputItem in psOutput)
{                                
     var name = outputItem.Members["Name"].Value.ToString();
     var distinguishedName = outputItem.Members["DistinguishedName"].Value.ToString();
     var displayName = outputItem.Members["DisplayName"].Value.ToString();
     var lName = outputItem.Members["LastName"].Value.ToString();

}

只要输入你想得到的 属性 的名字,它就会 return 你得到那个 属性 的值。

注:

  • 以下部分展示了如何进一步处理从远程 本地运行空间中的PowerShell运行空间,在这种情况下,出于安全原因,这是必需的。

    • 这里,Format-List命令必须在本地应用,但注意一般是 不需要 调用 Format-* cmdlet 来处理从 PowerShell SDK 调用返回的对象 - 请参阅下一点;你只需要 Format-* 如果你想创建一个 for-display, string 对象的表示,就像你在 PowerShell 控制台(终端)中看到的那样。
  • 底部部分讨论了如何处理从 PowerShell SDK 调用返回的对象一般.

    • 事实证明,Steffen 真正想要的是直接使用输出对象作为 data

Mathias R. Jessen 在评论中提供了关键指针:

  • 出于安全原因,您的远程运行空间在 language mode 和允许执行的特定 cmdlet 方面受到限制.

    • NoLanguage 模式阻止使用任何类型的 PowerShell 代码,这排除了 .AddScript() 方法的使用。
    • 似乎不​​允许使用 Format-List cmdlet。
  • 如果您确实需要将 Format-List 应用于远程运行空间的输出,您将需要 使用 第二个 PowerShell 本地 执行 Format-List 的实例,您将远程运行空间的输出 传递给它 - 见下文。

    • 正如其他人所暗示的,仅当您想要 for-display-onlystring 输出对象的表示,如通常在 PowerShell 控制台(终端)中所示。

      • 否则,处理数据,只需直接使用返回的对象及其属性,如底部所示。
    • 此外,Format-List 本身不输出 strings,但 objects 包含 格式化说明;要将后者转换为它们编码的格式化字符串表示形式,请将它们传递给 Out-String cmdlet。

一个简化的例子:

PowerShell psRemote = PowerShell.Create();
// Set up the remote runspace ...

PowerShell psLocal = PowerShell.Create();
// NO setup required for a local runspace.

using (psRemote)
using (psLocal) 
{

  // Get output from the remote runspace.
  var remoteOutput = psRemote.AddCommand("Get-Date").Invoke();

  // Pass the output to the local runspace for display formatting.
  foreach (var o in psLocal
                     .AddCommand("Format-List")
                     .AddCommand("Out-String")
                     .Invoke(remoteOutput)) 
  {
    // Print each object's display representation (a single, multi-line string)
    // To get the representation *line by line*, insert `.AddParameter("Stream")`
    // before the .Invoke()
    Console.WriteLine(o);
  }

}

直接使用 PowerShell SDK 调用的输出对象:

shows one way of processing the collection of PSObject instances returned from the non-generic .Invoke() 方法调用,使用 PSObject 类型的反射成员,例如 .Members.Properties.

但是,由于 PSObject 通过 dynamic 变量 实现了 IDynamicMetaObjectProvider interface, you can use the DLR 大大简化了事情:

而不是:

foreach (PSObject outputItem in ps.Invoke())
{                                
     string name = outputItem.Properties["Name"].Value;
     // ...
}

感谢键入枚举变量 dynamic,您可以简单地执行以下操作:

foreach (dynamic outputItem in ps.Invoke())
{                                
     string name = outputItem.Name; // use direct property access via the DLR
     // ...
}

一般最好使用.Invoke()方法的通用形式(例如, ps.Invoke<FileInfo>()) 以便尽早绑定静态类型。

但是,并不总是一个选项,即:

  • 如果输出对象是PSObject类型的动态对象,适用于:

    • [pscustomobject] 实例,它们是动态构造的自定义对象,最终由 PSObject.
    • 实现
    • 通过 remoting, as in your case, where the original .NET type identity is typically lost, and PSObject instances are used to emulate the original type - see 返回的对象,以概述 PowerShell 用于远程处理的基于 XML 的序列化以及对何时丢失类型保真度的解释。
  • 如果 输出对象不是所有 相同类型.

    • 但是,稍后您可以使用 as 运算符或 switch 表达式或语句来获取包装在 PSObject 实例中的强类型对象,使用 .BaseObject 属性.

以下示例代码说明了对象处理方法

  • 您可以将此代码编译为控制台应用程序,前提是您已添加对 PowerShell SDK NuGet 包的引用 - 请参阅
using System;
using System.IO;
using System.Management.Automation;

namespace demo
{

  class ConsoleApp
  {
    static void Main(string[] args)
    {

      using (var ps = PowerShell.Create())
      {

        // Use `dynamic` to enumerate the *Collection<PSObject>* instance that is 
        // returned from the non-generic .Invoke() call.
        // This is necessary for:
        //   - [pscustomobject] instances
        //   - "rehydrated" object instances received via *remoting* that have
        //     lost their original type identity ([psobject] == [pscustomobject])
        //   - multiple objects that don't all have the same type.
        foreach (dynamic o in ps.AddScript("[pscustomobject] @{ Foo = 42 }").Invoke())
        {
          // Note: Trying to access a nonexistent property quietly returns null.
          Console.WriteLine($"Dynamic: {o.Foo}"); // -> 42
        }

        ps.Commands.Clear();

        // If the return objects *all have the same type* (other than PSObject),
        // use the generic form of the .Invoke() method and specify that time <T>
        // This returns a *Collection<T>* instance, the members of which you
        // access with early binding, as usual.
        foreach (DateTime o in ps.AddCommand("Get-Date").Invoke<DateTime>())
        {
          Console.WriteLine($"Static: {o.Year}"); // -> this year
        }

        ps.Commands.Clear();

        // *Hybrid approach* for *non-PSCustomObjects* of *non-uniform type*
        // Work with Collection<PSObject>, but use `.BaseObject as <T>`
        // to work with statically typed objects.
        // Here, a `switch` expression (C# 8+) is used, but note that with `as`, when `<T>` is a *value type*,
        // `as <T>?` must be used, i.e. a *nullable* type.
        foreach (PSObject o in ps.AddCommand("Get-Date").AddStatement().AddCommand("Get-Item").AddArgument("~").AddStatement().AddCommand("Get-Location").Invoke())
        {
          Console.WriteLine(
            o.BaseObject switch
            {
              DateTime dt => $"DateTime: {dt}",
              DirectoryInfo fi => $"DirectoryInfo: {fi}",
              _ => $"Other ({o.BaseObject.GetType().FullName}): {o.BaseObject}"
            }
          );
        }
      }

    }
  }
}

上面的打印如下:

Dynamic: 42
Static: 2021
DateTime: 3/10/2021 10:39:36 AM
DirectoryInfo: /Users/jdoe
Other (System.Management.Automation.PathInfo): /Users/jdoe/Desktop