3

Im currently writing a Contact Manager for our Exchange Online Tenant in C# using the Powershell-Commands and Runspaces from "System.Management.Automation" and "System.Management.Automation.Runspaces" respectively. Its working fine for adding Contacts to the GAL. But im stuck at editing Contacts.

I need to get the Contact Details with the Powershell Commands. The code i can execute looks like this:

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

However: This gives me just the Contact-Name, of course. I would need to extend that Command. The equivalent native Powershell-Command i need to execute looks like this:

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

When i try to somehow add that "Format-List" to the Command Scheme from above - for example like this:

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

I get an Exception, telling me that "Format-List" is not the Name of a Cmdlet or anything ... I also tried adding it with AddParameter or even AddArgument - none of these work, i always end up with Errors.

Using Google i found threads here on Stackoverflow, where People passed a Script with the "AddScript()"-Command. But when i do something like this:

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

It tells me, that the Syntax is not recognized because that Remote-Powershell is running in No-Language Mode. I have no clue, how to change that Language Mode neither if it is even possible.

Below is the Full Code i use to execute Remote-Powershell-Commands on our Exchange Online Tenant:

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

I hope you find this excerpt useful for nailing down the Problem. Sorry for the German Comments in there. I need to get track of what i do, you know?! :-)

So ... can you tell me how to pass "Format-List" to that Remote Powershell without using a Script?

Many thanks for your help in advance! Steffen

Steffen
  • 33
  • 3
  • 2
    The `Get-Contact` cmdlet is going to run _on Microsofts server_ - and Microsoft obviously doesn't want you to just be able to run _anything_, so they only allow you to execute a restrictive set of commands and not run arbitrary code blocks (hence "NoLanguage" mode). You will have to feed the output from the remote runspace back to a local runspace if you want to process/format it further – Mathias R. Jessen Mar 09 '21 at 18:08

3 Answers3

3

Note:

  • The following section shows how to further process objects obtained from a remote PowerShell runspace in a local runspace, which in this case is necessary for security reasons.

    • Here, the Format-List command had to be applied locally, but note that it is generally not necessary to call Format-* cmdlets to work with objects returned from PowerShell SDK calls - see next point; you only need Format-* if you want to create a for-display, string representation of objects, as you would see in a PowerShell console (terminal).
  • The bottom section discusses how to process objects returned from PowerShell SDK calls in general.

    • As it turns out, working directly with the output objects as data is what Steffen truly wanted.

Mathias R. Jessen provided the crucial pointers in a comment:

  • For security reasons, your remote runspace is constrained both with respect to the language mode and what specific cmdlets you are allowed to execute.

    • The NoLanguage mode prevents use of PowerShell code of any kind, which rules out use of the .AddScript() method.
    • Seemingly, use of the Format-List cmdlet isn't permitted.
  • If you do need to apply Format-List to the remote runspace's output, you will need to use a second PowerShell instance for local execution of Format-List, to which you pass the remote runspace's output - see below.

    • As others have implied, Format-List is only required if you want a for-display-only, string representation of the output objects, as normally shown in a PowerShell console (terminal).

      • Otherwise, to process data, simply work with the returned objects and their properties directly, as shown in the bottom section.
    • Also, Format-List doesn't itself output strings, but objects containing formatting instructions; to turn the latter into the formatted string representations they encode, pass them to the Out-String cmdlet.

A simplified example:

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

}

Working directly with output objects from PowerShell SDK calls:

Muhammad Arsalan Altaf's answer shows one way of processing the collection of PSObject instances returned from the non-generic .Invoke() method call, using the PSObject type's reflection members such as .Members and .Properties.

However, since PSObject implements the IDynamicMetaObjectProvider interface, you can use the DLR, via dynamic variables, which greatly simplifies things:

Instead of:

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

thanks to typing the enumeration variable dynamic, you can simply do:

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

In general it is preferable to use the generic form of the .Invoke() method (e.g., ps.Invoke<FileInfo>()) so as to get early-bound, static typing.

However, this isn't always an option, namely:

  • If the output objects are dynamic objects of type PSObject, which applies to:

    • [pscustomobject] instances, which are dynamically constructed custom objects ultimately implemented by PSObject.
    • objects returned via 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 this answer for an overview of the XML-based serialization that PowerShell uses for remoting and an explanation of when type fidelity is lost.
  • If the output objects aren't all of the same type.

    • However, you can use the as operator or a switch expression or statement to obtain strongly typed objects wrapped in PSObject instances later, using the .BaseObject property.

The following sample code illustrates object-processing approaches:

  • You can compile this code as a console application, provided you have added a reference to a PowerShell SDK NuGet package - see this answer.
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}"
            }
          );
        }
      }

    }
  }
}

The above prints something like the following:

Dynamic: 42
Static: 2021
DateTime: 3/10/2021 10:39:36 AM
DirectoryInfo: /Users/jdoe
Other (System.Management.Automation.PathInfo): /Users/jdoe/Desktop
mklement0
  • 382,024
  • 64
  • 607
  • 775
1

PSObject returned by ps.Invoke() contains all the properties of object. You can get values of all properties. Just try this

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

}

Just enter the name of property you want to get and it will return you the value of that property.

  • 2
    Thank you. This is the solution! I wasnt aware that all i need is inside that Object already. Now it makes perfect sense. Thank you! – Steffen Mar 10 '21 at 08:59
  • Great pointer. Note that while working with the members of [`System.Management.Automation.PSObject`](https://learn.microsoft.com/en-US/dotnet/api/System.Management.Automation.PSObject) (such as `.Members` and `.Properties`) is definitely an option, it is much more convenient to use `dynamic` variables, which allow direct member access (e.g., `foreach (dynamic outputItem in psOutput) { var name = outputItem.Name; }`) – mklement0 Mar 10 '21 at 16:33
0

Help me understand why you need to pass your Contact object to Format-Table. This is basically destroying the Contact Object itself. Format-Table is just a way to layout a PSObject to the PS Host, once you pass an object to this function said object will lose all its properties and methods.

I'll show you an example of what I mean with an AD User Object.

Without Format-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

With Format-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

I hope this makes sense.

Santiago Squarzon
  • 41,465
  • 5
  • 14
  • 37
  • 1
    It does. I wasnt aware that the Output of the Command's Result is not the same compared to the Content of the acutal PSObject. This makes perfect sense. Thank you. – Steffen Mar 10 '21 at 09:02